diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index e63d869f..06f27030 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -12,10 +12,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 17 - uses: actions/setup-java@v2 + - name: Set up JDK 21 + uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'microsoft' - name: Build with Gradle run: ./gradlew check diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 5b781ec6..00000000 --- a/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,3 +0,0 @@ -eclipse.preferences.version=1 -encoding//src/main/java=UTF-8 -encoding//src/test/java=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 46235dc0..00000000 --- a/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,9 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 -org.eclipse.jdt.core.compiler.compliance=11 -org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore -org.eclipse.jdt.core.compiler.processAnnotations=disabled -org.eclipse.jdt.core.compiler.release=disabled -org.eclipse.jdt.core.compiler.source=11 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs deleted file mode 100644 index f897a7f1..00000000 --- a/.settings/org.eclipse.m2e.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -activeProfiles= -eclipse.preferences.version=1 -resolveWorkspaceProjects=true -version=1 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..c7f61a8e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic", + "java.format.settings.url": "code-formatting.xml", + "java.project.encoding": "warning" +} \ No newline at end of file diff --git a/bitwig-extensions.iml b/bitwig-extensions.iml index bca62fea..3c71311a 100644 --- a/bitwig-extensions.iml +++ b/bitwig-extensions.iml @@ -1,16 +1,17 @@ - + - + + - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 16d50d78..9df7e785 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ group = 'com.bitwig.extensions' description = 'Bitwig Studio Extensions (Github)' java { - sourceCompatibility = JavaVersion.VERSION_16 + sourceCompatibility = JavaVersion.VERSION_21 jar { archiveFileName = 'BitwigControllers.bwextension' diff --git a/doc-source/MIDIPLUS/XPro Keyboards.md b/doc-source/MIDIPLUS/XPro Keyboards.md index 14647c74..9ac820ff 100644 --- a/doc-source/MIDIPLUS/XPro Keyboards.md +++ b/doc-source/MIDIPLUS/XPro Keyboards.md @@ -4,3 +4,5 @@ * Pads are working and will be mapped to the first 8 pads of the Bitwig Studio drum machine * Keys, Pitch-Bend and Mod-Wheel are working * Transport buttons are working + +Make sure your controller has the latest firmware update. diff --git a/doc-source/MIDIPLUS/Xmini Keyboards.md b/doc-source/MIDIPLUS/Xmini Keyboards.md index dc845a87..a4f49cf7 100644 --- a/doc-source/MIDIPLUS/Xmini Keyboards.md +++ b/doc-source/MIDIPLUS/Xmini Keyboards.md @@ -3,3 +3,5 @@ * The four knobs controls the first four remote controls of the selected device * Keys, Pitch-Bend and Mod-Wheel are working * Transport buttons are working + +Make sure your controller has the latest firmware update. diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 943f0cbf..d64cd491 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f398c33c..a80b22ce 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 65dcd68d..1aa94a42 100755 --- a/gradlew +++ b/gradlew @@ -83,10 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f..25da30db 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/pom.xml b/pom.xml index 29e4df67..06817cce 100644 --- a/pom.xml +++ b/pom.xml @@ -26,8 +26,8 @@ true true - 16 - 16 + 21 + 21 UTF-8 1024m diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/AbstractSessionLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/AbstractSessionLayer.java new file mode 100644 index 00000000..8c4156c5 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/AbstractSessionLayer.java @@ -0,0 +1,91 @@ +package com.bitwig.extensions.controllers.akai.apc.common; + +import com.bitwig.extension.controller.api.ClipLauncherSlot; +import com.bitwig.extension.controller.api.SettableBooleanValue; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.controllers.akai.apc.common.led.LedBehavior; +import com.bitwig.extensions.controllers.akai.apc.common.led.RgbLightState; +import com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; + +public abstract class AbstractSessionLayer extends Layer { + protected final int[][] colorIndex = new int[8][8]; + protected SettableBooleanValue clipLauncherOverdub; + + public AbstractSessionLayer(final Layers layers) { + super(layers, "SESSION_LAYER"); + } + + protected abstract boolean isPlaying(); + + protected abstract boolean isShiftHeld(); + + protected RgbLightState getState(final Track track, final ClipLauncherSlot slot, final int trackIndex, + final int sceneIndex) { + if (slot.hasContent().get()) { + final int color = colorIndex[sceneIndex][trackIndex]; + if (slot.isSelected().get() && isShiftHeld()) { + return RgbLightState.WHITE_BRIGHT; + } + if (slot.isRecordingQueued().get()) { + return RgbLightState.RED.behavior(LedBehavior.BLINK_4); + } else if (slot.isRecording().get()) { + return RgbLightState.RED.behavior(LedBehavior.PULSE_2); + } else if (slot.isPlaybackQueued().get()) { + return RgbLightState.of(color, LedBehavior.BLINK_4); + } else if (slot.isStopQueued().get()) { + return RgbLightState.GREEN_PLAY.behavior(LedBehavior.BLINK_8); + } else if (slot.isPlaying().get() && track.isQueuedForStop().get()) { + return RgbLightState.GREEN.behavior(LedBehavior.BLINK_8); + } else if (slot.isPlaying().get()) { + if (clipLauncherOverdub.get() && track.arm().get()) { + return RgbLightState.RED.behavior(LedBehavior.PULSE_2); + } else { + if (isPlaying()) { + return RgbLightState.GREEN_PLAY; + } + return RgbLightState.GREEN; + } + } + return RgbLightState.of(color); + } + if (slot.isSelected().get() && isShiftHeld()) { + return RgbLightState.WHITE_DIM; + } + if (slot.isRecordingQueued().get()) { + return RgbLightState.RED.behavior(LedBehavior.BLINK_8); // Possibly Track Color + } else if (track.arm().get()) { + return RgbLightState.RED.behavior(LedBehavior.LIGHT_25); + } + return RgbLightState.OFF; + } // V ultra_X_39-- + + protected void markTrackBank(TrackBank bank) { + bank.canScrollBackwards().markInterested(); + bank.canScrollForwards().markInterested(); + bank.sceneBank().canScrollBackwards().markInterested(); + bank.sceneBank().canScrollForwards().markInterested(); + } + + protected void markTrack(final Track track) { + track.isStopped().markInterested(); + track.mute().markInterested(); + track.solo().markInterested(); + track.isQueuedForStop().markInterested(); + track.arm().markInterested(); + } + + protected void prepareSlot(final ClipLauncherSlot slot, final int sceneIndex, final int trackIndex) { + 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) -> colorIndex[sceneIndex][trackIndex] = ColorLookup.toColor(r, g, b)); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/midi/MidiProcessor.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/MidiProcessor.java similarity index 89% rename from src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/midi/MidiProcessor.java rename to src/main/java/com/bitwig/extensions/controllers/akai/apc/common/MidiProcessor.java index 3147c4d1..d5daba43 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/midi/MidiProcessor.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/MidiProcessor.java @@ -1,4 +1,4 @@ -package com.bitwig.extensions.controllers.akai.apcmk2.midi; +package com.bitwig.extensions.controllers.akai.apc.common; import com.bitwig.extension.controller.api.MidiIn; import com.bitwig.extension.controller.api.NoteInput; @@ -19,4 +19,5 @@ public interface MidiProcessor { void setModeChangeListener(final IntConsumer modeChangeListener); MidiIn getMidiIn(); + } diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/OrientationFollowType.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/OrientationFollowType.java new file mode 100644 index 00000000..ddd99f09 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/OrientationFollowType.java @@ -0,0 +1,32 @@ +package com.bitwig.extensions.controllers.akai.apc.common; + +import java.util.Arrays; + +public enum OrientationFollowType { + AUTOMATIC("Automatic", "Auto"), // + FIXED_VERTICAL("Mix Panel Layout", "Mixer"), // + FIXED_HORIZONTAL("Arrange Panel Layout", "Arrange"); + + private final String label; + private final String shortLabel; + + OrientationFollowType(final String label, final String shortLabel) { + this.label = label; + this.shortLabel = shortLabel; + } + + public String getLabel() { + return label; + } + + public String getShortLabel() { + return shortLabel; + } + + public static OrientationFollowType toType(final String value) { + return Arrays.stream(OrientationFollowType.values()) + .filter(type -> type.label.equals(value)) + .findFirst() + .orElse(OrientationFollowType.FIXED_VERTICAL); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/PanelLayout.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/PanelLayout.java new file mode 100644 index 00000000..7b78bdf4 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/PanelLayout.java @@ -0,0 +1,6 @@ +package com.bitwig.extensions.controllers.akai.apc.common; + +public enum PanelLayout { + VERTICAL, + HORIZONTAL +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/ApcButton.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/ApcButton.java similarity index 58% rename from src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/ApcButton.java rename to src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/ApcButton.java index 20e74066..69f89083 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/ApcButton.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/ApcButton.java @@ -1,12 +1,14 @@ -package com.bitwig.extensions.controllers.akai.apcmk2.control; +package com.bitwig.extensions.controllers.akai.apc.common.control; import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.akai.apcmk2.led.RgbLightState; -import com.bitwig.extensions.controllers.akai.apcmk2.midi.MidiProcessor; +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; import com.bitwig.extensions.framework.time.TimeRepeatEvent; +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; @@ -21,7 +23,7 @@ public abstract class ApcButton { private long recordedDownTime; protected final int midiId; - protected ApcButton(final int channel, final int midiId, String name, HardwareSurface surface, + protected ApcButton(final int channel, final int midiId, final String name, final HardwareSurface surface, final MidiProcessor midiProcessor) { this.midiProcessor = midiProcessor; final MidiIn midiIn = midiProcessor.getMidiIn(); @@ -31,13 +33,20 @@ protected ApcButton(final int channel, final int midiId, String name, HardwareSu hwButton.releasedAction().setActionMatcher(midiIn.createNoteOffActionMatcher(channel, midiId)); light = surface.createMultiStateHardwareLight(name + "_LIGHT_" + midiId); light.state().setValue(RgbLightState.OFF); + 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); } @@ -54,6 +63,10 @@ public void bindLight(final Layer layer, final Supplier 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); } @@ -63,6 +76,39 @@ public void bindLightPressed(final Layer layer, final InternalHardwareLightState 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. + * 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) { + recordedDownTime = System.currentTimeMillis(); + 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 diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/ClickEncoder.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/ClickEncoder.java new file mode 100644 index 00000000..a90d2081 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/ClickEncoder.java @@ -0,0 +1,52 @@ +package com.bitwig.extensions.controllers.akai.apc.common.control; + +import java.util.function.IntConsumer; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareActionBindable; +import com.bitwig.extension.controller.api.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.RelativeHardwareValueMatcher; +import com.bitwig.extension.controller.api.SettableRangedValue; +import com.bitwig.extensions.framework.Layer; + +public class ClickEncoder { + private final RelativeHardwareKnob encoder; + private final ControllerHost host; + + public ClickEncoder(int ccNr, final ControllerHost host, final HardwareSurface surface, MidiIn midiIn) { + encoder = surface.createRelativeHardwareKnob("ENCODER_" + ccNr); + this.host = host; + final RelativeHardwareValueMatcher stepUpMatcher = + midiIn.createRelativeValueMatcher("(status == 176 && data1 == %d && data2==1)".formatted(ccNr), 1); + final RelativeHardwareValueMatcher stepDownMatcher = + midiIn.createRelativeValueMatcher("(status == 176 && data1 == %d && data2==127)".formatted(ccNr), -1); + + final RelativeHardwareValueMatcher matcher = + host.createOrRelativeHardwareValueMatcher(stepDownMatcher, stepUpMatcher); + encoder.setAdjustValueMatcher(matcher); + encoder.setStepSize(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 bind(final Layer layer, final SettableRangedValue value) { + final RelativeValueBinding binding = new RelativeValueBinding(encoder, value); + layer.addBinding(binding); + } + + public void bind(final Layer layer, IntConsumer action) { + final HardwareActionBindable incAction = host.createAction(() -> action.accept(1), () -> "+"); + final HardwareActionBindable decAction = host.createAction(() -> action.accept(-1), () -> "-"); + layer.bind(encoder, host.createRelativeHardwareControlStepTarget(incAction, decAction)); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/Encoder.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/Encoder.java similarity index 68% rename from src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/Encoder.java rename to src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/Encoder.java index 434d3aed..f6e30ab7 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/Encoder.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/Encoder.java @@ -1,4 +1,6 @@ -package com.bitwig.extensions.controllers.akai.apcmk2.control; +package com.bitwig.extensions.controllers.akai.apc.common.control; + +import java.util.function.IntConsumer; import com.bitwig.extension.controller.api.*; import com.bitwig.extensions.framework.Layer; @@ -9,6 +11,7 @@ public class Encoder { public Encoder(int ccNr, final HardwareSurface surface, MidiIn midiIn) { encoder = surface.createRelativeHardwareKnob("ENCODER_" + ccNr); + final String matchExpr = String.format("(status==%d && data1==%d && data2>0)", Midi.CC, ccNr); encoder.setAdjustValueMatcher(midiIn.createRelative2sComplementValueMatcher(matchExpr, "data2", 7, 200)); encoder.setStepSize(0.1); @@ -27,4 +30,10 @@ public void bind(final Layer layer, final SettableRangedValue value) { final RelativeValueBinding binding = new RelativeValueBinding(encoder, value); layer.addBinding(binding); } + + public void bind(ControllerHost host, final Layer layer, IntConsumer changeAction) { + final HardwareActionBindable incAction = host.createAction(() -> changeAction.accept(1), () -> "+"); + final HardwareActionBindable decAction = host.createAction(() -> changeAction.accept(-1), () -> "-"); + layer.bind(encoder, host.createRelativeHardwareControlStepTarget(incAction, decAction)); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/RelativeValueBinding.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/RelativeValueBinding.java similarity index 94% rename from src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/RelativeValueBinding.java rename to src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/RelativeValueBinding.java index 8416f663..7535919b 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/RelativeValueBinding.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/RelativeValueBinding.java @@ -1,4 +1,4 @@ -package com.bitwig.extensions.controllers.akai.apcmk2.control; +package com.bitwig.extensions.controllers.akai.apc.common.control; import com.bitwig.extension.controller.api.HardwareBinding; import com.bitwig.extension.controller.api.RelativeHardwareControlBinding; diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/RgbButton.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/RgbButton.java similarity index 57% rename from src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/RgbButton.java rename to src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/RgbButton.java index 6a33215c..fb0f22b8 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/RgbButton.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/RgbButton.java @@ -1,17 +1,20 @@ -package com.bitwig.extensions.controllers.akai.apcmk2.control; +package com.bitwig.extensions.controllers.akai.apc.common.control; +import com.bitwig.extension.api.Color; import com.bitwig.extension.controller.api.HardwareSurface; import com.bitwig.extension.controller.api.InternalHardwareLightState; -import com.bitwig.extensions.controllers.akai.apcmk2.led.RgbLightState; -import com.bitwig.extensions.controllers.akai.apcmk2.midi.MidiProcessor; +import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; +import com.bitwig.extensions.controllers.akai.apc.common.led.RgbLightState; +import com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup; import com.bitwig.extensions.framework.values.Midi; public class RgbButton extends ApcButton { - protected RgbButton(final int channel, final int noteNr, String name, final HardwareSurface surface, - final MidiProcessor midiProcessor) { + public RgbButton(final int channel, final int noteNr, final String name, final HardwareSurface surface, + final MidiProcessor midiProcessor) { super(channel, noteNr, name, surface, midiProcessor); light.state().setValue(RgbLightState.OFF); + light.setColorToStateFunction(this::colorToState); if (channel == 9) { light.state().onUpdateHardware(this::updateDrumState); } else { @@ -19,9 +22,12 @@ protected RgbButton(final int channel, final int noteNr, String name, final Hard } } + private InternalHardwareLightState colorToState(final Color color) { + return RgbLightState.of(ColorLookup.toColor(color.getRed255(), color.getGreen255(), color.getBlue255())); + } + private void updateDrumState(final InternalHardwareLightState internalHardwareLightState) { - if (internalHardwareLightState instanceof RgbLightState) { - RgbLightState state = (RgbLightState) internalHardwareLightState; + if (internalHardwareLightState instanceof RgbLightState state) { midiProcessor.sendMidi(Midi.NOTE_ON | 0x9, midiId, state.getColorIndex()); } else { midiProcessor.sendMidi(Midi.NOTE_ON, midiId, 0); @@ -30,8 +36,7 @@ private void updateDrumState(final InternalHardwareLightState internalHardwareLi private void updateState(final InternalHardwareLightState internalHardwareLightState) { - if (internalHardwareLightState instanceof RgbLightState) { - RgbLightState state = (RgbLightState) internalHardwareLightState; + if (internalHardwareLightState instanceof RgbLightState state) { midiProcessor.sendMidi(state.getMidiCode(), midiId, state.getColorIndex()); } else { midiProcessor.sendMidi(Midi.NOTE_ON, midiId, 0); diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/SingleLedButton.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/SingleLedButton.java similarity index 77% rename from src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/SingleLedButton.java rename to src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/SingleLedButton.java index e6857bb0..dfd523a0 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/SingleLedButton.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/control/SingleLedButton.java @@ -1,10 +1,10 @@ -package com.bitwig.extensions.controllers.akai.apcmk2.control; +package com.bitwig.extensions.controllers.akai.apc.common.control; import com.bitwig.extension.controller.api.HardwareSurface; import com.bitwig.extension.controller.api.InternalHardwareLightState; -import com.bitwig.extensions.controllers.akai.apcmk2.led.RgbLightState; -import com.bitwig.extensions.controllers.akai.apcmk2.led.SingleLedState; -import com.bitwig.extensions.controllers.akai.apcmk2.midi.MidiProcessor; +import com.bitwig.extensions.controllers.akai.apc.common.led.RgbLightState; +import com.bitwig.extensions.controllers.akai.apc.common.led.SingleLedState; +import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; import com.bitwig.extensions.framework.values.Midi; public class SingleLedButton extends ApcButton { diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/led/ColorLookup.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/led/ColorLookup.java similarity index 97% rename from src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/led/ColorLookup.java rename to src/main/java/com/bitwig/extensions/controllers/akai/apc/common/led/ColorLookup.java index 280968e2..9846c410 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/led/ColorLookup.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/led/ColorLookup.java @@ -1,4 +1,4 @@ -package com.bitwig.extensions.controllers.akai.apcmk2.led; +package com.bitwig.extensions.controllers.akai.apc.common.led; public class ColorLookup { private static final Hsb BLACK_HSB = new Hsb(0, 0, 0); diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/led/LedBehavior.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/led/LedBehavior.java similarity index 84% rename from src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/led/LedBehavior.java rename to src/main/java/com/bitwig/extensions/controllers/akai/apc/common/led/LedBehavior.java index f92087bc..6c7ae03c 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/led/LedBehavior.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/led/LedBehavior.java @@ -1,4 +1,4 @@ -package com.bitwig.extensions.controllers.akai.apcmk2.led; +package com.bitwig.extensions.controllers.akai.apc.common.led; public enum LedBehavior { LIGHT_10(0), @@ -18,11 +18,13 @@ public enum LedBehavior { BLINK_4(14), BLINK_2(15); final int code; + LedBehavior(int code) { this.code = code; - } + } public int getCode() { return code; } -} + + } diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/led/RgbLightState.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/led/RgbLightState.java new file mode 100644 index 00000000..9a493dd7 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/led/RgbLightState.java @@ -0,0 +1,89 @@ +package com.bitwig.extensions.controllers.akai.apc.common.led; + +import com.bitwig.extension.api.Color; +import com.bitwig.extension.controller.api.HardwareLightVisualState; +import com.bitwig.extension.controller.api.InternalHardwareLightState; +import com.bitwig.extensions.framework.values.Midi; + +import java.util.HashMap; +import java.util.Map; + +public class RgbLightState extends InternalHardwareLightState { + + private static final Map STATE_MAP = new HashMap<>(); + + public static final RgbLightState OFF = new RgbLightState(0); + public static final RgbLightState WHITE = RgbLightState.of(3); + public static final RgbLightState WHITE_BRIGHT = RgbLightState.of(3, LedBehavior.FULL); + public static final RgbLightState WHITE_SEL = RgbLightState.of(3, LedBehavior.PULSE_2); + public static final RgbLightState WHITE_DIM = RgbLightState.of(1); + public static final RgbLightState RED = new RgbLightState(5); + public static final RgbLightState GREEN = new RgbLightState(21); + public static final RgbLightState RED_FULL = new RgbLightState(5, LedBehavior.FULL); + public static final RgbLightState RED_DIM = new RgbLightState(5, LedBehavior.LIGHT_10); + public static final RgbLightState YELLOW_FULL = new RgbLightState(13, LedBehavior.FULL); + public static final RgbLightState YELLOW_DIM = new RgbLightState(13, LedBehavior.LIGHT_10); + public static final RgbLightState ORANGE_FULL = new RgbLightState(9, LedBehavior.FULL); + public static final RgbLightState ORANGE_SEL = new RgbLightState(9, LedBehavior.PULSE_2); + public static final RgbLightState ORANGE_DIM = new RgbLightState(9, LedBehavior.LIGHT_10); + public static final RgbLightState GREEN_PLAY = new RgbLightState(21, LedBehavior.PULSE_2); + + public static final RgbLightState MUTE_PLAY_DIM = new RgbLightState(10, LedBehavior.LIGHT_10); + public static final RgbLightState MUTE_PLAY_FULL = new RgbLightState(10, LedBehavior.FULL); + public static final RgbLightState SOLO_PLAY_FULL = new RgbLightState(14, LedBehavior.FULL); + public static final RgbLightState SOLO_PLAY_YELLOW_DIM = new RgbLightState(14, LedBehavior.LIGHT_10); + + private final int colorIndex; + private final LedBehavior ledBehavior; + + public static RgbLightState of(final int colorIndex) { + return STATE_MAP.computeIfAbsent(colorIndex | LedBehavior.FULL.getCode() << 8, + index -> new RgbLightState(colorIndex)); + } + + public static RgbLightState of(final int colorIndex, final LedBehavior behavior) { + return STATE_MAP.computeIfAbsent(colorIndex | behavior.getCode() << 8, + index -> new RgbLightState(colorIndex, behavior)); + } + + public RgbLightState behavior(final LedBehavior behavior) { + if (this.ledBehavior == behavior) { + return this; + } + return of(this.colorIndex, behavior); + } + + private RgbLightState(final int colorIndex) { + this(colorIndex, LedBehavior.FULL); + } + + private RgbLightState(final int colorIndex, final LedBehavior ledBehavior) { + this.colorIndex = colorIndex; + this.ledBehavior = ledBehavior; + } + + public int getColorIndex() { + return colorIndex; + } + + public int getMidiCode() { + return Midi.NOTE_ON | ledBehavior.getCode(); + } + + @Override + public HardwareLightVisualState getVisualState() { + if (colorIndex == 0) { + return null; + } + return HardwareLightVisualState.createForColor(Color.fromRGB(255, 0, 0)); + } + + @Override + public boolean equals(final Object o) { + if (o instanceof RgbLightState other) { + return other.colorIndex == colorIndex && other.ledBehavior == ledBehavior; + } + return false; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/led/SingleLedState.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/led/SingleLedState.java similarity index 93% rename from src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/led/SingleLedState.java rename to src/main/java/com/bitwig/extensions/controllers/akai/apc/common/led/SingleLedState.java index 78fab21a..01fa0c31 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/led/SingleLedState.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/led/SingleLedState.java @@ -1,4 +1,4 @@ -package com.bitwig.extensions.controllers.akai.apcmk2.led; +package com.bitwig.extensions.controllers.akai.apc.common.led; import com.bitwig.extension.controller.api.HardwareLightVisualState; import com.bitwig.extension.controller.api.InternalHardwareLightState; diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/led/VarSingleLedState.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/led/VarSingleLedState.java new file mode 100644 index 00000000..cd88b2af --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc/common/led/VarSingleLedState.java @@ -0,0 +1,55 @@ +package com.bitwig.extensions.controllers.akai.apc.common.led; + +import com.bitwig.extension.controller.api.HardwareLightVisualState; +import com.bitwig.extension.controller.api.InternalHardwareLightState; + +public class VarSingleLedState extends InternalHardwareLightState { + + public static final VarSingleLedState OFF = new VarSingleLedState(0); + public static final VarSingleLedState LIGHT_10= new VarSingleLedState(1); + public static final VarSingleLedState LIGHT_25= new VarSingleLedState(2); + public static final VarSingleLedState LIGHT_50= new VarSingleLedState(3); + public static final VarSingleLedState LIGHT_60= new VarSingleLedState(4); + public static final VarSingleLedState LIGHT_75= new VarSingleLedState(5); + public static final VarSingleLedState LIGHT_90= new VarSingleLedState(6); + public static final VarSingleLedState FULL= new VarSingleLedState(7); + public static final VarSingleLedState PULSE_16= new VarSingleLedState(8); + public static final VarSingleLedState PULSE_8= new VarSingleLedState(9); + public static final VarSingleLedState PULSE_4= new VarSingleLedState(10); + public static final VarSingleLedState PULSE_2= new VarSingleLedState(11); + public static final VarSingleLedState BLINK_24= new VarSingleLedState(12); + public static final VarSingleLedState BLINK_16= new VarSingleLedState(13); + public static final VarSingleLedState BLINK_8= new VarSingleLedState(14); + public static final VarSingleLedState BLINK_4= new VarSingleLedState(15); + public static final VarSingleLedState BLINK_2= new VarSingleLedState(16); + + private final int code; + + protected VarSingleLedState(int code) { + this.code = code; + } + + public int getCode() { + return code == 0 ? 0 : 1; + } + + public int getChannel() { + return code == 0 ? 0 : code-1; + } + + @Override + public HardwareLightVisualState getVisualState() { + return null; + } + + @Override + public boolean equals(final Object o) { + if(o == this) { + return true; + } + if(o instanceof VarSingleLedState) { + return ((VarSingleLedState)o).code == code; + } + return false; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/APC40MKIIControllerExtension.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/APC40MKIIControllerExtension.java index dc0e2f4f..ac97677c 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/APC40MKIIControllerExtension.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/APC40MKIIControllerExtension.java @@ -17,10 +17,10 @@ import com.bitwig.extension.controller.api.HardwareControlType; import com.bitwig.extension.controller.api.HardwareSlider; import com.bitwig.extension.controller.api.HardwareSurface; -import com.bitwig.extension.controller.api.HardwareTextDisplay; import com.bitwig.extension.controller.api.MasterTrack; import com.bitwig.extension.controller.api.MidiIn; import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extension.controller.api.MultiStateHardwareLight; import com.bitwig.extension.controller.api.OnOffHardwareLight; import com.bitwig.extension.controller.api.PinnableCursorDevice; import com.bitwig.extension.controller.api.Preferences; @@ -33,7 +33,6 @@ import com.bitwig.extension.controller.api.Send; import com.bitwig.extension.controller.api.SendBank; 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; @@ -41,7 +40,7 @@ import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; -public class APC40MKIIControllerExtension extends ControllerExtension +class APC40MKIIControllerExtension extends ControllerExtension { private static final boolean ENABLE_DEBUG_LAYER = false; @@ -193,8 +192,8 @@ public void init() mIsMasterSelected = mTrackCursor.createEqualsValue(mMasterTrack); - PinnableCursorDevice channelStripDevice = - mTrackCursor.createCursorDevice("channel-strip", "Channel Strip", 4, CursorDeviceFollowMode.LAST_DEVICE); + final PinnableCursorDevice channelStripDevice = mTrackCursor.createCursorDevice("channel-strip", + "Channel Strip", 4, CursorDeviceFollowMode.LAST_DEVICE); channelStripDevice.exists().markInterested(); mChannelStripRemoteControls = channelStripDevice.createCursorRemoteControlsPage(8); mChannelStripRemoteControls.setHardwareLayout(HardwareControlType.KNOB, 8); @@ -325,8 +324,8 @@ private void createTransportObject(final ControllerHost host) private void createSettingsObjects(final ControllerHost host) { final Preferences preferences = host.getPreferences(); - mPanAsTrackRemoteSetting = preferences.getBooleanSetting("Replace PAN by Track Remotes", - "Controls", false); + mPanAsTrackRemoteSetting = preferences.getBooleanSetting("Replace PAN by Track Remotes", "Controls", + false); mPanAsTrackRemoteSetting.markInterested(); if (mPanAsTrackRemoteSetting.get()) mTopMode = TopMode.TRACK_CONTROLS; @@ -438,7 +437,9 @@ private void createShiftLayer() for (int i = 0; i < 8; ++i) { final int x = i; - mShiftLayer.bindPressed(mTrackSelectButtons[x], getHost().createAction(() -> setLaunchQuantizationFromTrackSelect(x), () -> "Configures the default launch quantization")); + mShiftLayer.bindPressed(mTrackSelectButtons[x], + getHost().createAction(() -> setLaunchQuantizationFromTrackSelect(x), + () -> "Configures the default launch quantization")); mShiftLayer.bind(() -> x == computeLaunchQuantizationIndex(), mTrackSelectLeds[x]); final Track track = mTrackBank.getItemAt(i); @@ -464,17 +465,17 @@ private void createShiftLayer() private void setLaunchQuantizationFromTrackSelect(final int x) { final String quantization = switch (x) - { - case 0 -> "none"; - case 1 -> "8"; - case 2 -> "4"; - case 3 -> "2"; - case 4 -> "1"; - case 5 -> "1/4"; - case 6 -> "1/8"; - case 7 -> "1/16"; - default -> "1"; - }; + { + case 0 -> "none"; + case 1 -> "8"; + case 2 -> "4"; + case 3 -> "2"; + case 4 -> "1"; + case 5 -> "1/4"; + case 6 -> "1/8"; + case 7 -> "1/16"; + default -> "1"; + }; mTransport.defaultLaunchQuantization().set(quantization); } @@ -515,7 +516,7 @@ private void createDebugLayer() { if (ENABLE_DEBUG_LAYER) { - Layer debugLayer = DebugUtilities.createDebugLayer(mLayers, mHardwareSurface); + final Layer debugLayer = DebugUtilities.createDebugLayer(mLayers, mHardwareSurface); debugLayer.activate(); } } @@ -547,11 +548,9 @@ private void createMainLayer() mMainLayer.bindToggle(mSoloButtons[x], track.solo()); mMainLayer.bindToggle(mArmButtons[x], track.arm()); mMainLayer.bindPressed(mABButtons[x], getHost().createAction(() -> { - final SettableEnumValue crossFadeMode = track.crossFadeMode(); - final int nextValue = (crossFadeToInt(crossFadeMode.get()) + 1) % 3; - crossFadeMode.set(intToCrossFade(nextValue)); + track.crossFadeMode().set(CrossFadeMode.forTrack(track).getNext().getEnumName()); }, () -> "Cycle through crossfade values")); - mMainLayer.bind(track.crossFadeMode(), mABLeds[x]); + mMainLayer.bindLightState(() -> CrossFadeMode.forTrack(track), mABLeds[x]); mMainLayer.bindPressed(mTrackSelectButtons[x], getHost().createAction(() -> mTrackCursor.selectChannel(track), () -> "Selects the track")); @@ -645,12 +644,34 @@ private void createMainLayer() () -> "Activate Pan mode or Track Remote Controls mode")); mMainLayer.bindPressed(mSendsButton, getHost().createAction(() -> activateTopMode(TopMode.SENDS), () -> "Activate Sends mode")); - mMainLayer.bindPressed(mUserButton, - getHost().createAction(() -> activateTopMode(TopMode.PROJECT_CONTROLS), () -> "Activate Project Remote Controls mode")); + mMainLayer.bindPressed(mUserButton, getHost().createAction( + () -> activateTopMode(TopMode.PROJECT_CONTROLS), () -> "Activate Project Remote Controls mode")); mMainLayer.bindPressed(mShiftButton, mShiftLayer.getActivateAction()); mMainLayer.bindReleased(mShiftButton, mShiftLayer.getDeactivateAction()); + for (int i = 0; i < 8; ++i) + { + final Track track = mTrackBank.getItemAt(i); + final ClipLauncherSlotBank clipLauncherSlotBank = track.clipLauncherSlotBank(); + + for (int j = 0; j < 5; ++j) + { + final ClipLauncherSlot slot = clipLauncherSlotBank.getItemAt(j); + final RgbLed rgbLed = mGridLeds[i][j]; + + mMainLayer.bindLightState(() -> computeRGBLedStateForSlot(slot), rgbLed.getLight()); + } + } + + for (int i = 0; i < 5; ++i) + { + final RgbLed rgbLed = mSceneLeds[i]; + final int sceneButtonIndex = i; + + mMainLayer.bindLightState(() -> computeRGBLedStateForScene(sceneButtonIndex), rgbLed.getLight()); + } + mMainLayer.activate(); } @@ -842,7 +863,7 @@ private void createTransportControls() mPlayButton.pressedAction().setActionMatcher(mMidiIn.createNoteOnActionMatcher(0, BT_PLAY)); mPlayButton.releasedAction().setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_PLAY)); mPlayLed = mHardwareSurface.createOnOffHardwareLight("PlayLed"); - mPlayLed.onUpdateHardware(() -> sendLedUpdate(BT_PLAY, mPlayLed)); + mPlayLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_PLAY, isOn)); mPlayLed.setOnColor(Color.fromRGB(0, 1, 0)); mPlayButton.setBackgroundLight(mPlayLed); @@ -853,7 +874,7 @@ private void createTransportControls() mRecordButton.releasedAction().setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_RECORD)); mRecordLed = mHardwareSurface.createOnOffHardwareLight("RecordLed"); mRecordLed.setOnColor(Color.fromRGB(1, 0, 0)); - mRecordLed.onUpdateHardware(() -> sendLedUpdate(BT_RECORD, mRecordLed)); + mRecordLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_RECORD, isOn)); mRecordButton.setBackgroundLight(mRecordLed); mSessionButton = mHardwareSurface.createHardwareButton("Session"); @@ -863,7 +884,7 @@ private void createTransportControls() mSessionButton.releasedAction().setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_SESSION)); mSessionLed = mHardwareSurface.createOnOffHardwareLight("SessionLed"); mSessionLed.setOnColor(Color.fromRGB(1, 0, 0)); - mSessionLed.onUpdateHardware(() -> sendLedUpdate(BT_SESSION, mSessionLed)); + mSessionLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_SESSION, isOn)); mSessionButton.setBackgroundLight(mSessionLed); mMetronomeButton = mHardwareSurface.createHardwareButton("Metronome"); @@ -872,8 +893,8 @@ private void createTransportControls() mMetronomeButton.pressedAction().setActionMatcher(mMidiIn.createNoteOnActionMatcher(0, BT_METRONOME)); mMetronomeButton.releasedAction().setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_METRONOME)); mMetronomeLed = mHardwareSurface.createOnOffHardwareLight("MetronomeLed"); - mMetronomeLed.setOnColor(Color.fromRGB255(255,165,0)); - mMetronomeLed.onUpdateHardware(() -> sendLedUpdate(BT_METRONOME, mMetronomeLed)); + mMetronomeLed.setOnColor(Color.fromRGB255(255, 165, 0)); + mMetronomeLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_METRONOME, isOn)); mMetronomeButton.setBackgroundLight(mMetronomeLed); mTapTempoButton = mHardwareSurface.createHardwareButton("TapTempo"); @@ -882,18 +903,16 @@ private void createTransportControls() mTapTempoButton.pressedAction().setActionMatcher(mMidiIn.createNoteOnActionMatcher(0, BT_TAP_TEMPO)); mTapTempoButton.releasedAction().setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_TAP_TEMPO)); - HardwareButton nudgePlusButton = mHardwareSurface.createHardwareButton("Nudge+"); + final HardwareButton nudgePlusButton = mHardwareSurface.createHardwareButton("Nudge+"); nudgePlusButton.setLabel("NUDGE +"); nudgePlusButton.setLabelPosition(RelativePosition.ABOVE); nudgePlusButton.pressedAction().setActionMatcher(mMidiIn.createNoteOnActionMatcher(0, BT_NUDGE_PLUS)); - nudgePlusButton.releasedAction() - .setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_NUDGE_PLUS)); + nudgePlusButton.releasedAction().setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_NUDGE_PLUS)); - HardwareButton nudgeMinusButton = mHardwareSurface.createHardwareButton("Nudge-"); + final HardwareButton nudgeMinusButton = mHardwareSurface.createHardwareButton("Nudge-"); nudgeMinusButton.setLabel("NUDGE -"); nudgeMinusButton.setLabelPosition(RelativePosition.ABOVE); - nudgeMinusButton.pressedAction() - .setActionMatcher(mMidiIn.createNoteOnActionMatcher(0, BT_NUDGE_MINUS)); + nudgeMinusButton.pressedAction().setActionMatcher(mMidiIn.createNoteOnActionMatcher(0, BT_NUDGE_MINUS)); nudgeMinusButton.releasedAction() .setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_NUDGE_MINUS)); @@ -918,8 +937,8 @@ private void createTrackStopButtons() final int channel = x; final OnOffHardwareLight led = mHardwareSurface.createOnOffHardwareLight("TrackStopLed-" + x); - led.setOnColor(Color.fromRGB255(255,165,0)); - led.onUpdateHardware(() -> sendLedUpdate(BT_TRACK_STOP, channel, led)); + led.setOnColor(Color.fromRGB255(255, 165, 0)); + led.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_TRACK_STOP, channel, isOn)); bt.setBackgroundLight(led); mTrackStopLeds[x] = led; } @@ -930,9 +949,9 @@ private void createTrackStopButtons() mMasterTrackStopButton.releasedAction() .setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_MASTER_STOP)); mMasterTrackStopLed = mHardwareSurface.createOnOffHardwareLight("MasterTrackStopLed"); - mMasterTrackStopLed.setOnColor(Color.fromRGB255(255,165,0)); + mMasterTrackStopLed.setOnColor(Color.fromRGB255(255, 165, 0)); mMasterTrackStopButton.setBackgroundLight(mMasterTrackStopLed); - mMasterTrackStopLed.onUpdateHardware(() -> sendLedUpdate(BT_MASTER_STOP, mMasterTrackStopLed)); + mMasterTrackStopLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_MASTER_STOP, isOn)); } private void createTrackSelectButtons() @@ -949,8 +968,8 @@ private void createTrackSelectButtons() final int channel = x; final OnOffHardwareLight led = mHardwareSurface.createOnOffHardwareLight("TrackSelectLed-" + x); - led.setOnColor(Color.fromRGB255(255,165,0)); - led.onUpdateHardware(() -> sendLedUpdate(BT_TRACK_SELECT, channel, led)); + led.setOnColor(Color.fromRGB255(255, 165, 0)); + led.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_TRACK_SELECT, channel, isOn)); bt.setBackgroundLight(led); mTrackSelectLeds[x] = led; } @@ -961,15 +980,15 @@ private void createTrackSelectButtons() mMasterTrackSelectButton.releasedAction() .setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_MASTER_SELECT)); mMasterTrackSelectLed = mHardwareSurface.createOnOffHardwareLight("MasterTrackSelectLed"); - mMasterTrackSelectLed.setOnColor(Color.fromRGB255(255,165,0)); + mMasterTrackSelectLed.setOnColor(Color.fromRGB255(255, 165, 0)); mMasterTrackSelectButton.setBackgroundLight(mMasterTrackSelectLed); - mMasterTrackSelectLed.onUpdateHardware(() -> sendLedUpdate(BT_MASTER_SELECT, mMasterTrackSelectLed)); + mMasterTrackSelectLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_MASTER_SELECT, isOn)); } private void createABButtons() { mABButtons = new HardwareButton[8]; - mABLeds = new HardwareTextDisplay[8]; + mABLeds = new MultiStateHardwareLight[8]; for (int x = 0; x < 8; ++x) { final HardwareButton bt = mHardwareSurface.createHardwareButton("AB-" + x); @@ -978,26 +997,18 @@ private void createABButtons() mABButtons[x] = bt; final int channel = x; - final HardwareTextDisplay led = mHardwareSurface.createHardwareTextDisplay("ABLed-" + x, 1); - led.onUpdateHardware(() -> sendLedUpdate(BT_TRACK_AB, channel, crossFadeToInt(led.line(0).text().currentValue()))); + final MultiStateHardwareLight led = mHardwareSurface.createMultiStateHardwareLight("ABLed-" + x); + led.setColorToStateFunction(CrossFadeMode::getBestModeForColor); + led.state().onUpdateHardware(state -> sendLedUpdate(BT_TRACK_AB, channel, (CrossFadeMode)state)); + bt.setBackgroundLight(led); mABLeds[x] = led; } } - private static Color getABLedColor(final int i) - { - return switch (i) - { - case 1 -> Color.fromRGB(1.0, 0.5, 0); - case 2 -> Color.fromRGB(0, 0, 1.0); - default -> Color.fromRGB(0, 0, 0); - }; - } - private void createArmButtons() { mArmButtons = new HardwareButton[8]; - OnOffHardwareLight[] armLeds = new OnOffHardwareLight[8]; + final OnOffHardwareLight[] armLeds = new OnOffHardwareLight[8]; for (int x = 0; x < 8; ++x) { final HardwareButton bt = mHardwareSurface.createHardwareButton("Arm-" + x); @@ -1010,7 +1021,7 @@ private void createArmButtons() final int channel = x; final OnOffHardwareLight led = mHardwareSurface.createOnOffHardwareLight("ArmLed-" + x); led.setOnColor(Color.fromRGB(1, 0, 0)); - led.onUpdateHardware(() -> sendLedUpdate(BT_TRACK_ARM, channel, led)); + led.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_TRACK_ARM, channel, isOn)); bt.setBackgroundLight(led); armLeds[x] = led; } @@ -1019,7 +1030,7 @@ private void createArmButtons() private void createSoloButtons() { mSoloButtons = new HardwareButton[8]; - OnOffHardwareLight[] soloLeds = new OnOffHardwareLight[8]; + final OnOffHardwareLight[] soloLeds = new OnOffHardwareLight[8]; for (int x = 0; x < 8; ++x) { final HardwareButton bt = mHardwareSurface.createHardwareButton("Solo-" + x); @@ -1032,7 +1043,7 @@ private void createSoloButtons() final int channel = x; final OnOffHardwareLight led = mHardwareSurface.createOnOffHardwareLight("SoloLed-" + x); led.setOnColor(Color.fromRGB(0, 0, 1)); - led.onUpdateHardware(() -> sendLedUpdate(BT_TRACK_SOLO, channel, led)); + led.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_TRACK_SOLO, channel, isOn)); bt.setBackgroundLight(led); soloLeds[x] = led; } @@ -1053,8 +1064,8 @@ private void createMuteButtons() final int channel = x; final OnOffHardwareLight led = mHardwareSurface.createOnOffHardwareLight("MuteLed-" + x); - led.setOnColor(Color.fromRGB255(255,165,0)); - led.onUpdateHardware(() -> sendLedUpdate(BT_TRACK_MUTE, channel, led)); + led.setOnColor(Color.fromRGB255(255, 165, 0)); + led.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_TRACK_MUTE, channel, isOn)); bt.setBackgroundLight(led); mMuteLeds[x] = led; } @@ -1074,7 +1085,8 @@ private void createGridButtons() bt.releasedAction().setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, note)); mGridButtons[y * 8 + x] = bt; - mGridLeds[x][y] = new RgbLed(bt, mHardwareSurface, MSG_NOTE_ON, BT_GRID0 + x + (4 - y) * 8); + mGridLeds[x][y] = new RgbLed(bt, mHardwareSurface, MSG_NOTE_ON, BT_GRID0 + x + (4 - y) * 8, + mMidiOut); } } @@ -1087,7 +1099,7 @@ private void createGridButtons() bt.releasedAction().setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_SCENE0 + y)); mSceneButtons[y] = bt; - mSceneLeds[y] = new RgbLed(bt, mHardwareSurface, MSG_NOTE_ON, BT_SCENE0 + y); + mSceneLeds[y] = new RgbLed(bt, mHardwareSurface, MSG_NOTE_ON, BT_SCENE0 + y, mMidiOut); } } @@ -1138,9 +1150,9 @@ private void createTopControls() mPanButton.pressedAction().setActionMatcher(mMidiIn.createNoteOnActionMatcher(0, BT_PAN)); mPanButton.releasedAction().setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_PAN)); mPanLed = mHardwareSurface.createOnOffHardwareLight("PanLed"); - mPanLed.setOnColor(Color.fromRGB255(255,165,0)); + mPanLed.setOnColor(Color.fromRGB255(255, 165, 0)); mPanButton.setBackgroundLight(mPanLed); - mPanLed.onUpdateHardware(() -> sendLedUpdate(BT_PAN, mPanLed)); + mPanLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_PAN, isOn)); mSendsButton = mHardwareSurface.createHardwareButton("Sends"); mSendsButton.setLabel("SENDS"); @@ -1155,9 +1167,9 @@ private void createTopControls() mSendSelectLayer.deactivate(); }); mSendsLed = mHardwareSurface.createOnOffHardwareLight("SendsLed"); - mSendsLed.setOnColor(Color.fromRGB255(255,165,0)); + mSendsLed.setOnColor(Color.fromRGB255(255, 165, 0)); mSendsButton.setBackgroundLight(mSendsLed); - mSendsLed.onUpdateHardware(() -> sendLedUpdate(BT_SENDS, mSendsLed)); + mSendsLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_SENDS, isOn)); mUserButton = mHardwareSurface.createHardwareButton("User"); mUserButton.setLabel("USER"); @@ -1172,9 +1184,9 @@ private void createTopControls() mProjectSelectLayer.deactivate(); }); mUserLed = mHardwareSurface.createOnOffHardwareLight("UserLed"); - mUserLed.setOnColor(Color.fromRGB255(255,165,0)); + mUserLed.setOnColor(Color.fromRGB255(255, 165, 0)); mUserButton.setBackgroundLight(mUserLed); - mUserLed.onUpdateHardware(() -> sendLedUpdate(BT_USER, mUserLed)); + mUserLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_USER, isOn)); } private void createDeviceControls() @@ -1207,9 +1219,9 @@ private void createDeviceControls() mPrevDeviceButton.releasedAction() .setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_PREV_DEVICE)); mPrevDeviceLed = mHardwareSurface.createOnOffHardwareLight("PrevDeviceLed"); - mPrevDeviceLed.setOnColor(Color.fromRGB255(255,165,0)); + mPrevDeviceLed.setOnColor(Color.fromRGB255(255, 165, 0)); mPrevDeviceButton.setBackgroundLight(mPrevDeviceLed); - mPrevDeviceLed.onUpdateHardware(() -> sendLedUpdate(BT_PREV_DEVICE, mPrevDeviceLed)); + mPrevDeviceLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_PREV_DEVICE, isOn)); mNextDeviceButton = mHardwareSurface.createHardwareButton("NextDevice"); mNextDeviceButton.setLabel("DEVICE→"); @@ -1219,9 +1231,9 @@ private void createDeviceControls() mNextDeviceButton.releasedAction() .setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_NEXT_DEVICE)); mNextDeviceLed = mHardwareSurface.createOnOffHardwareLight("NextDeviceLed"); - mNextDeviceLed.setOnColor(Color.fromRGB255(255,165,0)); + mNextDeviceLed.setOnColor(Color.fromRGB255(255, 165, 0)); mNextDeviceButton.setBackgroundLight(mNextDeviceLed); - mNextDeviceLed.onUpdateHardware(() -> sendLedUpdate(BT_NEXT_DEVICE, mNextDeviceLed)); + mNextDeviceLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_NEXT_DEVICE, isOn)); mPrevBankButton = mHardwareSurface.createHardwareButton("PrevBank"); mPrevBankButton.setLabel("←BANK"); @@ -1229,9 +1241,9 @@ private void createDeviceControls() mPrevBankButton.pressedAction().setActionMatcher(mMidiIn.createNoteOnActionMatcher(0, BT_PREV_BANK)); mPrevBankButton.releasedAction().setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_PREV_BANK)); mPrevBankLed = mHardwareSurface.createOnOffHardwareLight("PrevBankLed"); - mPrevBankLed.setOnColor(Color.fromRGB255(255,165,0)); + mPrevBankLed.setOnColor(Color.fromRGB255(255, 165, 0)); mPrevBankButton.setBackgroundLight(mPrevBankLed); - mPrevBankLed.onUpdateHardware(() -> sendLedUpdate(BT_PREV_BANK, mPrevBankLed)); + mPrevBankLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_PREV_BANK, isOn)); mNextBankButton = mHardwareSurface.createHardwareButton("NextBank"); mNextBankButton.setLabel("BANK→"); @@ -1239,9 +1251,9 @@ private void createDeviceControls() mNextBankButton.pressedAction().setActionMatcher(mMidiIn.createNoteOnActionMatcher(0, BT_NEXT_BANK)); mNextBankButton.releasedAction().setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_NEXT_BANK)); mNextBankLed = mHardwareSurface.createOnOffHardwareLight("NextBankLed"); - mNextBankLed.setOnColor(Color.fromRGB255(255,165,0)); + mNextBankLed.setOnColor(Color.fromRGB255(255, 165, 0)); mNextBankButton.setBackgroundLight(mNextBankLed); - mNextBankLed.onUpdateHardware(() -> sendLedUpdate(BT_NEXT_BANK, mNextBankLed)); + mNextBankLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_NEXT_BANK, isOn)); mDeviceOnOffButton = mHardwareSurface.createHardwareButton("DeviceOnOff"); mDeviceOnOffButton.setLabel("DEV ON/OFF"); @@ -1251,9 +1263,9 @@ private void createDeviceControls() mDeviceOnOffButton.releasedAction() .setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_DEVICE_ONOFF)); mDeviceOnOffLed = mHardwareSurface.createOnOffHardwareLight("DeviceOnOffLed"); - mDeviceOnOffLed.setOnColor(Color.fromRGB255(255,165,0)); + mDeviceOnOffLed.setOnColor(Color.fromRGB255(255, 165, 0)); mDeviceOnOffButton.setBackgroundLight(mDeviceOnOffLed); - mDeviceOnOffLed.onUpdateHardware(() -> sendLedUpdate(BT_DEVICE_ONOFF, mDeviceOnOffLed)); + mDeviceOnOffLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_DEVICE_ONOFF, isOn)); mDeviceLockButton = mHardwareSurface.createHardwareButton("DeviceLock"); mDeviceLockButton.setLabel("DEV LOCK"); @@ -1263,9 +1275,9 @@ private void createDeviceControls() mDeviceLockButton.releasedAction() .setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_DEVICE_LOCK)); mDeviceLockLed = mHardwareSurface.createOnOffHardwareLight("DeviceLockLed"); - mDeviceLockLed.setOnColor(Color.fromRGB255(255,165,0)); + mDeviceLockLed.setOnColor(Color.fromRGB255(255, 165, 0)); mDeviceLockButton.setBackgroundLight(mDeviceLockLed); - mDeviceLockLed.onUpdateHardware(() -> sendLedUpdate(BT_DEVICE_LOCK, mDeviceLockLed)); + mDeviceLockLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_DEVICE_LOCK, isOn)); mClipDeviceViewButton = mHardwareSurface.createHardwareButton("ClipDeviceView"); mClipDeviceViewButton.setLabel("CLIP/DEV VIEW"); @@ -1275,9 +1287,9 @@ private void createDeviceControls() mClipDeviceViewButton.releasedAction() .setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_CLIP_DEVICE_VIEW)); mClipDeviceViewLed = mHardwareSurface.createOnOffHardwareLight("ClipDeviceViewLed"); - mClipDeviceViewLed.setOnColor(Color.fromRGB255(255,165,0)); + mClipDeviceViewLed.setOnColor(Color.fromRGB255(255, 165, 0)); mClipDeviceViewButton.setBackgroundLight(mClipDeviceViewLed); - mClipDeviceViewLed.onUpdateHardware(() -> sendLedUpdate(BT_CLIP_DEVICE_VIEW, mClipDeviceViewLed)); + mClipDeviceViewLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_CLIP_DEVICE_VIEW, isOn)); mDetailViewButton = mHardwareSurface.createHardwareButton("DetailView"); mDetailViewButton.setLabel("DETAIL VIEW"); @@ -1287,9 +1299,9 @@ private void createDeviceControls() mDetailViewButton.releasedAction() .setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_DETAIL_VIEW)); mDetailViewLed = mHardwareSurface.createOnOffHardwareLight("DetailViewLed"); - mDetailViewLed.setOnColor(Color.fromRGB255(255,165,0)); + mDetailViewLed.setOnColor(Color.fromRGB255(255, 165, 0)); mDetailViewButton.setBackgroundLight(mDetailViewLed); - mDetailViewLed.onUpdateHardware(() -> sendLedUpdate(BT_DETAIL_VIEW, mDetailViewLed)); + mDetailViewLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_DETAIL_VIEW, isOn)); mShiftButton = mHardwareSurface.createHardwareButton("Shift"); mShiftButton.setLabel("SHIFT"); @@ -1298,7 +1310,7 @@ private void createDeviceControls() mShiftButton.releasedAction().setActionMatcher(mMidiIn.createNoteOffActionMatcher(0, BT_SHIFT)); mShiftButton.isPressed().markInterested(); - HardwareButton bankButton = mHardwareSurface.createHardwareButton("Bank"); + final HardwareButton bankButton = mHardwareSurface.createHardwareButton("Bank"); bankButton.setLabel("BANK"); bankButton.setLabelPosition(RelativePosition.BELOW); bankButton.pressedAction().setActionMatcher(mMidiIn.createNoteOnActionMatcher(0, BT_BANK)); @@ -1317,9 +1329,9 @@ private void createDeviceControls() } }); mBankLed = mHardwareSurface.createOnOffHardwareLight("BankLed"); - mBankLed.setOnColor(Color.fromRGB255(255,165,0)); + mBankLed.setOnColor(Color.fromRGB255(255, 165, 0)); bankButton.setBackgroundLight(mBankLed); - mBankLed.onUpdateHardware(() -> sendLedUpdate(BT_BANK, mBankLed)); + mBankLed.isOn().onUpdateHardware(isOn -> sendLedUpdate(BT_BANK, isOn)); mLauncherUpButton = mHardwareSurface.createHardwareButton("LauncherUp"); mLauncherUpButton.setLabel("↑"); @@ -1361,12 +1373,12 @@ private void updateTopControlRing(final int knobIndex) knobLed.setDisplayedValue(value); final int ring = switch (mTopMode) - { - case PAN -> KnobLed.RING_PAN; - case SENDS -> KnobLed.RING_VOLUME; - case TRACK_CONTROLS, PROJECT_CONTROLS -> KnobLed.RING_SINGLE; - default -> throw new IllegalStateException(); - }; + { + case PAN -> KnobLed.RING_PAN; + case SENDS -> KnobLed.RING_VOLUME; + case TRACK_CONTROLS, PROJECT_CONTROLS -> KnobLed.RING_SINGLE; + default -> throw new IllegalStateException(); + }; knobLed.setRing(knob.hasTargetValue().get() ? ring : KnobLed.RING_OFF); if (knobLed.wantsFlush()) @@ -1388,14 +1400,20 @@ private void updateDeviceControlRing(final int knobIndex) getHost().requestFlush(); } - private void sendLedUpdate(final int note, final OnOffHardwareLight led) + private void sendLedUpdate(final int note, final boolean isOn) { - sendLedUpdate(note, 0, led); + sendLedUpdate(note, 0, isOn); } - private void sendLedUpdate(final int note, final int channel, final OnOffHardwareLight led) + private void sendLedUpdate(final int note, final int channel, final boolean isOn) { - mMidiOut.sendMidi((MSG_NOTE_ON << 4) | channel, note, led.isOn().currentValue() ? 1 : 0); + mMidiOut.sendMidi((MSG_NOTE_ON << 4) | channel, note, isOn ? 1 : 0); + } + + private void sendLedUpdate(final int note, final int channel, final CrossFadeMode lightState) + { + mMidiOut.sendMidi((MSG_NOTE_ON << 4) | channel, note, + lightState != null ? lightState.getColorIndex() : CrossFadeMode.AB.getColorIndex()); } private void sendLedUpdate(final int note, final int channel, final int color) @@ -1428,25 +1446,6 @@ private void onSysexIn(final String sysex) mMidiOut.sendSysex("F0 47 7F 29 60 00 04 41 02 01 00 F7"); } - private String intToCrossFade(final int index) - { - return switch (index) - { - case 1 -> "A"; - case 2 -> "B"; - default -> "AB"; - }; - } - - private int crossFadeToInt(final String s) - { - if (s.equals("A")) - return 1; - if (s.equals("B")) - return 2; - return 0; - } - @Override public void exit() { @@ -1456,98 +1455,86 @@ public void exit() public void flush() { flushKnobs(); - paintPads(); - paintScenes(); mHardwareSurface.updateHardware(); } - private void paintScenes() + private RGBLedState computeRGBLedStateForSlot(final ClipLauncherSlot slot) { - for (int i = 0; i < 5; ++i) - { - final RgbLed rgbLed = mSceneLeds[i]; - if (mSendsOn.isOn()) - { - final boolean isSelected = mSendIndex == i; - rgbLed.setColor(isSelected ? RGBLedState.COLOR_SELECTED : RGBLedState.COLOR_SELECTABLE); - rgbLed.setBlinkType(RGBLedState.BLINK_NONE); - rgbLed.setBlinkColor(RGBLedState.COLOR_NONE); - } - else if (mUserOn.isOn()) - { - final boolean exists = i < mProjectRemoteControls.pageCount().get(); - final boolean isSelected = exists && mProjectRemoteControls.selectedPageIndex().get() == i; - rgbLed.setColor(isSelected ? RGBLedState.COLOR_SELECTED : (exists ? RGBLedState.COLOR_SELECTABLE : RGBLedState.COLOR_NONE)); - rgbLed.setBlinkType(RGBLedState.BLINK_NONE); - rgbLed.setBlinkColor(RGBLedState.COLOR_NONE); - } - else - { - final Scene scene = mSceneBank.getScene(i); - if (scene.exists().get()) - rgbLed.setColor(scene.color()); - else - rgbLed.setColor(RGBLedState.COLOR_NONE); - rgbLed.setBlinkType(RGBLedState.BLINK_NONE); - rgbLed.setBlinkColor(RGBLedState.COLOR_NONE); - } + int colorValue = RGBLedState.COLOR_NONE, blinkColorValue = RGBLedState.COLOR_NONE, + blinkType = RGBLedState.BLINK_NONE; + + if (slot.exists().get() && slot.hasContent().get()) + colorValue = RGBLedState.getClosestColorIndex(slot.color().get()); + + /* + * if (slot.isStopQueued().get()) { rgbLed.setBlinkType(RgbLed.BLINK_STOP_QUEUED); + * rgbLed.setBlinkColor(RgbLed.COLOR_STOPPING); } else + */ - rgbLed.paint(mMidiOut); + if (slot.isRecordingQueued().get()) + { + blinkType = RGBLedState.BLINK_RECORD_QUEUED; + blinkColorValue = RGBLedState.COLOR_RECORDING; } + else if (slot.isPlaybackQueued().get()) + { + blinkType = RGBLedState.BLINK_PLAY_QUEUED; + blinkColorValue = RGBLedState.COLOR_PLAYING_QUEUED; + } + else if (slot.isRecording().get()) + { + colorValue = RGBLedState.COLOR_NONE; + blinkType = RGBLedState.BLINK_ACTIVE; + blinkColorValue = RGBLedState.COLOR_RECORDING; + } + else if (slot.isPlaying().get()) + { + colorValue = RGBLedState.COLOR_NONE; + blinkType = RGBLedState.BLINK_ACTIVE; + blinkColorValue = RGBLedState.COLOR_PLAYING; + } + else /* stopped */ + { + blinkType = RGBLedState.BLINK_NONE; + blinkColorValue = RGBLedState.COLOR_NONE; + } + + return new RGBLedState(colorValue, blinkColorValue, blinkType); } - private void paintPads() + private RGBLedState computeRGBLedStateForScene(final int sceneButtonIndex) { - for (int i = 0; i < 8; ++i) - { - final Track track = mTrackBank.getItemAt(i); - final ClipLauncherSlotBank clipLauncherSlotBank = track.clipLauncherSlotBank(); - for (int j = 0; j < 5; ++j) - { - final ClipLauncherSlot slot = clipLauncherSlotBank.getItemAt(j); - final RgbLed rgbLed = mGridLeds[i][j]; + int colorValue = RGBLedState.COLOR_NONE, blinkColorValue = RGBLedState.COLOR_NONE, + blinkType = RGBLedState.BLINK_NONE; - if (slot.exists().get() && slot.hasContent().get()) - rgbLed.setColor(slot.color().red(), slot.color().green(), slot.color().blue()); - else - rgbLed.setColor(RGBLedState.COLOR_NONE); - - /* - * if (slot.isStopQueued().get()) { rgbLed.setBlinkType(RgbLed.BLINK_STOP_QUEUED); - * rgbLed.setBlinkColor(RgbLed.COLOR_STOPPING); } else - */ - - if (slot.isRecordingQueued().get()) - { - rgbLed.setBlinkType(RGBLedState.BLINK_RECORD_QUEUED); - rgbLed.setBlinkColor(RGBLedState.COLOR_RECORDING); - } - else if (slot.isPlaybackQueued().get()) - { - rgbLed.setBlinkType(RGBLedState.BLINK_PLAY_QUEUED); - rgbLed.setBlinkColor(RGBLedState.COLOR_PLAYING_QUEUED); - } - else if (slot.isRecording().get()) - { - rgbLed.setColor(RGBLedState.COLOR_NONE); - rgbLed.setBlinkType(RGBLedState.BLINK_ACTIVE); - rgbLed.setBlinkColor(RGBLedState.COLOR_RECORDING); - } - else if (slot.isPlaying().get()) - { - rgbLed.setColor(RGBLedState.COLOR_NONE); - rgbLed.setBlinkType(RGBLedState.BLINK_ACTIVE); - rgbLed.setBlinkColor(RGBLedState.COLOR_PLAYING); - } - else /* stopped */ - { - rgbLed.setBlinkType(RGBLedState.BLINK_NONE); - rgbLed.setBlinkColor(RGBLedState.COLOR_NONE); - } - - rgbLed.paint(mMidiOut); - } + if (mSendsOn.isOn()) + { + final boolean isSelected = mSendIndex == sceneButtonIndex; + colorValue = isSelected ? RGBLedState.COLOR_SELECTED : RGBLedState.COLOR_SELECTABLE; + blinkType = RGBLedState.BLINK_NONE; + blinkColorValue = RGBLedState.COLOR_NONE; + } + else if (mUserOn.isOn()) + { + final boolean exists = sceneButtonIndex < mProjectRemoteControls.pageCount().get(); + final boolean isSelected = exists && mProjectRemoteControls.selectedPageIndex().get() == sceneButtonIndex; + colorValue = isSelected ? RGBLedState.COLOR_SELECTED + : (exists ? RGBLedState.COLOR_SELECTABLE : RGBLedState.COLOR_NONE); + blinkType = RGBLedState.BLINK_NONE; + blinkColorValue = RGBLedState.COLOR_NONE; } + else + { + final Scene scene = mSceneBank.getScene(sceneButtonIndex); + if (scene.exists().get()) + colorValue = RGBLedState.getClosestColorIndex(scene.color().get()); + else + colorValue = (RGBLedState.COLOR_NONE); + blinkType = RGBLedState.BLINK_NONE; + blinkColorValue = RGBLedState.COLOR_NONE; + } + + return new RGBLedState(colorValue, blinkColorValue, blinkType); } private void flushKnobs() @@ -1562,17 +1549,17 @@ private void flushKnobs() private int computeLaunchQuantizationIndex() { return switch (mTransport.defaultLaunchQuantization().get()) - { - case "none" -> 0; - case "8" -> 1; - case "4" -> 2; - case "2" -> 3; - case "1" -> 4; - case "1/4" -> 5; - case "1/8" -> 6; - case "1/16" -> 7; - default -> -1; - }; + { + case "none" -> 0; + case "8" -> 1; + case "4" -> 2; + case "2" -> 3; + case "1" -> 4; + case "1/4" -> 5; + case "1/8" -> 6; + case "1/16" -> 7; + default -> -1; + }; } /** @@ -1641,7 +1628,9 @@ void clear() private CursorRemoteControlsPage mRemoteControls = null; private CursorRemoteControlsPage mChannelStripRemoteControls; + private CursorRemoteControlsPage mTrackRemoteControls; + private CursorRemoteControlsPage mProjectRemoteControls; private MidiIn mMidiIn = null; @@ -1687,6 +1676,7 @@ void clear() private Layer[] mSendLayers; private Layer mTrackRemoteControlsLayer; + private Layer mProjectRemoteControlsLayer; private Layer mShiftLayer; @@ -1813,7 +1803,7 @@ void clear() private OnOffHardwareLight[] mMuteLeds; - private HardwareTextDisplay[] mABLeds; + private MultiStateHardwareLight[] mABLeds; private OnOffHardwareLight[] mTrackSelectLeds; diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/CrossFadeMode.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/CrossFadeMode.java new file mode 100644 index 00000000..422aae91 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/CrossFadeMode.java @@ -0,0 +1,117 @@ +package com.bitwig.extensions.controllers.akai.apc40_mkii; + +import com.bitwig.extension.api.Color; +import com.bitwig.extension.controller.api.HardwareLightVisualState; +import com.bitwig.extension.controller.api.InternalHardwareLightState; +import com.bitwig.extension.controller.api.Track; + +class CrossFadeMode extends InternalHardwareLightState +{ + public static final CrossFadeMode A = new CrossFadeMode("A", 0, 1); + + public static final CrossFadeMode B = new CrossFadeMode("B", 1, 2); + + public static final CrossFadeMode AB = new CrossFadeMode("AB", 2, 0); + + public static CrossFadeMode getBestModeForColor(final Color color) + { + if (color == null || color.getAlpha() == 0 + || color.getRed() == 0 && color.getGreen() == 0 && color.getBlue() == 0) + return AB; + + if (B_COLOR.equals(color)) + return B; + + return A; + } + + public static CrossFadeMode forEnumName(final String name) + { + if (name.equals(A.mEnumName)) + return A; + if (name.equals(B.mEnumName)) + return B; + return AB; + } + + public static CrossFadeMode forTrack(final Track track) + { + return forEnumName(track.crossFadeMode().get()); + } + + private static CrossFadeMode forIndex(final int index) + { + final CrossFadeMode mode = switch (index) + { + case 0 -> A; + case 1 -> B; + default -> AB; + }; + + assert mode.getIndex() == index; + + return mode; + } + + private CrossFadeMode(final String enumName, final int index, final int colorIndex) + { + mEnumName = enumName; + mIndex = index; + mColorIndex = colorIndex; + } + + public int getIndex() + { + return mIndex; + } + + /** The color value we need to send to the hardware */ + public int getColorIndex() + { + return mColorIndex; + } + + public CrossFadeMode getNext() + { + final int index = (mColorIndex + 1) % 3; + + return forIndex(index); + } + + public String getEnumName() + { + return mEnumName; + } + + @Override + public HardwareLightVisualState getVisualState() + { + if (this == AB) + return null; + + if (this == A) + return A_VISUAL_STATE; + + return B_VISUAL_STATE; + } + + @Override + public boolean equals(final Object obj) + { + return this == obj; + } + + private final String mEnumName; + + private final int mColorIndex, mIndex; + + private static final Color A_COLOR = Color.fromRGB(1, 0.64, 0); + + private static final Color B_COLOR = Color.fromRGB(0, 0, 1); + + private static final HardwareLightVisualState A_VISUAL_STATE = HardwareLightVisualState + .createForColor(A_COLOR); + + private static final HardwareLightVisualState B_VISUAL_STATE = HardwareLightVisualState + .createForColor(B_COLOR); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/Led.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/Led.java deleted file mode 100644 index af17b6a8..00000000 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/Led.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.bitwig.extensions.controllers.akai.apc40_mkii; - -import com.bitwig.extension.controller.api.MidiOut; - -public class Led -{ - public void paint(final MidiOut midiOut, final int msg, final int channel, final int data1) - { - if (mValue == mDisplayedValue) - return; - - midiOut.sendMidi((msg << 4) | channel, data1, mValue); - mDisplayedValue = mValue; - } - - public void set(final int value) - { - mValue = value; - } - - private int mValue = 0; - - private int mDisplayedValue = -1; -} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/RGBLedState.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/RGBLedState.java index cb992648..d0720ee5 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/RGBLedState.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/RGBLedState.java @@ -1,5 +1,6 @@ package com.bitwig.extensions.controllers.akai.apc40_mkii; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -9,15 +10,18 @@ class RGBLedState extends InternalHardwareLightState { + /** Array of colors that the protocol specifies. */ + private static final Color[] COLORS = new Color[128]; + public static final int COLOR_NONE = 0; - public static final int COLOR_RED = 2; + public static final int COLOR_WHITE = 3; - public static final int COLOR_GREEN = 18; + public static final int COLOR_RED = 5; - public static final int COLOR_BLUE = 42; + public static final int COLOR_GREEN = 21; - public static final int COLOR_YELLOW = 10; + public static final int COLOR_YELLOW = 13; public static final int COLOR_RECORDING = COLOR_RED; @@ -27,8 +31,6 @@ class RGBLedState extends InternalHardwareLightState public static final int COLOR_STOPPING = COLOR_NONE; - public static final int COLOR_SCENE = COLOR_YELLOW; - public static final int COLOR_SELECTED = COLOR_YELLOW; public static final int COLOR_SELECTABLE = 1; @@ -43,76 +45,243 @@ class RGBLedState extends InternalHardwareLightState public static final int BLINK_STOP_QUEUED = 13; - private static final Map RGB_TO_COLOR_VALUE_MAP = new HashMap<>(); - - private static final Map COLOR_VALUE_TO_COLOR_MAP = new HashMap<>(); + public static final RGBLedState OFF_STATE = new RGBLedState(COLOR_NONE, COLOR_NONE, BLINK_NONE); + /** + * Registers a color as defined in the APC 40 mkii MIDI protocol. The color value is the velocity to use + * for the provided RGB integer color. + */ private static void registerColor(final int rgb, final int value) { - COLOR_VALUE_TO_COLOR_MAP.put(value, - Color.fromRGB255((rgb & 0xFF0000) >> 16, (rgb & 0xFF00) >> 8, rgb & 0xFF)); + assert value >= 0 && value <= 127; + assert COLORS[value] == null; - RGB_TO_COLOR_VALUE_MAP.put(rgb, value); + COLORS[value] = createColorForRGBInt(rgb); } - static + private static Color createColorForRGBInt(final int rgb) + { + final int red = (rgb & 0xFF0000) >> 16; + final int green = (rgb & 0xFF00) >> 8; + final int blue = rgb & 0xFF; + + return Color.fromRGB255(red, green, blue); + } + + private static double[] rgbToHsv(final Color color) + { + final double[] hsv = new double[3]; + + final double r = color.getRed(); + final double g = color.getGreen(); + final double b = color.getBlue(); + + final double max = Math.max(r, Math.max(g, b)); + final double min = Math.min(r, Math.min(g, b)); + final double delta = max - min; + + // Calculate hue + if (delta == 0) + { + hsv[0] = 0; + } + else if (max == r) + { + hsv[0] = (60 * ((g - b) / delta) + 360) % 360; + } + else if (max == g) + { + hsv[0] = (60 * ((b - r) / delta) + 120) % 360; + } + else if (max == b) + { + hsv[0] = (60 * ((r - g) / delta) + 240) % 360; + } + + // Calculate saturation + hsv[1] = (max == 0) ? 0 : (delta / max); + + // Calculate value + hsv[2] = max; + + return hsv; + } + + private static double colorDistance(final Color color1, final Color color2) + { + return 0.5 * colorDistanceRGB(color1, color2) + 0.5 * colorDistanceHSV(color1, color2); + } + + private static double colorDistanceRGB(final Color color1, final Color color2) + { + final double r1 = color1.getRed(); + final double g1 = color1.getGreen(); + final double b1 = color1.getBlue(); + + final double r2 = color2.getRed(); + final double g2 = color2.getGreen(); + final double b2 = color2.getBlue(); + + final double dr = r2 - r1; + final double dg = g2 - g1; + final double db = b2 - b1; + + return Math.sqrt(dr * dr + dg * dg + db * db); + } + + private static double colorDistanceHSV(final Color color1, final Color color2) + { + final double[] hsv1 = rgbToHsv(color1); + final double[] hsv2 = rgbToHsv(color2); + + final double dh = Math.min(Math.abs(hsv1[0] - hsv2[0]), 1 - Math.abs(hsv1[0] - hsv2[0])); + final double ds = Math.abs(hsv1[1] - hsv2[1]); + final double dv = Math.abs(hsv1[2] - hsv2[2]); + + return Math.sqrt(dh * dh + ds * ds + dv * dv); + } + + private static record ColorToIndexCacheEntry(int rgb, int index) + { + } + + private static final int colorToRGBInt(final Color color) + { + return color.getRed255() << 16 | color.getGreen255() << 8 | color.getBlue255(); + } + + private static final Map HANDPICKED_RGBINT_TO_CLOSEST_COLOR_INDEX = new HashMap<>(); + + private static void registerHandpickedClosestColor(final int rgb, final int colorIndex) + { + HANDPICKED_RGBINT_TO_CLOSEST_COLOR_INDEX.put(rgb, colorIndex); + } + + static { - registerColor(0xFF0000, COLOR_RED); - registerColor(0xFF00, COLOR_GREEN); - registerColor(0xFF, COLOR_BLUE); - registerColor(0xFFD90F, COLOR_YELLOW); + registerHandpickedClosestColor(0xFF0000, COLOR_RED); + registerHandpickedClosestColor(0xFF00, COLOR_GREEN); + registerHandpickedClosestColor(0xFF, 45); + registerHandpickedClosestColor(0xFFD90F, COLOR_YELLOW); - registerColor(0, 0); + registerHandpickedClosestColor(0, 0); - registerColor(14235761, 57); - registerColor(14771857, 107); - registerColor(5526612, 1); + registerHandpickedClosestColor(14235761, 57); + registerHandpickedClosestColor(14771857, 107); + registerHandpickedClosestColor(5526612, 1); - registerColor(14233124, 6); - registerColor(15491415, 5); - registerColor(8026746, 2); + registerHandpickedClosestColor(14233124, 6); + registerHandpickedClosestColor(15491415, 5); + registerHandpickedClosestColor(8026746, 2); - registerColor(16733958, 9); - registerColor(16745278, 12); - registerColor(13224393, 3); + registerHandpickedClosestColor(16733958, 9); + registerHandpickedClosestColor(16745278, 12); + registerHandpickedClosestColor(13224393, 3); - registerColor(14261520, 14); - registerColor(14989134, 13); - registerColor(8817068, 104); + registerHandpickedClosestColor(14261520, 14); + registerHandpickedClosestColor(14989134, 13); + registerHandpickedClosestColor(8817068, 104); - registerColor(7575572, 18); - registerColor(10534988, 17); - registerColor(10713411, 125); + registerHandpickedClosestColor(7575572, 18); + registerHandpickedClosestColor(10534988, 17); + registerHandpickedClosestColor(10713411, 125); - registerColor(40263, 22); - registerColor(4111202, 21); - registerColor(13016944, 124); + registerHandpickedClosestColor(40263, 22); + registerHandpickedClosestColor(4111202, 21); + registerHandpickedClosestColor(13016944, 124); - registerColor(42644, 34); - registerColor(4444857, 33); - registerColor(5726662, 43); + registerHandpickedClosestColor(42644, 34); + registerHandpickedClosestColor(4444857, 33); + registerHandpickedClosestColor(5726662, 43); - registerColor(39385, 38); - registerColor(4507903, 37); - registerColor(8686304, 115); + registerHandpickedClosestColor(39385, 38); + registerHandpickedClosestColor(4507903, 37); + registerHandpickedClosestColor(8686304, 115); - registerColor(9783755, 50); - registerColor(12351216, 49); + registerHandpickedClosestColor(9783755, 50); + registerHandpickedClosestColor(12351216, 49); } - public static int getColorValueForRGB(final int rgb) + private static final ArrayList RGB_TO_COMPUTED_CLOSEST_COLOR_INDEX_CACHE = new ArrayList<>(); + + public static int getClosestColorIndex(final Color color) { - final Integer c = RGB_TO_COLOR_VALUE_MAP.get(rgb); + if (color == null || color.getAlpha() == 0) + return 0; + + final int rgb = colorToRGBInt(color); + + final Integer handPickedColorIndex = HANDPICKED_RGBINT_TO_CLOSEST_COLOR_INDEX.get(rgb); + + if (handPickedColorIndex != null) + return handPickedColorIndex; - if (c != null) - return c; + final int MAX_CACHE_SIZE = 64; - return 13; + synchronized (RGB_TO_COMPUTED_CLOSEST_COLOR_INDEX_CACHE) + { + final int cacheSize = RGB_TO_COMPUTED_CLOSEST_COLOR_INDEX_CACHE.size(); + + for (int i = 0; i < cacheSize; i++) + { + final var cacheEntry = RGB_TO_COMPUTED_CLOSEST_COLOR_INDEX_CACHE.get(i); + + if (cacheEntry.rgb == rgb) + return cacheEntry.index; + } + + final int colorIndex = computeClosestColorIndex(color); + + if (cacheSize == MAX_CACHE_SIZE) + RGB_TO_COMPUTED_CLOSEST_COLOR_INDEX_CACHE.remove(MAX_CACHE_SIZE - 1); + + RGB_TO_COMPUTED_CLOSEST_COLOR_INDEX_CACHE.add(0, new ColorToIndexCacheEntry(rgb, colorIndex)); + + return colorIndex; + } + } + + private static int computeClosestColorIndex(final Color color) + { + if (color == null || color.getAlpha() == 0) + return 0; + + int closestIndex = 0; + double closestDistance = Double.MAX_VALUE; + + for (int i = 0; i < COLORS.length; i++) + { + final Color currentColor = COLORS[i]; + final double distance = colorDistance(color, currentColor); + + if (distance == 0) + return i; + + if (distance < closestDistance) + { + closestIndex = i; + closestDistance = distance; + } + } + + return closestIndex; } public static Color getColorForColorValue(final int colorValue) { - return COLOR_VALUE_TO_COLOR_MAP.get(colorValue); + assert colorValue >= 0 && colorValue < COLORS.length; + + if (colorValue < 0 || colorValue >= COLORS.length) + return COLORS[0]; + + return COLORS[colorValue]; + } + + public static RGBLedState getBestStateForColor(final Color color) + { + final int colorIndex = getClosestColorIndex(color); + + return new RGBLedState(colorIndex, COLOR_NONE, BLINK_NONE); } public RGBLedState(final int color, final int blinkColor, final int blinkType) @@ -186,4 +355,135 @@ public HardwareLightVisualState getVisualState() private final int mColor, mBlinkColor, mBlinkType; + static + { + registerColor(0x000000, 0); + registerColor(0x1E1E1E, 1); + registerColor(0x7F7F7F, 2); + registerColor(0xFFFFFF, 3); + registerColor(0xFF4C4C, 4); + registerColor(0xFF0000, 5); + registerColor(0x590000, 6); + registerColor(0x190000, 7); + registerColor(0xFFBD6C, 8); + registerColor(0xFF5400, 9); + registerColor(0x591D00, 10); + registerColor(0x271B00, 11); + registerColor(0xFFFF4C, 12); + registerColor(0xFFFF00, 13); + registerColor(0x595900, 14); + registerColor(0x191900, 15); + registerColor(0x88FF4C, 16); + registerColor(0x54FF00, 17); + registerColor(0x1D5900, 18); + registerColor(0x142B00, 19); + registerColor(0x4CFF4C, 20); + registerColor(0x00FF00, 21); + registerColor(0x005900, 22); + registerColor(0x001900, 23); + registerColor(0x4CFF5E, 24); + registerColor(0x00FF19, 25); + registerColor(0x00590D, 26); + registerColor(0x001902, 27); + registerColor(0x4CFF88, 28); + registerColor(0x00FF55, 29); + registerColor(0x00591D, 30); + registerColor(0x001F12, 31); + registerColor(0x4CFFB7, 32); + registerColor(0x00FF99, 33); + registerColor(0x005935, 34); + registerColor(0x001912, 35); + registerColor(0x4CC3FF, 36); + registerColor(0x00A9FF, 37); + registerColor(0x004152, 38); + registerColor(0x001019, 39); + registerColor(0x4C88FF, 40); + registerColor(0x0055FF, 41); + registerColor(0x001D59, 42); + registerColor(0x000819, 43); + registerColor(0x4C4CFF, 44); + registerColor(0x0000FF, 45); + registerColor(0x000059, 46); + registerColor(0x000019, 47); + registerColor(0x874CFF, 48); + registerColor(0x5400FF, 49); + registerColor(0x190064, 50); + registerColor(0x0F0030, 51); + registerColor(0xFF4CFF, 52); + registerColor(0xFF00FF, 53); + registerColor(0x590059, 54); + registerColor(0x190019, 55); + registerColor(0xFF4C87, 56); + registerColor(0xFF0054, 57); + registerColor(0x59001D, 58); + registerColor(0x220013, 59); + registerColor(0xFF1500, 60); + registerColor(0x993500, 61); + registerColor(0x795100, 62); + registerColor(0x436400, 63); + registerColor(0x033900, 64); + registerColor(0x005735, 65); + registerColor(0x00547F, 66); + registerColor(0x0000FF, 67); + registerColor(0x00454F, 68); + registerColor(0x2500CC, 69); + registerColor(0x7F7F7F, 70); + registerColor(0x202020, 71); + registerColor(0xFF0000, 72); + registerColor(0xBDFF2D, 73); + registerColor(0xAFED06, 74); + registerColor(0x64FF09, 75); + registerColor(0x108B00, 76); + registerColor(0x00FF87, 77); + registerColor(0x00A9FF, 78); + registerColor(0x002AFF, 79); + registerColor(0x3F00FF, 80); + registerColor(0x7A00FF, 81); + registerColor(0xB21A7D, 82); + registerColor(0x402100, 83); + registerColor(0xFF4A00, 84); + registerColor(0x88E106, 85); + registerColor(0x72FF15, 86); + registerColor(0x00FF00, 87); + registerColor(0x3BFF26, 88); + registerColor(0x59FF71, 89); + registerColor(0x38FFCC, 90); + registerColor(0x5B8AFF, 91); + registerColor(0x3151C6, 92); + registerColor(0x877FE9, 93); + registerColor(0xD31DFF, 94); + registerColor(0xFF005D, 95); + registerColor(0xFF7F00, 96); + registerColor(0xB9B000, 97); + registerColor(0x90FF00, 98); + registerColor(0x835D07, 99); + registerColor(0x392b00, 100); + registerColor(0x144C10, 101); + registerColor(0x0D5038, 102); + registerColor(0x15152A, 103); + registerColor(0x16205A, 104); + registerColor(0x693C1C, 105); + registerColor(0xA8000A, 106); + registerColor(0xDE513D, 107); + registerColor(0xD86A1C, 108); + registerColor(0xFFE126, 109); + registerColor(0x9EE12F, 110); + registerColor(0x67B50F, 111); + registerColor(0x1E1E30, 112); + registerColor(0xDCFF6B, 113); + registerColor(0x80FFBD, 114); + registerColor(0x9A99FF, 115); + registerColor(0x8E66FF, 116); + registerColor(0x404040, 117); + registerColor(0x757575, 118); + registerColor(0xE0FFFF, 119); + registerColor(0xA00000, 120); + registerColor(0x350000, 121); + registerColor(0x1AD000, 122); + registerColor(0x074200, 123); + registerColor(0xB9B000, 124); + registerColor(0x3F3100, 125); + registerColor(0xB35F00, 126); + registerColor(0x4B1502, 127); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/RgbLed.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/RgbLed.java index 71e779fb..0ec089cd 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/RgbLed.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc40_mkii/RgbLed.java @@ -1,6 +1,5 @@ package com.bitwig.extensions.controllers.akai.apc40_mkii; -import com.bitwig.extension.controller.api.ColorValue; import com.bitwig.extension.controller.api.HardwareButton; import com.bitwig.extension.controller.api.HardwareSurface; import com.bitwig.extension.controller.api.MidiOut; @@ -8,92 +7,51 @@ class RgbLed { - - protected RgbLed( final HardwareButton button, final HardwareSurface surface, final int message, - final int data1) + final int data1, + final MidiOut midiOut) { super(); mMessage = message; mData1 = data1; - MultiStateHardwareLight hardwareLight = surface.createMultiStateHardwareLight(button.getId() + "-light"); - hardwareLight.state().setValueSupplier(this::getState); - button.setBackgroundLight(hardwareLight); - } - - public void paint(final MidiOut midiOut) - { - if (mColor != mDisplayedColor || mBlinkColor != mDisplayedBlinkColor - || mBlinkType != mDisplayedBlinkType) - { - midiOut.sendMidi(mMessage << 4, mData1, mColor); - - if (mBlinkType != RGBLedState.BLINK_NONE) - { - midiOut.sendMidi(mMessage << 4, mData1, mBlinkColor); - midiOut.sendMidi((mMessage << 4) | mBlinkType, mData1, mColor); - } - else - { - midiOut.sendMidi(mMessage << 4, mData1, mColor); - } - - mDisplayedColor = mColor; - mDisplayedBlinkColor = mBlinkColor; - mDisplayedBlinkType = mBlinkType; - } - } - - public void setColor(final float red, final float green, final float blue) - { - final int r8 = (int)(red * 255); - final int g8 = (int)(green * 255); - final int b8 = (int)(blue * 255); - final int total = (r8 << 16) | (g8 << 8) | b8; - - mColor = RGBLedState.getColorValueForRGB(total); - } - - public void setColor(final int color) - { - mColor = color; + mLight = surface.createMultiStateHardwareLight(button.getId() + "-light"); + mLight.setColorToStateFunction(RGBLedState::getBestStateForColor); + mLight.state().onUpdateHardware(state -> sendLightState(midiOut, (RGBLedState)state)); + button.setBackgroundLight(mLight); } - public void setColor(final ColorValue color) + public MultiStateHardwareLight getLight() { - setColor(color.red(), color.green(), color.blue()); + return mLight; } - public void setBlinkType(final int blinkType) + private void sendLightState(final MidiOut midiOut, RGBLedState state) { - mBlinkType = blinkType; - } + if (state == null) + state = RGBLedState.OFF_STATE; + + final var color = state.getColor(); + final var blinkColor = state.getBlinkColor(); + final var blinkType = state.getBlinkType(); + + midiOut.sendMidi(mMessage << 4, mData1, color); - public void setBlinkColor(final int blinkColor) - { - mBlinkColor = blinkColor; + if (blinkType != RGBLedState.BLINK_NONE) + { + midiOut.sendMidi(mMessage << 4, mData1, blinkColor); + midiOut.sendMidi((mMessage << 4) | blinkType, mData1, color); + } + else + { + midiOut.sendMidi(mMessage << 4, mData1, color); + } } - public RGBLedState getState() - { - return new RGBLedState(mDisplayedColor, mDisplayedBlinkColor, mDisplayedBlinkType); - } + private final MultiStateHardwareLight mLight; private final int mMessage, mData1; - - private int mColor = RGBLedState.COLOR_NONE; - - private int mDisplayedColor = -1; - - private int mBlinkColor = RGBLedState.COLOR_NONE; - - private int mDisplayedBlinkColor = -1; - - private int mBlinkType = RGBLedState.BLINK_NONE; - - private int mDisplayedBlinkType = -1; } diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/Apc64CcAssignments.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/Apc64CcAssignments.java new file mode 100644 index 00000000..8faa8e04 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/Apc64CcAssignments.java @@ -0,0 +1,56 @@ +package com.bitwig.extensions.controllers.akai.apc64; + +public enum Apc64CcAssignments { + SCENE_BUTTON_BASE(0x70, true), // + GRID_BASE(0x0, true), + STRIP_TOUCH(0x52, true), + TRACKS_BASE(0x64, true), + TRACK_CONTROL_BASE(0x40, true), + NAV_LEFT(0x60), + NAV_RIGHT(0x61), + NAV_DOWN(0x5E), + NAV_UP(0x5F), + MODE_REC(0x6C), + MODE_MUTE(0x6D), + MODE_SOLO(0x6E), + MODE_STOP(0x6F), + STRIP_DEVICE(0x79), + STRIP_VOLUME(0x7A), + STRIP_PAN(0x7B), + STRIP_SENDS(0x7C), + STRIP_CHANNEL(0x7D), + STRIP_OFF(0x7E), + CLEAR(0x49), + DUPLICATE(0x4A), + FIXED(0x4C), + QUANTIZE(0x4B), + UNDO(0x4D), + TEMPO(0x48), + SHIFT(0x78), + PLAY(0x5B), + STOP(0x5D), + REC(0x5C); + + private int stateId; + private boolean isBaseStart; + + Apc64CcAssignments(final int stateId) { + this(stateId, false); + } + Apc64CcAssignments(final int stateId, boolean isBaseStart) { + this.isBaseStart = isBaseStart; + this.stateId = stateId; + } + + public int getStateId() { + return stateId; + } + + public boolean isBaseStart() { + return isBaseStart; + } + + public boolean isSingle() { + return !isBaseStart; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/Apc64Extension.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/Apc64Extension.java new file mode 100644 index 00000000..8cfd221c --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/Apc64Extension.java @@ -0,0 +1,254 @@ +package com.bitwig.extensions.controllers.akai.apc64; + +import com.bitwig.extension.controller.ControllerExtension; +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.HardwareSurface; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extension.controller.api.NoteInput; +import com.bitwig.extension.controller.api.Project; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.Transport; +import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; +import com.bitwig.extensions.controllers.akai.apc.common.led.VarSingleLedState; +import com.bitwig.extensions.controllers.akai.apc64.control.SingleLedButton; +import com.bitwig.extensions.controllers.akai.apc64.layer.OverviewLayer; +import com.bitwig.extensions.controllers.akai.apc64.layer.PadLayer; +import com.bitwig.extensions.controllers.akai.apc64.layer.SessionLayer; +import com.bitwig.extensions.controllers.akai.apc64.layer.TrackAndSceneLayer; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.di.Context; +import com.bitwig.extensions.framework.values.FocusMode; + +import java.time.LocalDateTime; + +public class Apc64Extension extends ControllerExtension { + private static ControllerHost debugHost; + private HardwareSurface surface; + private Apc64MidiProcessor midiProcessor; + private Layer mainLayer; + private Layer shiftLayer; + private Transport transport; + private ViewControl viewControl; + private FocusClip focusClip; + private Project project; + private SessionLayer sessionLayer; + private OverviewLayer overviewLayer; + private ApcPreferences preferences; + private TrackAndSceneLayer sceneAndTrackLayer; + private PadLayer padLayer; + private ModifierStates modifierSection; + + public static void println(final String format, final Object... args) { + if (debugHost != null) { + final LocalDateTime now = LocalDateTime.now(); + debugHost.println(format.formatted(args)); + } + } + + protected Apc64Extension(final Apc64ExtensionDefinition definition, final ControllerHost host) { + super(definition, host); + } + + @Override + public void init() { + debugHost = getHost(); + this.project = getHost().getProject(); + final Context diContext = new Context(this); + mainLayer = new Layer(diContext.getService(Layers.class), "MAIN_LAYER"); + surface = diContext.getService(HardwareSurface.class); + initMidi(diContext); + sessionLayer = diContext.create(SessionLayer.class); + sceneAndTrackLayer = diContext.create(TrackAndSceneLayer.class); + overviewLayer = diContext.create(OverviewLayer.class); + shiftLayer = new Layer(diContext.getService(Layers.class), "SHIFT_LAYER"); + viewControl = diContext.getService(ViewControl.class); + modifierSection = diContext.getService(ModifierStates.class); + + initMainSection(diContext); + initTransport(diContext); + midiProcessor.setHwElements(diContext.getService(HardwareElements.class)); + focusClip = diContext.getService(FocusClip.class); + preferences = diContext.getService(ApcPreferences.class); + padLayer = diContext.getService(PadLayer.class); + sessionLayer.activate(); + sceneAndTrackLayer.activate(); + diContext.activate(); + mainLayer.setIsActive(true); + midiProcessor.addModeChangeListener(this::handleModeChange); + } + + private void handleModeChange(final PadMode mode) { + sessionLayer.setIsActive(mode == PadMode.SESSION); + overviewLayer.setIsActive(mode == PadMode.OVERVIEW); + padLayer.setIsActive(mode.isKeyRelated()); + } + + private void initMainSection(final Context context) { + final HardwareElements hwElements = context.getService(HardwareElements.class); + final Application application = context.getService(Application.class); + + final SingleLedButton shiftButton = hwElements.getButton(Apc64CcAssignments.SHIFT); + shiftButton.bindIsPressed(mainLayer, shiftActive -> { + modifierSection.setShift(shiftActive); + shiftLayer.setIsActive(shiftActive); + if (preferences.useShiftForAltMode()) { + modifierSection.getAltActive().set(shiftActive); + } + }); + + final SingleLedButton clearButton = hwElements.getButton(Apc64CcAssignments.CLEAR); + clearButton.bindIsPressed(mainLayer, pressed -> modifierSection.setClear(pressed)); + clearButton.bindLightPressed(mainLayer, + pressed -> pressed ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_10); + + final SingleLedButton duplicateButton = hwElements.getButton(Apc64CcAssignments.DUPLICATE); + duplicateButton.bindIsPressed(mainLayer, this::handleDuplicatePressed); + duplicateButton.bindLightPressed(mainLayer, + pressed -> pressed ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_10); + + application.canUndo().markInterested(); + application.canRedo().markInterested(); + + final SingleLedButton undoButton = hwElements.getButton(Apc64CcAssignments.UNDO); + undoButton.bindPressed(mainLayer, () -> application.undo()); + undoButton.bindLightPressed(mainLayer, pressed -> { + if (application.canUndo().get()) { + return pressed ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_60; + } + return VarSingleLedState.OFF; + }); + undoButton.bindPressed(shiftLayer, () -> application.redo()); + undoButton.bindLightPressed(shiftLayer, pressed -> { + if (application.canRedo().get()) { + return pressed ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_60; + } + return VarSingleLedState.OFF; + }); + } + + + private void handleDuplicatePressed(final boolean pressed) { + modifierSection.setDuplicate(pressed); + if (padLayer.isActive() && modifierSection.isShift() & pressed) { + padLayer.duplicateContent(); + } + } + + private void initTransport(final Context diContext) { + final HardwareElements hwElements = diContext.getService(HardwareElements.class); + final FocusClip focusClip = diContext.getService(FocusClip.class); + transport = diContext.getService(Transport.class); + transport.isPlaying().markInterested(); + transport.isArrangerRecordEnabled().markInterested(); + transport.isClipLauncherOverdubEnabled().markInterested(); + transport.isArrangerOverdubEnabled().markInterested(); + + final SingleLedButton playButton = hwElements.getButton(Apc64CcAssignments.PLAY); + playButton.bindPressed(mainLayer, () -> transport.play()); + playButton.bindLight(mainLayer, + () -> transport.isPlaying().get() ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_10); + + final SingleLedButton stopButton = hwElements.getButton(Apc64CcAssignments.STOP); + final Track rootTrack = getHost().getProject().getRootTrackGroup(); + stopButton.bindPressed(mainLayer, () -> transport.stop()); + stopButton.bindLight(mainLayer, + () -> transport.isPlaying().get() ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_10); + stopButton.bindPressed(shiftLayer, () -> rootTrack.stop()); + + final SingleLedButton recButton = hwElements.getButton(Apc64CcAssignments.REC); + recButton.bindPressed(mainLayer, () -> handleRecordButton(transport, focusClip)); + recButton.bindLight(mainLayer, () -> recordActive(transport)); + recButton.bindPressed(shiftLayer, () -> handleRecordButtonShift(transport)); + recButton.bindLight(mainLayer, () -> recordActiveShift(transport)); + } + + private void handleRecordButton(final Transport transport, final FocusClip focusClip) { + if (preferences.getRecordFocusMode() == FocusMode.LAUNCHER) { + focusClip.invokeRecord(); + } else { + if (transport.isPlaying().get()) { + transport.isArrangerRecordEnabled().toggle(); + } else { + transport.isArrangerRecordEnabled().set(true); + transport.play(); + } + } + } + + private void handleRecordButtonShift(final Transport transport) { + if (preferences.getRecordFocusMode() == FocusMode.LAUNCHER) { + transport.isClipLauncherOverdubEnabled().toggle(); + } else { + transport.isArrangerOverdubEnabled().toggle(); + } + } + + private VarSingleLedState recordActive(final Transport transport) { + if (preferences.getRecordFocusMode() == FocusMode.LAUNCHER) { + return transport.isClipLauncherOverdubEnabled().get() ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_10; + } + return transport.isArrangerRecordEnabled().get() ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_10; + } + + private VarSingleLedState recordActiveShift(final Transport transport) { + if (preferences.getRecordFocusMode() == FocusMode.LAUNCHER) { + return transport.isClipLauncherOverdubEnabled().get() ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_10; + } + return transport.isArrangerOverdubEnabled().get() ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_10; + } + + protected void initMidi(final Context diContext) { + final ControllerHost host = diContext.getService(ControllerHost.class); + final MidiIn midiIn = host.getMidiInPort(0); + final MidiIn midiIn2 = host.getMidiInPort(1); +// midiIn2.setMidiCallback((msg, d1,d2)-> { +// Apc64Extension.println("IN2 = %02X %02X %02X",msg,d1,d2); +// }); + final MidiOut midiOut = host.getMidiOutPort(0); + midiProcessor = new Apc64MidiProcessor(host, midiIn, midiOut, diContext.getService(ModifierStates.class)); + diContext.registerService(MidiProcessor.class, midiProcessor); + diContext.registerService(Apc64MidiProcessor.class, midiProcessor); + final NoteInput noteInput = midiIn2.createNoteInput("MIDI", "8?????", "9?????", "A?????", "D?????", "B?????"); + noteInput.setShouldConsumeEvents(true); + midiProcessor.setPrintToClipSeqConsumer(this::handlePrintToClip); + midiProcessor.start(); + } + + int ptcCount = 1; + + private void handlePrintToClip(final PrintToClipSeq printToClipSeq) { + if (printToClipSeq.hasNotes()) { + focusClip.focusOnNextEmpty(slot -> { + if (slot.exists().get()) { + createClipFromPrint(printToClipSeq, slot); + } else { + project.createScene(); + getHost().scheduleTask(() -> { + createClipFromPrint(printToClipSeq, slot); + }, 40); + } + }); + } + } + + private void createClipFromPrint(final PrintToClipSeq printToClipSeq, final ClipLauncherSlot slot) { + slot.select(); + slot.showInEditor(); + slot.createEmptyClip(4); + getHost().scheduleTask(() -> printToClipSeq.applyToClip(viewControl.getCursorClip(), ptcCount++), 40); + } + + @Override + public void flush() { + surface.updateHardware(); + } + + @Override + public void exit() { + midiProcessor.exitSessionMode(); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/Apc64ExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/Apc64ExtensionDefinition.java new file mode 100644 index 00000000..7fcacaf2 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/Apc64ExtensionDefinition.java @@ -0,0 +1,85 @@ +package com.bitwig.extensions.controllers.akai.apc64; + +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 Apc64ExtensionDefinition extends ControllerExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("bc2cae98-42ed-45ef-a191-aef1dfd4e00d"); + + public Apc64ExtensionDefinition() { + } + + @Override + public String getName() { + return "APC64"; + } + + @Override + public String getAuthor() { + return "Bitwig"; + } + + @Override + public String getVersion() { + return "1.0"; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + + @Override + public String getHardwareVendor() { + return "Akai"; + } + + @Override + public String getHardwareModel() { + return "APC64"; + } + + @Override + public int getRequiredAPIVersion() { + return 18; + } + + @Override + public int getNumMidiInPorts() { + return 2; + } + + @Override + public int getNumMidiOutPorts() { + return 1; + } + + @Override + public String getHelpFilePath() { + return "Controllers/Akai/AKAI APC64.pdf"; + } + + // MIDIOUT2 (APC64) + // MIDIIN2 (APC64) + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) { + if (platformType == PlatformType.WINDOWS) { + list.add(new String[]{"APC64", "MIDIIN2 (APC64)"}, new String[]{"APC64"}); + } else if (platformType == PlatformType.MAC) { + list.add(new String[]{"APC64 DAW (APC64)", "APC64 Notes (APC64)"}, new String[]{"APC64 DAW (APC64)"}); + } else if (platformType == PlatformType.LINUX) { + list.add(new String[]{"APC64 DAW (APC64)", "APC64 Notes (APC64)"}, new String[]{"APC64 DAW (APC64)"}); + } + } + + @Override + public Apc64Extension createInstance(final ControllerHost host) { + return new Apc64Extension(this, host); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/Apc64MidiProcessor.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/Apc64MidiProcessor.java new file mode 100644 index 00000000..ba4ba6b8 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/Apc64MidiProcessor.java @@ -0,0 +1,278 @@ +package com.bitwig.extensions.controllers.akai.apc64; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +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.apc.common.MidiProcessor; +import com.bitwig.extensions.framework.time.TimedEvent; +import com.bitwig.extensions.framework.values.BooleanValueObject; + +public class Apc64MidiProcessor implements MidiProcessor { + private static final String MODE_CHANGE_MSG = "f0470053190001"; + + private static final String DEVICE_VALUE = "f07e00060247530019010"; + + //2F0 47 00 53 19 00 01 02 F7 + public static final String PRINT_TO_CLIP_HEAD = "f0470053200002"; + public static final String PRINT_TO_CLIP_TAIL = "f0470053220000f7"; + public static final String PRINT_TO_CLIP_BODY = "f047005321"; + private static final String TEXT_PREFIX = "F0 47 00 53 10 00 "; + protected final MidiIn midiIn; + protected final MidiOut midiOut; + protected final NoteInput noteInput; + protected final Queue timedEvents = new ConcurrentLinkedQueue<>(); + protected final ControllerHost host; + protected List> modeChangeListeners = new ArrayList<>(); + private final int[] noteState = new int[128]; + private final int[] noteValueState = new int[128]; + private HardwareElements hwElements; + private final BooleanValueObject shiftMode; + private final BooleanValueObject clearMode; + private Consumer printToClipSeqConsumer; + private PrintToClipSeq currentPrintToClip; + private boolean sessionModeState = false; + private boolean initState = true; + private PadMode currentMode = PadMode.SESSION; + + public Apc64MidiProcessor(final ControllerHost host, final MidiIn midiIn, final MidiOut midiOut, + final ModifierStates modifierStates) { + this.host = host; + this.midiIn = midiIn; + this.midiOut = midiOut; + noteInput = midiIn.createNoteInput("MIDI", "86????", "96????", "A?????", "D?????"); + setupNoteInput(); + Arrays.fill(noteState, 0); + Arrays.fill(noteValueState, 0); + this.shiftMode = modifierStates.getShiftActive(); + this.clearMode = modifierStates.getClearActive(); + midiIn.setMidiCallback(this::handleMidiIn); + midiIn.setSysexCallback(this::handleSysEx); + } + + private void setupNoteInput() { + noteInput.setShouldConsumeEvents(true); + final Integer[] noAssignTable = new Integer[128]; + Arrays.fill(noAssignTable, Integer.valueOf(-1)); + noteInput.setKeyTranslationTable(noAssignTable); + } + + @Override + public NoteInput createNoteInput(final String name, final String... mask) { + return midiIn.createNoteInput(name, mask); + } + + @Override + public void sendMidi(final int status, final int val1, final int val2) { + midiOut.sendMidi(status, val1, val2); + noteState[val1] = status & 0xF; + noteValueState[val1] = val2; + } + + public void setHwElements(final HardwareElements elements) { + this.hwElements = elements; + } + + public void restoreState() { + if (hwElements == null) { + return; + } + hwElements.invokeRefresh(); + // for (int i = 0; i < noteState.length; i++) { + // if (noteState[i] != -1) { + // midiOut.sendMidi(0x90 | noteState[i], i, noteValueState[i]); + // } + // } + } + + @Override + public void start() { + midiOut.sendSysex("F0 47 00 53 1B 00 01 00 F7"); + midiOut.sendSysex("F0 47 00 53 19 00 01 00 F7"); + midiOut.sendSysex("F0 7E 7F 06 01 F7"); + host.scheduleTask(this::handlePing, 50); + } + + public NoteInput getNoteInput() { + return noteInput; + } + + public void setDrumMode(final boolean drumMode) { + if (drumMode) { + enterSessionMode(); + midiOut.sendSysex("F0 47 00 53 1B 00 01 01 F7"); + activateDawMode(true); + } else { + midiOut.sendSysex("F0 47 00 53 1B 00 01 00 F7"); + midiOut.sendSysex("F0 47 00 53 19 00 01 02 F7"); + exitSessionMode(); + } + } + + + public boolean isSessionModeState() { + return sessionModeState; + } + + public boolean modeHasTextControl() { + return currentMode.hasLocalControl(); + } + + public void exitSessionMode() { + if (sessionModeState) { + activateDawMode(false); + sessionModeState = false; + } + } + + public void enterSessionMode() { + if (!sessionModeState) { + activateDawMode(true); + sessionModeState = true; + } + } + + public void activateDawMode(final boolean active) { + midiOut.sendSysex("F0 47 00 53 1C 00 01 %02X F7".formatted(active ? 1 : 0)); + } + + private void handlePing() { + if (!timedEvents.isEmpty()) { + for (final TimedEvent event : timedEvents) { + event.process(); + if (event.isCompleted()) { + timedEvents.remove(event); + } + } + } + host.scheduleTask(this::handlePing, 50); + } + + public void queueEvent(final TimedEvent event) { + timedEvents.add(event); + } + + public MidiIn getMidiIn() { + return midiIn; + } + + public void setPrintToClipSeqConsumer(final Consumer printToClipSeqConsumer) { + this.printToClipSeqConsumer = printToClipSeqConsumer; + } + + public void addModeChangeListener(final Consumer modeChangeListener) { + this.modeChangeListeners.add(modeChangeListener); + } + + @Override + public void setModeChangeListener(final IntConsumer modeChangeListener) { + // nothing to do + } + + private void handleMidiIn(final int status, final int data1, final int data2) { + //Apc64Extension.println("MIDI => %02X %02X %02X", status, data1, data2); + } + + public BooleanValueObject getShiftMode() { + return shiftMode; + } + + public BooleanValueObject getClearMode() { + return clearMode; + } + + // Text F0 47 00 53 10 00 0A 00 20 31 2D 4D 49 44 49 20 00 F7 + // 1-MIDI + // Text F0 47 00 53 10 00 0A 00 41 42 43 44 61 31 32 33 00 F7 + // ABCDa123 + // Confirmation F0 7E 00 06 02 47 53 00 19 01 01 00 0E 00 00 00 00 00 41 34 32 33 30 37 32 35 37 34 30 32 37 31 + // 31 00 F7 + + protected void handleSysEx(final String sysExString) { + //Apc64Extension.println("SysEx = %s mode=%s", sysExString, sysExString.startsWith(MODE_CHANGE_MSG)); + if (sysExString.startsWith(DEVICE_VALUE)) { + Apc64Extension.println("#### Connect to APC #### "); + initState = false; + enterSessionMode(); + } else if (sysExString.startsWith(MODE_CHANGE_MSG)) { + final int mode = + Integer.parseInt(sysExString.substring(MODE_CHANGE_MSG.length(), MODE_CHANGE_MSG.length() + 2), 16); + handleModeChange(mode); + } else if (sysExString.startsWith(PRINT_TO_CLIP_HEAD)) { + final String value = sysExString.substring(PRINT_TO_CLIP_HEAD.length(), sysExString.length() - 2); + final int length = fromHexValue(value); + currentPrintToClip = new PrintToClipSeq(length); + } else if (sysExString.startsWith(PRINT_TO_CLIP_BODY)) { + final String data = sysExString.substring(PRINT_TO_CLIP_BODY.length() + 2, sysExString.length() - 4); + final int headValue = + fromHexValue(sysExString.substring(PRINT_TO_CLIP_BODY.length(), PRINT_TO_CLIP_BODY.length() + 2)); + currentPrintToClip.addNoteData(data); + currentPrintToClip.setHeadValue(headValue); + } else if (sysExString.startsWith(PRINT_TO_CLIP_TAIL)) { + if (printToClipSeqConsumer != null) { + printToClipSeqConsumer.accept(currentPrintToClip); + } + } else { + //Apc64Extension.println("Unknown SysEx = %s", sysExString); + } + } + + private void handleModeChange(final int mode) { + if (initState) { + return; + } + currentMode = PadMode.fromId(mode); + //Apc64Extension.println(" MODE =%d ==> %s", mode, currentMode); + + if (currentMode.hasLocalControl()) { + //Apc64Extension.println(" DAW MODE IN %s", sessionModeState); + activateDawMode(true); + sessionModeState = true; + restoreState(); + } else { + exitSessionMode(); + } + modeChangeListeners.forEach(listener -> listener.accept(currentMode)); + } + + private int fromHexValue(final String hex) { + if (hex.length() == 4) { + final int v1 = Integer.parseInt(hex.substring(0, 2), 16); + final int v2 = Integer.parseInt(hex.substring(2, 4), 16); + return (v1 << 7) | v2; + } + if (hex.length() < 3) { + return Integer.parseInt(hex, 16); + } + return 0; + } + + public void sendText(final int row, final String text) { + final StringBuilder sb = new StringBuilder(TEXT_PREFIX); + final int len = Math.min(14, Math.max(3, text.length())); + sb.append("%02X ".formatted(len + 2)); + sb.append("%02X ".formatted(row)); + final String asciiText = StringUtil.toAsciiDisplay(text, len); + for (int i = 0; i < len; i++) { + if (i < asciiText.length()) { + sb.append("%02X ".formatted((int) asciiText.charAt(i))); + } else { + sb.append("20 "); + } + } + sb.append("00 "); + sb.append("F7"); + //Apc64Extension.println(" SEND TEXT %d => %s", row, text); + midiOut.sendSysex(sb.toString()); + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/ApcPreferences.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/ApcPreferences.java new file mode 100644 index 00000000..5f4cf2d3 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/ApcPreferences.java @@ -0,0 +1,86 @@ +package com.bitwig.extensions.controllers.akai.apc64; + +import com.bitwig.extension.controller.api.*; +import com.bitwig.extensions.controllers.akai.apc.common.OrientationFollowType; +import com.bitwig.extensions.controllers.akai.apc.common.PanelLayout; +import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.values.FocusMode; +import com.bitwig.extensions.framework.values.ValueObject; + +@Component +public class ApcPreferences { + + private final ValueObject orientationFollow; + private final ValueObject panelLayout = new ValueObject<>(PanelLayout.VERTICAL); + private final SettableBooleanValue altModeWithShift; + private final SettableEnumValue recordButtonAssignment; + private final SettableEnumValue gridLayoutSettings; + private PanelLayout bitwigPanelLayout; + private FocusMode recordFocusMode = FocusMode.LAUNCHER; + + public ApcPreferences(final ControllerHost host, final Application application) { + final Preferences preferences = host.getPreferences(); // THIS + orientationFollow = new ValueObject<>(OrientationFollowType.AUTOMATIC); + gridLayoutSettings = preferences.getEnumSetting("Orientation determined by", "Grid Layout", + new String[]{OrientationFollowType.AUTOMATIC.getLabel(), // + OrientationFollowType.FIXED_VERTICAL.getLabel(), // + OrientationFollowType.FIXED_HORIZONTAL.getLabel()}, // + OrientationFollowType.FIXED_VERTICAL.getLabel()); + gridLayoutSettings.addValueObserver(newValue -> orientationFollow.set(OrientationFollowType.toType(newValue))); + application.panelLayout().addValueObserver(this::handlePanelLayoutChanged); + altModeWithShift = preferences.getBooleanSetting("Use as ALT trigger modifier", "Shift Button", true); + altModeWithShift.markInterested(); + orientationFollow.addValueObserver(((oldValue, newValue) -> { + determinePanelLayout(orientationFollow.get()); + })); + final DocumentState documentState = host.getDocumentState(); // THIS + recordButtonAssignment = documentState.getEnumSetting("Record Button assignment", // + "Transport", new String[]{FocusMode.LAUNCHER.getDescriptor(), FocusMode.ARRANGER.getDescriptor()}, + recordFocusMode.getDescriptor()); + recordButtonAssignment.addValueObserver(value -> { + recordFocusMode = FocusMode.toMode(value); + }); + } + + private void handlePanelLayoutChanged(final String layout) { + if (layout.equals("MIX")) { + bitwigPanelLayout = PanelLayout.VERTICAL; + } else if (layout.equals("ARRANGE")) { + bitwigPanelLayout = PanelLayout.HORIZONTAL; + } else { + bitwigPanelLayout = PanelLayout.VERTICAL; + } + determinePanelLayout(orientationFollow.get()); + } + + public SettableEnumValue getGridLayoutSettings() { + return gridLayoutSettings; + } + + public SettableBooleanValue getAltModeWithShift() { + return altModeWithShift; + } + + public boolean useShiftForAltMode() { + return altModeWithShift.get(); + } + + public FocusMode getRecordFocusMode() { + return recordFocusMode; + } + + public ValueObject getPanelLayout() { + return panelLayout; + } + + private void determinePanelLayout(final OrientationFollowType followType) { + if (followType == OrientationFollowType.FIXED_VERTICAL) { + panelLayout.set(PanelLayout.VERTICAL); + } else if (followType == OrientationFollowType.FIXED_HORIZONTAL) { + panelLayout.set(PanelLayout.HORIZONTAL); + } else { + panelLayout.set(bitwigPanelLayout); + } + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/DeviceControl.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/DeviceControl.java new file mode 100644 index 00000000..edbe7796 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/DeviceControl.java @@ -0,0 +1,277 @@ +package com.bitwig.extensions.controllers.akai.apc64; + +import com.bitwig.extension.controller.api.*; +import com.bitwig.extensions.framework.values.BasicStringValue; +import com.bitwig.extensions.framework.values.IntValueObject; + +import java.util.function.Consumer; + +public class DeviceControl { + private final CursorRemoteControlsPage deviceRemotePages; + private final CursorRemoteControlsPage trackRemotes; + private final CursorRemoteControlsPage projectRemotes; + private final PinnableCursorDevice cursorDevice; + private final PinnableCursorDevice primaryDevice; + private final DrumPadBank drumPadBank; + private Focus currentFocus = Focus.DEVICE; + private final BasicStringValue deviceName = new BasicStringValue(""); + private final BasicStringValue pageName = new BasicStringValue(""); + private String[] devicePageNames = new String[]{}; + private String deviceRawName = ""; + private int devicePageIndex = 0; + private String[] trackRemotePageNames = new String[]{}; + private int trackRemotePageIndex = 0; + private String[] projectRemotePageNames = new String[]{}; + private int projectRemotePageIndex = 0; + private Consumer focusListener = null; + private String padRawName = ""; + private final IntValueObject selectedPadIndex = new IntValueObject(-1, -1, 15); + + public enum Focus { + DEVICE, + TRACK, + PROJECT + } + + public DeviceControl(final CursorTrack cursorTrack, final Track rootTrack) { + cursorDevice = cursorTrack.createCursorDevice(); + cursorDevice.hasDrumPads().markInterested(); + cursorDevice.name().addValueObserver(name -> { + deviceRawName = name.isBlank() ? "" : name; + if (currentFocus == Focus.DEVICE) { + deviceName.set(deviceRawName); + } + }); + cursorDevice.hasNext().markInterested(); + cursorDevice.hasPrevious().markInterested(); + cursorDevice.hasLayers().markInterested(); + cursorDevice.hasSlots().markInterested(); + cursorDevice.slotNames().markInterested(); + + deviceRemotePages = cursorDevice.createCursorRemoteControlsPage(8); + deviceRemotePages.pageNames().addValueObserver(names -> { + devicePageNames = names; + applyCurrentValues(Focus.DEVICE); + }); + deviceRemotePages.selectedPageIndex().addValueObserver(index -> { + devicePageIndex = index; + applyCurrentValues(Focus.DEVICE); + }); + deviceRemotePages.setHardwareLayout(HardwareControlType.SLIDER, 8); + primaryDevice = cursorTrack.createCursorDevice("drumdetection", "Pad Device", 8, + CursorDeviceFollowMode.FIRST_INSTRUMENT); + primaryDevice.hasDrumPads().markInterested(); + primaryDevice.exists().markInterested(); + + trackRemotes = cursorTrack.createCursorRemoteControlsPage("track-remotes", 8, null); + trackRemotes.setHardwareLayout(HardwareControlType.SLIDER, 8); + trackRemotes.pageNames().addValueObserver(names -> { + trackRemotePageNames = names; + applyCurrentValues(Focus.TRACK); + }); + trackRemotes.selectedPageIndex().addValueObserver(index -> { + trackRemotePageIndex = index; + applyCurrentValues(Focus.TRACK); + }); + + projectRemotes = rootTrack.createCursorRemoteControlsPage("project-remotes", 8, null); + projectRemotes.setHardwareLayout(HardwareControlType.SLIDER, 8); + projectRemotes.pageNames().addValueObserver(names -> { + projectRemotePageNames = names; + applyCurrentValues(Focus.PROJECT); + }); + projectRemotes.selectedPageIndex().addValueObserver(index -> { + projectRemotePageIndex = index; + applyCurrentValues(Focus.PROJECT); + }); + drumPadBank = primaryDevice.createDrumPadBank(16); + for (int i = 0; i < 16; i++) { + final int index = i; + final DrumPad pad = drumPadBank.getItemAt(i); + pad.name().addValueObserver(name -> handlePadNameChanged(index, name)); + pad.addIsSelectedInEditorObserver(selected -> handlePadSelection(selected, index, pad)); + } + + initRemotesPage(deviceRemotePages); + initRemotesPage(trackRemotes); + initRemotesPage(projectRemotes); + } + + private void handlePadNameChanged(final int index, final String name) { + if (index == selectedPadIndex.get()) { + padRawName = name; + if (cursorDevice.hasDrumPads().get()) { + pageName.set(padRawName); + } + } + } + + private void handlePadSelection(final boolean selected, final int index, final DrumPad pad) { + if (selected) { + selectedPadIndex.set(index); + padRawName = pad.name().get(); + if (cursorDevice.hasDrumPads().get()) { + pageName.set(padRawName); + } + } + } + + public void setCurrentFocus(final Focus focus) { + if (this.currentFocus != focus) { + this.currentFocus = focus; + applyCurrentValues(focus); + if (this.focusListener != null) { + this.focusListener.accept(this.currentFocus); + } + } + } + + public PinnableCursorDevice getPrimaryDevice() { + return primaryDevice; + } + + public PinnableCursorDevice getCursorDevice() { + return cursorDevice; + } + + public void setFocusListener(final Consumer focusListener) { + this.focusListener = focusListener; + } + + private void applyCurrentValues(final Focus focus) { + if (focus != this.currentFocus) { + return; + } + if (this.currentFocus == Focus.DEVICE) { + deviceName.set(deviceRawName); + if (devicePageIndex >= 0 && devicePageIndex < devicePageNames.length) { + pageName.set(devicePageNames[devicePageIndex]); + } else { + pageName.set(""); + } + } else if (this.currentFocus == Focus.TRACK) { + deviceName.set("Track Remotes"); + if (trackRemotePageIndex >= 0 && trackRemotePageIndex < trackRemotePageNames.length) { + pageName.set(trackRemotePageNames[trackRemotePageIndex]); + } else { + pageName.set(""); + } + } else if (this.currentFocus == Focus.PROJECT) { + deviceName.set("Project Remotes"); + if (projectRemotePageIndex >= 0 && projectRemotePageIndex < projectRemotePageNames.length) { + pageName.set(projectRemotePageNames[projectRemotePageIndex]); + } else { + pageName.set(""); + } + } + if (cursorDevice.hasDrumPads().get()) { + pageName.set(padRawName); + } + } + + public BasicStringValue getDeviceName() { + return deviceName; + } + + public BasicStringValue getPageName() { + return pageName; + } + + public void selectDevice(final int dir) { + switch (currentFocus) { + case DEVICE -> navigateDevice(dir); + case TRACK -> navigateTrack(dir); + case PROJECT -> navigateProject(dir); + } + } + + private void navigateTrack(final int dir) { + if (dir > 0) { + setCurrentFocus(Focus.DEVICE); + } else { + setCurrentFocus(Focus.PROJECT); + } + } + + private void navigateProject(final int dir) { + if (dir > 0) { + setCurrentFocus(Focus.TRACK); + } + } + + public void navigateDevice(final int dir) { + if (dir > 0) { + cursorDevice.selectNext(); + } else if (cursorDevice.hasPrevious().get()) { + cursorDevice.selectPrevious(); + } else { + setCurrentFocus(Focus.TRACK); + } + } + + public boolean canScrollDevices(final int dir) { + return switch (currentFocus) { + case DEVICE -> dir <= 0 || cursorDevice.hasNext().get(); + case TRACK -> true; + case PROJECT -> dir > 0; + }; + } + + public void selectParameterPage(final int dir) { + if (dir > 0) { + getCurrentPage().selectNext(); + } else { + getCurrentPage().selectPrevious(); + } + } + + public CursorRemoteControlsPage getCurrentPage() { + return getPage(currentFocus); + } + + public CursorRemoteControlsPage getPage(final Focus focus) { + return switch (focus) { + case TRACK -> trackRemotes; + case DEVICE -> deviceRemotePages; + case PROJECT -> projectRemotes; + }; + } + + public DrumPadBank getDrumPadBank() { + return drumPadBank; + } + + public boolean canScrollParameterPages(final int dir) { + if (dir > 0) { + return getCurrentPage().hasNext().get(); + } + return getCurrentPage().hasPrevious().get(); + } + + public boolean canNavigateIntoDevice(final int dir) { + if (dir > 0) { + return cursorDevice.hasLayers().get() || cursorDevice.hasDrumPads().get() || cursorDevice.hasSlots().get(); + } + return true; + } + + public void navigateVertical(final int dir) { + if (dir > 0) { + if (cursorDevice.hasDrumPads().get()) { + cursorDevice.selectFirstInKeyPad(36); // to do get from pad + } else if (cursorDevice.hasLayers().get()) { + cursorDevice.selectFirstInLayer(0); + } else if (cursorDevice.hasSlots().get()) { + final String[] slotNames = cursorDevice.slotNames().get(); + cursorDevice.selectFirstInSlot(slotNames[0]); + } + } else { + cursorDevice.selectParent(); + } + } + + private void initRemotesPage(final CursorRemoteControlsPage remotesPage) { + remotesPage.hasPrevious().markInterested(); + remotesPage.hasNext().markInterested(); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/FocusClip.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/FocusClip.java new file mode 100644 index 00000000..96acb796 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/FocusClip.java @@ -0,0 +1,164 @@ +package com.bitwig.extensions.controllers.akai.apc64; + +import com.bitwig.extension.controller.api.*; +import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; +import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.di.Inject; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +@Component +public class FocusClip { + private static final int SINGLE_SLOT_RANGE = 8; + + private final CursorTrack cursorTrack; + private final Application application; + private final Transport transport; + private final Clip mainCursorClip; + private final Project project; + private final ControllerHost host; + private final OverviewGrid overviewGrid; + + private int selectedSlotIndex = -1; + private int scrollOffset = 0; + + private String currentTrackName = ""; + + private final Map indexMemory = new HashMap<>(); + private final ClipLauncherSlotBank slotBank; + private ClipLauncherSlot focusSlot; + private Runnable scrollTask = null; + + @Inject + private MidiProcessor midiProcessor; + + public FocusClip(ControllerHost host, Application application, Transport transport, ViewControl viewControl, + Project project) { + this.cursorTrack = viewControl.getCursorTrack(); + this.project = project; + this.host = host; + this.overviewGrid = viewControl.getOverviewGrid(); + slotBank = cursorTrack.clipLauncherSlotBank(); + for (int i = 0; i < slotBank.getSizeOfBank(); i++) { + final ClipLauncherSlot slot = slotBank.getItemAt(i); + slot.exists().markInterested(); + slot.isRecording().markInterested(); + slot.isPlaying().markInterested(); + slot.hasContent().markInterested(); + } + + this.application = application; + this.transport = transport; + + slotBank.addPlaybackStateObserver((slotIndex, playbackState, isQueued) -> { + if (playbackState != 0 && !isQueued) { + slotBank.select(slotIndex); + } + }); + slotBank.addIsSelectedObserver((index, selected) -> { + if (selected) { + selectedSlotIndex = index; + indexMemory.put(currentTrackName, selectedSlotIndex); + focusSlot = slotBank.getItemAt(selectedSlotIndex); + } + }); + slotBank.scrollPosition().addValueObserver(scrollPos -> { + //Apc64Extension.println(" SB %d %d", scrollPos, overviewGrid.getNumberOfScenes()); + scrollOffset = scrollPos; + if (scrollTask != null) { + scrollTask.run(); + scrollTask = null; + } + }); + + this.cursorTrack.name().addValueObserver(name -> { + selectedSlotIndex = -1; + currentTrackName = name; + final Integer index = indexMemory.get(name); + if (index != null) { + selectedSlotIndex = index.intValue(); + } + }); + mainCursorClip = viewControl.getCursorClip(); + } + + public void invokeRecord() { + if (selectedSlotIndex != -1) { + final ClipLauncherSlot slot = slotBank.getItemAt(selectedSlotIndex); + if (slot.isRecording().get()) { + slot.launch(); + transport.isClipLauncherOverdubEnabled().set(false); + } else { + Optional emptySlot = getFirstEmptySlot(selectedSlotIndex); + if (emptySlot.isPresent()) { + recordAction(emptySlot.get()); + } else { + project.createScene(); + host.scheduleTask( + () -> getFirstEmptySlot(selectedSlotIndex).ifPresent(newSlot -> recordAction(newSlot)), 50); + } + } + } else { + getFirstEmptySlot(selectedSlotIndex).ifPresent(slot -> recordAction(slot)); + } + } + + private void recordAction(ClipLauncherSlot emptySlot) { + emptySlot.launch(); + transport.isClipLauncherOverdubEnabled().set(true); + } + + public void duplicateContent() { + mainCursorClip.duplicateContent(); + } + + public void quantize(final double amount) { + mainCursorClip.quantize(amount); + } + + public void clearSteps() { + mainCursorClip.clearSteps(); + } + + public void transpose(final int semitones) { + mainCursorClip.transpose(semitones); + } + + public void focusOnNextEmpty(Consumer postCreation) { + if (focusSlotIsEmpty()) { + postCreation.accept(focusSlot); + } else { + getFirstEmptySlot(selectedSlotIndex) // + .ifPresentOrElse(slot -> postCreation.accept(slot), // + () -> ensureEmptySlot(postCreation)); + } + } + + private void ensureEmptySlot(Consumer postCreation) { + project.createScene(); + host.scheduleTask(() -> getFirstEmptySlot(selectedSlotIndex).ifPresent(newSlot -> postCreation.accept(newSlot)), + 50); + } + + private boolean focusSlotIsEmpty() { + return focusSlot != null && !focusSlot.hasContent().get() && focusSlot.exists().get(); + } + + private Optional getFirstEmptySlot(int startIndex) { + int start = startIndex < 0 ? 0 : startIndex; + for (int i = start; i < slotBank.getSizeOfBank(); i++) { + final ClipLauncherSlot slot = slotBank.getItemAt(i); + if (!slot.hasContent().get() && slot.exists().get()) { + return Optional.of(slot); + } + } + return Optional.empty(); + } + + public void clearNotes(int noteToClear) { + mainCursorClip.clearStepsAtY(0, noteToClear); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/HardwareElements.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/HardwareElements.java new file mode 100644 index 00000000..4691328c --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/HardwareElements.java @@ -0,0 +1,110 @@ +package com.bitwig.extensions.controllers.akai.apc64; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extensions.controllers.akai.apc.common.control.ClickEncoder; +import com.bitwig.extensions.controllers.akai.apc.common.control.RgbButton; +import com.bitwig.extensions.controllers.akai.apc64.control.OledBacklight; +import com.bitwig.extensions.controllers.akai.apc64.control.SingleLedButton; +import com.bitwig.extensions.controllers.akai.apc64.control.TouchSlider; +import com.bitwig.extensions.framework.di.Component; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +@Component +public class HardwareElements { + private RgbButton[][] buttons; + private RgbButton[][] drumButtons; + private SingleLedButton[] sceneButtons; + private TouchSlider[] sliders = new TouchSlider[8]; + private final RgbButton[] trackButtons = new RgbButton[8]; + private final RgbButton[] trackControlButtons = new RgbButton[8]; + private final ClickEncoder mainEncoder; + private final SingleLedButton encoderPress; + private final Map mainButtons; + private final OledBacklight oledBackLight; + + public HardwareElements(ControllerHost host, HardwareSurface surface, Apc64MidiProcessor midiProcessor) { + MidiIn midiIn = midiProcessor.getMidiIn(); + final int numberOfScenes = 8; + drumButtons = new RgbButton[numberOfScenes][8]; + int noteNr = Apc64CcAssignments.GRID_BASE.getStateId(); + buttons = new RgbButton[numberOfScenes][8]; + sceneButtons = new SingleLedButton[numberOfScenes]; + for (int row = 0; row < numberOfScenes; row++) { + for (int col = 0; col < 8; col++) { + buttons[row][col] = new RgbButton(6, noteNr++, "PAD", surface, midiProcessor); + } + sceneButtons[row] = new SingleLedButton(Apc64CcAssignments.SCENE_BUTTON_BASE.getStateId() + row, "SCENE", + surface, midiProcessor); + } + mainEncoder = new ClickEncoder(0x5A, host, surface, midiIn); + encoderPress = new SingleLedButton(0x5A, "ENCODER_PRESS", surface, midiProcessor); + oledBackLight = new OledBacklight(surface, midiProcessor, 0x59); + + mainButtons = Arrays.stream(Apc64CcAssignments.values()) // + .filter(Apc64CcAssignments::isSingle) // + .collect(Collectors.toMap(assignment -> assignment,// + assignment -> new SingleLedButton(assignment.getStateId(), assignment.toString(), surface, + midiProcessor))); + + for (int i = 0; i < 8; i++) { + sliders[i] = new TouchSlider(i, surface, midiProcessor); + trackButtons[i] = new RgbButton(0, Apc64CcAssignments.TRACKS_BASE.getStateId() + i, "TRACK_SEL", surface, + midiProcessor); + trackControlButtons[i] = new RgbButton(0, Apc64CcAssignments.TRACK_CONTROL_BASE.getStateId() + i, + "TRACK_CTL", surface, midiProcessor); + } + } + + public void invokeRefresh() { + for (int i = 0; i < 8; i++) { + for (int j = 0; j < 8; j++) { + buttons[i][j].refresh(); + } + } + } + + public SingleLedButton getSceneButton(int index) { + return sceneButtons[index]; + } + + public OledBacklight getOledBackLight() { + return oledBackLight; + } + + public SingleLedButton getButton(Apc64CcAssignments assignment) { + return mainButtons.get(assignment); + } + + public ClickEncoder getMainEncoder() { + return mainEncoder; + } + + public SingleLedButton getEncoderPress() { + return encoderPress; + } + + public RgbButton getTrackSelectButton(int index) { + return trackButtons[index]; + } + + public RgbButton getTrackControlButtons(int index) { + return trackControlButtons[index]; + } + + public RgbButton getGridButton(final int sceneIndex, final int trackIndex) { + return buttons[buttons.length - sceneIndex - 1][trackIndex]; + } + + public RgbButton getDrumButton(final int sceneIndex, final int trackIndex) { + return drumButtons[buttons.length - sceneIndex - 1][trackIndex]; + } + + public TouchSlider[] getTouchSliders() { + return sliders; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/Menu.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/Menu.java new file mode 100644 index 00000000..f6dd30f8 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/Menu.java @@ -0,0 +1,228 @@ +package com.bitwig.extensions.controllers.akai.apc64; + +import com.bitwig.extension.controller.api.SettableBooleanValue; +import com.bitwig.extension.controller.api.SettableEnumValue; +import com.bitwig.extensions.controllers.akai.apc64.layer.MainDisplay; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class Menu { + private final MainDisplay.Screen screen; + private final List items = new ArrayList<>(); + private int itemIndex = 0; + private boolean onMenu = true; + private MenuItem currentMenu; + + public record EnumMenuValue(String value, String displayValue) { + + } + + public abstract static class MenuItem { + private final String name; + protected Consumer updater; + + protected MenuItem(final String name) { + this.name = name; + } + + public void setFocusScreen(final Consumer updater) { + this.updater = updater; + } + + public void release() { + this.updater = null; + } + + public void update(final String newValue) { + if (updater != null) { + updater.accept(newValue); + } + } + + public abstract String getCurrentValue(); + + public abstract void handleIncrement(final int dir); + + public boolean isMomentary() { + return false; + } + + public void handlePressed(final boolean pressed) { + } + } + + public static class EnumMenuItem extends MenuItem { + private final SettableEnumValue value; + private final List selection; + private EnumMenuValue current; + + public EnumMenuItem(final String name, final SettableEnumValue value, final List selection) { + super(name); + value.markInterested(); + this.value = value; + this.selection = selection; + this.current = selection.get(0); + value.addValueObserver(enumValue -> this.update(enumValue)); + } + + public void update(final String newValue) { + current = selection.stream().filter(v -> v.value.equals(newValue)).findFirst().orElse(null); + if (updater != null) { + updater.accept(current.displayValue()); + } + } + + @Override + public String getCurrentValue() { + return current.displayValue(); + } + + public void handleIncrement(final int dir) { + current = nextValue(value.get(), selection, dir, false); + value.set(current.value()); + } + + } + + public static class BooleanToggleMenuItem extends MenuItem { + private final SettableBooleanValue value; + + public BooleanToggleMenuItem(final String name, final SettableBooleanValue value) { + super(name); + this.value = value; + value.addValueObserver(boolValue -> this.update(boolValue ? "On" : "Off")); + } + + public void handlePressed(final boolean pressed) { + if (pressed) { + value.toggle(); + } + } + + @Override + public boolean isMomentary() { + return true; + } + + @Override + public String getCurrentValue() { + return value.get() ? "On" : "Off"; + } + + public void handleIncrement(final int dir) { + value.toggle(); + } + + } + + public static class HoldMenuItem extends MenuItem { + private final SettableBooleanValue value; + + public HoldMenuItem(final String name, final SettableBooleanValue value) { + super(name); + this.value = value; + value.addValueObserver(boolValue -> this.update(boolValue ? "On" : "Off")); + } + + @Override + public String getCurrentValue() { + return value.get() ? "On" : "Off"; + } + + @Override + public void handleIncrement(final int dir) { + } + + @Override + public void handlePressed(final boolean pressed) { + value.set(pressed); + } + + public boolean isMomentary() { + return true; + } + } + + public Menu(final MainDisplay.Screen screen) { + this.screen = screen; + screen.setRow(0, "Bitwig Menu"); + } + + public void addMenuItem(final MenuItem item) { + this.items.add(item); + } + + public void init() { + if (this.items.isEmpty()) { + return; + } + currentMenu = this.items.get(0); + currentMenu.setFocusScreen(this::updateValue); + update(); + } + + private void update() { + final MenuItem menuItem = items.get(itemIndex); + screen.setRow(1, "%s %s ".formatted(onMenu ? ">" : " ", menuItem.name)); + updateValue(menuItem.getCurrentValue()); + } + + public void handleInc(final int dir) { + if (onMenu) { + final int nextIndex = itemIndex + dir; + if (nextIndex >= 0 && nextIndex < items.size()) { + items.get(itemIndex).release(); + itemIndex = nextIndex; + items.get(itemIndex).setFocusScreen(this::updateValue); + currentMenu = this.items.get(itemIndex); + update(); + } + } else { + final MenuItem menuItem = items.get(itemIndex); + menuItem.handleIncrement(dir); + update(); + } + } + + private void updateValue(final String value) { + screen.setRow(2, "%s%s ".formatted(!onMenu ? ">" : "", value)); + } + + public void handEncoderClick(final boolean pressed) { + Apc64Extension.println(" ON menu %s %s", currentMenu.getClass().getName(), onMenu); + if (currentMenu.isMomentary()) { + onMenu = true; + currentMenu.handlePressed(pressed); + update(); + } else { + if (pressed) { + onMenu = !onMenu; + update(); + } + } + } + + public static EnumMenuValue nextValue(final String currentValue, final List list, final int inc, + final boolean wrap) { + int index = -1; + final int size = list.size(); + for (int i = 0; i < size; i++) { + if (currentValue.equals(list.get(i).value())) { + index = i; + break; + } + } + if (index != -1) { + final int next = index + inc; + if (next >= 0 && next < size) { + return list.get(next); + } else if (wrap) { + index = next < 0 ? size - 1 : next >= size ? 0 : next; + } + return list.get(index); + } + return list.get(0); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/ModifierStates.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/ModifierStates.java new file mode 100644 index 00000000..92727749 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/ModifierStates.java @@ -0,0 +1,86 @@ +package com.bitwig.extensions.controllers.akai.apc64; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.SettableBooleanValue; +import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.values.BooleanValueObject; + +@Component +public class ModifierStates { + + public static final int MSK_SHIFT = 0x1; + public static final int MSK_CLEAR = 0x2; + + private final BooleanValueObject shiftActive = new BooleanValueObject(); + private final BooleanValueObject clearActive = new BooleanValueObject(); + private final BooleanValueObject duplicateActive = new BooleanValueObject(); + private final SettableBooleanValue quantizeActive = new BooleanValueObject(); + private final BooleanValueObject altActive = new BooleanValueObject(); + + private int modifierMask = 0; + + public ModifierStates(final ControllerHost host) { + shiftActive.addValueObserver(active -> setMask(MSK_SHIFT, active)); + clearActive.addValueObserver(active -> setMask(MSK_CLEAR, active)); + } + + private void setMask(final int mask, final boolean value) { + if (value) { + modifierMask |= mask; + } else { + modifierMask &= ~mask; + } + } + + public void setShift(final boolean active) { + shiftActive.set(active); + } + + public void setClear(final boolean active) { + clearActive.set(active); + } + + public BooleanValueObject getShiftActive() { + return shiftActive; + } + + public BooleanValueObject getClearActive() { + return clearActive; + } + + public boolean isShift() { + return shiftActive.get(); + } + + public boolean isClear() { + return clearActive.get(); + } + + public boolean anyModifierHeld() { + return modifierMask > 0; + } + + public boolean noModifier() { + return modifierMask == 0; + } + + public boolean onlyShift() { + return modifierMask == MSK_SHIFT; + } + + public void setDuplicate(final boolean active) { + duplicateActive.set(active); + } + + public boolean isDuplicate() { + return duplicateActive.get(); + } + + public SettableBooleanValue getQuantizeActive() { + return quantizeActive; + } + + public BooleanValueObject getAltActive() { + return altActive; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/OverviewGrid.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/OverviewGrid.java new file mode 100644 index 00000000..af2fd8c6 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/OverviewGrid.java @@ -0,0 +1,93 @@ +package com.bitwig.extensions.controllers.akai.apc64; + +public class OverviewGrid { + + private int sceneOffset; + private int trackOffset; + private int numberOfScenes; + private int numberOfTracks; + + private int trackPosition; + private int scenePosition; + + private final int[][] hasClips = new int[8][8]; + private final int[] sceneQueuedClips = new int[64]; + + public int getNumberOfScenes() { + return numberOfScenes; + } + + public void setNumberOfScenes(final int numberOfScenes) { + this.numberOfScenes = numberOfScenes; + } + + public int getNumberOfTracks() { + return numberOfTracks; + } + + public void setNumberOfTracks(final int numberOfTracks) { + this.numberOfTracks = numberOfTracks; + } + + public int getTrackPosition() { + return trackPosition - trackOffset; + } + + public int getTrackOffset() { + return trackOffset; + } + + public void setTrackPosition(final int trackPosition) { + this.trackPosition = trackPosition; + this.trackOffset = (trackPosition / 64) * 64; + } + + public int getScenePosition() { + return scenePosition - sceneOffset; + } + + public void setScenePosition(final int scenePosition) { + this.scenePosition = scenePosition; + this.sceneOffset = (scenePosition / 64) * 64; + } + + public int getSceneOffset() { + return sceneOffset; + } + + public void markSceneQueued(int sceneIndex, boolean isQueued) { + if (isQueued) { + sceneQueuedClips[sceneIndex]++; + } else if (sceneQueuedClips[sceneIndex] > 0) { + sceneQueuedClips[sceneIndex]--; + } + } + + public void setHasClips(int trackIndex, int sceneIndex, boolean hasClip) { + int gridScene = (sceneIndex) / 8; + int gridTrack = (trackIndex) / 8; + if (hasClip) { + this.hasClips[gridTrack][gridScene]++; + } else if (this.hasClips[gridTrack][gridScene] > 0) { + this.hasClips[gridTrack][gridScene]--; + } + } + + public boolean hasClips(int trackIndex, int sceneIndex) { + return this.hasClips[trackIndex][sceneIndex] > 0; + } + + public boolean hasQueuedScenes(int sceneIndex) { + int index = sceneIndex - sceneOffset; + if (index > 63) { + return false; + } + return this.sceneQueuedClips[sceneIndex - sceneOffset] > 0; + } + + public boolean inGrid(int trackIndex, int sceneIndex) { + final int posX = trackIndex * 8; + final int posY = sceneIndex * 8; + return posX < (numberOfTracks - trackOffset) && posY < (numberOfScenes - sceneOffset); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/PadMode.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/PadMode.java new file mode 100644 index 00000000..36f8d27f --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/PadMode.java @@ -0,0 +1,48 @@ +package com.bitwig.extensions.controllers.akai.apc64; + +import java.util.Arrays; + +public enum PadMode { + SESSION(0, true, false), + OVERVIEW(1, true, false), + NOTE(2, false, true), + CHORD(3), + CHORD_SETTINGS(4), + DRUM(5, true, true), + STEP_SEQUENCER(6), + STEP_SEQUENCER_SETTINGS(7), + PROJECT(8), + CUSTOM(9), + CUSTOM_SETTINGS(10), + UNKNOWN(-1); + + private final int modeId; + private final boolean hasLocalControl; + private final boolean isKeyRelated; + + PadMode(int modeId, boolean hasLocalControl, boolean isKeyRelated) { + this.modeId = modeId; + this.hasLocalControl = hasLocalControl; + this.isKeyRelated = isKeyRelated; + } + + PadMode(int modeId) { + this(modeId, false, false); + } + + public int getModeId() { + return modeId; + } + + public static PadMode fromId(int id) { + return Arrays.stream(PadMode.values()).filter(mode -> mode.getModeId() == id).findFirst().orElse(UNKNOWN); + } + + public boolean hasLocalControl() { + return hasLocalControl; + } + + public boolean isKeyRelated() { + return isKeyRelated; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/PrintToClipSeq.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/PrintToClipSeq.java new file mode 100644 index 00000000..daf928c6 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/PrintToClipSeq.java @@ -0,0 +1,103 @@ +package com.bitwig.extensions.controllers.akai.apc64; + +import com.bitwig.extension.controller.api.Clip; + +import java.util.ArrayList; +import java.util.List; + +public class PrintToClipSeq { + public static final double PPQ_RESOLUTION = 96.0; + private static int[] STEP_DIVISOR = {96, 64, 48, 32, 24, 16, 12, 8}; + private static final double[] RESOLUTIONS = {1.0, 0.666666, 0.5, 0.33333, 0.25, 0.1666666, 0.125, 0.0833333}; + + private List notes = new ArrayList<>(); + private int length; + private int headValue; + + public record StepNote(int start, int end, int note, int vel, int block, int tail) { + public StepNote(String sysEx) { + this(fromHexValueMask(sysEx, 1), fromHexValueMask(sysEx, 5), fromHexValue(sysEx, 3), fromHexValue(sysEx, 4), + fromHexValue(sysEx, 0), fromHexValue(sysEx, 7)); + } + } + + public PrintToClipSeq(int length) { + this.length = length; + } + + public void setNotes(List notes) { + this.notes = notes; + } + + public void setHeadValue(int headValue) { + this.headValue = headValue; + } + + public int getLength() { + return length; + } + + public int getHeadValue() { + return headValue; + } + + public double getClipLen() { + return (double) length / PPQ_RESOLUTION; + } + + private static int fromHexValue(String overall, int offset) { + return Integer.parseInt(overall.substring(offset * 2, offset * 2 + 2), 16); + } + + public boolean hasNotes() { + return !notes.isEmpty(); + } + + private static int fromHexValueMask(String overall, int offset) { + String hex = overall.substring(offset * 2, offset * 2 + 4); + int v1 = Integer.parseInt(hex.substring(0, 2), 16); + int v2 = Integer.parseInt(hex.substring(2, 4), 16); + return ((v1 & 0x3F) << 7) | v2; + } + + public void addNoteData(String data) { + int nrOfNotes = data.length() / 16; + for (int i = 0; i < nrOfNotes; i++) { + int offset = i * 16; + String noteData = data.substring(offset, offset + 16); + notes.add(new StepNote(noteData)); + } + } + + public int getFittingIndex(int position) { + for (int i = 0; i < STEP_DIVISOR.length; i++) { + if (position % STEP_DIVISOR[i] == 0) { + return i; + } + } + return -1; + } + + private int calculateResolutionIndex() { + int res = 0; + for (StepNote note : notes) { + res = Math.max(res, getFittingIndex(note.start)); + } + return res; + } + + public void applyToClip(final Clip clip, int count) { + double clipLen = getClipLen(); + clip.getPlayStop().set(clipLen); + clip.getLoopLength().set(clipLen); + clip.setName(String.format("SEQ APC %d".formatted(count))); + final int resIndex = calculateResolutionIndex(); + clip.setStepSize(RESOLUTIONS[resIndex]); + for (StepNote note : notes) { + int x = note.start / STEP_DIVISOR[resIndex]; + int y = note.note; + double len = (note.end - note.start) / PPQ_RESOLUTION; + clip.setStep(x, y, note.vel, len); + } + } +} 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 new file mode 100644 index 00000000..78d53e69 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/StringUtil.java @@ -0,0 +1,58 @@ +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"}; + + 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++) { + if (currentValue.equals(list[i])) { + index = i; + break; + } + } + if (index != -1) { + final int next = index + inc; + if (next >= 0 && next < list.length) { + return list[next]; + } else if (wrap) { + index = next < 0 ? list.length - 1 : next >= list.length ? 0 : next; + } + return list[index]; + } + 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 < 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; + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/ViewControl.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/ViewControl.java new file mode 100644 index 00000000..272bd7de --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/ViewControl.java @@ -0,0 +1,170 @@ +package com.bitwig.extensions.controllers.akai.apc64; + +import com.bitwig.extension.controller.api.*; +import com.bitwig.extensions.controllers.akai.apc.common.led.ColorLookup; +import com.bitwig.extensions.framework.di.Component; + +@Component +public class ViewControl { + + private final TrackBank trackBank; + private final TrackBank maxTrackBank; + private final CursorTrack cursorTrack; + private final Track rootTrack; + private final Clip cursorClip; + private final DeviceControl deviceControl; + private int selectedTrackIndex; + private final int[] trackColors = new int[8]; + private int cursorTrackColor = 0; + private final OverviewGrid overviewGrid = new OverviewGrid(); + + public ViewControl(final ControllerHost host) { + rootTrack = host.getProject().getRootTrackGroup(); + trackBank = host.createTrackBank(8, 1, 8, true); + maxTrackBank = host.createTrackBank(64, 1, 64, false); + maxTrackBank.sceneBank().scrollPosition().markInterested(); + maxTrackBank.scrollPosition().markInterested(); + + trackBank.sceneBank().itemCount().addValueObserver(overviewGrid::setNumberOfScenes); + trackBank.channelCount().addValueObserver(overviewGrid::setNumberOfTracks); + trackBank.scrollPosition().addValueObserver(pos -> { + overviewGrid.setTrackPosition(pos); + if (maxTrackBank.scrollPosition().get() != overviewGrid.getTrackOffset()) { + maxTrackBank.scrollPosition().set(overviewGrid.getTrackOffset()); + } + }); + trackBank.sceneBank().scrollPosition().addValueObserver(pos -> { + overviewGrid.setScenePosition(pos); + if (maxTrackBank.sceneBank().scrollPosition().get() != overviewGrid.getSceneOffset()) { + maxTrackBank.sceneBank().scrollPosition().set(overviewGrid.getSceneOffset()); + } + }); + + cursorTrack = host.createCursorTrack(6, 128); + trackBank.followCursorTrack(cursorTrack); + cursorTrack.exists().markInterested(); + for (int i = 0; i < 8; i++) { + int index = i; + Track track = trackBank.getItemAt(i); + prepareTrack(track); + track.color().addValueObserver((r, g, b) -> { + trackColors[index] = ColorLookup.toColor(r, g, b); + }); + track.addIsSelectedInMixerObserver(select -> { + if (select) { + this.selectedTrackIndex = index; + } + }); + } + setUpFocusScene(); + + deviceControl = new DeviceControl(cursorTrack, rootTrack); + cursorTrack.name().markInterested(); + cursorClip = host.createLauncherCursorClip(32, 128); + cursorClip.setStepSize(0.125); + + cursorTrack.color().addValueObserver((r, g, b) -> { + this.cursorTrackColor = com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup.toColor(r, g, b); + }); + prepareTrack(cursorTrack); + } + + private void setUpFocusScene() { + for (int i = 0; i < 64; i++) { + final int trackIndex = i; + Track track = maxTrackBank.getItemAt(trackIndex); + for (int j = 0; j < 64; j++) { + int sceneIndex = j; + final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); + slot.hasContent().addValueObserver(hasContent -> { + overviewGrid.setHasClips(trackIndex, sceneIndex, hasContent); + }); + slot.isPlaybackQueued().addValueObserver(isQueued -> { + overviewGrid.markSceneQueued(sceneIndex, isQueued); + }); + } + } + } + + public int getTrackColor(int index) { + return trackColors[index]; + } + + public int getCursorTrackColor() { + return cursorTrackColor; + } + + public int getSelectedTrackIndex() { + return selectedTrackIndex; + } + + private void prepareTrack(final Track track) { + track.arm().markInterested(); + track.exists().markInterested(); + track.solo().markInterested(); + track.mute().markInterested(); + } + + public void scrollToOverview(final int trackIndex, final int sceneIndex) { + final int posX = trackIndex * 8 + overviewGrid.getTrackOffset(); + final int posY = sceneIndex * 8 + overviewGrid.getSceneOffset(); + if (posX < overviewGrid.getNumberOfTracks() && posY < overviewGrid.getNumberOfScenes()) { + trackBank.scrollPosition().set(posX); + trackBank.sceneBank().scrollPosition().set(posY); + } + } + + public boolean inOverviewGrid(final int trackIndex, final int sceneIndex) { + return overviewGrid.inGrid(trackIndex, sceneIndex); + } + + public boolean canScrollVertical(final int delta) { + int newPos = overviewGrid.getScenePosition() + delta; + return newPos >= 0 && newPos < overviewGrid.getNumberOfScenes(); + } + + + public boolean canScrollHorizontal(final int delta) { + int newPos = overviewGrid.getTrackPosition() + delta; + return newPos >= 0 && newPos < overviewGrid.getNumberOfTracks(); + } + + public boolean inOverviewGridFocus(final int trackIndex, final int sceneIndex) { + final int locX = overviewGrid.getTrackPosition() / 8; + final int locY = overviewGrid.getScenePosition() / 8; + return locX == trackIndex && locY == sceneIndex; + } + + + public TrackBank getTrackBank() { + return trackBank; + } + + public CursorTrack getCursorTrack() { + return cursorTrack; + } + + public Track getRootTrack() { + return rootTrack; + } + + public Clip getCursorClip() { + return cursorClip; + } + + public OverviewGrid getOverviewGrid() { + return overviewGrid; + } + + public DeviceControl getDeviceControl() { + return deviceControl; + } + + public boolean hasQueuedClips(int sceneIndex) { + return overviewGrid.hasQueuedScenes(sceneIndex); + } + + public boolean hasClips(int trackIndex, int sceneIndex) { + return overviewGrid.hasClips(trackIndex, sceneIndex); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/FaderBinding.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/FaderBinding.java new file mode 100644 index 00000000..f5dc8822 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/FaderBinding.java @@ -0,0 +1,31 @@ +package com.bitwig.extensions.controllers.akai.apc64.control; + +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extensions.framework.Binding; + +public class FaderBinding extends Binding { + + private double lastValue = 0.0; + + public FaderBinding(final Parameter source, final FaderResponse target) { + super(target, source, target); + source.value().addValueObserver(this::valueChange); + } + + private void valueChange(final double value) { + lastValue = value; + if (isActive()) { + getTarget().sendValue(value); + } + } + + @Override + protected void deactivate() { + } + + @Override + protected void activate() { + getTarget().sendValue(lastValue); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/FaderLightState.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/FaderLightState.java new file mode 100644 index 00000000..29958ad0 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/FaderLightState.java @@ -0,0 +1,40 @@ +package com.bitwig.extensions.controllers.akai.apc64.control; + +import com.bitwig.extension.controller.api.HardwareLightVisualState; +import com.bitwig.extension.controller.api.InternalHardwareLightState; +import com.bitwig.extensions.controllers.akai.apc.common.led.SingleLedState; + +public class FaderLightState extends InternalHardwareLightState { + + public static final FaderLightState OFF = new FaderLightState(0); + public static final FaderLightState V_WHITE = new FaderLightState(1); + public static final FaderLightState V_RED = new FaderLightState(2); + public static final FaderLightState BIPOLOAR_WHITE = new FaderLightState(3); + public static final FaderLightState BIPOLOAR_RED = new FaderLightState(4); + + private int code; + + private FaderLightState(int code) { + this.code = code; + } + + @Override + public HardwareLightVisualState getVisualState() { + return null; + } + + @Override + public boolean equals(final Object o) { + if(o == this) { + return true; + } + if(o instanceof FaderLightState state) { + return state.code == code; + } + return false; + } + + public int getCode() { + return code; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/FaderResponse.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/FaderResponse.java new file mode 100644 index 00000000..895ebd0c --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/FaderResponse.java @@ -0,0 +1,35 @@ +package com.bitwig.extensions.controllers.akai.apc64.control; + +import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; + +public class FaderResponse { + private final MidiProcessor midiProcessor; + private final int aftertouchValue; + int lastValue = -1; + + public FaderResponse(final MidiProcessor midi, final int which) { + aftertouchValue = 0xE0 | which; + this.midiProcessor = midi; + } + + public void sendValue(final double v) { + final int value = (int) (v * 16383); + if (value != lastValue) { + lastValue = value; + final int lsb = value & 0x7F; + final int msb = value >> 7; + midiProcessor.sendMidi(aftertouchValue, lsb, msb); + } + } + + public int getWhich() { + return aftertouchValue & 0xF; + } + + public void refresh() { + final int lsb = lastValue & 0x7F; + final int msb = lastValue >> 7; + midiProcessor.sendMidi(aftertouchValue, lsb, msb); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/OledBacklight.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/OledBacklight.java new file mode 100644 index 00000000..20bc65e8 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/OledBacklight.java @@ -0,0 +1,48 @@ +package com.bitwig.extensions.controllers.akai.apc64.control; + +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.InternalHardwareLightState; +import com.bitwig.extension.controller.api.MultiStateHardwareLight; +import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; +import com.bitwig.extensions.controllers.akai.apc.common.led.RgbLightState; +import com.bitwig.extensions.framework.Layer; + +import java.util.function.Supplier; + +public class OledBacklight { + + private final MultiStateHardwareLight light; + private final MidiProcessor midiProcessor; + private final int midiId; + + public OledBacklight(HardwareSurface hwSurface, MidiProcessor midiProcessor, int midiId) { + light = hwSurface.createMultiStateHardwareLight("OLED_COLOR_" + midiId); + this.midiProcessor = midiProcessor; + this.midiId = midiId; + light.state().onUpdateHardware(this::updateState); + } + + // Touch State Base 0x68 + // 0 - Off + // 1 - V white + // 2 - V red + // 3 - P white + // 4 - P red + + private void updateState(final InternalHardwareLightState internalHardwareLightState) { + if (internalHardwareLightState instanceof RgbLightState) { + RgbLightState state = (RgbLightState) internalHardwareLightState; + //midiProcessor.sendMidi(0xB0, 0x68, 1); + midiProcessor.sendMidi(0xB0, midiId, state.getColorIndex()); + } + } + + public int getState() { + return ((RgbLightState) light.state().currentValue()).getColorIndex(); + } + + public void bind(Layer layer, Supplier supplier) { + layer.bindLightState(supplier, light); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/SingleLedButton.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/SingleLedButton.java new file mode 100644 index 00000000..ba809de3 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/SingleLedButton.java @@ -0,0 +1,36 @@ +package com.bitwig.extensions.controllers.akai.apc64.control; + +import com.bitwig.extension.api.Color; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.InternalHardwareLightState; +import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; +import com.bitwig.extensions.controllers.akai.apc.common.control.ApcButton; +import com.bitwig.extensions.controllers.akai.apc.common.led.RgbLightState; +import com.bitwig.extensions.controllers.akai.apc.common.led.VarSingleLedState; +import com.bitwig.extensions.framework.values.Midi; + +public class SingleLedButton extends ApcButton { + + public SingleLedButton(final int noteNr, final String name, final HardwareSurface surface, + final MidiProcessor midiProcessor) { + super(0, noteNr, name, surface, midiProcessor); + light.state().setValue(RgbLightState.OFF); + light.state().onUpdateHardware(this::updateState); + light.setColorToStateFunction(this::colorToState); + } + + private InternalHardwareLightState colorToState(final Color color) { + if (color.getRed255() == 0 && color.getBlue255() == 0 && color.getGreen255() == 0) { + return VarSingleLedState.OFF; + } + return VarSingleLedState.FULL; + } + + private void updateState(final InternalHardwareLightState internalHardwareLightState) { + if (internalHardwareLightState instanceof VarSingleLedState state) { + midiProcessor.sendMidi(Midi.NOTE_ON | state.getChannel(), midiId, state.getCode()); + } else { + midiProcessor.sendMidi(Midi.NOTE_ON, midiId, 0); + } + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/TouchSlider.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/TouchSlider.java new file mode 100644 index 00000000..4c5fe52c --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/TouchSlider.java @@ -0,0 +1,96 @@ +package com.bitwig.extensions.controllers.akai.apc64.control; + +import com.bitwig.extension.controller.api.*; +import com.bitwig.extensions.controllers.akai.apc.common.led.RgbLightState; +import com.bitwig.extensions.controllers.akai.apc64.Apc64MidiProcessor; +import com.bitwig.extensions.controllers.akai.apc64.layer.MainDisplay; +import com.bitwig.extensions.framework.Layer; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class TouchSlider { + + private final HardwareSlider fader; + private final FaderResponse response; + private final HardwareButton touchButton; + private final MultiStateHardwareLight light; + private final MultiStateHardwareLight lightState; + + private final int index; + private final Apc64MidiProcessor midiProcessor; + + public TouchSlider(final int index, final HardwareSurface surface, final Apc64MidiProcessor midiProcessor) { + fader = surface.createHardwareSlider("FADER_" + index); + this.index = index; + this.midiProcessor = midiProcessor; + final MidiIn midiIn = midiProcessor.getMidiIn(); + fader.setAdjustValueMatcher(midiIn.createAbsolutePitchBendValueMatcher(index)); + + response = new FaderResponse(midiProcessor, index); + + touchButton = surface.createHardwareButton("FADER_TOUCH_" + index); + touchButton.pressedAction().setActionMatcher(midiIn.createNoteOnActionMatcher(0, 0x52 + index)); + touchButton.releasedAction().setActionMatcher(midiIn.createNoteOffActionMatcher(0, 0x52 + index)); + touchButton.isPressed().markInterested(); + fader.setHardwareButton(touchButton); + light = surface.createMultiStateHardwareLight("FADER_COLOR_" + index); + light.state().onUpdateHardware(this::updateLight); + lightState = surface.createMultiStateHardwareLight("FADER_STATE_" + index); + lightState.state().onUpdateHardware(this::updateState); + } + + private void updateLight(final InternalHardwareLightState internalHardwareLightState) { + if (internalHardwareLightState instanceof RgbLightState state) { + midiProcessor.sendMidi(0xB0, 0x70 + index, state.getColorIndex()); + } + } + + private void updateState(final InternalHardwareLightState internalHardwareLightState) { + if (internalHardwareLightState instanceof FaderLightState state) { + midiProcessor.sendMidi(0xB0, 0x68 + index, state.getCode()); + } + } + + public void bindParameter(final Layer layer, MainDisplay display, StringValue parameterOwner, + final Parameter parameter) { + layer.addBinding(new FaderBinding(parameter, response)); + layer.addBinding( + new TouchSliderControlBinding(index, this, parameter, parameterOwner, midiProcessor.getShiftMode(), + midiProcessor.getClearMode(), display)); + } + + public void bindIsPressed(final Layer layer, Consumer consumer) { + layer.bind(touchButton, touchButton.pressedAction(), () -> consumer.accept(true)); + layer.bind(touchButton, touchButton.releasedAction(), () -> consumer.accept(false)); + } + + public void bindLightColor(final Layer layer, Supplier supplier) { + layer.bindLightState(supplier, light); + } + + public void bindLightState(final Layer layer, Supplier supplier) { + layer.bindLightState(supplier, lightState); + } + + public boolean isTouched() { + return touchButton.isPressed().get(); + } + + public HardwareSlider getFader() { + return fader; + } + + public void sendValue(final int value) { + response.sendValue(0); + } + + + public HardwareButton getTouchButton() { + return touchButton; + } + + public boolean isAutomated() { + return false; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/TouchSliderControlBinding.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/TouchSliderControlBinding.java new file mode 100644 index 00000000..f095212e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/control/TouchSliderControlBinding.java @@ -0,0 +1,144 @@ +package com.bitwig.extensions.controllers.akai.apc64.control; + +import com.bitwig.extension.controller.api.*; +import com.bitwig.extensions.controllers.akai.apc64.layer.MainDisplay; +import com.bitwig.extensions.framework.Binding; +import com.bitwig.extensions.framework.values.BooleanValueObject; + +public class TouchSliderControlBinding extends Binding { + + private final MainDisplay display; + private final StringValue parameterOwner; + private final int sliderIndex; + private AbsoluteHardwareControlBinding hardwareBinding; + private final Parameter parameter; + private double downParameterValue; + private final TouchSlider slider; + private boolean active = false; + private double sliderDownValue; + private boolean stripTouched; + private boolean fineModeActive = false; + private boolean stripJustTouched = false; + private boolean clearActive = false; + + public TouchSliderControlBinding(int sliderIndex, final TouchSlider source, final Parameter target, + StringValue parameterOwner, BooleanValueObject fineModifierActive, + BooleanValue clearModifier, MainDisplay display) { + super(source, source.getFader(), target); + this.sliderIndex = sliderIndex; + this.parameter = target; + this.slider = source; + this.display = display; + this.parameterOwner = parameterOwner; + this.parameterOwner.markInterested(); + parameter.name().markInterested(); + fineModifierActive.addValueObserver(this::enableFineMode); + clearModifier.addValueObserver(this::handleClearActive); + slider.getTouchButton().isPressed().addValueObserver(this::handleStripTouched); + source.getFader().value().addValueObserver(this::handleSliderValue); + target.displayedValue().addValueObserver(this::handleParamChanged); + } + + private void handleClearActive(boolean clearActive) { + this.clearActive = clearActive; + if (!active) { + return; + } + if (clearActive) { + deactivateValueBinding(); + } else { + activate(); + } + } + + private void handleParamChanged(String value) { + if (active && stripTouched) { + display.setParameterValue(value); + } + } + + private void handleStripTouched(boolean touched) { + if (!active) { + return; + } + if (clearActive) { + if (touched) { + parameter.restoreAutomationControl(); + } + } else if (touched) { + stripJustTouched = true; + this.downParameterValue = parameter.value().get(); + if (active) { + display.touchParameter(parameterOwner.get(), parameter.name().get(), parameter.displayedValue().get()); + } + } else if (active && this.stripTouched) { + display.releaseTouchParameter(sliderIndex); + } + this.stripTouched = touched; + } + + private void handleSliderValue(double value) { + if (stripJustTouched) { + this.sliderDownValue = value; + stripJustTouched = false; + } + if (!active) { + return; + } + if (fineModeActive && stripTouched) { + handleDelta(value); + } + } + + private void enableFineMode(boolean fineActive) { + fineModeActive = fineActive; + if (!active) { + return; + } + if (fineActive) { + if (stripTouched) { + downParameterValue = parameter.getAsDouble(); + } + deactivateValueBinding(); + } else { + activate(); + } + } + + private void handleDelta(double value) { + if (!active) { + return; + } + double delta = (value - sliderDownValue) * 0.25; + double newValue = Math.max(0, Math.min(1, downParameterValue + delta)); + parameter.setImmediately(newValue); + } + + private void deactivateValueBinding() { + if (hardwareBinding != null) { + hardwareBinding.removeBinding(); + hardwareBinding = null; + } + } + + @Override + protected void activate() { + active = true; + hardwareBinding = addHardwareBinding(); + } + + @Override + protected void deactivate() { + active = false; + if (hardwareBinding == null) { + return; + } + hardwareBinding.removeBinding(); + hardwareBinding = null; + } + + protected AbsoluteHardwareControlBinding addHardwareBinding() { + return getSource().addBindingWithRange(getTarget(), 0, 1); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/MainDisplay.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/MainDisplay.java new file mode 100644 index 00000000..4b01ef83 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/MainDisplay.java @@ -0,0 +1,544 @@ +package com.bitwig.extensions.controllers.akai.apc64.layer; + +import com.bitwig.extension.controller.api.Application; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.Groove; +import com.bitwig.extension.controller.api.SettableBeatTimeValue; +import com.bitwig.extension.controller.api.SettableBooleanValue; +import com.bitwig.extension.controller.api.SettableEnumValue; +import com.bitwig.extension.controller.api.Transport; +import com.bitwig.extensions.controllers.akai.apc.common.OrientationFollowType; +import com.bitwig.extensions.controllers.akai.apc.common.led.RgbLightState; +import com.bitwig.extensions.controllers.akai.apc.common.led.VarSingleLedState; +import com.bitwig.extensions.controllers.akai.apc64.Apc64CcAssignments; +import com.bitwig.extensions.controllers.akai.apc64.Apc64MidiProcessor; +import com.bitwig.extensions.controllers.akai.apc64.ApcPreferences; +import com.bitwig.extensions.controllers.akai.apc64.DeviceControl; +import com.bitwig.extensions.controllers.akai.apc64.HardwareElements; +import com.bitwig.extensions.controllers.akai.apc64.Menu; +import com.bitwig.extensions.controllers.akai.apc64.ModifierStates; +import com.bitwig.extensions.controllers.akai.apc64.PadMode; +import com.bitwig.extensions.controllers.akai.apc64.ViewControl; +import com.bitwig.extensions.controllers.akai.apc64.control.OledBacklight; +import com.bitwig.extensions.controllers.akai.apc64.control.SingleLedButton; +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 java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +public class MainDisplay { + + private final Layer mainLayer; + private final Groove groove; + private final Menu bitwigMenu; + private final OledBacklight oledBacklight; + private final DeviceControl deviceControl; + private final ApcPreferences preferences; + private boolean swingModeActive; + private Screen currentScreen; + + private final double[] FIXED_LENGTH_PRESET_VALUES = {1, 2, 4, 8, 12, 16, 20, 24, 28, 32.0, 40, 48, 56, 64}; + private final String[] RECORD_QUANTIZE = {"OFF", "1/32", "1/16", "1/8", "1/4"}; + + private final Map screens = new HashMap<>(); + + private final ControllerHost host; + + private final ViewControl viewControl; + private final Apc64MidiProcessor midiProcessor; + private long releaseTime = -1; + private long currentReleaseTime = 1000; + private final Transport transport; + + private final SettableEnumValue recordQuantizeGrid; + private SettableEnumValue postRecordingAction; + private SettableBeatTimeValue postRecordingTimeOffset; + private final SettableBooleanValue recordQuantizeLength; + private EncoderMode encoderMode = EncoderMode.TRACK; + private int touchCount = 0; + + private final ModifierStates modifierStates; + + public enum ScreenMode { + MAIN, + PARAMETER, + METRO, + FIXED, + RECORD_QUANTIZE, + LAUNCH_QUANTIZE, + MENU(true), + TEMPO, + INFO; + + private final boolean hasEmptyBackLight; + + ScreenMode() { + this(false); + } + + ScreenMode(final boolean hasEmptyBackLight) { + this.hasEmptyBackLight = hasEmptyBackLight; + } + } + + private enum EncoderMode { + TRACK, + FIXED, + RECORD_QUANTIZE, + MENU, + TEMPO + } + + public class Screen { + private final String[] rows = {"", "", ""}; + private boolean active; + private final ScreenMode mode; + + public Screen(final ScreenMode mode) { + this.mode = mode; + } + + public ScreenMode getMode() { + return mode; + } + + public void setScreen(final String row1, final String row2, final String row3) { + setRow(0, row1); + setRow(1, row2); + setRow(2, row3); + } + + public void setRow(final int row, final String value) { + if (!rows[row].equals(value)) { + rows[row] = value; + if (active) { + midiProcessor.sendText(row, value); + } + } + } + + public void setActive(final boolean active) { + if (active != this.active) { + this.active = active; + if (active) { + refresh(); + } + } + } + + public void refresh() { + for (int i = 0; i < 3; i++) { + midiProcessor.sendText(i, rows[i]); + } + } + } + + public MainDisplay(final Layers layers, final HardwareElements hwElements, final ViewControl viewControl, + final Apc64MidiProcessor midiProcessor, final ControllerHost host, final Transport transport, + final Application application, final ModifierStates modifierStates, + final ApcPreferences preferences) { + mainLayer = new Layer(layers, "ENCODER_LAYER"); + this.viewControl = viewControl; + this.midiProcessor = midiProcessor; + this.host = host; + this.transport = transport; + this.modifierStates = modifierStates; + this.oledBacklight = hwElements.getOledBackLight(); + this.preferences = preferences; + + Arrays.stream(ScreenMode.values()).forEach(mode -> screens.put(mode, new Screen(mode))); + final Screen mainScreen = screens.get(ScreenMode.MAIN); + + hwElements.getMainEncoder().bind(mainLayer, this::handleEncoder); + viewControl.getCursorTrack().name().addValueObserver(name -> mainScreen.setRow(0, name)); + + deviceControl = viewControl.getDeviceControl(); + deviceControl.getDeviceName().addValueObserver(name -> toScreen(ScreenMode.MAIN, 1, name)); + deviceControl.getPageName().addValueObserver(name -> toScreen(ScreenMode.MAIN, 2, name)); + transport.isMetronomeEnabled() + .addValueObserver(metroActive -> toScreen(ScreenMode.METRO, 2, metroActive ? "On" : "Off")); + + + recordQuantizeGrid = application.recordQuantizationGrid(); + recordQuantizeGrid.addValueObserver(value -> toScreen(ScreenMode.RECORD_QUANTIZE, 2, value)); + recordQuantizeLength = application.recordQuantizeNoteLength(); + recordQuantizeLength.markInterested(); + this.groove = host.createGroove(); + this.groove.getEnabled().markInterested(); + + currentScreen = mainScreen; + currentScreen.setActive(true); + bitwigMenu = new Menu(screens.get(ScreenMode.MENU)); + initBitwigMenu(); + hwElements.getOledBackLight().bind(mainLayer, () -> RgbLightState.of(viewControl.getCursorTrackColor())); + hwElements.getEncoderPress().bindIsPressed(mainLayer, this::handleBasicClick); + host.scheduleTask(this::handlePing, 100); + initFixedLengthEdit(hwElements); + initQuantizeEdit(hwElements); + initTempoEdit(hwElements); + + midiProcessor.addModeChangeListener(newMode -> { + if (newMode == PadMode.DRUM) { + currentScreen.refresh(); + midiProcessor.activateDawMode(true); + } + }); + } + + public void refresh() { + currentScreen.refresh(); + } + + public void initBitwigMenu() { + transport.automationWriteMode().markInterested(); + transport.isArrangerAutomationWriteEnabled().markInterested(); + final String[] autoWriteModeValues = {"latch", "touch", "write"}; + // TODO Exit with long press + // TODO Improved Value Handling int/double. + bitwigMenu.addMenuItem(new Menu.HoldMenuItem("ALT Modifier", modifierStates.getAltActive())); + bitwigMenu.addMenuItem(new Menu.EnumMenuItem("Auto W.Mode", transport.automationWriteMode(), + List.of(new Menu.EnumMenuValue("latch", "LATCH"), + new Menu.EnumMenuValue("touch", "TOUCH"), + new Menu.EnumMenuValue("write", "WRITE")))); + bitwigMenu.addMenuItem( + new Menu.BooleanToggleMenuItem("Arrange Auto", transport.isArrangerAutomationWriteEnabled())); + bitwigMenu.addMenuItem( + new Menu.BooleanToggleMenuItem("Launch Auto", transport.isClipLauncherAutomationWriteEnabled())); + bitwigMenu.addMenuItem(new Menu.BooleanToggleMenuItem("SHIFT as ALT", preferences.getAltModeWithShift())); + //bitwigMenu.addMenuItem(new Menu.BooleanMenuItem("Groove Enabled", )); + bitwigMenu.addMenuItem(new Menu.EnumMenuItem("Grid Layout", preferences.getGridLayoutSettings(), + List.of(new Menu.EnumMenuValue(OrientationFollowType.AUTOMATIC.getLabel(), + OrientationFollowType.AUTOMATIC.getShortLabel()), + new Menu.EnumMenuValue(OrientationFollowType.FIXED_VERTICAL.getLabel(), + OrientationFollowType.FIXED_VERTICAL.getShortLabel()), + new Menu.EnumMenuValue(OrientationFollowType.FIXED_HORIZONTAL.getLabel(), + OrientationFollowType.FIXED_HORIZONTAL.getShortLabel())))); + + bitwigMenu.init(); + } + + @Activate + public void init() { + mainLayer.setIsActive(true); + } + + private void toScreen(final ScreenMode mode, final int row, final String value) { + screens.get(mode).setRow(row, value); + } + + private void initQuantizeEdit(final HardwareElements hwElements) { + final SingleLedButton quantizeButton = hwElements.getButton(Apc64CcAssignments.QUANTIZE); + + quantizeButton.bindIsPressed(mainLayer, this::handleQuantizePressed); + quantizeButton.bindLightPressed(mainLayer, + pressed -> pressed ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_10); + } + + private void handleQuantizePressed(final boolean pressed) { + modifierStates.getQuantizeActive().set(pressed); + if (modifierStates.isShift()) { + if (pressed) { + activatePageDisplay(ScreenMode.RECORD_QUANTIZE, "RecordQuantize", recordQuantizeGrid.get()); + encoderMode = EncoderMode.RECORD_QUANTIZE; + } else { + returnToMain(); + encoderMode = EncoderMode.TRACK; + } + } else { + // No Quantize Value available via API + } + } + + private void initFixedLengthEdit(final HardwareElements hwElements) { + final SingleLedButton fixedLengthButton = hwElements.getButton(Apc64CcAssignments.FIXED); + postRecordingTimeOffset = transport.getClipLauncherPostRecordingTimeOffset(); + postRecordingAction = transport.clipLauncherPostRecordingAction(); + postRecordingAction.markInterested(); + postRecordingTimeOffset.markInterested(); + postRecordingTimeOffset.addValueObserver(v -> { + toScreen(ScreenMode.FIXED, 2, beatValueToString(v)); + }); + fixedLengthButton.bindDelayedHold(mainLayer, this::toggleFixedMode, this::editFixedLength, 500); + fixedLengthButton.bindLight(mainLayer, pressed -> postRecordingAction.get() + .equals("play_recorded") ? (pressed ? VarSingleLedState.PULSE_4 : VarSingleLedState.FULL) : VarSingleLedState.LIGHT_10); + } + + private void initTempoEdit(final HardwareElements hwElements) { + final SingleLedButton button = hwElements.getButton(Apc64CcAssignments.TEMPO); + modifierStates.getShiftActive().addValueObserver(active -> { + if (!active && swingModeActive) { + setSwingActive(false); + } + }); + //button.bindDelayedHold(mainLayer, () -> transport.tapTempo(), this::handleTempoPressed, 400); + button.bindIsPressed(mainLayer, this::handleTempoPressed); + transport.tempo().value().markInterested(); + transport.tempo().displayedValue().markInterested(); + transport.tempo().displayedValue().addValueObserver(value -> toScreen(ScreenMode.TEMPO, 2, value)); + } + + private void handleTempoPressed(final boolean pressed) { + if (pressed) { + if (modifierStates.isShift()) { + setSwingActive(true); + } else { + transport.tapTempo(); + encoderMode = EncoderMode.TEMPO; + activatePageDisplay(ScreenMode.TEMPO, "Tempo", transport.tempo().displayedValue().get(), 500); + } + } else { + setSwingActive(false); + encoderMode = EncoderMode.TRACK; + notifyRelease(); + } + } + + private void setSwingActive(final boolean active) { + if (this.swingModeActive != active) { + this.swingModeActive = active; + if (active) { + midiProcessor.exitSessionMode(); + midiProcessor.sendMidi(0x96, 0x48, 0x7f); + } else { + midiProcessor.enterSessionMode(); + midiProcessor.sendMidi(0x96, 0x48, 0x00); + } + } + } + + public void notifyRelease() { + releaseTime = System.currentTimeMillis(); + } + + private void editFixedLength(final boolean held) { + if (held) { + activatePageDisplay(ScreenMode.FIXED, "Fixed Length", beatValueToString(postRecordingTimeOffset.get())); + encoderMode = EncoderMode.FIXED; + } else { + returnToMain(); + encoderMode = EncoderMode.TRACK; + } + } + + private void toggleFixedMode() { + if (postRecordingAction.get().equals("play_recorded")) { + postRecordingAction.set("off"); + } else { + postRecordingAction.set("play_recorded"); + } + } + + private void handlePing() { + if (releaseTime != -1 && (System.currentTimeMillis() - releaseTime) > currentReleaseTime) { + changeScreenMode(stashedMode == null ? ScreenMode.MAIN : stashedMode); + releaseTime = -1; + stashedMode = null; + if (!midiProcessor.modeHasTextControl() && midiProcessor.isSessionModeState()) { + midiProcessor.exitSessionMode(); + } + } + host.scheduleTask(this::handlePing, 100); + } + + private void handleBasicClick(final boolean pressed) { + if (encoderMode == EncoderMode.TRACK) { + handleClickMainMode(pressed); + } else if (encoderMode == EncoderMode.MENU) { + handleMenuClick(pressed); + } else if (encoderMode == EncoderMode.RECORD_QUANTIZE) { + if (pressed) { + recordQuantizeLength.toggle(); + activatePageDisplay(ScreenMode.RECORD_QUANTIZE, "RecordQ.Len", + recordQuantizeLength.get() ? "OFF" : "ON"); + } else { + activatePageDisplay(ScreenMode.RECORD_QUANTIZE, "RecordQuantize", recordQuantizeGrid.get()); + } + } + } + + private void handleClickMainMode(final boolean pressed) { + if (modifierStates.isShift()) { + if (pressed) { + if (currentScreen.getMode() == ScreenMode.MAIN && encoderMode != EncoderMode.MENU) { + encoderMode = EncoderMode.MENU; + changeScreenMode(ScreenMode.MENU); + } + } + } else { + if (pressed) { + activatePageDisplay(ScreenMode.METRO, "Metronome", ""); + transport.isMetronomeEnabled().toggle(); + } else { + releaseToMain(500); + } + } + } + + private String beatValueToString(final double v) { + final double bars = v / 4; + if (bars == 1.0) { + return "1 Bar"; + } else if (bars > 1) { + return "%d Bars".formatted((int) bars); + } else { + return "%d Beats".formatted((int) v); + } + } + + private void handleEncoder(final int dir) { + if (encoderMode == EncoderMode.TRACK) { + if (dir < 0) { + viewControl.getCursorTrack().selectPrevious(); + } else { + viewControl.getCursorTrack().selectNext(); + } + } else if (encoderMode == EncoderMode.FIXED) { + final int current = valueIndex(postRecordingTimeOffset.get(), FIXED_LENGTH_PRESET_VALUES); + final int next = current + dir; + if (next >= 0 && next < FIXED_LENGTH_PRESET_VALUES.length) { + postRecordingTimeOffset.set(FIXED_LENGTH_PRESET_VALUES[next]); + } + } else if (encoderMode == EncoderMode.RECORD_QUANTIZE) { + final int current = valueIndex(recordQuantizeGrid.get(), RECORD_QUANTIZE); + final int next = current + dir; + if (next >= 0 && next < RECORD_QUANTIZE.length) { + recordQuantizeGrid.set(RECORD_QUANTIZE[next]); + } + } else if (encoderMode == EncoderMode.TEMPO) { + double value = transport.tempo().getRaw(); + value += dir; + transport.tempo().setRaw(value); + } else if (encoderMode == EncoderMode.MENU) { + handleMenuEncoder(dir); + } + } + + private void handleMenuEncoder(final int dir) { + bitwigMenu.handleInc(dir); + } + + private void handleMenuClick(final boolean pressed) { + if (modifierStates.isShift() && pressed) { + encoderMode = EncoderMode.TRACK; + returnToMain(); + } else { + bitwigMenu.handEncoderClick(pressed); + } + } + + private int valueIndex(final String value, final String[] values) { + for (int i = 0; i < values.length; i++) { + if (value.equals(values[i])) { + return i; + } + } + return -1; + } + + private int valueIndex(final double value, final double[] values) { + for (int i = 0; i < values.length; i++) { + if (values[i] == value) { + return i; + } + } + for (int i = 0; i < values.length - 1; i++) { + final double v1 = values[i]; + final double v2 = values[i + 1]; + if (v1 < value && value < v2) { + if (Math.abs(v1 - value) < Math.abs(v2 - value)) { + return i; + } else { + return i + 1; + } + } + } + return values.length - 1; + } + + public void setParameterValue(final String value) { + toScreen(ScreenMode.PARAMETER, 2, value); + } + + public void activatePageDisplay(final ScreenMode mode, final String parameterName, final String value) { + final Screen screen = screens.get(mode); + screen.setRow(0, ""); + screen.setRow(1, parameterName); + screen.setRow(2, value); + changeScreenMode(mode); + midiProcessor.enterSessionMode(); + releaseTime = -1; + } + + public void activatePageDisplay(final ScreenMode mode, final String parameterName, final String value, + final long releaseTime) { + activatePageDisplay(mode, parameterName, value); + midiProcessor.enterSessionMode(); + currentReleaseTime = releaseTime; + } + + public void enterMode(final ScreenMode mode, final String parameterName, final String value) { + final Screen screen = screens.get(mode); + screen.setRow(0, ""); + screen.setRow(1, parameterName); + screen.setRow(2, value); + changeScreenMode(mode); + releaseToMain(2000); + } + + public void returnToMain() { + changeScreenMode(ScreenMode.MAIN); + } + + private ScreenMode stashedMode = null; + + public void touchParameter(final String destination, final String parameterName, final String value) { + final Screen screen = screens.get(ScreenMode.PARAMETER); + + screen.setRow(0, destination); + screen.setRow(1, parameterName); + screen.setRow(2, value); + midiProcessor.enterSessionMode(); + if (currentScreen.getMode() != ScreenMode.PARAMETER) { + stashedMode = currentScreen.getMode(); + changeScreenMode(ScreenMode.PARAMETER); + } + touchCount++; + releaseTime = -1; + } + + public void releaseToMain(final long waitTime) { + releaseTime = System.currentTimeMillis(); + currentReleaseTime = waitTime; + } + + public void releaseTouchParameter(final int sliderIndex) { + touchCount--; + if (touchCount <= 0) { + releaseToMain(1500); + touchCount = 0; + } + } + + public void changeScreenMode(final ScreenMode mode) { + if (mode != currentScreen.getMode()) { + currentScreen.setActive(false); + final boolean changeBackLight = mode.hasEmptyBackLight != currentScreen.getMode().hasEmptyBackLight; + currentScreen = screens.get(mode); + if (changeBackLight) { + if (mode.hasEmptyBackLight) { + midiProcessor.sendMidi(0xB0, 0x59, 0); + } else { + midiProcessor.sendMidi(0xB0, 0x59, oledBacklight.getState()); + } + } + currentScreen.setActive(true); + } + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/NavigationLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/NavigationLayer.java new file mode 100644 index 00000000..a2000a68 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/NavigationLayer.java @@ -0,0 +1,296 @@ +package com.bitwig.extensions.controllers.akai.apc64.layer; + +import com.bitwig.extension.controller.api.BooleanValue; +import com.bitwig.extension.controller.api.SendBank; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.controllers.akai.apc.common.PanelLayout; +import com.bitwig.extensions.controllers.akai.apc.common.led.VarSingleLedState; +import com.bitwig.extensions.controllers.akai.apc64.*; +import com.bitwig.extensions.controllers.akai.apc64.control.SingleLedButton; +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.ValueObject; + +import java.util.function.BooleanSupplier; + +@Component +public class NavigationLayer { + + @Inject + private PadLayer padLayer; + + private final Layer sessionNavigationVertical; + private final Layer sessionNavigationHorizontal; + private final Layer padNavigation; + private final Layer deviceNavLayer; + private final Layer sendsNavLayer; + private final ViewControl viewControl; + private final ModifierStates modifierState; + private final TrackBank trackBank; + private final ValueObject panelLayout; + private PadMode currentMode = PadMode.SESSION; + + public NavigationLayer(final Layers layers, final HardwareElements hwElement, final ViewControl viewControl, + final ModifierStates modifierStates, final ApcPreferences preferences, + final Apc64MidiProcessor midiProcessor) { + sessionNavigationVertical = new Layer(layers, "SESSION_NAVIGATION_VERTICAL"); + sessionNavigationHorizontal = new Layer(layers, "SESSION_NAVIGATION_HORIZONTAL"); + padNavigation = new Layer(layers, "PAD_LAYER_NAVIGATION"); + this.deviceNavLayer = new Layer(layers, "DEVICE_NAVIGATION"); + this.sendsNavLayer = new Layer(layers, "SENDS_NAVIGATION"); + this.viewControl = viewControl; + this.modifierState = modifierStates; + this.trackBank = viewControl.getTrackBank(); + this.panelLayout = preferences.getPanelLayout(); + this.panelLayout.addValueObserver((oldValue, newValue) -> { + this.sessionNavigationVertical.setIsActive(newValue == PanelLayout.VERTICAL); + this.sessionNavigationHorizontal.setIsActive(newValue == PanelLayout.HORIZONTAL); + }); + midiProcessor.addModeChangeListener(this::handleModeChange); + + initSessionNavigation(sessionNavigationVertical, hwElement, Apc64CcAssignments.NAV_DOWN, + Apc64CcAssignments.NAV_UP, Apc64CcAssignments.NAV_LEFT, Apc64CcAssignments.NAV_RIGHT); + initSessionNavigation(sessionNavigationHorizontal, hwElement, Apc64CcAssignments.NAV_LEFT, + Apc64CcAssignments.NAV_RIGHT, Apc64CcAssignments.NAV_DOWN, Apc64CcAssignments.NAV_UP); + initPadLayerNavigation(padNavigation, hwElement); + initDeviceNavigation(deviceNavLayer, hwElement); + initSendsNavigation(sendsNavLayer, hwElement); + } + + private void handleModeChange(final PadMode mode) { + this.currentMode = mode; + activateSessionNavigation(true); + } + + @Activate + public void activateLayer() { + this.sessionNavigationVertical.setIsActive(panelLayout.get() == PanelLayout.VERTICAL); + this.sessionNavigationHorizontal.setIsActive(panelLayout.get() == PanelLayout.HORIZONTAL); + } + + private void initSessionNavigation(final Layer layer, final HardwareElements hwElements, + final Apc64CcAssignments downButton, final Apc64CcAssignments upButton, + final Apc64CcAssignments leftButton, final Apc64CcAssignments rightButton) { + final SingleLedButton navDown = hwElements.getButton(downButton); + navDown.bindRepeatHold(layer, () -> handleSessionVertical(-1)); + navDown.bindLightPressed(layer, pressed -> canNavigateVertical(pressed, -1)); + + final SingleLedButton navUp = hwElements.getButton(upButton); + navUp.bindRepeatHold(layer, () -> handleSessionVertical(1)); + navUp.bindLightPressed(layer, pressed -> canNavigateVertical(pressed, 1)); + + final SingleLedButton navLeft = hwElements.getButton(leftButton); + navLeft.bindRepeatHold(layer, () -> handleSessionHorizontal(-1)); + navLeft.bindLightPressed(layer, pressed -> canNavigateHorizontal(pressed, -1)); + + final SingleLedButton navRight = hwElements.getButton(rightButton); + navRight.bindRepeatHold(layer, () -> handleSessionHorizontal(1)); + navRight.bindLightPressed(layer, pressed -> canNavigateHorizontal(pressed, 1)); + } + + private void initPadLayerNavigation(final Layer layer, final HardwareElements hwElements) { + final SingleLedButton navDown = hwElements.getButton(Apc64CcAssignments.NAV_DOWN); + navDown.bindRepeatHold(layer, () -> handlePadModeNavigation(-1)); + navDown.bindLightPressed(layer, pressed -> canNavigatePadMode(pressed, -1)); + + final SingleLedButton navUp = hwElements.getButton(Apc64CcAssignments.NAV_UP); + navUp.bindRepeatHold(layer, () -> handlePadModeNavigation(1)); + navUp.bindLightPressed(layer, pressed -> canNavigatePadMode(pressed, 1)); + + final SingleLedButton navLeft = hwElements.getButton(Apc64CcAssignments.NAV_LEFT); + navLeft.bindIsPressed(layer, pressed -> { + }); + navLeft.bindLight(layer, () -> VarSingleLedState.OFF); + + final SingleLedButton navRight = hwElements.getButton(Apc64CcAssignments.NAV_RIGHT); + navRight.bindIsPressed(layer, pressed -> { + }); + navRight.bindLight(layer, () -> VarSingleLedState.OFF); + } + + private void handlePadModeNavigation(final int dir) { + final int amount = modifierState.isShift() ? dir * 16 : dir * 4; + padLayer.navigateBy(amount); + } + + public VarSingleLedState canNavigatePadMode(final boolean pressedState, final int dir) { + final int amount = modifierState.isShift() ? dir * 8 : dir * 4; + if (padLayer.canNavigateBy(amount)) { + return pressedState ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_25; + } + return VarSingleLedState.OFF; + } + + public void handleSessionVertical(final int dir) { + final int amount = modifierState.isShift() ? dir * 8 : dir; + trackBank.sceneBank().scrollBy(amount); + } + + public void handleSessionHorizontal(final int dir) { + final int amount = modifierState.isShift() ? dir * 8 : dir; + trackBank.scrollBy(amount); + } + + public VarSingleLedState canNavigateVertical(final boolean pressedState, final int dir) { + final int amount = modifierState.isShift() ? dir * 8 : dir; + if (viewControl.canScrollVertical(amount)) { + return pressedState ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_25; + } + return VarSingleLedState.OFF; + } + + public VarSingleLedState canNavigateHorizontal(final boolean pressedState, final int dir) { + final int amount = modifierState.isShift() ? dir * 8 : dir; + if (viewControl.canScrollHorizontal(amount)) { + return pressedState ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_25; + } + return VarSingleLedState.OFF; + } + + private void initDeviceNavigation(final Layer layer, final HardwareElements hwElements) { + final DeviceControl deviceControl = viewControl.getDeviceControl(); + final SingleLedButton leftNav = hwElements.getButton(Apc64CcAssignments.NAV_LEFT); + final SingleLedButton rightNav = hwElements.getButton(Apc64CcAssignments.NAV_RIGHT); + final SingleLedButton upNav = hwElements.getButton(Apc64CcAssignments.NAV_UP); + final SingleLedButton downNav = hwElements.getButton(Apc64CcAssignments.NAV_DOWN); + + rightNav.bindPressed(layer, () -> deviceControl.selectDevice(1)); + rightNav.bindLightPressed(layer, pressed -> canNavigate(pressed, () -> deviceControl.canScrollDevices(1))); + leftNav.bindPressed(layer, () -> deviceControl.selectDevice(-1)); + rightNav.bindLightPressed(layer, pressed -> canNavigate(pressed, () -> deviceControl.canScrollDevices(-1))); + + upNav.bindPressed(layer, () -> navigateDeviceVertical(deviceControl, 1)); + upNav.bindLightPressed(layer, pressed -> canNavigateVertical(pressed, deviceControl, 1)); + downNav.bindPressed(layer, () -> navigateDeviceVertical(deviceControl, -1)); + downNav.bindLightPressed(layer, pressed -> canNavigateVertical(pressed, deviceControl, -1)); + } + + private void navigateDeviceVertical(final DeviceControl deviceControl, final int dir) { + if (modifierState.isShift()) { + deviceControl.navigateVertical(dir); + } else { + deviceControl.selectParameterPage(dir); + } + } + + private VarSingleLedState canNavigateVertical(final boolean pressed, final DeviceControl deviceControl, + final int dir) { + if (modifierState.isShift()) { + if (deviceControl.canNavigateIntoDevice(dir)) { + return pressed ? VarSingleLedState.FULL : VarSingleLedState.PULSE_2; + } + } else { + if (deviceControl.canScrollParameterPages(dir)) { + return pressed ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_25; + } + } + return VarSingleLedState.OFF; + } + + + private void initSendsNavigation(final Layer layer, final HardwareElements hwElements) { + final SingleLedButton leftNav = hwElements.getButton(Apc64CcAssignments.NAV_LEFT); + final SingleLedButton rightNav = hwElements.getButton(Apc64CcAssignments.NAV_RIGHT); + final SingleLedButton upNav = hwElements.getButton(Apc64CcAssignments.NAV_UP); + final SingleLedButton downNav = hwElements.getButton(Apc64CcAssignments.NAV_DOWN); + for (int i = 0; i < viewControl.getTrackBank().getSizeOfBank(); i++) { + final SendBank sendsBank = viewControl.getTrackBank().getItemAt(i).sendBank(); + sendsBank.canScrollBackwards().markInterested(); + sendsBank.canScrollForwards().markInterested(); + sendsBank.scrollPosition().markInterested(); + } + final SendBank sendsBank = viewControl.getTrackBank().getItemAt(0).sendBank(); + rightNav.bindPressed(layer, this::scrollSendsBackward); + rightNav.bindLightPressed(layer, pressed -> canNavigate(pressed, sendsBank.canScrollBackwards())); + leftNav.bindPressed(layer, this::scrollSendsForward); + leftNav.bindLightPressed(layer, pressed -> canNavigate(pressed, sendsBank.canScrollForwards())); + + downNav.bindPressed(layer, () -> { + }); + downNav.bindLightPressed(layer, pressed -> VarSingleLedState.OFF); + upNav.bindPressed(layer, () -> { + }); + upNav.bindLightPressed(layer, pressed -> VarSingleLedState.OFF); + } + + private void scrollSendsForward() { + final TrackBank bank = viewControl.getTrackBank(); + for (int i = 0; i < bank.getSizeOfBank(); i++) { + bank.getItemAt(i).sendBank().scrollForwards(); + } + } + + private void scrollSendsBackward() { + final TrackBank bank = viewControl.getTrackBank(); + for (int i = 0; i < bank.getSizeOfBank(); i++) { + bank.getItemAt(i).sendBank().scrollBackwards(); + } + } + + + public void navigateSends() { + final TrackBank bank = viewControl.getTrackBank(); + + for (int i = 0; i < bank.getSizeOfBank(); i++) { + scrollRoundRobin(bank.getItemAt(i).sendBank()); + } + + } + + private void scrollRoundRobin(final SendBank sendBank) { + if (sendBank.canScrollForwards().get()) { + sendBank.scrollForwards(); + } else { + sendBank.scrollPosition().set(0); + } + } + + + private VarSingleLedState canNavigate(final boolean pressed, final BooleanValue value) { + if (value.get()) { + return pressed ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_25; + } + return VarSingleLedState.OFF; + } + + private VarSingleLedState canNavigate(final boolean pressed, final BooleanSupplier value) { + if (value.getAsBoolean()) { + return pressed ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_25; + } + return VarSingleLedState.OFF; + } + + public void setDeviceNavigationActive(final boolean active) { + sendsNavLayer.setIsActive(false); + deviceNavLayer.setIsActive(active); + activateSessionNavigation(!active); + } + + public void setSendsNavigationActive(final boolean active) { + deviceNavLayer.setIsActive(false); + sendsNavLayer.setIsActive(active); + activateSessionNavigation(!active); + } + + public void activateSessionNavigation(final boolean active) { + if (active) { + if (currentMode == PadMode.SESSION || currentMode == PadMode.OVERVIEW) { + this.sessionNavigationVertical.setIsActive(panelLayout.get() == PanelLayout.VERTICAL); + this.sessionNavigationHorizontal.setIsActive(panelLayout.get() == PanelLayout.HORIZONTAL); + this.padNavigation.setIsActive(false); + } else if (currentMode == PadMode.DRUM) { + sessionNavigationVertical.setIsActive(false); + sessionNavigationHorizontal.setIsActive(false); + this.padNavigation.setIsActive(true); + } + } else { + sessionNavigationVertical.setIsActive(false); + sessionNavigationHorizontal.setIsActive(false); + padNavigation.setIsActive(false); + } + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/OverviewLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/OverviewLayer.java new file mode 100644 index 00000000..af94816c --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/OverviewLayer.java @@ -0,0 +1,48 @@ +package com.bitwig.extensions.controllers.akai.apc64.layer; + +import com.bitwig.extensions.controllers.akai.apc.common.control.RgbButton; +import com.bitwig.extensions.controllers.akai.apc.common.led.RgbLightState; +import com.bitwig.extensions.controllers.akai.apc64.HardwareElements; +import com.bitwig.extensions.controllers.akai.apc64.ViewControl; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; + + +public class OverviewLayer extends Layer { + + private final ViewControl viewControl; + + public OverviewLayer(final Layers layers, ViewControl viewControl, HardwareElements hwElements) { + super(layers, "OVERVIEW_LAYER"); + this.viewControl = viewControl; + for (int i = 0; i < 8; i++) { + final int trackIndex = i; + for (int j = 0; j < 8; j++) { + final int sceneIndex = j; + final RgbButton button = hwElements.getGridButton(sceneIndex, trackIndex); + button.bindPressed(this, () -> handleSelection(trackIndex, sceneIndex)); + button.bindLight(this, () -> getState(trackIndex, sceneIndex)); + } + } + } + + private void handleSelection(final int trackIndex, final int sceneIndex) { + viewControl.scrollToOverview(trackIndex, sceneIndex); + } + + private RgbLightState getState(final int trackIndex, final int sceneIndex) { + if (viewControl.inOverviewGridFocus(trackIndex, sceneIndex)) { + if (viewControl.hasClips(trackIndex, sceneIndex)) { + return RgbLightState.ORANGE_SEL; + } + return RgbLightState.WHITE_SEL; + } + if (viewControl.hasClips(trackIndex, sceneIndex)) { + return RgbLightState.ORANGE_FULL; + } + if (viewControl.inOverviewGrid(trackIndex, sceneIndex)) { + return RgbLightState.WHITE_DIM; + } + return RgbLightState.OFF; + } +} \ No newline at end of file diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/PadLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/PadLayer.java new file mode 100644 index 00000000..04adfaf1 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/PadLayer.java @@ -0,0 +1,424 @@ +package com.bitwig.extensions.controllers.akai.apc64.layer; + +import com.bitwig.extension.controller.api.*; +import com.bitwig.extensions.controllers.akai.apc.common.control.RgbButton; +import com.bitwig.extensions.controllers.akai.apc.common.led.ColorLookup; +import com.bitwig.extensions.controllers.akai.apc.common.led.LedBehavior; +import com.bitwig.extensions.controllers.akai.apc.common.led.RgbLightState; +import com.bitwig.extensions.controllers.akai.apc64.*; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.di.Inject; +import com.bitwig.extensions.framework.di.PostConstruct; + +import java.util.Arrays; + +@Component +public class PadLayer extends Layer { + + private static final int[] VEL_TABLE = {5, 10, 25, 60, 75, 90, 100, 127}; + private static final int[] FIXED_COLORS = {42, 42, 41, 41, 46, 46, 45, 45}; + + private final double[] rateTable = {0.0833333, 0.125, 0.1666666, 0.25, 0.33333, 0.5, 0.666666, 1.0}; + + //private final double[] rateTable = {0.125, 0.25, 0.5, 1.0, 2.0}; + //private final String[] rateDisplayValues = {"1/32T", "1/32", "1/16T", "1/16", "1/8T", "1/8", "1/4T", "1/4"}; + + private final double[] arpRateTable = {1.0, 0.5, 0.33333, 0.25, 0.1666666, 0.125, 0.0833333, 0.0625}; + private final String[] rateDisplayValues = {"1/4", "1/8", "1/8T", "1/16", "1/16T", "1/32", "1/32T", "1/64"}; + private static final int[] ARP_COLORS = {53, 53, 56, 53, 56, 53, 56, 53}; + + @Inject + private MainDisplay mainDisplay; + @Inject + private FocusClip focusClip; + private final ModifierStates states; + + private final Apc64MidiProcessor midiProcessor; + private final ViewControl viewControl; + private final DrumPadBank drumPadBank; + private final NoteInput noteInput; + private PadMode currentMode = PadMode.SESSION; + private boolean inDrumMode = false; + + private final Layer shiftLayer; + private final Layer clearLayer; + private final Layer muteLayer; + private final Layer soloLayer; + + protected final int[] padToNote = new int[16]; + private final Integer[] noteTable = new Integer[128]; + private final Integer[] velocityTable = new Integer[128]; + private final int[] padColors = new int[16]; + private final boolean[] isSelected = new boolean[16]; + private final boolean[] isPlaying = new boolean[128]; + private int padOffset = 36; + private int fixedVelocity = -1; + private int selectedVelocityIndex = -1; + private int selectedNoteRepeatIndex = -1; + private int soloHeld = 0; + private final Arpeggiator arp; + + public PadLayer(Layers layers, ViewControl viewControl, Apc64MidiProcessor midiProcessor, ModifierStates states) { + super(layers, "PAD_LAYER"); + + this.shiftLayer = new Layer(layers, "PAD_SHIFT_LAYER"); + this.clearLayer = new Layer(layers, "PAD_CLEAR_LAYER"); + this.muteLayer = new Layer(layers, "PAD_MUTE_LAYER"); + this.soloLayer = new Layer(layers, "PAD_SOLO_LAYER"); + this.midiProcessor = midiProcessor; + this.noteInput = midiProcessor.getNoteInput(); + arp = noteInput.arpeggiator(); + initArp(); + this.viewControl = viewControl; + this.states = states; + viewControl.getCursorTrack().playingNotes().addValueObserver(this::handleNotes); + this.states.getShiftActive().addValueObserver(mod -> applyLayers()); + this.states.getClearActive().addValueObserver(mod -> applyLayers()); + PinnableCursorDevice primaryDevice = viewControl.getDeviceControl().getPrimaryDevice(); + primaryDevice.hasDrumPads().addValueObserver(this::handleHasDrumPadsChanged); + drumPadBank = viewControl.getDeviceControl().getDrumPadBank(); + drumPadBank.scrollPosition().addValueObserver(this::handlePadBankScrolling); + + Arrays.fill(padColors, 0); + Arrays.fill(noteTable, -1); + Arrays.fill(padToNote, -1); + setVelocity(-1); + midiProcessor.addModeChangeListener(currentMode -> { + this.currentMode = currentMode; + if (isActive()) { + midiProcessor.restoreState(); + } + }); + } + + public void duplicateContent() { + if (inDrumMode) { + focusClip.duplicateContent(); + } + } + + private void initArp() { + arp.isEnabled().markInterested(); + arp.usePressureToVelocity().markInterested(); + arp.usePressureToVelocity().set(true); + arp.octaves().markInterested(); + arp.rate().markInterested(); + arp.mode().markInterested(); + arp.rate().set(arpRateTable[0]); + } + + private void setVelocity(int fixedValue) { + for (int i = 0; i < 128; i++) { + velocityTable[i] = fixedValue == -1 ? i : fixedValue; + } + } + + @PostConstruct + public void init(HardwareElements hwElements) { + for (int i = 0; i < 4; i++) { + final int columnIndex = i; + for (int j = 0; j < 4; j++) { + final int rowIndex = j; + int padIndex = rowIndex * 4 + columnIndex; + DrumPad pad = drumPadBank.getItemAt(padIndex); + setUpPad(padIndex, pad); + final RgbButton button = hwElements.getGridButton(7 - rowIndex, columnIndex); + button.bindLight(this, () -> getPadLight(padIndex, pad)); + button.bindPressed(muteLayer, () -> pad.mute().toggle()); + button.bindLight(muteLayer, () -> getPadMuteLight(padIndex, pad)); + button.bindIsPressed(soloLayer, pressed -> handleSolo(pressed, pad)); + button.bindLight(soloLayer, () -> getPadSoloLight(padIndex, pad)); + button.bindPressed(shiftLayer, () -> handleSelect(padIndex, pad)); + button.bindPressed(clearLayer, () -> clearNotes(padIndex)); + } + } + for (int row = 4; row < 6; row++) { + for (int col = 4; col < 8; col++) { + final RgbButton button = hwElements.getGridButton(row, col); + int index = (5 - row) * 4 + (col - 4); + button.bindPressed(this, () -> selectVelocity(index)); + button.bindLight(this, () -> getVelocityColors(index)); + } + } + for (int row = 6; row < 8; row++) { + for (int col = 4; col < 8; col++) { + final RgbButton button = hwElements.getGridButton(row, col); + int index = (7 - row) * 4 + (col - 4); + button.bindIsPressed(this, pressed -> setNoteRepeat(index, pressed)); + button.bindLight(this, () -> getNoteRepeatColors(index)); + } + } + } + + private void handleSelect(int padIndex, DrumPad pad) { + if (isSelected[padIndex]) { + PinnableCursorDevice cursorDevice = viewControl.getDeviceControl().getCursorDevice(); + if (cursorDevice.hasDrumPads().get()) { + cursorDevice.selectFirstInKeyPad(padToNote[padIndex]); + } else { + cursorDevice.selectParent(); + } + } else { + pad.selectInEditor(); + } + } + + private void handleSolo(boolean pressed, DrumPad pad) { + if (pressed) { + pad.solo().toggle(soloHeld == 0); + soloHeld++; + } else { + soloHeld--; + } + } + + private void setNoteRepeat(int index, boolean pressed) { + if (pressed) { + if (index == selectedNoteRepeatIndex) { + selectedNoteRepeatIndex = -1; + arp.isEnabled().set(false); + mainDisplay.enterMode(MainDisplay.ScreenMode.INFO, "Note Repeat", "Off"); + } else { + selectedNoteRepeatIndex = index; + mainDisplay.enterMode(MainDisplay.ScreenMode.INFO, "Note Repeat", + rateDisplayValues[selectedNoteRepeatIndex]); + double arpRate = arpRateTable[selectedNoteRepeatIndex]; + arp.rate().set(arpRate); + arp.mode().set("all"); // that's the note repeat way + arp.octaves().set(0); + arp.humanize().set(0); + arp.isFreeRunning().set(false); + arp.isEnabled().set(true); + } + } + } + + private RgbLightState getNoteRepeatColors(int padIndex) { + if (selectedNoteRepeatIndex == padIndex) { + return RgbLightState.WHITE; + } + return RgbLightState.of(ARP_COLORS[padIndex]); + } + + + private void selectVelocity(int index) { + if (index == selectedVelocityIndex) { + selectedVelocityIndex = -1; + fixedVelocity = -1; + setVelocity(-1); + this.noteInput.setVelocityTranslationTable(velocityTable); + mainDisplay.enterMode(MainDisplay.ScreenMode.INFO, "Fixed Velocity", "Off"); + } else { + selectedVelocityIndex = index; + fixedVelocity = VEL_TABLE[selectedVelocityIndex]; + mainDisplay.enterMode(MainDisplay.ScreenMode.INFO, "Fixed Velocity", "%d".formatted(fixedVelocity)); + setVelocity(fixedVelocity); + this.noteInput.setVelocityTranslationTable(velocityTable); + } + } + + private RgbLightState getVelocityColors(int padIndex) { + if (selectedVelocityIndex == padIndex) { + return RgbLightState.WHITE; + } + LedBehavior behavior = padIndex % 2 == 0 ? LedBehavior.LIGHT_50 : LedBehavior.FULL; + return RgbLightState.of(FIXED_COLORS[padIndex], behavior); + } + + private void clearNotes(int padIndex) { + if (padToNote[padIndex] != -1) { + focusClip.clearNotes(padToNote[padIndex]); + } + } + + private RgbLightState getPadMuteLight(int padIndex, DrumPad pad) { + if (pad.exists().get()) { + if (pad.mute().get()) { + return isPlaying(padIndex) ? RgbLightState.MUTE_PLAY_FULL : RgbLightState.ORANGE_FULL; + } else { + return isPlaying(padIndex) ? RgbLightState.MUTE_PLAY_DIM : RgbLightState.ORANGE_DIM; + } + } + return isPlaying(padIndex) ? RgbLightState.WHITE : RgbLightState.WHITE_DIM; + } + + private RgbLightState getPadSoloLight(int padIndex, DrumPad pad) { + if (pad.exists().get()) { + if (pad.solo().get()) { + return isPlaying(padIndex) ? RgbLightState.SOLO_PLAY_FULL : RgbLightState.YELLOW_FULL; + } else { + return isPlaying(padIndex) ? RgbLightState.SOLO_PLAY_YELLOW_DIM : RgbLightState.YELLOW_DIM; + } + } + return isPlaying(padIndex) ? RgbLightState.WHITE : RgbLightState.WHITE_DIM; + } + + private RgbLightState getPadLight(int padIndex, DrumPad pad) { + if (isSelected[padIndex]) { + return isPlaying(padIndex) ? RgbLightState.WHITE : RgbLightState.WHITE_SEL; + } + if (pad.exists().get()) { + LedBehavior lightState = isPlaying(padIndex) ? LedBehavior.FULL : LedBehavior.LIGHT_25; + if (padColors[padIndex] != 0) { + return RgbLightState.of(padColors[padIndex], lightState); + } else { + return RgbLightState.of(viewControl.getCursorTrackColor(), lightState); + } + } + return isPlaying(padIndex) ? RgbLightState.WHITE : RgbLightState.WHITE_DIM; + } + + private void handleHasDrumPadsChanged(boolean hasDrumPads) { + this.inDrumMode = hasDrumPads; + if (isActive() && currentMode.isKeyRelated()) { + midiProcessor.setDrumMode(hasDrumPads); + } + } + + private void handleNotes(final PlayingNote[] playingNotes) { + if (!isActive()) { + return; + } + Arrays.fill(isPlaying, false); + for (final PlayingNote playingNote : playingNotes) { + isPlaying[playingNote.pitch()] = true; + } + } + + public void activateMute(boolean activated) { + if (!isActive()) { + return; + } + soloHeld = 0; + muteLayer.setIsActive(activated); + padActivation(activated); + } + + public void activateSolo(boolean activated) { + if (!isActive()) { + return; + } + soloLayer.setIsActive(activated); + padActivation(activated); + } + + private void padActivation(boolean activated) { + if (activated) { + deactivateNotes(); + } else if (!shiftLayer.isActive() && !clearLayer.isActive()) { + applyScale(); + } + } + + public boolean isPlaying(final int index) { + final int offset = padOffset + index; + if (offset < 128) { + return isPlaying[offset]; + } + return false; + } + + private void handlePadBankScrolling(int scrollPos) { + padOffset = scrollPos; + selectPad(getSelectedIndex()); + if (isActive()) { + applyScale(); + } + } + + void selectPad(final int index) { + final DrumPad pad = drumPadBank.getItemAt(index); + pad.selectInEditor(); + } + + private int getSelectedIndex() { + for (int i = 0; i < 16; i++) { + if (isSelected[i]) { + return i; + } + } + return 0; + } + + public void navigateBy(int amount) { + drumPadBank.scrollBy(amount); + } + + public boolean canNavigateBy(int amount) { + int newOffset = amount + padOffset; + return newOffset >= 0 && newOffset < 112; + } + + void applyScale() { + Arrays.fill(noteTable, -1); + if (inDrumMode) { + for (int i = 0; i < 16; i++) { + int noteIndex = (i / 4) * 8 + i % 4; + noteTable[noteIndex] = padOffset + i; + padToNote[i] = padOffset + i; + } + } + if (isActive()) { + noteInput.setKeyTranslationTable(noteTable); + noteInput.setVelocityTranslationTable(velocityTable); + this.noteInput.setShouldConsumeEvents(true); + } + } + + private void setUpPad(int index, DrumPad pad) { + pad.color().addValueObserver((r, g, b) -> padColors[index] = ColorLookup.toColor(r, g, b)); + pad.name().markInterested(); + pad.exists().markInterested(); + pad.solo().markInterested(); + pad.mute().markInterested(); + pad.addIsSelectedInEditorObserver(selected -> isSelected[index] = selected); + } + + private void applyLayers() { + if (!isActive()) { + return; + } + if (states.isClear()) { + clearLayer.setIsActive(true); + shiftLayer.setIsActive(false); + deactivateNotes(); + } else if (states.isShift()) { + shiftLayer.setIsActive(true); + clearLayer.setIsActive(false); + deactivateNotes(); + } else { + clearLayer.setIsActive(false); + shiftLayer.setIsActive(false); + applyScale(); + } + } + + @Override + protected void onActivate() { + super.onActivate(); + if ((currentMode.isKeyRelated()) && inDrumMode) { + midiProcessor.setDrumMode(true); + applyScale(); + } + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + deactivateNotes(); + soloHeld = 0; + shiftLayer.setIsActive(false); + clearLayer.setIsActive(false); + muteLayer.setIsActive(false); + soloLayer.setIsActive(false); + } + + private void deactivateNotes() { + Arrays.fill(noteTable, -1); + noteInput.setKeyTranslationTable(noteTable); + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/ParameterControlLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/ParameterControlLayer.java new file mode 100644 index 00000000..079050b9 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/ParameterControlLayer.java @@ -0,0 +1,286 @@ +package com.bitwig.extensions.controllers.akai.apc64.layer; + +import com.bitwig.extension.controller.api.*; +import com.bitwig.extensions.controllers.akai.apc.common.led.ColorLookup; +import com.bitwig.extensions.controllers.akai.apc.common.led.RgbLightState; +import com.bitwig.extensions.controllers.akai.apc.common.led.VarSingleLedState; +import com.bitwig.extensions.controllers.akai.apc64.Apc64CcAssignments; +import com.bitwig.extensions.controllers.akai.apc64.DeviceControl; +import com.bitwig.extensions.controllers.akai.apc64.HardwareElements; +import com.bitwig.extensions.controllers.akai.apc64.ViewControl; +import com.bitwig.extensions.controllers.akai.apc64.control.FaderLightState; +import com.bitwig.extensions.controllers.akai.apc64.control.SingleLedButton; +import com.bitwig.extensions.controllers.akai.apc64.control.TouchSlider; +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 java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Component +public class ParameterControlLayer extends Layer { + + + private enum Mode { + DEVICE(Apc64CcAssignments.STRIP_DEVICE), + VOLUME(Apc64CcAssignments.STRIP_VOLUME), + PAN(Apc64CcAssignments.STRIP_PAN), + SENDS(Apc64CcAssignments.STRIP_SENDS), + CHANNEL_STRIP(Apc64CcAssignments.STRIP_CHANNEL), + OFF(Apc64CcAssignments.STRIP_OFF); + private final Apc64CcAssignments assignment; + + Mode(Apc64CcAssignments assignment) { + this.assignment = assignment; + } + + public Apc64CcAssignments getAssignment() { + return assignment; + } + } + + private Mode currentMode = Mode.VOLUME; + private DeviceControl.Focus currentDeviceFocus = DeviceControl.Focus.DEVICE; + private final Map modes = new HashMap<>(); + private final Map deviceModes = new HashMap<>(); + + @Inject + private NavigationLayer navigationSection; + + private final ViewControl viewControl; + private int cursorTrackColor = 0; + private final MainDisplay display; + + public ParameterControlLayer(final Layers layers, HardwareElements hwElements, ViewControl viewControl, + MainDisplay mainDisplay) { + super(layers, "PARAMETER CONTROL"); + this.viewControl = viewControl; + this.display = mainDisplay; + Arrays.stream(Mode.values()).forEach(mode -> modes.put(mode, new Layer(layers, "STRIP_" + mode.toString()))); + Arrays.stream(DeviceControl.Focus.values()) + .forEach(mode -> deviceModes.put(mode, new Layer(layers, "DEVICE_" + mode.toString()))); + deviceModes.put(DeviceControl.Focus.DEVICE, modes.get(Mode.DEVICE)); + bindModeToButton(hwElements, Mode.DEVICE); + bindModeToButton(hwElements, Mode.VOLUME); + bindModeToButton(hwElements, Mode.PAN); + bindModeToButton(hwElements, Mode.SENDS); + bindModeToButton(hwElements, Mode.CHANNEL_STRIP); + bindModeToButton(hwElements, Mode.OFF); + TouchSlider[] touchSliders = hwElements.getTouchSliders(); + bindVolumeLayer(touchSliders, viewControl.getTrackBank()); + bindPanLayer(touchSliders, viewControl.getTrackBank()); + bindSendsLayer(touchSliders, viewControl.getTrackBank()); + bindCursorLayer(touchSliders, viewControl.getCursorTrack()); + bindDeviceLayer(hwElements, viewControl.getDeviceControl()); + bindOffLayer(touchSliders); + } + + private void bindDeviceLayer(HardwareElements hwElements, DeviceControl deviceControl) { + TouchSlider[] sliders = hwElements.getTouchSliders(); + + bindToPage(sliders, deviceModes.get(DeviceControl.Focus.DEVICE), + deviceControl.getPage(DeviceControl.Focus.DEVICE), this::faderTrackColorProvider); + bindToPage(sliders, deviceModes.get(DeviceControl.Focus.TRACK), + deviceControl.getPage(DeviceControl.Focus.TRACK), this::faderTrackColorProvider); + bindToPage(sliders, deviceModes.get(DeviceControl.Focus.PROJECT), + deviceControl.getPage(DeviceControl.Focus.PROJECT), this::faderProjectColorProvider); + deviceControl.setFocusListener(focus -> changeDeviceFocus(focus)); + } + + private void bindToPage(TouchSlider[] sliders, Layer layer, CursorRemoteControlsPage remotePage, + Function colorProvider) { + for (int i = 0; i < sliders.length; i++) { + TouchSlider slider = sliders[i]; + RemoteControl parameter = remotePage.getParameter(i); + parameter.exists().markInterested(); + parameter.name().markInterested(); + bindSlider(layer, slider, parameter, colorProvider); + } + } + + private void bindSlider(Layer layer, TouchSlider slider, RemoteControl parameter, + Function colorProvider) { + slider.bindParameter(layer, display, parameter.name(), parameter); + slider.bindIsPressed(layer, pressed -> parameter.touch(pressed)); + slider.bindLightColor(layer, () -> colorProvider.apply(parameter)); + slider.bindLightState(layer, () -> !parameter.exists().get() ? FaderLightState.OFF : FaderLightState.V_WHITE); + } + + private RgbLightState faderTrackColorProvider(Parameter parameter) { + if (parameter.exists().get()) { + return RgbLightState.of(cursorTrackColor); + } + return RgbLightState.OFF; + } + + private RgbLightState faderProjectColorProvider(Parameter parameter) { + if (parameter.exists().get()) { + return RgbLightState.WHITE; + } + return RgbLightState.OFF; + } + + private void bindModeToButton(HardwareElements hwElements, Mode mode) { + SingleLedButton button = hwElements.getButton(mode.getAssignment()); + button.bindLightPressed(this, + pressed -> pressed ? VarSingleLedState.FULL : currentMode == mode ? VarSingleLedState.LIGHT_75 : VarSingleLedState.LIGHT_10); + button.bindIsPressed(this, pressed -> this.handleModeChange(pressed, mode)); + } + + private void bindVolumeLayer(TouchSlider[] sliders, TrackBank trackBank) { + Layer layer = modes.get(Mode.VOLUME); + + for (int i = 0; i < sliders.length; i++) { + int index = i; + TouchSlider slider = sliders[i]; + Track track = trackBank.getItemAt(i); + slider.bindParameter(layer, display, track.name(), track.volume()); + slider.bindIsPressed(layer, pressed -> track.volume().touch(pressed)); + slider.bindLightColor(layer, () -> !track.exists().get() ? RgbLightState.OFF : RgbLightState.of( + viewControl.getTrackColor(index))); + slider.bindLightState(layer, () -> getVolumeState(track, slider)); + } + } + + private FaderLightState getVolumeState(Track track, TouchSlider slider) { + if (track.exists().get()) { + return slider.isAutomated() ? FaderLightState.V_RED : FaderLightState.V_WHITE; + } + return FaderLightState.OFF; + } + + private void bindPanLayer(TouchSlider[] sliders, TrackBank trackBank) { + Layer layer = modes.get(Mode.PAN); + + for (int i = 0; i < sliders.length; i++) { + int index = i; + TouchSlider slider = sliders[i]; + Track track = trackBank.getItemAt(i); + slider.bindParameter(layer, display, track.name(), track.pan()); + slider.bindIsPressed(layer, pressed -> track.pan().touch(pressed)); + slider.bindLightColor(layer, () -> !track.exists().get() ? RgbLightState.OFF : RgbLightState.of( + viewControl.getTrackColor(index))); + slider.bindLightState(layer, + () -> !track.exists().get() ? FaderLightState.OFF : FaderLightState.BIPOLOAR_WHITE); + } + } + + private void bindSendsLayer(TouchSlider[] sliders, TrackBank trackBank) { + Layer layer = modes.get(Mode.SENDS); + + for (int i = 0; i < sliders.length; i++) { + int index = i; + TouchSlider slider = sliders[i]; + Track track = trackBank.getItemAt(i); + Send send = track.sendBank().getItemAt(0); + send.exists().markInterested(); + slider.bindParameter(layer, display, track.name(), send); + slider.bindIsPressed(layer, pressed -> send.touch(pressed)); + slider.bindLightColor(layer, () -> getSendColor(track, send, index)); + slider.bindLightState(layer, () -> getSendState(track, send)); + } + } + + private void bindCursorLayer(TouchSlider[] sliders, CursorTrack cursorTrack) { + Layer layer = modes.get(Mode.CHANNEL_STRIP); + cursorTrack.color().addValueObserver((r, g, b) -> cursorTrackColor = ColorLookup.toColor(r, g, b)); + sliders[0].bindParameter(layer, display, cursorTrack.name(), cursorTrack.volume()); + sliders[0].bindIsPressed(layer, pressed -> cursorTrack.volume().touch(pressed)); + sliders[0].bindLightColor(layer, + () -> !cursorTrack.exists().get() ? RgbLightState.OFF : RgbLightState.of(cursorTrackColor)); + sliders[0].bindLightState(layer, + () -> !cursorTrack.exists().get() ? FaderLightState.OFF : FaderLightState.V_WHITE); + + sliders[1].bindParameter(layer, display, cursorTrack.name(), cursorTrack.pan()); + sliders[1].bindIsPressed(layer, pressed -> cursorTrack.pan().touch(pressed)); + sliders[1].bindLightColor(layer, + () -> !cursorTrack.exists().get() ? RgbLightState.OFF : RgbLightState.of(cursorTrackColor)); + sliders[1].bindLightState(layer, + () -> !cursorTrack.exists().get() ? FaderLightState.OFF : FaderLightState.BIPOLOAR_WHITE); + + for (int i = 0; i < 6; i++) { + int index = i; + TouchSlider slider = sliders[i + 2]; + Send send = cursorTrack.sendBank().getItemAt(i); + send.exists().markInterested(); + slider.bindParameter(layer, display, cursorTrack.name(), send); + slider.bindIsPressed(layer, pressed -> send.touch(pressed)); + slider.bindLightColor(layer, () -> getSendColor(cursorTrack, send, index)); + slider.bindLightState(layer, () -> getSendState(cursorTrack, send)); + } + } + + private RgbLightState getSendColor(Track track, Send send, int index) { + if (track.exists().get() && send.exists().get()) { + return RgbLightState.of(cursorTrackColor); + } + return RgbLightState.OFF; + } + + private FaderLightState getSendState(Track track, Send send) { + if (track.exists().get() && send.exists().get()) { + return FaderLightState.V_WHITE; + } + return FaderLightState.OFF; + } + + private void bindOffLayer(TouchSlider[] sliders) { + Layer layer = modes.get(Mode.OFF); + + for (int i = 0; i < sliders.length; i++) { + TouchSlider slider = sliders[i]; + slider.bindIsPressed(layer, pressed -> { + }); + slider.bindLightColor(layer, () -> RgbLightState.OFF); + slider.bindLightState(layer, () -> FaderLightState.OFF); + } + } + + private void handleModeChange(boolean pressed, Mode mode) { + if (pressed) { + if (currentMode != mode) { + getLayerFromMode(currentMode).setIsActive(false); + currentMode = mode; + getLayerFromMode(currentMode).setIsActive(true); + } else if (currentMode == Mode.SENDS) { + navigationSection.navigateSends(); + } + } + if (currentMode == Mode.DEVICE) { + navigationSection.setDeviceNavigationActive(pressed); + } else if (currentMode == Mode.SENDS) { + navigationSection.setSendsNavigationActive(pressed); + } + } + + private void changeDeviceFocus(DeviceControl.Focus newFocus) { + if (newFocus == currentDeviceFocus) { + return; + } + if (currentMode == Mode.DEVICE) { + getLayerFromMode(currentMode).setIsActive(false); + currentDeviceFocus = newFocus; + getLayerFromMode(currentMode).setIsActive(true); + } else { + currentDeviceFocus = newFocus; + } + } + + private Layer getLayerFromMode(Mode mode) { + if (mode == Mode.DEVICE) { + return deviceModes.get(currentDeviceFocus); + } + return modes.get(mode); + } + + @Activate + public void activateLayer() { + this.activate(); + modes.get(currentMode).setIsActive(true); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/SessionLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/SessionLayer.java new file mode 100644 index 00000000..c3117293 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/SessionLayer.java @@ -0,0 +1,163 @@ +package com.bitwig.extensions.controllers.akai.apc64.layer; + +import com.bitwig.extension.controller.api.*; +import com.bitwig.extensions.controllers.akai.apc.common.AbstractSessionLayer; +import com.bitwig.extensions.controllers.akai.apc.common.PanelLayout; +import com.bitwig.extensions.controllers.akai.apc.common.control.RgbButton; +import com.bitwig.extensions.controllers.akai.apc64.*; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.di.Inject; +import com.bitwig.extensions.framework.di.PostConstruct; + +public class SessionLayer extends AbstractSessionLayer { + + private final ControllerHost host; + private final ApcPreferences preferences; + @Inject + private ViewControl viewControl; + @Inject + private Transport transport; + @Inject + private ModifierStates modifiers; + @Inject + private Apc64MidiProcessor midiProcessor; + @Inject + private FocusClip focusClip; + + private final Layer horizontalLayer; + private final Layer verticalLayer; + private TrackBank trackBank; + private PanelLayout panelLayout; + + public SessionLayer(final Layers layers, final ControllerHost host, final ApcPreferences preferences) { + super(layers); + this.horizontalLayer = new Layer(layers, "HORIZONTAL_LAYER"); + this.verticalLayer = new Layer(layers, "VERTICAL_LAYER"); + this.host = host; + this.preferences = preferences; + + panelLayout = this.preferences.getPanelLayout().get(); + this.preferences.getPanelLayout() + .addValueObserver((layoutOld, layoutNew) -> handlePanelLayoutChange(layoutNew)); + } + + private void handlePanelLayoutChange(final PanelLayout layoutNew) { + panelLayout = layoutNew; + if (isActive()) { + horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); + verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); + } + } + + @PostConstruct + public void init(final HardwareElements hwElements) { + initClipControl(hwElements, 8); + } + + private void initClipControl(final HardwareElements hwElements, final int numberOfScenes) { + clipLauncherOverdub = transport.isClipLauncherOverdubEnabled(); + clipLauncherOverdub.markInterested(); + transport.isPlaying().markInterested(); + final CursorTrack cursorTrack = viewControl.getCursorTrack(); + + trackBank = viewControl.getTrackBank(); + markTrackBank(trackBank); + trackBank.setShouldShowClipLauncherFeedback(true); + initGridControl(numberOfScenes, hwElements, trackBank); + } + + private void initGridControl(final int numberOfScenes, final HardwareElements hwElements, + final TrackBank trackBank) { + for (int i = 0; i < 8; i++) { + final int trackIndex = i; + final Track track = trackBank.getItemAt(trackIndex); + markTrack(track); + for (int j = 0; j < numberOfScenes; j++) { + final int sceneIndex = j; + final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); + prepareSlot(slot, sceneIndex, trackIndex); + + final RgbButton button = hwElements.getGridButton(sceneIndex, trackIndex); + button.bindPressed(verticalLayer, () -> handleSlotPressed(slot, track)); + button.bindRelease(verticalLayer, () -> handleSlotReleased(slot)); + button.bindLight(verticalLayer, () -> getState(track, slot, trackIndex, sceneIndex)); + + final RgbButton horizontalButton = hwElements.getGridButton(trackIndex, sceneIndex); + horizontalButton.bindPressed(horizontalLayer, () -> handleSlotPressed(slot, track)); + horizontalButton.bindRelease(horizontalLayer, () -> handleSlotReleased(slot)); + horizontalButton.bindLight(horizontalLayer, () -> getState(track, slot, trackIndex, sceneIndex)); + } + } + } + + @Override + protected boolean isPlaying() { + return transport.isPlaying().get(); + } + + @Override + protected boolean isShiftHeld() { + return modifiers.isShift(); + } + + private void handleSlotPressed(final ClipLauncherSlot slot, final Track track) { + if (modifiers.getAltActive().get()) { + slot.launchAlt(); + } else if (modifiers.isShift()) { + if (modifiers.isDuplicate()) { + track.selectInMixer(); + sequence(slot, () -> focusClip.duplicateContent()); + } else if (modifiers.isClear()) { + if (slot.hasContent().get()) { + track.selectInMixer(); + sequence(slot, () -> focusClip.clearSteps()); + } + } else { + slot.select(); + } + } else if (modifiers.isClear()) { + slot.deleteObject(); + } else if (modifiers.isDuplicate()) { + slot.duplicateClip(); + } else if (modifiers.getQuantizeActive().get()) { + if (slot.hasContent().get()) { + track.selectInMixer(); + sequence(slot, () -> focusClip.quantize(1.0)); + } + } else { + slot.launch(); + } + } + + private void sequence(final ClipLauncherSlot slot, final Runnable action) { + slot.select(); + host.scheduleTask(() -> { + host.scheduleTask(action, 40); + }, 40); + } + + private void handleSlotReleased(final ClipLauncherSlot slot) { + if (modifiers.getAltActive().get()) { + slot.launchReleaseAlt(); + } else { + slot.launchRelease(); + } + } + + @Override + protected void onActivate() { + super.onActivate(); + horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); + verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + horizontalLayer.setIsActive(false); + verticalLayer.setIsActive(false); + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/TrackAndSceneLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/TrackAndSceneLayer.java new file mode 100644 index 00000000..6fef58d7 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/TrackAndSceneLayer.java @@ -0,0 +1,232 @@ +package com.bitwig.extensions.controllers.akai.apc64.layer; + +import com.bitwig.extension.controller.api.*; +import com.bitwig.extensions.controllers.akai.apc.common.PanelLayout; +import com.bitwig.extensions.controllers.akai.apc.common.control.RgbButton; +import com.bitwig.extensions.controllers.akai.apc.common.led.LedBehavior; +import com.bitwig.extensions.controllers.akai.apc.common.led.RgbLightState; +import com.bitwig.extensions.controllers.akai.apc.common.led.VarSingleLedState; +import com.bitwig.extensions.controllers.akai.apc64.ApcPreferences; +import com.bitwig.extensions.controllers.akai.apc64.HardwareElements; +import com.bitwig.extensions.controllers.akai.apc64.ModifierStates; +import com.bitwig.extensions.controllers.akai.apc64.ViewControl; +import com.bitwig.extensions.controllers.akai.apc64.control.SingleLedButton; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.di.Inject; +import com.bitwig.extensions.framework.di.PostConstruct; + +public class TrackAndSceneLayer extends Layer { + private final static String[] LAUNCH_QUANTIZE_VALUES = {"8", "4", "2", "1", "1/4", "1/8", "1/16", "1/2"}; + private final static String[] LAUNCH_QUANTIZE_DISPLAY_VALUES = {"8 Bars", "4 Bars", "2 Bars", "1 Bar", "1/4", "1" + "/8", "1/16", "1/2"}; + + @Inject + private ViewControl viewControl; + @Inject + private ModifierStates modifiers; + @Inject + private Transport transport; + @Inject + private MainDisplay mainDisplay; + + private final Layer horizontalLayer; + private final Layer verticalLayer; + private final Layer shiftLayer; + + private final Track rootTrack; + private int sceneOffset; + private TrackBank trackBank; + private PanelLayout panelLayout; + private final ApcPreferences preferences; + + public TrackAndSceneLayer(final Layers layers, final ApcPreferences preferences, final Project project) { + super(layers, "TRACKS_AND_SCENES"); + this.horizontalLayer = new Layer(layers, "HORIZONTAL_LAYER"); + this.verticalLayer = new Layer(layers, "VERTICAL_LAYER"); + this.shiftLayer = new Layer(layers, "TRACK_SHIFT_LAYER"); + rootTrack = project.getRootTrackGroup(); + this.preferences = preferences; + panelLayout = this.preferences.getPanelLayout().get(); + this.preferences.getPanelLayout().addValueObserver((layoutOld, layoutNew) -> { + panelLayout = layoutNew; + horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); + verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); + }); + } + + @PostConstruct + public void init(final HardwareElements hwElements) { + final int numberOfScenes = 8; + trackBank = viewControl.getTrackBank(); + final SceneBank sceneBank = trackBank.sceneBank(); + + final Scene targetScene = trackBank.sceneBank().getScene(0); + targetScene.clipCount().markInterested(); + sceneBank.setIndication(true); + sceneBank.scrollPosition().addValueObserver(value -> sceneOffset = value); + + modifiers.getShiftActive().addValueObserver(shiftActive -> { + if (!preferences.useShiftForAltMode() || panelLayout == PanelLayout.VERTICAL) { + shiftLayer.setIsActive(shiftActive); + } + }); + + for (int sceneIndex = 0; sceneIndex < numberOfScenes; sceneIndex++) { + final SingleLedButton sceneButton = hwElements.getSceneButton(sceneIndex); + final int index = sceneIndex; + final Scene scene = sceneBank.getScene(index); + scene.clipCount().markInterested(); + sceneButton.bindPressed(verticalLayer, () -> handleScenePressed(scene, index)); + sceneButton.bindRelease(verticalLayer, () -> handleSceneReleased(scene)); + sceneButton.bindLight(verticalLayer, () -> getSceneState(index, scene)); + final Track track = viewControl.getTrackBank().getItemAt(index); + sceneButton.bindIsPressed(horizontalLayer, pressed -> handleTrackSelect(pressed, index, track)); + sceneButton.bindLight(horizontalLayer, () -> getTrackState(index, track)); + } + + for (int i = 0; i < 8; i++) { + final int index = i; + final RgbButton button = hwElements.getTrackSelectButton(i); + final Track track = viewControl.getTrackBank().getItemAt(i); + button.bindIsPressed(verticalLayer, pressed -> handleTrackSelect(pressed, index, track)); + button.bindLight(verticalLayer, () -> getTrackColor(index, track)); + final Scene scene = sceneBank.getScene(index); + button.bindPressed(horizontalLayer, () -> handleScenePressed(scene, index)); + button.bindRelease(horizontalLayer, () -> handleSceneReleased(scene)); + button.bindLight(horizontalLayer, () -> getSceneColor(index, scene)); + } + initLaunchQuantizeControl(transport, hwElements); + } + + private void initLaunchQuantizeControl(final Transport transport, final HardwareElements hwElements) { + transport.defaultLaunchQuantization().markInterested(); + for (int i = 0; i < 8; i++) { + final int index = i; + final RgbButton button = hwElements.getTrackSelectButton(i); + button.bindLight(shiftLayer, () -> quantizeState(index)); + button.bindIsPressed(shiftLayer, pressed -> selectLaunchQuantize(index, pressed)); + } + } + + private void handleTrackSelect(final boolean pressed, final int index, final Track track) { + if (!pressed) { + return; + } + if (modifiers.isClear()) { + track.deleteObject(); + } else if (modifiers.isDuplicate()) { + track.duplicate(); + } else { + track.selectInMixer(); + } + } + + private void handleScenePressed(final Scene scene, final int index) { + if (modifiers.getAltActive().get() && !modifiers.isShift()) { + scene.launchAlt(); + } else if (modifiers.isShift()) { + handleSpecial(index); + } else if (modifiers.isClear()) { + scene.deleteObject(); + } else if (modifiers.isDuplicate()) { + // TODO This needs an API Change to work reliably + } else { + scene.launch(); + } + } + + private void handleSpecial(final int index) { + if (index == 7) { + rootTrack.stop(); + } + } + + private void handleSceneReleased(final Scene scene) { + if (modifiers.getAltActive().get() && !modifiers.isShift()) { + scene.launchReleaseAlt(); + } else { + scene.launchRelease(); + } + } + + private VarSingleLedState getSceneState(final int index, final Scene scene) { + if (scene.clipCount().get() > 0) { + if (viewControl.hasQueuedClips(sceneOffset + index)) { + return VarSingleLedState.BLINK_8; + } + return VarSingleLedState.LIGHT_10; + } + return VarSingleLedState.OFF; + } + + private RgbLightState getSceneColor(final int index, final Scene scene) { + if (scene.clipCount().get() > 0) { + if (viewControl.hasQueuedClips(sceneOffset + index)) { + return RgbLightState.WHITE_BRIGHT.behavior(LedBehavior.BLINK_8); + } + return RgbLightState.WHITE_BRIGHT.behavior(LedBehavior.LIGHT_25); + } + return RgbLightState.OFF; + } + + private RgbLightState getTrackColor(final int index, final Track track) { + if (track.exists().get()) { + if (index == viewControl.getSelectedTrackIndex()) { + return RgbLightState.WHITE_BRIGHT; + } + return RgbLightState.of(viewControl.getTrackColor(index), LedBehavior.LIGHT_50); + } + return RgbLightState.OFF; + } + + private VarSingleLedState getTrackState(final int index, final Track track) { + if (track.exists().get()) { + if (index == viewControl.getSelectedTrackIndex()) { + return VarSingleLedState.FULL; + } + return VarSingleLedState.LIGHT_10; + } + return VarSingleLedState.OFF; + } + + private void selectLaunchQuantize(final int index, final boolean pressed) { + if (pressed) { + final SettableEnumValue launchQuantizeValue = transport.defaultLaunchQuantization(); + if (launchQuantizeValue.get().equals(LAUNCH_QUANTIZE_VALUES[index])) { + launchQuantizeValue.set("none"); + mainDisplay.activatePageDisplay(MainDisplay.ScreenMode.LAUNCH_QUANTIZE, "LaunchQuantize", "none"); + } else { + launchQuantizeValue.set(LAUNCH_QUANTIZE_VALUES[index]); + mainDisplay.activatePageDisplay(MainDisplay.ScreenMode.LAUNCH_QUANTIZE, "LaunchQuantize", + LAUNCH_QUANTIZE_DISPLAY_VALUES[index]); + } + } else { + mainDisplay.notifyRelease(); + } + } + + private RgbLightState quantizeState(final int index) { + if (transport.defaultLaunchQuantization().get().equals(LAUNCH_QUANTIZE_VALUES[index])) { + return RgbLightState.WHITE_BRIGHT; + } + return RgbLightState.WHITE_DIM; + } + + @Override + protected void onActivate() { + super.onActivate(); + horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); + verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); + shiftLayer.setIsActive( + modifiers.isShift() && (panelLayout == PanelLayout.VERTICAL || !preferences.useShiftForAltMode())); + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + horizontalLayer.setIsActive(false); + verticalLayer.setIsActive(false); + shiftLayer.setIsActive(false); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/TrackControl.java b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/TrackControl.java new file mode 100644 index 00000000..acffa92e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apc64/layer/TrackControl.java @@ -0,0 +1,176 @@ +package com.bitwig.extensions.controllers.akai.apc64.layer; + +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extensions.controllers.akai.apc.common.control.RgbButton; +import com.bitwig.extensions.controllers.akai.apc.common.led.LedBehavior; +import com.bitwig.extensions.controllers.akai.apc.common.led.RgbLightState; +import com.bitwig.extensions.controllers.akai.apc.common.led.VarSingleLedState; +import com.bitwig.extensions.controllers.akai.apc64.Apc64CcAssignments; +import com.bitwig.extensions.controllers.akai.apc64.HardwareElements; +import com.bitwig.extensions.controllers.akai.apc64.ModifierStates; +import com.bitwig.extensions.controllers.akai.apc64.ViewControl; +import com.bitwig.extensions.controllers.akai.apc64.control.SingleLedButton; +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 java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +@Component +public class TrackControl extends Layer { + + private static final RgbLightState STOP_PLAY_COLOR = RgbLightState.of(21); + private static final RgbLightState STOP_COLOR = RgbLightState.of(21, LedBehavior.LIGHT_10); + private static final RgbLightState STOP_QUEDED_COLOR = RgbLightState.of(21, LedBehavior.BLINK_8); + + private final ModifierStates modifierStates; + private Mode mode = Mode.MUTE; + private final Map layerMap = new HashMap<>(); + private int soloHeld = 0; + + @Inject + private PadLayer padLayer; + + private enum Mode { + ARM, + MUTE, + SOLO, + STOP + } + + public TrackControl(Layers layers, HardwareElements hwElements, ModifierStates modifierStates, + ViewControl viewControl) { + super(layers, "TRACK_CONTROL"); + this.modifierStates = modifierStates; + Arrays.stream(Mode.values()).forEach(mode -> layerMap.put(mode, new Layer(layers, "TRACK_CONTROL_" + mode))); + bindModeButton(hwElements, Apc64CcAssignments.MODE_REC, Mode.ARM); + SingleLedButton muteButton = bindModeButton(hwElements, Apc64CcAssignments.MODE_MUTE, Mode.MUTE); + muteButton.bindIsPressed(this, pressed -> padLayer.activateMute(pressed)); + SingleLedButton soloButton = bindModeButton(hwElements, Apc64CcAssignments.MODE_SOLO, Mode.SOLO); + soloButton.bindIsPressed(this, pressed -> padLayer.activateSolo(pressed)); + bindModeButton(hwElements, Apc64CcAssignments.MODE_STOP, Mode.STOP); + bindArm(hwElements, viewControl); + bindMutes(hwElements, viewControl); + bindSolo(hwElements, viewControl); + bindStop(hwElements, viewControl); + } + + @Activate + public void contextActivate() { + setIsActive(true); + layerMap.get(mode).setIsActive(true); + } + + public SingleLedButton bindModeButton(HardwareElements hwElements, Apc64CcAssignments assignment, Mode mode) { + SingleLedButton button = hwElements.getButton(assignment); + button.bindPressed(this, () -> changeMode(mode)); + button.bindLight(this, () -> this.mode == mode ? VarSingleLedState.FULL : VarSingleLedState.LIGHT_10); + return button; + } + + + public void bindMutes(HardwareElements hwElements, ViewControl viewControl) { + Layer layer = layerMap.get(Mode.MUTE); + for (int i = 0; i < 8; i++) { + RgbButton button = hwElements.getTrackControlButtons(i); + Track track = viewControl.getTrackBank().getItemAt(i); + track.mute().markInterested(); + button.bindLight(layer, () -> muteState(track)); + button.bindPressed(layer, () -> track.mute().toggle()); + } + } + + public void bindSolo(HardwareElements hwElements, ViewControl viewControl) { + Layer layer = layerMap.get(Mode.SOLO); + for (int i = 0; i < 8; i++) { + RgbButton button = hwElements.getTrackControlButtons(i); + Track track = viewControl.getTrackBank().getItemAt(i); + track.mute().markInterested(); + button.bindLight(layer, () -> soloState(track)); + button.bindIsPressed(layer, pressed -> handleSolo(track, pressed)); + } + } + + private void handleSolo(Track track, boolean pressed) { + if (pressed) { + soloHeld++; + track.solo().toggle(!modifierStates.getShiftActive().get() && soloHeld == 1); + } else { + if (soloHeld > 0) { + soloHeld--; + } + } + } + + public void bindArm(HardwareElements hwElements, ViewControl viewControl) { + Layer layer = layerMap.get(Mode.ARM); + for (int i = 0; i < 8; i++) { + RgbButton button = hwElements.getTrackControlButtons(i); + Track track = viewControl.getTrackBank().getItemAt(i); + track.arm().markInterested(); + button.bindLight(layer, () -> armState(track)); + button.bindPressed(layer, () -> track.arm().toggle()); + } + } + + public void bindStop(HardwareElements hwElements, ViewControl viewControl) { + Layer layer = layerMap.get(Mode.STOP); + for (int i = 0; i < 8; i++) { + RgbButton button = hwElements.getTrackControlButtons(i); + Track track = viewControl.getTrackBank().getItemAt(i); + track.mute().markInterested(); + track.isStopped().markInterested(); + track.isQueuedForStop().markInterested(); + button.bindLight(layer, () -> stopState(track)); + button.bindPressed(layer, () -> track.stop()); + } + } + + public RgbLightState muteState(Track track) { + if (track.exists().get()) { + return track.mute().get() ? RgbLightState.ORANGE_FULL : RgbLightState.ORANGE_DIM; + } + return RgbLightState.OFF; + } + + public RgbLightState armState(Track track) { + if (track.exists().get()) { + return track.arm().get() ? RgbLightState.RED_FULL : RgbLightState.RED_DIM; + } + return RgbLightState.OFF; + } + + public RgbLightState soloState(Track track) { + if (track.exists().get()) { + return track.solo().get() ? RgbLightState.YELLOW_FULL : RgbLightState.YELLOW_DIM; + } + return RgbLightState.OFF; + } + + public RgbLightState stopState(Track track) { + if (track.exists().get()) { + if (track.isQueuedForStop().get()) { + return STOP_QUEDED_COLOR; + } + if (track.isStopped().get()) { + return STOP_COLOR; + } + return STOP_PLAY_COLOR; + } + return RgbLightState.OFF; + } + + + public void changeMode(Mode mode) { + if (this.mode == mode) { + return; + } + layerMap.get(this.mode).setIsActive(false); + this.mode = mode; + layerMap.get(this.mode).setIsActive(true); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/AbstractAkaiApcExtension.java b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/AbstractAkaiApcExtension.java index e69e6168..7d6160db 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/AbstractAkaiApcExtension.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/AbstractAkaiApcExtension.java @@ -1,16 +1,17 @@ package com.bitwig.extensions.controllers.akai.apcmk2; +import java.time.LocalDateTime; + import com.bitwig.extension.controller.ControllerExtension; import com.bitwig.extension.controller.ControllerExtensionDefinition; import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.akai.apcmk2.control.HardwareElementsApc; -import com.bitwig.extensions.controllers.akai.apcmk2.control.SingleLedButton; +import com.bitwig.extensions.controllers.akai.apc.common.control.SingleLedButton; import com.bitwig.extensions.controllers.akai.apcmk2.layer.AbstractControlLayer; import com.bitwig.extensions.controllers.akai.apcmk2.layer.SessionLayer; import com.bitwig.extensions.controllers.akai.apcmk2.layer.TrackControlLayer; import com.bitwig.extensions.controllers.akai.apcmk2.layer.TrackMode; -import com.bitwig.extensions.controllers.akai.apcmk2.led.SingleLedState; -import com.bitwig.extensions.controllers.akai.apcmk2.midi.MidiProcessor; +import com.bitwig.extensions.controllers.akai.apc.common.led.SingleLedState; +import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.di.Context; @@ -24,16 +25,24 @@ public abstract class AbstractAkaiApcExtension extends ControllerExtension { protected final ApcConfiguration configuration; protected Class controlLayerClass; protected ApcPreferences preferences; + private static ControllerHost debugHost; protected AbstractAkaiApcExtension(final ControllerExtensionDefinition definition, final ControllerHost host, ApcConfiguration configuration) { super(definition, host); this.configuration = configuration; } + + public static void println(final String format, final Object... args) { + if (debugHost != null) { + final LocalDateTime now = LocalDateTime.now(); + debugHost.println(format.formatted(args)); + } + } @Override public void init() { - DebugApc.registerHost(getHost()); + debugHost = getHost(); final Context diContext = new Context(this); surface = diContext.getService(HardwareSurface.class); preferences = new ApcPreferences(getHost(), configuration.isHasEncoders()); diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/AkaiApcKeys25Extension.java b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/AkaiApcKeys25Extension.java index 0597851a..16822054 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/AkaiApcKeys25Extension.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/AkaiApcKeys25Extension.java @@ -4,10 +4,10 @@ import com.bitwig.extension.controller.api.MidiIn; import com.bitwig.extension.controller.api.MidiOut; import com.bitwig.extension.controller.api.Transport; -import com.bitwig.extensions.controllers.akai.apcmk2.control.SingleLedButton; +import com.bitwig.extensions.controllers.akai.apc.common.control.SingleLedButton; import com.bitwig.extensions.controllers.akai.apcmk2.layer.EncoderLayer; -import com.bitwig.extensions.controllers.akai.apcmk2.led.SingleLedState; -import com.bitwig.extensions.controllers.akai.apcmk2.midi.MidiProcessor; +import com.bitwig.extensions.controllers.akai.apc.common.led.SingleLedState; +import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; import com.bitwig.extensions.controllers.akai.apcmk2.midi.MidiProcessorDirect; import com.bitwig.extensions.framework.di.Context; import com.bitwig.extensions.framework.values.FocusMode; diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/AkaiApcMiniExtension.java b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/AkaiApcMiniExtension.java index d6de8f89..f852c765 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/AkaiApcMiniExtension.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/AkaiApcMiniExtension.java @@ -1,61 +1,61 @@ package com.bitwig.extensions.controllers.akai.apcmk2; import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.akai.apcmk2.control.SingleLedButton; +import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; +import com.bitwig.extensions.controllers.akai.apc.common.control.SingleLedButton; +import com.bitwig.extensions.controllers.akai.apc.common.led.SingleLedState; import com.bitwig.extensions.controllers.akai.apcmk2.layer.DrumPadLayer; import com.bitwig.extensions.controllers.akai.apcmk2.layer.SessionLayer; import com.bitwig.extensions.controllers.akai.apcmk2.layer.SliderLayer; -import com.bitwig.extensions.controllers.akai.apcmk2.led.SingleLedState; -import com.bitwig.extensions.controllers.akai.apcmk2.midi.MidiProcessor; import com.bitwig.extensions.controllers.akai.apcmk2.midi.MidiProcessorBuffered; import com.bitwig.extensions.framework.di.Context; public class AkaiApcMiniExtension extends AbstractAkaiApcExtension { - protected AkaiApcMiniExtension(final AkaiApcMiniDefinition definition, final ControllerHost host, - ApcConfiguration configuration) { - super(definition, host, configuration); - controlLayerClass = SliderLayer.class; - } - - @Override - protected MidiProcessor createMidiProcessor(MidiIn midiIn, MidiOut midiOut) { - return new MidiProcessorBuffered(getHost(), midiIn, midiOut); - } - - @Override - protected void init(final Context diContext) { - final DrumPadLayer drumPadLayer = diContext.create(DrumPadLayer.class); - final SingleLedButton stopAllButton = hwElements.getSceneButton(7); - ViewControl viewControl = diContext.getService(ViewControl.class); - final Track rootTrack = viewControl.getRootTrack(); - stopAllButton.bindPressed(shiftLayer, rootTrack.stopAction()); - stopAllButton.bindLightPressed(shiftLayer, SingleLedState.OFF, SingleLedState.ON); - final HardwareSlider masterSlider = hwElements.getSlider(8); - mainLayer.bind(masterSlider, rootTrack.volume()); - SessionLayer sessionLayer = diContext.getService(SessionLayer.class); - final MidiProcessor midiProcessor = diContext.getService(MidiProcessor.class); - midiProcessor.setModeChangeListener(mode -> changeMode(drumPadLayer, sessionLayer, mode)); - } - - private void changeMode(DrumPadLayer drumPadLayer, SessionLayer sessionLayer, int mode) { - if (mode == 0) { // Session Mode - drumPadLayer.setIsActive(false); - sessionLayer.setIsActive(true); - shiftLayer.setIsActive(true); - hwElements.refreshStatusButtons(); - hwElements.refreshGridButtons(); - } else if (mode == 2) { // Drum Mode - drumPadLayer.setIsActive(true); - sessionLayer.setIsActive(false); - drumPadLayer.refreshButtons(); - hwElements.refreshStatusButtons(); - } else if (mode == 1) { // Key Mode - drumPadLayer.setIsActive(false); - sessionLayer.setIsActive(false); - hwElements.refreshStatusButtons(); - } - } + protected AkaiApcMiniExtension(final AkaiApcMiniDefinition definition, final ControllerHost host, + ApcConfiguration configuration) { + super(definition, host, configuration); + controlLayerClass = SliderLayer.class; + } + + @Override + protected MidiProcessor createMidiProcessor(MidiIn midiIn, MidiOut midiOut) { + return new MidiProcessorBuffered(getHost(), midiIn, midiOut); + } + + @Override + protected void init(final Context diContext) { + final DrumPadLayer drumPadLayer = diContext.create(DrumPadLayer.class); + final SingleLedButton stopAllButton = hwElements.getSceneButton(7); + ViewControl viewControl = diContext.getService(ViewControl.class); + final Track rootTrack = viewControl.getRootTrack(); + stopAllButton.bindPressed(shiftLayer, rootTrack.stopAction()); + stopAllButton.bindLightPressed(shiftLayer, SingleLedState.OFF, SingleLedState.ON); + final HardwareSlider masterSlider = hwElements.getSlider(8); + mainLayer.bind(masterSlider, rootTrack.volume()); + SessionLayer sessionLayer = diContext.getService(SessionLayer.class); + final MidiProcessor midiProcessor = diContext.getService(MidiProcessor.class); + midiProcessor.setModeChangeListener(mode -> changeMode(drumPadLayer, sessionLayer, mode)); + } + + private void changeMode(DrumPadLayer drumPadLayer, SessionLayer sessionLayer, int mode) { + if (mode == 0) { // Session Mode + drumPadLayer.setIsActive(false); + sessionLayer.setIsActive(true); + shiftLayer.setIsActive(true); + hwElements.refreshStatusButtons(); + hwElements.refreshGridButtons(); + } else if (mode == 2) { // Drum Mode + drumPadLayer.setIsActive(true); + sessionLayer.setIsActive(false); + drumPadLayer.refreshButtons(); + hwElements.refreshStatusButtons(); + } else if (mode == 1) { // Key Mode + drumPadLayer.setIsActive(false); + sessionLayer.setIsActive(false); + hwElements.refreshStatusButtons(); + } + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/DebugApc.java b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/DebugApc.java deleted file mode 100644 index 9bff4597..00000000 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/DebugApc.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.bitwig.extensions.controllers.akai.apcmk2; - -import com.bitwig.extension.controller.api.ControllerHost; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - -public class DebugApc { - - private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("hh:mm:ss SSS"); - private static ControllerHost host; - - public static void println(final String format, final Object... args) { - if (host != null) { - final LocalDateTime now = LocalDateTime.now(); - host.println(now.format(DF) + " > " + String.format(format, args)); - } - } - - public static void registerHost(final ControllerHost host) { - DebugApc.host = host; - } -} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/HardwareElementsApc.java b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/HardwareElementsApc.java similarity index 92% rename from src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/HardwareElementsApc.java rename to src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/HardwareElementsApc.java index d989a366..9ec77c13 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/control/HardwareElementsApc.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/HardwareElementsApc.java @@ -1,10 +1,13 @@ -package com.bitwig.extensions.controllers.akai.apcmk2.control; +package com.bitwig.extensions.controllers.akai.apcmk2; import com.bitwig.extension.controller.api.HardwareSlider; import com.bitwig.extension.controller.api.HardwareSurface; import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extensions.controllers.akai.apc.common.control.Encoder; +import com.bitwig.extensions.controllers.akai.apc.common.control.RgbButton; +import com.bitwig.extensions.controllers.akai.apc.common.control.SingleLedButton; import com.bitwig.extensions.controllers.akai.apcmk2.ApcConfiguration; -import com.bitwig.extensions.controllers.akai.apcmk2.midi.MidiProcessor; +import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; import com.bitwig.extensions.framework.di.PostConstruct; public class HardwareElementsApc { diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/PanelLayout.java b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/PanelLayout.java deleted file mode 100644 index 2bb30319..00000000 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/PanelLayout.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.bitwig.extensions.controllers.akai.apcmk2; - -public enum PanelLayout { - VERTICAL, - HORIZONTAL -} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/DrumPadLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/DrumPadLayer.java index e0f70f6f..a7378308 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/DrumPadLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/DrumPadLayer.java @@ -1,12 +1,12 @@ package com.bitwig.extensions.controllers.akai.apcmk2.layer; import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.akai.apcmk2.DebugApc; +import com.bitwig.extensions.controllers.akai.apcmk2.AbstractAkaiApcExtension; import com.bitwig.extensions.controllers.akai.apcmk2.ViewControl; -import com.bitwig.extensions.controllers.akai.apcmk2.control.HardwareElementsApc; -import com.bitwig.extensions.controllers.akai.apcmk2.control.RgbButton; -import com.bitwig.extensions.controllers.akai.apcmk2.led.RgbLightState; -import com.bitwig.extensions.controllers.akai.apcmk2.midi.MidiProcessor; +import com.bitwig.extensions.controllers.akai.apcmk2.HardwareElementsApc; +import com.bitwig.extensions.controllers.akai.apc.common.control.RgbButton; +import com.bitwig.extensions.controllers.akai.apc.common.led.RgbLightState; +import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; import com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup; import com.bitwig.extensions.controllers.novation.commonsmk3.SpecialDevices; import com.bitwig.extensions.framework.Layer; @@ -47,8 +47,7 @@ public void init(final ControllerHost host, final MidiProcessor midiProcessor, final CursorTrack cursorTrack = viewCursorControl.getCursorTrack(); cursorTrack.color() .addValueObserver( - (r, g, b) -> currentTrackColor = com.bitwig.extensions.controllers.akai.apcmk2.led.ColorLookup.toColor( - r, g, b)); + (r, g, b) -> currentTrackColor = ColorLookup.toColor(r, g, b)); cursorTrack.playingNotes().addValueObserver(this::handleNotes); final PinnableCursorDevice primaryDevice = viewCursorControl.getPrimaryDevice(); @@ -60,7 +59,7 @@ public void init(final ControllerHost host, final MidiProcessor midiProcessor, drumPadBank = primaryDevice.createDrumPadBank(64); drumPadBank.scrollPosition().addValueObserver(index -> { padsNoteOffset = index; - DebugApc.println(" PAD Offset = %d", padsNoteOffset); + AbstractAkaiApcExtension.println(" PAD Offset = %d", padsNoteOffset); if (isActive()) { applyNotes(padsNoteOffset); } diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/EncoderLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/EncoderLayer.java index 7037dea1..aadd8406 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/EncoderLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/EncoderLayer.java @@ -6,8 +6,8 @@ import com.bitwig.extension.controller.api.Track; import com.bitwig.extensions.controllers.akai.apcmk2.ControlMode; import com.bitwig.extensions.controllers.akai.apcmk2.ViewControl; -import com.bitwig.extensions.controllers.akai.apcmk2.control.Encoder; -import com.bitwig.extensions.controllers.akai.apcmk2.control.HardwareElementsApc; +import com.bitwig.extensions.controllers.akai.apc.common.control.Encoder; +import com.bitwig.extensions.controllers.akai.apcmk2.HardwareElementsApc; import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.PostConstruct; diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/SessionLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/SessionLayer.java index e23295ea..19b59492 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/SessionLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/SessionLayer.java @@ -1,253 +1,205 @@ package com.bitwig.extensions.controllers.akai.apcmk2.layer; import com.bitwig.extension.controller.api.*; +import com.bitwig.extensions.controllers.akai.apc.common.AbstractSessionLayer; +import com.bitwig.extensions.controllers.akai.apc.common.PanelLayout; +import com.bitwig.extensions.controllers.akai.apc.common.control.RgbButton; +import com.bitwig.extensions.controllers.akai.apc.common.control.SingleLedButton; +import com.bitwig.extensions.controllers.akai.apc.common.led.SingleLedState; import com.bitwig.extensions.controllers.akai.apcmk2.*; -import com.bitwig.extensions.controllers.akai.apcmk2.control.HardwareElementsApc; -import com.bitwig.extensions.controllers.akai.apcmk2.control.RgbButton; -import com.bitwig.extensions.controllers.akai.apcmk2.control.SingleLedButton; -import com.bitwig.extensions.controllers.akai.apcmk2.led.LedBehavior; -import com.bitwig.extensions.controllers.akai.apcmk2.led.RgbLightState; -import com.bitwig.extensions.controllers.akai.apcmk2.led.SingleLedState; -import com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.Inject; import com.bitwig.extensions.framework.di.PostConstruct; -public class SessionLayer extends Layer { - - @Inject - private ViewControl viewControl; - @Inject - private Transport transport; - @Inject - private ModifierStates modifiers; - - private SettableBooleanValue clipLauncherOverdub; - private int sceneOffset; - private PanelLayout panelLayout = PanelLayout.VERTICAL; - private final Layer verticalLayer; - private final Layer horizontalLayer; - private TrackBank trackBankVertical; - private TrackBank trackBankHorizontal; - private final SettableBooleanValue useAlt; - - protected final int[][] colorIndex = new int[8][8]; - - public SessionLayer(final Layers layers, ApcPreferences preferences) { - super(layers, "SESSION_LAYER"); - this.horizontalLayer = new Layer(layers, "HORIZONTAL_LAYER"); - this.verticalLayer = new Layer(layers, "VERTICAL_LAYER"); - this.useAlt = preferences.getRecordButtonAsAlt(); - } - - @PostConstruct - void init(ApcConfiguration configuration, HardwareElementsApc hwElements, Application application) { - //application.panelLayout().addValueObserver(this::handlePanelLayoutChanged); - initClipControl(configuration.getSceneRows(), hwElements); - } - - private void initClipControl(int numberOfScenes, final HardwareElementsApc hwElements) { - clipLauncherOverdub = transport.isClipLauncherOverdubEnabled(); - clipLauncherOverdub.markInterested(); - transport.isPlaying().markInterested(); - trackBankVertical = viewControl.getTrackBank(); - trackBankVertical.setShouldShowClipLauncherFeedback(true); - initVerticalControl(numberOfScenes, hwElements, trackBankVertical); - trackBankHorizontal = viewControl.getTrackBankHorizontal(); - initHorizontalControl(numberOfScenes, hwElements, trackBankHorizontal); - - final SceneBank sceneBank = trackBankVertical.sceneBank(); - final Scene targetScene = trackBankVertical.sceneBank().getScene(0); - targetScene.clipCount().markInterested(); - sceneBank.setIndication(true); - sceneBank.scrollPosition().addValueObserver(value -> sceneOffset = value); - - for (int sceneIndex = 0; sceneIndex < numberOfScenes; sceneIndex++) { - final SingleLedButton sceneButton = hwElements.getSceneButton(sceneIndex); - final int index = sceneIndex; - final Scene scene = sceneBank.getScene(index); - scene.clipCount().markInterested(); - sceneButton.bindPressed(this, () -> handleScenePressed(scene, index)); - sceneButton.bindPressed(this, () -> handleSceneReleased(scene)); - sceneButton.bindLight(this, () -> getSceneColor(index, scene)); - } - } - - private void initVerticalControl(int numberOfScenes, HardwareElementsApc hwElements, TrackBank trackBank) { - for (int i = 0; i < 8; i++) { - final int trackIndex = i; - final Track track = trackBank.getItemAt(trackIndex); - markTrack(track); - for (int j = 0; j < numberOfScenes; j++) { - final int sceneIndex = j; - final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); - prepareSlot(slot, sceneIndex, trackIndex); - - final RgbButton button = hwElements.getGridButton(sceneIndex, trackIndex); - button.bindPressed(verticalLayer, () -> handleSlotPressed(slot)); - button.bindRelease(verticalLayer, () -> handleSlotReleased(slot)); - button.bindLight(verticalLayer, () -> getState(track, slot, trackIndex, sceneIndex)); - } - } - } - - private void initHorizontalControl(int numberOfScenes, HardwareElementsApc hwElements, TrackBank trackBank) { - for (int i = 0; i < numberOfScenes; i++) { - final int trackIndex = i; - final Track track = trackBank.getItemAt(trackIndex); - markTrack(track); - for (int j = 0; j < 8; j++) { - final int sceneIndex = j; - final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); - prepareSlot(slot, sceneIndex, trackIndex); - - final RgbButton button = hwElements.getGridButton(trackIndex, sceneIndex); - button.bindPressed(horizontalLayer, () -> handleSlotPressed(slot)); - button.bindRelease(horizontalLayer, () -> handleSlotReleased(slot)); - button.bindLight(horizontalLayer, () -> getState(track, slot, trackIndex, sceneIndex)); - } - } - } - - private void handlePanelLayoutChanged(final String panelLayout) { - if (panelLayout.equals("MIX")) { - setLayout(PanelLayout.VERTICAL); - trackBankHorizontal.setShouldShowClipLauncherFeedback(false); - trackBankVertical.setShouldShowClipLauncherFeedback(true); - } else { - setLayout(PanelLayout.HORIZONTAL); - trackBankHorizontal.setShouldShowClipLauncherFeedback(true); - trackBankVertical.setShouldShowClipLauncherFeedback(false); - } - } - - public void setLayout(final PanelLayout layout) { - panelLayout = layout; - horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); - verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); - } - - private void handleScenePressed(final Scene scene, final int index) { - viewControl.focusScene(index + sceneOffset); - if (modifiers.isShift()) { - if (useAlt.get()) { - scene.launchAlt(); - } else { - scene.selectInEditor(); - } - } else { - scene.launch(); - } - } - - private void handleSceneReleased(final Scene scene) { - if (modifiers.isShift()) { - if (useAlt.get()) { - scene.launchReleaseAlt(); - } - } else { - scene.launchRelease(); - } - } - - private SingleLedState getSceneColor(final int index, final Scene scene) { - if (scene.clipCount().get() > 0) { - if (sceneOffset + index == viewControl.getFocusSceneIndex() && viewControl.hasQueuedForPlaying()) { - return SingleLedState.BLINK; - } - return SingleLedState.OFF; - } - return SingleLedState.OFF; - } - - private void handleSlotPressed(final ClipLauncherSlot slot) { - if (modifiers.isShift()) { - if (useAlt.get()) { - slot.launchAlt(); - } else { - slot.select(); - } - } else { - slot.launch(); - } - } - - private void handleSlotReleased(final ClipLauncherSlot slot) { - if (modifiers.isShift()) { - if (useAlt.get()) { - slot.launchReleaseAlt(); - } - } else { - slot.launchRelease(); - } - } - - private RgbLightState getState(final Track track, final ClipLauncherSlot slot, final int trackIndex, - final int sceneIndex) { - if (slot.hasContent().get()) { - final int color = colorIndex[sceneIndex][trackIndex]; - if (slot.isRecordingQueued().get()) { - return RgbLightState.RED.behavior(LedBehavior.BLINK_8); - } else if (slot.isRecording().get()) { - return RgbLightState.RED.behavior(LedBehavior.PULSE_2); - } else if (slot.isPlaybackQueued().get()) { - return RgbLightState.of(color, LedBehavior.BLINK_8); - } else if (slot.isStopQueued().get()) { - return RgbLightState.GREEN_PLAY.behavior(LedBehavior.BLINK_16); - } else if (slot.isPlaying().get() && track.isQueuedForStop().get()) { - return RgbLightState.GREEN.behavior(LedBehavior.BLINK_16); - } else if (slot.isPlaying().get()) { - if (clipLauncherOverdub.get() && track.arm().get()) { - return RgbLightState.RED.behavior(LedBehavior.PULSE_8); +public class SessionLayer extends AbstractSessionLayer { + + @Inject + private ViewControl viewControl; + @Inject + private Transport transport; + @Inject + private ModifierStates modifiers; + + private int sceneOffset; + private PanelLayout panelLayout = PanelLayout.VERTICAL; + private final Layer verticalLayer; + private final Layer horizontalLayer; + private TrackBank trackBankVertical; + private TrackBank trackBankHorizontal; + private final SettableBooleanValue useAlt; + + public SessionLayer(final Layers layers, ApcPreferences preferences) { + super(layers); + this.horizontalLayer = new Layer(layers, "HORIZONTAL_LAYER"); + this.verticalLayer = new Layer(layers, "VERTICAL_LAYER"); + this.useAlt = preferences.getRecordButtonAsAlt(); + } + + @PostConstruct + void init(ApcConfiguration configuration, HardwareElementsApc hwElements, Application application) { + //application.panelLayout().addValueObserver(this::handlePanelLayoutChanged); + initClipControl(configuration.getSceneRows(), hwElements); + } + + private void initClipControl(int numberOfScenes, final HardwareElementsApc hwElements) { + clipLauncherOverdub = transport.isClipLauncherOverdubEnabled(); + clipLauncherOverdub.markInterested(); + transport.isPlaying().markInterested(); + trackBankVertical = viewControl.getTrackBank(); + trackBankVertical.setShouldShowClipLauncherFeedback(true); + initVerticalControl(numberOfScenes, hwElements, trackBankVertical); + trackBankHorizontal = viewControl.getTrackBankHorizontal(); + initHorizontalControl(numberOfScenes, hwElements, trackBankHorizontal); + + final SceneBank sceneBank = trackBankVertical.sceneBank(); + final Scene targetScene = trackBankVertical.sceneBank().getScene(0); + targetScene.clipCount().markInterested(); + sceneBank.setIndication(true); + sceneBank.scrollPosition().addValueObserver(value -> sceneOffset = value); + + for (int sceneIndex = 0; sceneIndex < numberOfScenes; sceneIndex++) { + final SingleLedButton sceneButton = hwElements.getSceneButton(sceneIndex); + final int index = sceneIndex; + final Scene scene = sceneBank.getScene(index); + scene.clipCount().markInterested(); + sceneButton.bindPressed(this, () -> handleScenePressed(scene, index)); + sceneButton.bindPressed(this, () -> handleSceneReleased(scene)); + sceneButton.bindLight(this, () -> getSceneColor(index, scene)); + } + } + + private void initVerticalControl(int numberOfScenes, HardwareElementsApc hwElements, TrackBank trackBank) { + for (int i = 0; i < 8; i++) { + final int trackIndex = i; + final Track track = trackBank.getItemAt(trackIndex); + markTrack(track); + for (int j = 0; j < numberOfScenes; j++) { + final int sceneIndex = j; + final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); + prepareSlot(slot, sceneIndex, trackIndex); + + final RgbButton button = hwElements.getGridButton(sceneIndex, trackIndex); + button.bindPressed(verticalLayer, () -> handleSlotPressed(slot)); + button.bindRelease(verticalLayer, () -> handleSlotReleased(slot)); + button.bindLight(verticalLayer, () -> getState(track, slot, trackIndex, sceneIndex)); + } + } + } + + private void initHorizontalControl(int numberOfScenes, HardwareElementsApc hwElements, TrackBank trackBank) { + for (int i = 0; i < numberOfScenes; i++) { + final int trackIndex = i; + final Track track = trackBank.getItemAt(trackIndex); + markTrack(track); + for (int j = 0; j < 8; j++) { + final int sceneIndex = j; + final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); + prepareSlot(slot, sceneIndex, trackIndex); + + final RgbButton button = hwElements.getGridButton(trackIndex, sceneIndex); + button.bindPressed(horizontalLayer, () -> handleSlotPressed(slot)); + button.bindRelease(horizontalLayer, () -> handleSlotReleased(slot)); + button.bindLight(horizontalLayer, () -> getState(track, slot, trackIndex, sceneIndex)); + } + } + } + + private void handlePanelLayoutChanged(final String panelLayout) { + if (panelLayout.equals("MIX")) { + setLayout(PanelLayout.VERTICAL); + trackBankHorizontal.setShouldShowClipLauncherFeedback(false); + trackBankVertical.setShouldShowClipLauncherFeedback(true); + } else { + setLayout(PanelLayout.HORIZONTAL); + trackBankHorizontal.setShouldShowClipLauncherFeedback(true); + trackBankVertical.setShouldShowClipLauncherFeedback(false); + } + } + + @Override + protected boolean isPlaying() { + return transport.isPlaying().get(); + } + + @Override + protected boolean isShiftHeld() { + return modifiers.isShift(); + } + + public void setLayout(final PanelLayout layout) { + panelLayout = layout; + horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); + verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); + } + + private void handleScenePressed(final Scene scene, final int index) { + viewControl.focusScene(index + sceneOffset); + if (modifiers.isShift()) { + if (useAlt.get()) { + scene.launchAlt(); } else { - if (transport.isPlaying().get()) { - return RgbLightState.GREEN_PLAY; - } - return RgbLightState.GREEN; + scene.selectInEditor(); + } + } else { + scene.launch(); + } + } + + private void handleSceneReleased(final Scene scene) { + if (modifiers.isShift()) { + if (useAlt.get()) { + scene.launchReleaseAlt(); + } + } else { + scene.launchRelease(); + } + } + + private SingleLedState getSceneColor(final int index, final Scene scene) { + if (scene.clipCount().get() > 0) { + if (sceneOffset + index == viewControl.getFocusSceneIndex() && viewControl.hasQueuedForPlaying()) { + return SingleLedState.BLINK; + } + return SingleLedState.OFF; + } + return SingleLedState.OFF; + } + + private void handleSlotPressed(final ClipLauncherSlot slot) { + if (modifiers.isShift()) { + if (useAlt.get()) { + slot.launchAlt(); + } else { + slot.select(); + } + } else { + slot.launch(); + } + } + + private void handleSlotReleased(final ClipLauncherSlot slot) { + if (modifiers.isShift()) { + if (useAlt.get()) { + slot.launchReleaseAlt(); } - } - return RgbLightState.of(color); - } - if (slot.isRecordingQueued().get()) { - return RgbLightState.RED.behavior(LedBehavior.BLINK_8); // Possibly Track Color - } else if (track.arm().get()) { - return RgbLightState.RED.behavior(LedBehavior.LIGHT_25); - } - return RgbLightState.OFF; - } - - - private void markTrack(final Track track) { - track.isStopped().markInterested(); - track.mute().markInterested(); - track.solo().markInterested(); - track.isQueuedForStop().markInterested(); - track.arm().markInterested(); - } - - private void prepareSlot(final ClipLauncherSlot slot, final int sceneIndex, final int trackIndex) { - slot.hasContent().markInterested(); - slot.isPlaying().markInterested(); - slot.isStopQueued().markInterested(); - slot.isRecordingQueued().markInterested(); - slot.isRecording().markInterested(); - slot.isPlaybackQueued().markInterested(); - slot.color().addValueObserver((r, g, b) -> colorIndex[sceneIndex][trackIndex] = ColorLookup.toColor(r, g, b)); - } - - @Override - protected void onActivate() { - super.onActivate(); - horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); - verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); - } - - @Override - protected void onDeactivate() { - super.onDeactivate(); - horizontalLayer.setIsActive(false); - verticalLayer.setIsActive(false); - } + } else { + slot.launchRelease(); + } + } + + @Override + protected void onActivate() { + super.onActivate(); + horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); + verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + horizontalLayer.setIsActive(false); + verticalLayer.setIsActive(false); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/SliderLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/SliderLayer.java index bb4366e4..26c71a8d 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/SliderLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/SliderLayer.java @@ -3,7 +3,7 @@ import com.bitwig.extension.controller.api.*; import com.bitwig.extensions.controllers.akai.apcmk2.ControlMode; import com.bitwig.extensions.controllers.akai.apcmk2.ViewControl; -import com.bitwig.extensions.controllers.akai.apcmk2.control.HardwareElementsApc; +import com.bitwig.extensions.controllers.akai.apcmk2.HardwareElementsApc; import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.PostConstruct; diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/TrackControlLayer.java b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/TrackControlLayer.java index a91d7c28..91439272 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/TrackControlLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/layer/TrackControlLayer.java @@ -1,11 +1,11 @@ package com.bitwig.extensions.controllers.akai.apcmk2.layer; import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.akai.apcmk2.PanelLayout; +import com.bitwig.extensions.controllers.akai.apc.common.PanelLayout; import com.bitwig.extensions.controllers.akai.apcmk2.ViewControl; -import com.bitwig.extensions.controllers.akai.apcmk2.control.HardwareElementsApc; -import com.bitwig.extensions.controllers.akai.apcmk2.control.SingleLedButton; -import com.bitwig.extensions.controllers.akai.apcmk2.led.SingleLedState; +import com.bitwig.extensions.controllers.akai.apcmk2.HardwareElementsApc; +import com.bitwig.extensions.controllers.akai.apc.common.control.SingleLedButton; +import com.bitwig.extensions.controllers.akai.apc.common.led.SingleLedState; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.Activate; diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/led/RgbLightState.java b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/led/RgbLightState.java deleted file mode 100644 index d298bb0c..00000000 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/led/RgbLightState.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.bitwig.extensions.controllers.akai.apcmk2.led; - -import com.bitwig.extension.controller.api.HardwareLightVisualState; -import com.bitwig.extension.controller.api.InternalHardwareLightState; -import com.bitwig.extensions.framework.values.Midi; - -import java.util.HashMap; -import java.util.Map; - -public class RgbLightState extends InternalHardwareLightState { - - private static final Map STATE_MAP = new HashMap<>(); - - public static final RgbLightState OFF = new RgbLightState(0); - public static final RgbLightState WHITE = RgbLightState.of(3); - public static final RgbLightState WHITE_DIM = RgbLightState.of(1); - public static final RgbLightState RED = new RgbLightState(5); - public static final RgbLightState GREEN = new RgbLightState(21); - public static final RgbLightState GREEN_PLAY = new RgbLightState(21, LedBehavior.PULSE_2); - - private final int colorIndex; - private final LedBehavior ledBehavior; - - public static RgbLightState of(int colorIndex) { - return STATE_MAP.computeIfAbsent(colorIndex | LedBehavior.FULL.getCode() << 8, - index -> new RgbLightState(colorIndex)); - } - - public static RgbLightState of(int colorIndex, LedBehavior behavior) { - return STATE_MAP.computeIfAbsent(colorIndex | behavior.getCode() << 8, - index -> new RgbLightState(colorIndex, behavior)); - } - - public RgbLightState behavior(LedBehavior behavior) { - if (this.ledBehavior == behavior) { - return this; - } - return of(this.colorIndex, behavior); - } - - private RgbLightState(int colorIndex) { - this(colorIndex, LedBehavior.FULL); - } - - private RgbLightState(int colorIndex, LedBehavior ledBehavior) { - this.colorIndex = colorIndex; - this.ledBehavior = ledBehavior; - } - - public int getColorIndex() { - return colorIndex; - } - - public int getMidiCode() { - return Midi.NOTE_ON | ledBehavior.getCode(); - } - - @Override - public HardwareLightVisualState getVisualState() { - return null; - } - - @Override - public boolean equals(final Object o) { - if (o instanceof RgbLightState) { - RgbLightState other = (RgbLightState) o; - return other.colorIndex == colorIndex && other.ledBehavior == ledBehavior; - } - return false; - } - -} diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/midi/AbstractMidiProcessor.java b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/midi/AbstractMidiProcessor.java index 04bec62e..4f4a17be 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/midi/AbstractMidiProcessor.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/midi/AbstractMidiProcessor.java @@ -6,7 +6,8 @@ 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.apcmk2.DebugApc; +import com.bitwig.extensions.controllers.akai.apc.common.MidiProcessor; +import com.bitwig.extensions.controllers.akai.apcmk2.AbstractAkaiApcExtension; import com.bitwig.extensions.framework.time.TimedEvent; import java.util.Queue; @@ -52,7 +53,8 @@ public void setModeChangeListener(final IntConsumer modeChangeListener) { } protected void onMidi0(final ShortMidiMessage msg) { - DebugApc.println("Incoming %02X %02X %02X", msg.getStatusByte(), msg.getData1(), msg.getData2()); + AbstractAkaiApcExtension.println("Incoming %02X %02X %02X", msg.getStatusByte(), msg.getData1(), + msg.getData2()); } protected abstract void handleSysEx(final String sysExString); diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/midi/MidiProcessorBuffered.java b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/midi/MidiProcessorBuffered.java index 77b7a214..c087fa72 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/midi/MidiProcessorBuffered.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/midi/MidiProcessorBuffered.java @@ -3,7 +3,7 @@ import com.bitwig.extension.controller.api.ControllerHost; import com.bitwig.extension.controller.api.MidiIn; import com.bitwig.extension.controller.api.MidiOut; -import com.bitwig.extensions.controllers.akai.apcmk2.DebugApc; +import com.bitwig.extensions.controllers.akai.apcmk2.AbstractAkaiApcExtension; import com.bitwig.extensions.framework.time.TimedEvent; import java.util.LinkedList; @@ -55,7 +55,7 @@ protected void handleSysEx(final String sysExString) { modeChangeListener.accept(mode); } } else { - DebugApc.println("Sysex %s", sysExString); + AbstractAkaiApcExtension.println("Sysex %s", sysExString); } } diff --git a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/midi/MidiProcessorDirect.java b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/midi/MidiProcessorDirect.java index 1385e5a8..2effcc84 100644 --- a/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/midi/MidiProcessorDirect.java +++ b/src/main/java/com/bitwig/extensions/controllers/akai/apcmk2/midi/MidiProcessorDirect.java @@ -3,7 +3,7 @@ import com.bitwig.extension.controller.api.ControllerHost; import com.bitwig.extension.controller.api.MidiIn; import com.bitwig.extension.controller.api.MidiOut; -import com.bitwig.extensions.controllers.akai.apcmk2.DebugApc; +import com.bitwig.extensions.controllers.akai.apcmk2.AbstractAkaiApcExtension; import com.bitwig.extensions.framework.time.TimedEvent; public class MidiProcessorDirect extends AbstractMidiProcessor { @@ -32,7 +32,7 @@ private void handlePing() { protected void handleSysEx(final String sysExString) { if (sysExString.startsWith(KEYS_DEVICE_RESPONSE)) { - DebugApc.println(" Response KEYS"); + AbstractAkaiApcExtension.println(" Response KEYS"); } } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/BrowserLayer.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/BrowserLayer.java index 5804fe4d..5b3bb276 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/BrowserLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/BrowserLayer.java @@ -1,10 +1,22 @@ package com.bitwig.extensions.controllers.arturia.keylab.essentialMk3; -import com.bitwig.extension.controller.api.*; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.CursorBrowserFilterItem; +import com.bitwig.extension.controller.api.CursorBrowserResultItem; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.HardwareButton; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extension.controller.api.PopupBrowser; +import com.bitwig.extension.controller.api.RelativeHardwareKnob; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.color.RgbLightState; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.components.HwElements; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.components.ViewControl; -import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.display.*; +import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.display.ContextPageConfiguration; +import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.display.ContextPart; +import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.display.FooterIconDisplayBinding; +import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.display.LcdDisplay; +import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.display.MainScreenSection; +import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.display.MainViewDisplayBinding; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.Inject; @@ -13,163 +25,164 @@ import com.bitwig.extensions.framework.values.BooleanValueObject; public class BrowserLayer extends Layer { - private static final int BUTTON_WAIT_UTIL_REPEAT = 500; - private static final int HOLD_REPEAT_FREQUENCY = 200; - @Inject - private MainScreenSection mainScreenSection; - @Inject - private LcdDisplay display; - - private final PopupBrowser browser; - private final CursorBrowserResultItem resultCursorItem; - private String[] contentTypeNames = new String[0]; - private final CursorBrowserFilterItem categoryItem; - - private final BooleanValueObject browsingInitiated = new BooleanValueObject(); - private PinnableCursorDevice cursorDevice; - private CursorTrack cursorTrack; - private boolean enforceDeviceContent = true; - - private int encoderClickCount = 0; - - public BrowserLayer(Layers layers, ControllerHost host) { - super(layers, "BROWSER_LAYER"); - browser = host.createPopupBrowser(); - browser.exists().addValueObserver(this::handleBrowserOpened); - browser.contentTypeNames().addValueObserver(contentTypeNames -> this.contentTypeNames = contentTypeNames); - resultCursorItem = (CursorBrowserResultItem) browser.resultsColumn().createCursorItem(); - categoryItem = (CursorBrowserFilterItem) browser.categoryColumn().createCursorItem(); - categoryItem.hasNext().markInterested(); - categoryItem.hasPrevious().markInterested(); - //resultCursorItem.exists().addValueObserver(this::handleResultItemCursorExists); - browser.selectedContentTypeIndex().addValueObserver(selected -> { - if (selected < contentTypeNames.length) { - //currentContentType = contentTypeNames[selected]; - if (enforceDeviceContent) { - enforceDeviceContent = false; - forceDeviceContent(); + private static final int BUTTON_WAIT_UTIL_REPEAT = 500; + private static final int HOLD_REPEAT_FREQUENCY = 200; + @Inject + private MainScreenSection mainScreenSection; + @Inject + private LcdDisplay display; + + private final PopupBrowser browser; + private final CursorBrowserResultItem resultCursorItem; + private String[] contentTypeNames = new String[0]; + private final CursorBrowserFilterItem categoryItem; + + private final BooleanValueObject browsingInitiated = new BooleanValueObject(); + private PinnableCursorDevice cursorDevice; + private CursorTrack cursorTrack; + private boolean enforceDeviceContent = true; + + private int encoderClickCount = 0; + + public BrowserLayer(final Layers layers, final ControllerHost host) { + super(layers, "BROWSER_LAYER"); + browser = host.createPopupBrowser(); + browser.exists().addValueObserver(this::handleBrowserOpened); + browser.contentTypeNames().addValueObserver(contentTypeNames -> this.contentTypeNames = contentTypeNames); + resultCursorItem = (CursorBrowserResultItem) browser.resultsColumn().createCursorItem(); + categoryItem = (CursorBrowserFilterItem) browser.categoryColumn().createCursorItem(); + categoryItem.hasNext().markInterested(); + categoryItem.hasPrevious().markInterested(); + //resultCursorItem.exists().addValueObserver(this::handleResultItemCursorExists); + browser.selectedContentTypeIndex().addValueObserver(selected -> { + if (selected < contentTypeNames.length) { + //currentContentType = contentTypeNames[selected]; + if (enforceDeviceContent) { + enforceDeviceContent = false; + forceDeviceContent(); + } } - } - }); - } - - private void forceDeviceContent() { - int index = -1; - for (int i = 0; i < contentTypeNames.length; i++) { - if (contentTypeNames[i].equals("Devices")) { - index = i; - break; - } - } - if (index != -1) { - browser.selectedContentTypeIndex().set(index); - } - } - - @PostConstruct - public void init(ViewControl viewCursorControl, HwElements hwElements) { - cursorDevice = viewCursorControl.getCursorDevice(); - cursorTrack = viewCursorControl.getCursorTrack(); - cursorDevice.exists().markInterested(); - RelativeHardwareKnob encoder = hwElements.getMainEncoder(); - HardwareButton encoderPress = hwElements.getEncoderPress(); - hwElements.bindEncoder(this, encoder, this::mainEncoderAction); - bindContextButtons(); - ContextPageConfiguration contextPage = mainScreenSection.getContextPage(); - - BasicStringValue resultNameValue = new BasicStringValue(""); - resultCursorItem.name() - .addValueObserver(name -> resultNameValue.set(getResultData(resultCursorItem.exists().get(), name))); - resultCursorItem.exists() - .addValueObserver(exists -> resultNameValue.set(getResultData(exists, resultCursorItem.name().get()))); - - - this.addBinding(new MainViewDisplayBinding(contextPage, display, resultNameValue, categoryItem.name())); - this.bindPressed(encoderPress, () -> { - if (resultCursorItem.exists().get()) { - if (encoderClickCount == 0) { - display.sendPopup("Click again ", "to load", KeylabIcon.NONE); - encoderClickCount++; + }); + } + + private void forceDeviceContent() { + int index = -1; + for (int i = 0; i < contentTypeNames.length; i++) { + if (contentTypeNames[i].equals("Devices")) { + index = i; + break; + } + } + if (index != -1) { + browser.selectedContentTypeIndex().set(index); + } + } + + @PostConstruct + public void init(final ViewControl viewCursorControl, final HwElements hwElements) { + cursorDevice = viewCursorControl.getCursorDevice(); + cursorTrack = viewCursorControl.getCursorTrack(); + cursorDevice.exists().markInterested(); + final RelativeHardwareKnob encoder = hwElements.getMainEncoder(); + final HardwareButton encoderPress = hwElements.getEncoderPress(); + hwElements.bindEncoder(this, encoder, this::mainEncoderAction); + bindContextButtons(); + final ContextPageConfiguration contextPage = mainScreenSection.getContextPage(); + + final BasicStringValue resultNameValue = new BasicStringValue(""); + resultCursorItem.name() + .addValueObserver(name -> resultNameValue.set(getResultData(resultCursorItem.exists().get(), name))); + resultCursorItem.exists() + .addValueObserver(exists -> resultNameValue.set(getResultData(exists, resultCursorItem.name().get()))); + + + this.addBinding(new MainViewDisplayBinding(contextPage, display, resultNameValue, categoryItem.name())); + this.bindPressed(encoderPress, () -> { + if (resultCursorItem.exists().get()) { + if (encoderClickCount == 0) { + display.sendPopup("Click again ", "to load", KeylabIcon.NONE); + encoderClickCount++; + } else { + browser.commit(); + encoderClickCount = 0; + } } else { - browser.commit(); - encoderClickCount = 0; + display.sendPopup("Nothing selected ", "", KeylabIcon.NONE); } - } else { - display.sendPopup("Nothing selected ", "", KeylabIcon.NONE); - } - }); - } - - private String getResultData(boolean exists, String resultName) { - return !exists || resultName.isBlank() ? "" : resultName; - } - - private void bindContextButtons() { - RgbButton context1 = mainScreenSection.getContextButton(0); - Layer navigationLayer = mainScreenSection.getLayer(); - ContextPageConfiguration contextPage = mainScreenSection.getContextPage(); - context1.bindPressed(navigationLayer, this::toggleBrowserAction); - FooterIconDisplayBinding footerIconBinding = new FooterIconDisplayBinding(contextPage, display, browsingInitiated, - 0, ContextPart.FrameType.FRAME_SMALL, ContextPart.FrameType.BAR); - navigationLayer.addBinding(footerIconBinding); - context1.bindLight(navigationLayer, () -> this.isActive() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); - RgbButton context3 = mainScreenSection.getContextButton(2); - RgbButton context4 = mainScreenSection.getContextButton(3); - context3.bindRepeatHold(this, categoryItem::selectPrevious, BUTTON_WAIT_UTIL_REPEAT, HOLD_REPEAT_FREQUENCY); - context3.bindLight(this, - () -> categoryItem.hasPrevious().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); - context4.bindRepeatHold(this, categoryItem::selectNext, BUTTON_WAIT_UTIL_REPEAT, HOLD_REPEAT_FREQUENCY); - context4.bindLight(this, () -> categoryItem.hasNext().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); - } - - private void toggleBrowserAction() { - this.toggleIsActive(); - if (this.isActive()) { - openBrowser(); - } else { - exitBrowser(); - } - } - - private void handleBrowserOpened(boolean exists) { -// if (browsingInitiated.get()) { -// browser.shouldAudition().set(false); -// } - // driver.browserDisplayMode(exists); - if (!exists) { - browsingInitiated.set(false); - this.setIsActive(false); - } - } - - private void mainEncoderAction(int increment) { - if (increment > 0) { - resultCursorItem.selectNext(); - } else { - resultCursorItem.selectPrevious(); - } - encoderClickCount = 0; - } - - private void openBrowser() { - encoderClickCount = 0; - if (cursorDevice.exists().get()) { - browsingInitiated.set(true); - enforceDeviceContent = true; - cursorDevice.replaceDeviceInsertionPoint().browse(); - } else { - browsingInitiated.set(true); - cursorTrack.endOfDeviceChainInsertionPoint().browse(); - } - } - - private void exitBrowser() { - encoderClickCount = 0; - if (browser.exists().get()) { - browser.cancel(); - } - browsingInitiated.set(false); - this.setIsActive(true); - } - + }); + } + + private String getResultData(final boolean exists, final String resultName) { + return !exists || resultName.isBlank() ? "" : resultName; + } + + private void bindContextButtons() { + final RgbButton context1 = mainScreenSection.getContextButton(0); + final Layer navigationLayer = mainScreenSection.getLayer(); + final ContextPageConfiguration contextPage = mainScreenSection.getContextPage(); + context1.bindPressed(navigationLayer, this::toggleBrowserAction); + final FooterIconDisplayBinding footerIconBinding = + new FooterIconDisplayBinding(contextPage, display, browsingInitiated, 0, ContextPart.FrameType.FRAME_SMALL, + ContextPart.FrameType.BAR); + navigationLayer.addBinding(footerIconBinding); + context1.bindLight(navigationLayer, () -> this.isActive() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); + final RgbButton context3 = mainScreenSection.getContextButton(2); + final RgbButton context4 = mainScreenSection.getContextButton(3); + context3.bindRepeatHold(this, categoryItem::selectPrevious, BUTTON_WAIT_UTIL_REPEAT, HOLD_REPEAT_FREQUENCY); + context3.bindLight(this, + () -> categoryItem.hasPrevious().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); + context4.bindRepeatHold(this, categoryItem::selectNext, BUTTON_WAIT_UTIL_REPEAT, HOLD_REPEAT_FREQUENCY); + context4.bindLight(this, () -> categoryItem.hasNext().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); + } + + private void toggleBrowserAction() { + this.toggleIsActive(); + if (this.isActive()) { + openBrowser(); + } else { + exitBrowser(); + } + } + + private void handleBrowserOpened(final boolean exists) { + // if (browsingInitiated.get()) { + // browser.shouldAudition().set(false); + // } + // driver.browserDisplayMode(exists); + if (!exists) { + browsingInitiated.set(false); + this.setIsActive(false); + } + } + + private void mainEncoderAction(final int increment) { + if (increment > 0) { + resultCursorItem.selectNext(); + } else { + resultCursorItem.selectPrevious(); + } + encoderClickCount = 0; + } + + private void openBrowser() { + encoderClickCount = 0; + if (cursorDevice.exists().get()) { + browsingInitiated.set(true); + enforceDeviceContent = true; + cursorDevice.replaceDeviceInsertionPoint().browse(); + } else { + browsingInitiated.set(true); + cursorTrack.endOfDeviceChainInsertionPoint().browse(); + } + } + + private void exitBrowser() { + encoderClickCount = 0; + if (browser.exists().get()) { + browser.cancel(); + } + browsingInitiated.set(false); + this.setIsActive(true); + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/RgbButton.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/RgbButton.java index d1d96fa5..a3d40805 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/RgbButton.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/RgbButton.java @@ -1,15 +1,22 @@ package com.bitwig.extensions.controllers.arturia.keylab.essentialMk3; -import com.bitwig.extension.controller.api.*; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import com.bitwig.extension.controller.api.Action; +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.arturia.keylab.essentialMk3.color.RgbLightState; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.components.SysExHandler; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.time.TimeRepeatEvent; import com.bitwig.extensions.framework.time.TimedEvent; -import java.util.function.Consumer; -import java.util.function.Supplier; - public class RgbButton { private final byte[] rgbCommand = {(byte) 0xF0, 0x00, 0x20, 0x6B, 0x7F, 0x42, 0x04, // 0x01, // 7 - Patch Id @@ -51,6 +58,7 @@ public RgbButton(final String name, final int padId, final Type type, final int } hwButton.isPressed().markInterested(); light = surface.createMultiStateHardwareLight(name + "_LIGHT_" + "_" + value); + light.setColorToStateFunction(RgbLightState::forColor); hwButton.setBackgroundLight(light); light.state().onUpdateHardware(this::updateState); // if (bankId.getIndex() == -1) { // Individual updates handled @@ -73,8 +81,8 @@ public MultiStateHardwareLight getLight() { } private void updateState(final InternalHardwareLightState state) { - if (state instanceof RgbLightState) { - ((RgbLightState) state).apply(rgbCommand); + if (state instanceof final RgbLightState ligtState) { + ligtState.apply(rgbCommand); sysExHandler.sendSysex(rgbCommand); } else { setRgbOff(); diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/SliderEncoderControl.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/SliderEncoderControl.java index 6e6c12b4..0f8acf8a 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/SliderEncoderControl.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/SliderEncoderControl.java @@ -1,6 +1,12 @@ package com.bitwig.extensions.controllers.arturia.keylab.essentialMk3; -import com.bitwig.extension.controller.api.*; +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.RelativeHardwareKnob; +import com.bitwig.extension.controller.api.RemoteControl; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.color.RgbLightState; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.components.HwElements; import com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.components.KeyLabEncoderBinding; @@ -14,199 +20,203 @@ import com.bitwig.extensions.framework.values.ValueObject; public class SliderEncoderControl extends Layer { - private final ViewControl viewControl; - private final Layer deviceControlLayer; - private final Layer deviceControlLayer2; - private final Layer partModifierLayer; - private final LcdDisplay lcdDisplay; - private CursorRemoteControlsPage parameterBank1; - private String[] devicePageNames = new String[0]; - private TrackBank mixerTrackBank; - private boolean deviceScrollingOccurred = false; - private boolean parameterSteppingOccurred = false; - - public enum State { - MIXER, - DEVICE - } - - private final ValueObject currentState = new ValueObject<>(State.MIXER); - - public SliderEncoderControl(final Layers layers, final ViewControl viewControl, final HwElements hwElements, - LcdDisplay lcdDisplay) { - super(layers, "SLIDER_ENCODER_LAYER"); - this.viewControl = viewControl; - deviceControlLayer = new Layer(layers, "DEVICE_CONTROL"); - deviceControlLayer2 = new Layer(layers, "DEVICE_CONTROL2"); - // TODO figure out how to give this highest Priority - partModifierLayer = new Layer(layers, "PART_MODIFIER"); - this.lcdDisplay = lcdDisplay; - initPartButton(hwElements); - assignMixLayer(hwElements); - } - - @Activate - public void doActivation() { - activate(); - } - - private void assignMixLayer(final HwElements hwElements) { - final PinnableCursorDevice cursorDevice = viewControl.getCursorDevice(); - cursorDevice.name().addValueObserver(name -> { - if (deviceScrollingOccurred) { - lcdDisplay.sendPopup("Device Selected", // - name, KeylabIcon.SFX_SMALL); - } - }); - //CursorRemoteControlsPage trackRemotes = cursorDevice.createCursorRemoteControlsPage("track-remotes", 8, null); - - parameterBank1 = cursorDevice.createCursorRemoteControlsPage(8); - cursorDevice.hasPrevious().markInterested(); - - parameterBank1.pageNames().addValueObserver(pageNames -> devicePageNames = pageNames); - final CursorRemoteControlsPage parameterBank2 = cursorDevice.createCursorRemoteControlsPage("sliders", 8, null); - parameterBank2.selectedPageIndex().markInterested(); - parameterBank1.pageCount().addValueObserver(pages -> { - if (deviceControlLayer.isActive()) { - deviceControlLayer2.setIsActive(pages < 2); - } - }); - parameterBank1.selectedPageIndex() - .addValueObserver(index -> mainParameterBankIndexChanged(parameterBank2, index)); - - mixerTrackBank = viewControl.getMixerTrackBank(); - mixerTrackBank.itemCount().markInterested(); - mixerTrackBank.scrollPosition().addValueObserver(scrollPosition -> // - lcdDisplay.sendPopup("Tracks", String.format("%d - %d", scrollPosition + 1, - Math.min(scrollPosition + 8, mixerTrackBank.itemCount().get())), KeylabIcon.NONE)); - - cursorDevice.name().markInterested(); - - final KeylabAbsoluteControl[] sliders = hwElements.getSliders(); - final KeylabAbsoluteControl[] knobs = hwElements.getKnobs(); - - for (int i = 0; i < 8; i++) { - final Track track = mixerTrackBank.getItemAt(i); - addBinding( - new KeyLabEncoderBinding(sliders[i], track.volume(), track.name(), LcdDisplayMode.VOLUME, lcdDisplay)); - addBinding(new KeyLabEncoderBinding(knobs[i], track.pan(), track.name(), LcdDisplayMode.PANNING, lcdDisplay)); - } - final CursorTrack cursorTrack = viewControl.getCursorTrack(); - addBinding(new KeyLabEncoderBinding(sliders[8], cursorTrack.volume(), cursorTrack.name(), LcdDisplayMode.VOLUME, - lcdDisplay)); - addBinding( - new KeyLabEncoderBinding(knobs[8], cursorTrack.pan(), cursorTrack.name(), LcdDisplayMode.PANNING, lcdDisplay)); - - for (int i = 0; i < 8; i++) { - final RemoteControl parameter4Knob = parameterBank1.getParameter(i); - deviceControlLayer.addBinding( - new KeyLabEncoderBinding(knobs[i], parameter4Knob, parameter4Knob.name(), LcdDisplayMode.DEVICE1, - lcdDisplay)); - deviceControlLayer2.addBinding( - new KeyLabEncoderBinding(sliders[i], parameter4Knob, parameter4Knob.name(), LcdDisplayMode.DEVICE2, - lcdDisplay)); - - final RemoteControl parameter4Slider = parameterBank2.getParameter(i); - deviceControlLayer.addBinding( - new KeyLabEncoderBinding(sliders[i], parameter4Slider, parameter4Slider.name(), LcdDisplayMode.DEVICE2, - lcdDisplay)); - } - } - - private void mainParameterBankIndexChanged(CursorRemoteControlsPage parameterBank2, int index) { - parameterBank2.selectedPageIndex().set(index + 1); - if (parameterSteppingOccurred && !deviceScrollingOccurred) { - lcdDisplay.sendPopup(getParameterPageLabeling(index), // - "Parameter Page " + (index + 1), KeylabIcon.SFX_SMALL); - parameterSteppingOccurred = false; - } - } - - private String getParameterPageLabeling(int index) { - String pageKnob = shorten(devicePageNames[index]); - String pageFader = shorten(devicePageNames[(index + 1) % devicePageNames.length]); - String result = pageKnob + "/" + pageFader; - if (result.length() > 15) { - return result.substring(0, 15); - } - return result; - } - - private String shorten(String value) { - return value.replace(" ", "").replace("-", ""); - } - - public ValueObject getCurrentState() { - return currentState; - } - - private void initPartButton(final HwElements hwElements) { - // Mixer Dim => control track 1-8 - // Mixer Bright => control track 9-16 - // Device Dim => control first page - // Device Bright => control other pages - - // Long press Part + Encoder Mixer Bank - - final RgbButton partButton = hwElements.getButton(CCAssignment.PART); - partButton.bindPressed(this, this::handlePartDown); - partButton.bindReleased(this, () -> handlePartRelease(partButton)); - partButton.bindLight(this, - () -> mixerTrackBank.scrollPosition().get() == 0 ? RgbLightState.WHITE_DIMMED : RgbLightState.WHITE); - partButton.bindLight(deviceControlLayer, - () -> parameterBank1.selectedPageIndex().get() == 0 ? RgbLightState.WHITE_DIMMED : RgbLightState.WHITE); - RelativeHardwareKnob encoder = hwElements.getMainEncoder(); - hwElements.bindEncoder(partModifierLayer, encoder, this::handlePartEncoder); - } - - private void handlePartEncoder(int increment) { - if (deviceControlLayer.isActive()) { - PinnableCursorDevice cursorDevice = viewControl.getCursorDevice(); - if (increment > 0) { - cursorDevice.selectNext(); - } else { - cursorDevice.selectPrevious(); - } - deviceScrollingOccurred = true; - } else { - mixerTrackBank.scrollBy(increment); - } - } - - public void handlePartDown() { - deviceScrollingOccurred = false; - parameterSteppingOccurred = false; - partModifierLayer.setIsActive(true); - } - - public void handlePartRelease(RgbButton button) { - partModifierLayer.setIsActive(false); - if (!deviceScrollingOccurred) { - parameterSteppingOccurred = true; - if (currentState.get() == State.DEVICE) { - parameterBank1.selectNextPage(true); - } else { - if (mixerTrackBank.scrollPosition().get() > 0) { - mixerTrackBank.scrollPosition().set(0); + private final ViewControl viewControl; + private final Layer deviceControlLayer; + private final Layer deviceControlLayer2; + private final Layer partModifierLayer; + private final LcdDisplay lcdDisplay; + private CursorRemoteControlsPage parameterBank1; + private String[] devicePageNames = new String[0]; + private TrackBank mixerTrackBank; + private boolean deviceScrollingOccurred = false; + private boolean parameterSteppingOccurred = false; + + public enum State { + MIXER, DEVICE + } + + private final ValueObject currentState = new ValueObject<>(State.MIXER); + + public SliderEncoderControl(final Layers layers, final ViewControl viewControl, final HwElements hwElements, + final LcdDisplay lcdDisplay) { + super(layers, "SLIDER_ENCODER_LAYER"); + this.viewControl = viewControl; + deviceControlLayer = new Layer(layers, "DEVICE_CONTROL"); + deviceControlLayer2 = new Layer(layers, "DEVICE_CONTROL2"); + // TODO figure out how to give this highest Priority + partModifierLayer = new Layer(layers, "PART_MODIFIER"); + this.lcdDisplay = lcdDisplay; + initPartButton(hwElements); + assignMixLayer(hwElements); + } + + @Activate + public void doActivation() { + activate(); + } + + private void assignMixLayer(final HwElements hwElements) { + final PinnableCursorDevice cursorDevice = viewControl.getCursorDevice(); + cursorDevice.name().addValueObserver(name -> { + if (deviceScrollingOccurred) { + lcdDisplay.sendPopup("Device Selected", // + name, KeylabIcon.SFX_SMALL); + } + }); + + parameterBank1 = cursorDevice.createCursorRemoteControlsPage(8); + cursorDevice.hasPrevious().markInterested(); + + final CursorRemoteControlsPage parameterBank2 = cursorDevice.createCursorRemoteControlsPage("sliders", 8, null); + parameterBank2.selectedPageIndex().markInterested(); + parameterBank1.pageCount().addValueObserver(pages -> { + if (deviceControlLayer.isActive()) { + deviceControlLayer2.setIsActive(pages < 2); + } + }); + parameterBank1.pageNames().addValueObserver(pageNames -> { + devicePageNames = pageNames; + if (devicePageNames != null && devicePageNames.length > 0 + && parameterBank1.selectedPageIndex().get() == -1) { + mainParameterBankIndexChanged(parameterBank2, parameterBank1.selectedPageIndex().get()); + } + }); + parameterBank1.selectedPageIndex() + .addValueObserver(index -> mainParameterBankIndexChanged(parameterBank2, index)); + + mixerTrackBank = viewControl.getMixerTrackBank(); + mixerTrackBank.itemCount().markInterested(); + mixerTrackBank.scrollPosition().addValueObserver(scrollPosition -> // + lcdDisplay.sendPopup( + "Tracks", String.format("%d - %d", scrollPosition + 1, + Math.min(scrollPosition + 8, mixerTrackBank.itemCount().get())), KeylabIcon.NONE)); + + cursorDevice.name().markInterested(); + + final KeylabAbsoluteControl[] sliders = hwElements.getSliders(); + final KeylabAbsoluteControl[] knobs = hwElements.getKnobs(); + + for (int i = 0; i < 8; i++) { + final Track track = mixerTrackBank.getItemAt(i); + addBinding( + new KeyLabEncoderBinding(sliders[i], track.volume(), track.name(), LcdDisplayMode.VOLUME, lcdDisplay)); + addBinding( + new KeyLabEncoderBinding(knobs[i], track.pan(), track.name(), LcdDisplayMode.PANNING, lcdDisplay)); + } + final CursorTrack cursorTrack = viewControl.getCursorTrack(); + addBinding(new KeyLabEncoderBinding(sliders[8], cursorTrack.volume(), cursorTrack.name(), LcdDisplayMode.VOLUME, + lcdDisplay)); + addBinding(new KeyLabEncoderBinding(knobs[8], cursorTrack.pan(), cursorTrack.name(), LcdDisplayMode.PANNING, + lcdDisplay)); + + for (int i = 0; i < 8; i++) { + final RemoteControl parameter4Knob = parameterBank1.getParameter(i); + deviceControlLayer.addBinding( + new KeyLabEncoderBinding(knobs[i], parameter4Knob, parameter4Knob.name(), LcdDisplayMode.DEVICE1, + lcdDisplay)); + deviceControlLayer2.addBinding( + new KeyLabEncoderBinding(sliders[i], parameter4Knob, parameter4Knob.name(), LcdDisplayMode.DEVICE2, + lcdDisplay)); + + final RemoteControl parameter4Slider = parameterBank2.getParameter(i); + deviceControlLayer.addBinding( + new KeyLabEncoderBinding(sliders[i], parameter4Slider, parameter4Slider.name(), LcdDisplayMode.DEVICE2, + lcdDisplay)); + } + } + + private void mainParameterBankIndexChanged(final CursorRemoteControlsPage parameterBank2, final int index) { + if (index == -1 || devicePageNames.length == 0) { + return; + } + parameterBank2.selectedPageIndex().set((index + 1) % devicePageNames.length); + if (parameterSteppingOccurred && !deviceScrollingOccurred) { + lcdDisplay.sendPopup(getParameterPageLabeling(index), // + "Parameter Page " + (index + 1), KeylabIcon.SFX_SMALL); + parameterSteppingOccurred = false; + } + } + + private String getParameterPageLabeling(final int index) { + if (index >= 0 && index < devicePageNames.length) { + final String pageName = devicePageNames[index]; + final String pageKnob = shorten(pageName); + final String pageFader = shorten(devicePageNames[(index + 1) % devicePageNames.length]); + final String result = pageKnob + "/" + pageFader; + if (result.length() > 15) { + return result.substring(0, 15); + } + return result; + } + return ""; + } + + private String shorten(final String value) { + return value.replace(" ", "").replace("-", ""); + } + + public ValueObject getCurrentState() { + return currentState; + } + + private void initPartButton(final HwElements hwElements) { + final RgbButton partButton = hwElements.getButton(CCAssignment.PART); + partButton.bindPressed(this, this::handlePartDown); + partButton.bindReleased(this, () -> handlePartRelease(partButton)); + partButton.bindLight(this, + () -> mixerTrackBank.scrollPosition().get() == 0 ? RgbLightState.WHITE_DIMMED : RgbLightState.WHITE); + partButton.bindLight(deviceControlLayer, + () -> parameterBank1.selectedPageIndex().get() == 0 ? RgbLightState.WHITE_DIMMED : RgbLightState.WHITE); + final RelativeHardwareKnob encoder = hwElements.getMainEncoder(); + hwElements.bindEncoder(partModifierLayer, encoder, this::handlePartEncoder); + } + + private void handlePartEncoder(final int increment) { + if (deviceControlLayer.isActive()) { + final PinnableCursorDevice cursorDevice = viewControl.getCursorDevice(); + if (increment > 0) { + cursorDevice.selectNext(); } else { - mixerTrackBank.scrollPosition().set(8); + cursorDevice.selectPrevious(); } - } - } - button.forceDelayedRefresh(); - } - - public void toggleMode() { - if (!deviceControlLayer.isActive()) { - deviceControlLayer.setIsActive(true); - deviceControlLayer2.setIsActive(parameterBank1.pageCount().get() < 2); - currentState.set(State.DEVICE); - } else { - deviceControlLayer.setIsActive(false); - deviceControlLayer2.setIsActive(false); - currentState.set(State.MIXER); - } - } - - + deviceScrollingOccurred = true; + } else { + mixerTrackBank.scrollBy(increment); + } + } + + public void handlePartDown() { + deviceScrollingOccurred = false; + parameterSteppingOccurred = false; + partModifierLayer.setIsActive(true); + } + + public void handlePartRelease(final RgbButton button) { + partModifierLayer.setIsActive(false); + if (!deviceScrollingOccurred) { + parameterSteppingOccurred = true; + if (currentState.get() == State.DEVICE) { + parameterBank1.selectNextPage(true); + } else { + if (mixerTrackBank.scrollPosition().get() > 0) { + mixerTrackBank.scrollPosition().set(0); + } else { + mixerTrackBank.scrollPosition().set(8); + } + } + } + button.forceDelayedRefresh(); + } + + public void toggleMode() { + if (!deviceControlLayer.isActive()) { + deviceControlLayer.setIsActive(true); + deviceControlLayer2.setIsActive(parameterBank1.pageCount().get() < 2); + currentState.set(State.DEVICE); + } else { + deviceControlLayer.setIsActive(false); + deviceControlLayer2.setIsActive(false); + currentState.set(State.MIXER); + } + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/color/RgbLightState.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/color/RgbLightState.java index 5d54a58f..e1805c51 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/color/RgbLightState.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/essentialMk3/color/RgbLightState.java @@ -1,16 +1,12 @@ package com.bitwig.extensions.controllers.arturia.keylab.essentialMk3.color; +import java.util.Arrays; + import com.bitwig.extension.api.Color; import com.bitwig.extension.controller.api.HardwareLightVisualState; import com.bitwig.extension.controller.api.InternalHardwareLightState; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - public class RgbLightState extends InternalHardwareLightState { - private static final Map indexLookup = new HashMap<>(); - private static final int DIM_VAL = 0x20; private static final int MAX_VAL = 0x7F; @@ -37,6 +33,18 @@ public class RgbLightState extends InternalHardwareLightState { private final HardwareLightVisualState visualState; private final BlinkState state; + public static RgbLightState forColor(final Color color) + { + if (color == null || color.getAlpha() == 0) + return OFF; + + final int red = (int)(color.getRed() * MAX_VAL); + final int green = (int)(color.getGreen() * MAX_VAL); + final int blue = (int)(color.getBlue() * MAX_VAL); + + return new RgbLightState(red, green, blue); + } + RgbLightState(final int red, final int green, final int blue, final BlinkState state) { rgb = saturate(red, green, blue, SAT_AMOUNT); this.state = state; diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk2/ArturiaKeylabMkII.java b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk2/ArturiaKeylabMkII.java index 83c1bf91..6168fbc7 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk2/ArturiaKeylabMkII.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/keylab/mk2/ArturiaKeylabMkII.java @@ -16,6 +16,7 @@ import com.bitwig.extension.controller.api.BrowserResultsItem; import com.bitwig.extension.controller.api.ClipLauncherSlot; import com.bitwig.extension.controller.api.ClipLauncherSlotBank; +import com.bitwig.extension.controller.api.ContinuousHardwareControl; import com.bitwig.extension.controller.api.ControllerHost; import com.bitwig.extension.controller.api.CursorBrowserFilterItem; import com.bitwig.extension.controller.api.CursorDevice; @@ -293,10 +294,14 @@ protected void onDeactivate() }; mBrowserLayer = new Layer(mLayers, "Browser"); + mAdjustingContinuousHardwareControlNotificationLayer = new Layer(mLayers, "Notifications"); + initBaseLayer(); initDAWLayer(); initBrowserLayer(); initMultiLayer(); + initAdjustingContinuousHardwareControlNotificationLayer(); + mBaseLayer.activate(); mDAWLayer.activate(); @@ -506,6 +511,93 @@ private void initMultiLayer() }); } + private void initAdjustingContinuousHardwareControlNotificationLayer() + { + mAdjustingContinuousHardwareControlNotificationLayer.showText( + this::getAdjustingContinuousHardwareControlNotificationTopLine, + this::getAdjustingContinuousHardwareControlNotificationBottomLine); + + for (final RelativeHardwareControl encoder : mEncoders) + { + encoder.isUpdatingTargetValue().markInterested(); + encoder.targetName().markInterested(); + encoder.targetDisplayedValue().markInterested(); + encoder.targetValue().addValueObserver((v) -> { + if (encoder.isUpdatingTargetValue().get()) + showAdjustingContinuousHardwareControlNotification(encoder); + }); + } + + for (final AbsoluteHardwareControl fader : mFaders) + { + fader.targetName().markInterested(); + fader.targetDisplayedValue().markInterested(); + fader.value().addValueObserver((v) -> showAdjustingContinuousHardwareControlNotification(fader)); + } + } + + private void showAdjustingContinuousHardwareControlNotification(final ContinuousHardwareControl control) + { + final int notificationDurationInMs = 500; + + mContinuousHardwareControlThatIsBeingAdjusted = control; + + // Set end time for notification. If there is an active notification, we will respect the new end time. + mHideAdjustingContinuousHardwareControlNotificationTime = System.currentTimeMillis() + notificationDurationInMs; + + // Enable notification layer. + if (!mAdjustingContinuousHardwareControlNotificationLayer.isActive()) + { + mAdjustingContinuousHardwareControlNotificationLayer.activate(); + scheduleHideAdjustingContinuousHardwareControlNotificationTask(mHideAdjustingContinuousHardwareControlNotificationTime, notificationDurationInMs); + } + } + + private void scheduleHideAdjustingContinuousHardwareControlNotificationTask(final long hideNotificationTime, final int durationInMs) + { + if (durationInMs <= 0) + { + hideAdjustingContinuousHardwareControlNotification(); + return; + } + + getHost().scheduleTask(() -> { + if (hideNotificationTime == mHideAdjustingContinuousHardwareControlNotificationTime) + { + // No other notification was shown in between + hideAdjustingContinuousHardwareControlNotification(); + } + else + { + // Another notification was shown since this task was scheduled. That means we should not hide the + // notification now, but schedule another task. + final long remainingTimeInMs = mHideAdjustingContinuousHardwareControlNotificationTime - System.currentTimeMillis(); + scheduleHideAdjustingContinuousHardwareControlNotificationTask( + mHideAdjustingContinuousHardwareControlNotificationTime, + (int) remainingTimeInMs); + } + }, durationInMs); + } + + private void hideAdjustingContinuousHardwareControlNotification() + { + mAdjustingContinuousHardwareControlNotificationLayer.deactivate(); + } + + private String getAdjustingContinuousHardwareControlNotificationTopLine() + { + return mContinuousHardwareControlThatIsBeingAdjusted != null + ? mContinuousHardwareControlThatIsBeingAdjusted.targetName().getLimited(8) + : ""; + } + + private String getAdjustingContinuousHardwareControlNotificationBottomLine() + { + return mContinuousHardwareControlThatIsBeingAdjusted != null + ? mContinuousHardwareControlThatIsBeingAdjusted.targetDisplayedValue().getLimited(8) + : ""; + } + @Override public void flush() { @@ -843,6 +935,12 @@ private void clearDisplay() private Layer mDAWLayer; + private Layer mAdjustingContinuousHardwareControlNotificationLayer; + + private ContinuousHardwareControl mContinuousHardwareControlThatIsBeingAdjusted; + + private long mHideAdjustingContinuousHardwareControlNotificationTime; + private Layer mBrowserLayer; private TrackBank mTrackBank; diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/ClipLaunchingLayer.java b/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/ClipLaunchingLayer.java index a8605e80..aded9016 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/ClipLaunchingLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/ClipLaunchingLayer.java @@ -1,13 +1,13 @@ package com.bitwig.extensions.controllers.arturia.minilab3; +import java.util.Arrays; + import com.bitwig.extension.controller.api.ClipLauncherSlot; import com.bitwig.extension.controller.api.MultiStateHardwareLight; import com.bitwig.extension.controller.api.Track; import com.bitwig.extension.controller.api.TrackBank; import com.bitwig.extensions.framework.Layer; -import java.util.Arrays; - public class ClipLaunchingLayer extends Layer { private final RgbLightState[] sceneSlotColors = new RgbLightState[8]; @@ -17,8 +17,6 @@ public class ClipLaunchingLayer extends Layer { private final MiniLab3Extension driver; private int blinkState; private long clipsStopTiming = 800; - private final byte[] colorBuffer = new byte[24]; - private final byte[] currentBuffer = new byte[24]; public ClipLaunchingLayer(final MiniLab3Extension driver) { super(driver.getLayers(), "CLIP LAUNCHER"); diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/MiniLab3Extension.java b/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/MiniLab3Extension.java index ac41b8f2..5d6fa8f4 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/MiniLab3Extension.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/MiniLab3Extension.java @@ -1,721 +1,760 @@ package com.bitwig.extensions.controllers.arturia.minilab3; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.function.IntConsumer; + import com.bitwig.extension.api.util.midi.ShortMidiMessage; import com.bitwig.extension.callback.ShortMidiMessageReceivedCallback; import com.bitwig.extension.controller.ControllerExtension; -import com.bitwig.extension.controller.api.*; +import com.bitwig.extension.controller.api.AbsoluteHardwareControl; +import com.bitwig.extension.controller.api.AbsoluteHardwareKnob; +import com.bitwig.extension.controller.api.BooleanValue; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.CursorDeviceFollowMode; +import com.bitwig.extension.controller.api.CursorRemoteControlsPage; +import com.bitwig.extension.controller.api.CursorTrack; +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.DocumentState; +import com.bitwig.extension.controller.api.HardwareActionBindable; +import com.bitwig.extension.controller.api.HardwareButton; +import com.bitwig.extension.controller.api.HardwareSlider; +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.MidiOut; +import com.bitwig.extension.controller.api.NoteInput; +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extension.controller.api.Preferences; +import com.bitwig.extension.controller.api.RelativeHardwareKnob; +import com.bitwig.extension.controller.api.RelativeHardwareValueMatcher; +import com.bitwig.extension.controller.api.RemoteControl; +import com.bitwig.extension.controller.api.Scene; +import com.bitwig.extension.controller.api.SettableEnumValue; +import com.bitwig.extension.controller.api.SettableRangedValue; +import com.bitwig.extension.controller.api.StringValue; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extension.controller.api.Transport; 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; import com.bitwig.extensions.framework.values.ValueObject; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.function.IntConsumer; - public class MiniLab3Extension extends ControllerExtension { - - public static final int NUM_PADS_TRACK = 8; - - private static final int NUM_SLIDERS = 4; - private static final int[] SLIDER_CC_MAPPING = new int[]{0x0E, 0x0F, 0x1E, 0x1F}; - private static final int[] ENCODER_CC_MAPPING = new int[]{0x56, 0x57, 0x59, 0x5A, 0x6E, 0x6F, 0x74, 0x75}; - - private static final String ANALOG_LAB_V_DEVICE_ID = "4172747541564953416C617650726F63"; - - private Layers layers; - private MidiIn midiIn; - private MidiOut midiOut; - private Layer mainLayer; - private Layer shiftLayer; - private HardwareSurface surface; - private ControllerHost host; - private OledDisplay oled; - - private final AbsoluteHardwareKnob[] knobs = new AbsoluteHardwareKnob[8]; - private final HardwareSlider[] sliders = new HardwareSlider[NUM_SLIDERS]; - private final RgbButton[] padBankAButtons = new RgbButton[NUM_PADS_TRACK]; - private final RgbButton[] padBankBButtons = new RgbButton[NUM_PADS_TRACK]; - - private HardwareButton shiftButton; - private int blinkState = 0; - private CursorTrack cursorTrack; - - private PinnableCursorDevice cursorDevice; - private CursorRemoteControlsPage parameterBank; - - private ClipLaunchingLayer clipLaunchingLayer; - private DrumPadLayer drumPadLayer; - private ArturiaModeLayer arturiaModeLayer; - - private Scene sceneTrackItem; - - private final ValueObject padBank = new ValueObject<>(PadBank.BANK_A); - private SysExHandler sysExHandler; - - private TrackBank viewTrackBank; - private Runnable nextPingAction = null; - private BrowserLayer browserLayer; - private String[] pageNames = new String[0]; - private RelativeHardwareKnob mainEncoder; - private RelativeHardwareKnob shiftMainEncoder; - private HardwareButton encoderPress; - private HardwareButton shiftEncoderPress; - private Transport transport; - private PinnableCursorDevice primaryDevice; - private FocusMode recordFocusMode = FocusMode.ARRANGER; - private EncoderStateMaschine encoderStateMaschine = new EncoderStateMaschine(); - - protected MiniLab3Extension(final MiniLab3ExtensionDefinition definition, final ControllerHost host) { - super(definition, host); - } - - private static ControllerHost debugHost; - - public static void println(final String format, final Object... args) { - if (debugHost != null) { - debugHost.println(format.formatted(args)); - } - } - - @Override - public void init() { - host = getHost(); - debugHost = host; - layers = new Layers(this); - midiIn = host.getMidiInPort(0); - midiIn.setMidiCallback((ShortMidiMessageReceivedCallback) this::onMidi0); - midiIn.setSysexCallback(this::handleSysExData); - midiOut = host.getMidiOutPort(0); - sysExHandler = new SysExHandler(midiOut, host); - surface = host.createHardwareSurface(); - oled = new OledDisplay(sysExHandler, host); - transport = host.createTransport(); - final String[] inputMasks = getInputMask(0x09, - new int[]{0x01, 0x09, 0x10, 0x11, 0x12, 0x13, 0x40, 0x47, 0x4a, 0x4c, 0x4d, 0x52, 0x53, 0x55, 0x5d, 0x70, 0x71, 0x72, 0x73}); - final NoteInput noteInput = midiIn.createNoteInput("MIDI", inputMasks); // - - noteInput.setShouldConsumeEvents(true); - initCursors(); - setUpHardware(); - - mainLayer = new Layer(layers, "MAIN"); - shiftLayer = new Layer(layers, "SHIFT"); - //shiftDown.addValueObserver(active -> shiftLayer.setIsActive(active)); - browserLayer = new BrowserLayer(this); - shiftLayer.setIsActive(true); - - bindSliderValue(mainLayer, cursorTrack.volume(), sliders[0], cursorTrack.name(), new BasicStringValue("Vol")); - bindKnobValue(4, mainLayer, cursorTrack.pan(), sliders[3], cursorTrack.name(), new BasicStringValue("Pan"), - "Fader"); - bindKnobValue(2, mainLayer, cursorTrack.sendBank().getItemAt(0), sliders[1], cursorTrack.name(), - cursorTrack.sendBank().getItemAt(0).name(), "Fader"); - bindKnobValue(3, mainLayer, cursorTrack.sendBank().getItemAt(1), sliders[2], cursorTrack.name(), - cursorTrack.sendBank().getItemAt(1).name(), "Fader"); - - shiftButton.isPressed().addValueObserver(this::handleShift); - - for (int i = 0; i < NUM_PADS_TRACK; i++) { - final RemoteControl parameter = parameterBank.getParameter(i); - bindKnobValue(i + 1, mainLayer, parameter, knobs[i], cursorDevice.name(), parameter.name(), "Knob"); - } - - setUpTransportControl(); - - initEncoders(); - - clipLaunchingLayer = new ClipLaunchingLayer(this); - drumPadLayer = new DrumPadLayer(this); - arturiaModeLayer = new ArturiaModeLayer(this); - - mainLayer.activate(); - clipLaunchingLayer.activate(); - drumPadLayer.activate(); - - sysExHandler.deviceInquiry(); - setUpPreferences(); - host.scheduleTask(this::handlePing, 100); - } - - private void handleSysExData(final String sysEx) { - //MiniLab3Extension.println("<%s>", sysEx); - switch (sysEx) { - case "f000206b7f420200406300f7": - MiniLab3Extension.println(" ==> BANK A"); - toBankMode(PadBank.BANK_A); - break; - case "f000206b7f420200406301f7": - MiniLab3Extension.println(" ==> BANK B"); - toBankMode(PadBank.BANK_B); - break; - case "f000206b7f420200406201f7": // Arturia Mode - drumPadLayer.deactivate(); - clipLaunchingLayer.deactivate(); - arturiaModeLayer.activate(); - break; - case "f000206b7f420200406202f7": // In DAW Mode - arturiaModeLayer.deactivate(); - drumPadLayer.activate(); - clipLaunchingLayer.activate(); - break; - case "f000206b7f420200400100f7": // Confirm in Arturia Mode - sysExHandler.enableProcessing(); - oled.notifyInit(); - drumPadLayer.deactivate(); - clipLaunchingLayer.deactivate(); - arturiaModeLayer.activate(); - host.showPopupNotification("MiniLab 3 Initialized"); - break; - case "f000206b7f420200400101f7": // Confirm Connected to BW Studio - sysExHandler.enableProcessing(); - oled.notifyInit(); - arturiaModeLayer.resetNotes(); - host.showPopupNotification("MiniLab 3 Initialized"); - break; - default: - if (sysEx.startsWith("f07e7f060200206b0200040")) { - //host.println(" DEVICE ID " + sysEx); - sysExHandler.requestInitState(); - } else { - host.println("Unknown Received SysEx : " + sysEx); + + public static final int NUM_PADS_TRACK = 8; + + private static final int NUM_SLIDERS = 4; + private static final int[] SLIDER_CC_MAPPING = new int[] {0x0E, 0x0F, 0x1E, 0x1F}; + private static final int[] ENCODER_CC_MAPPING = new int[] {0x56, 0x57, 0x59, 0x5A, 0x6E, 0x6F, 0x74, 0x75}; + + private static final String ANALOG_LAB_V_DEVICE_ID = "4172747541564953416C617650726F63"; + + private Layers layers; + private MidiIn midiIn; + private MidiOut midiOut; + private Layer mainLayer; + private HardwareSurface surface; + private ControllerHost host; + private OledDisplay oled; + + private final AbsoluteHardwareKnob[] knobs = new AbsoluteHardwareKnob[8]; + private final HardwareSlider[] sliders = new HardwareSlider[NUM_SLIDERS]; + private final RgbButton[] padBankAButtons = new RgbButton[NUM_PADS_TRACK]; + private final RgbButton[] padBankBButtons = new RgbButton[NUM_PADS_TRACK]; + + private HardwareButton shiftButton; + private int blinkState = 0; + private CursorTrack cursorTrack; + + private PinnableCursorDevice cursorDevice; + private CursorRemoteControlsPage parameterBank; + + private ClipLaunchingLayer clipLaunchingLayer; + private DrumPadLayer drumPadLayer; + private ArturiaModeLayer arturiaModeLayer; + + private Scene sceneTrackItem; + + private final ValueObject padBank = new ValueObject<>(PadBank.BANK_A); + private SysExHandler sysExHandler; + + private TrackBank viewTrackBank; + private Runnable nextPingAction = null; + private BrowserLayer browserLayer; + private String[] pageNames = new String[0]; + private RelativeHardwareKnob mainEncoder; + private RelativeHardwareKnob shiftMainEncoder; + private HardwareButton encoderPress; + private HardwareButton shiftEncoderPress; + private Transport transport; + private PinnableCursorDevice primaryDevice; + private FocusMode recordFocusMode = FocusMode.ARRANGER; + private final EncoderStateMaschine encoderStateMaschine = new EncoderStateMaschine(); + + protected MiniLab3Extension(final MiniLab3ExtensionDefinition definition, final ControllerHost host) { + super(definition, host); + } + + private static ControllerHost debugHost; + + public static void println(final String format, final Object... args) { + if (debugHost != null) { + debugHost.println(format.formatted(args)); + } + } + + @Override + public void init() { + host = getHost(); + debugHost = host; + layers = new Layers(this); + midiIn = host.getMidiInPort(0); + midiIn.setMidiCallback((ShortMidiMessageReceivedCallback) this::onMidi0); + midiIn.setSysexCallback(this::handleSysExData); + midiOut = host.getMidiOutPort(0); + sysExHandler = new SysExHandler(midiOut, host); + surface = host.createHardwareSurface(); + oled = new OledDisplay(sysExHandler, host); + transport = host.createTransport(); + final String[] inputMasks = getInputMask(0x09, new int[] { + 0x01, + 0x09, + 0x10, + 0x11, + 0x12, + 0x13, + 0x40, + 0x47, + 0x4a, + 0x4c, + 0x4d, + 0x52, + 0x53, + 0x55, + 0x5d, + 0x70, + 0x71, + 0x72, + 0x73 + }); + final NoteInput noteInput = midiIn.createNoteInput("MIDI", inputMasks); // + + noteInput.setShouldConsumeEvents(true); + initCursors(); + setUpHardware(); + + mainLayer = new Layer(layers, "MAIN"); + browserLayer = new BrowserLayer(this); + + bindSliderValue(mainLayer, cursorTrack.volume(), sliders[0], cursorTrack.name(), new BasicStringValue("Vol")); + bindKnobValue(4, mainLayer, cursorTrack.pan(), sliders[3], cursorTrack.name(), new BasicStringValue("Pan"), + "Fader"); + bindKnobValue(2, mainLayer, cursorTrack.sendBank().getItemAt(0), sliders[1], cursorTrack.name(), + cursorTrack.sendBank().getItemAt(0).name(), "Fader"); + bindKnobValue(3, mainLayer, cursorTrack.sendBank().getItemAt(1), sliders[2], cursorTrack.name(), + cursorTrack.sendBank().getItemAt(1).name(), "Fader"); + + shiftButton.isPressed().addValueObserver(this::handleShift); + + for (int i = 0; i < NUM_PADS_TRACK; i++) { + final RemoteControl parameter = parameterBank.getParameter(i); + bindKnobValue(i + 1, mainLayer, parameter, knobs[i], cursorDevice.name(), parameter.name(), "Knob"); + } + + setUpTransportControl(); + + initEncoders(); + + clipLaunchingLayer = new ClipLaunchingLayer(this); + drumPadLayer = new DrumPadLayer(this); + arturiaModeLayer = new ArturiaModeLayer(this); + + mainLayer.activate(); + clipLaunchingLayer.activate(); + drumPadLayer.activate(); + + sysExHandler.deviceInquiry(); + setUpPreferences(); + host.scheduleTask(this::handlePing, 100); + } + + private void handleSysExData(final String sysEx) { + //MiniLab3Extension.println("<%s>", sysEx); + switch (sysEx) { + case "f000206b7f420200406300f7": + MiniLab3Extension.println(" ==> BANK A"); + toBankMode(PadBank.BANK_A); + break; + case "f000206b7f420200406301f7": + MiniLab3Extension.println(" ==> BANK B"); + toBankMode(PadBank.BANK_B); + break; + case "f000206b7f420200406201f7": // Arturia Mode + drumPadLayer.deactivate(); + clipLaunchingLayer.deactivate(); + arturiaModeLayer.activate(); + break; + case "f000206b7f420200406202f7": // In DAW Mode + arturiaModeLayer.deactivate(); + drumPadLayer.activate(); + clipLaunchingLayer.activate(); + break; + case "f000206b7f420200400100f7": // Confirm in Arturia Mode + sysExHandler.enableProcessing(); + oled.notifyInit(); + drumPadLayer.deactivate(); + clipLaunchingLayer.deactivate(); + arturiaModeLayer.activate(); + host.showPopupNotification("MiniLab 3 Initialized"); + break; + case "f000206b7f420200400101f7": // Confirm Connected to BW Studio + sysExHandler.enableProcessing(); + oled.notifyInit(); + arturiaModeLayer.resetNotes(); + host.showPopupNotification("MiniLab 3 Initialized"); + break; + default: + if (sysEx.startsWith("f07e7f060200206b0200040")) { + //host.println(" DEVICE ID " + sysEx); + sysExHandler.requestInitState(); + } else { + host.println("Unknown Received SysEx : " + sysEx); + } + break; + } + } + + private void handlePing() { + blinkState++; + clipLaunchingLayer.notifyBlink(blinkState); + oled.handleTransient(); + if (nextPingAction != null) { + nextPingAction.run(); + nextPingAction = null; + } + host.scheduleTask(this::handlePing, 100); + } + + private String[] getInputMask(final int excludeChannel, final int[] miniLabPassThroughCcs) { + final List masks = new ArrayList<>(); + for (int i = 0; i < 16; i++) { + if (i != excludeChannel) { + masks.add(String.format("8%01x????", i)); + masks.add(String.format("9%01x????", i)); } - break; - } - } - - private void handlePing() { - blinkState++; - clipLaunchingLayer.notifyBlink(blinkState); - oled.handleTransient(); - if (nextPingAction != null) { - nextPingAction.run(); - nextPingAction = null; - } - host.scheduleTask(this::handlePing, 100); - } - - private String[] getInputMask(final int excludeChannel, final int[] miniLabPassThroughCcs) { - final List masks = new ArrayList<>(); - for (int i = 0; i < 16; i++) { - if (i != excludeChannel) { - masks.add(String.format("8%01x????", i)); - masks.add(String.format("9%01x????", i)); - } - } - masks.add("A?????"); // Poly Aftertouch - masks.add("D?????"); // Channel Aftertouch - masks.add("E?????"); // Pitchbend - masks.add("B1????"); // CCs Channel 2 - //masks.add("B0????"); - for (final int miniLabPassThroughCc : miniLabPassThroughCcs) { - masks.add(String.format("B0%02x??", miniLabPassThroughCc)); - } - return masks.toArray(String[]::new); - } - - - private void initCursors() { - cursorTrack = host.createCursorTrack(2, NUM_PADS_TRACK); - viewTrackBank = host.createTrackBank(NUM_PADS_TRACK, 2, 1); - viewTrackBank.followCursorTrack(cursorTrack); - sceneTrackItem = viewTrackBank.sceneBank().getScene(0); - sceneTrackItem.name() - .addValueObserver( + } + masks.add("A?????"); // Poly Aftertouch + masks.add("D?????"); // Channel Aftertouch + masks.add("E?????"); // Pitchbend + masks.add("B1????"); // CCs Channel 2 + //masks.add("B0????"); + for (final int miniLabPassThroughCc : miniLabPassThroughCcs) { + masks.add(String.format("B0%02x??", miniLabPassThroughCc)); + } + return masks.toArray(String[]::new); + } + + + private void initCursors() { + cursorTrack = host.createCursorTrack(2, NUM_PADS_TRACK); + viewTrackBank = host.createTrackBank(NUM_PADS_TRACK, 2, 1); + viewTrackBank.followCursorTrack(cursorTrack); + sceneTrackItem = viewTrackBank.sceneBank().getScene(0); + sceneTrackItem.name().addValueObserver( sceneName -> oled.sendTextInfo(DisplayMode.SCENE, cursorTrack.name().get(), sceneName, true)); - - cursorDevice = cursorTrack.createCursorDevice(); - cursorDevice.hasNext().markInterested(); - cursorDevice.hasPrevious().markInterested(); - cursorDevice.exists().markInterested(); - - primaryDevice = cursorTrack.createCursorDevice("DrumDetection", "Pad Device", NUM_PADS_TRACK, - CursorDeviceFollowMode.FIRST_INSTRUMENT); - - setUpFollowArturiaDevice(); - - - cursorDevice.presetName().markInterested(); - cursorTrack.name() - .addValueObserver( + + cursorDevice = cursorTrack.createCursorDevice(); + cursorDevice.hasNext().markInterested(); + cursorDevice.hasPrevious().markInterested(); + cursorDevice.exists().markInterested(); + + primaryDevice = cursorTrack.createCursorDevice("DrumDetection", "Pad Device", NUM_PADS_TRACK, + CursorDeviceFollowMode.FIRST_INSTRUMENT); + + setUpFollowArturiaDevice(); + + + cursorDevice.presetName().markInterested(); + cursorTrack.name().addValueObserver( name -> updateTrackInfo(transport.isPlaying().get(), transport.isArrangerRecordEnabled().get(), name, - cursorDevice.name().get(), cursorDevice.exists().get())); - cursorDevice.name() - .addValueObserver( + cursorDevice.name().get(), cursorDevice.exists().get())); + cursorDevice.name().addValueObserver( deviceName -> updateTrackInfo(transport.isPlaying().get(), transport.isArrangerRecordEnabled().get(), - cursorTrack.name().get(), deviceName, cursorDevice.exists().get())); - cursorDevice.exists() - .addValueObserver( + cursorTrack.name().get(), deviceName, cursorDevice.exists().get())); + cursorDevice.exists().addValueObserver( deviceExists -> updateTrackInfo(transport.isPlaying().get(), transport.isArrangerRecordEnabled().get(), - cursorTrack.name().get(), cursorDevice.name().get(), deviceExists)); - parameterBank = cursorDevice.createCursorRemoteControlsPage(NUM_PADS_TRACK); - } - - private void setUpFollowArturiaDevice() { - final DeviceMatcher arturiaMatcher = host.createVST3DeviceMatcher(ANALOG_LAB_V_DEVICE_ID); - final DeviceBank matcherBank = cursorTrack.createDeviceBank(1); - matcherBank.setDeviceMatcher(arturiaMatcher); - final Device matcherDevice = matcherBank.getItemAt(0); - matcherDevice.exists().markInterested(); - - final BooleanValueObject controlsAnalogLab = new BooleanValueObject(); - - controlsAnalogLab.addValueObserver(controlsLab -> sysExHandler.fireArturiaMode( - controlsLab ? SysExHandler.GeneralMode.ANALOG_LAB : SysExHandler.GeneralMode.DAW_MODE, - arturiaModeLayer.isActive())); - - final BooleanValue onArturiaDevice = cursorDevice.createEqualsValue(matcherDevice); - cursorTrack.arm() - .addValueObserver( + cursorTrack.name().get(), cursorDevice.name().get(), deviceExists)); + parameterBank = cursorDevice.createCursorRemoteControlsPage(NUM_PADS_TRACK); + } + + private void setUpFollowArturiaDevice() { + final DeviceMatcher arturiaMatcher = host.createVST3DeviceMatcher(ANALOG_LAB_V_DEVICE_ID); + final DeviceBank matcherBank = cursorTrack.createDeviceBank(1); + matcherBank.setDeviceMatcher(arturiaMatcher); + final Device matcherDevice = matcherBank.getItemAt(0); + matcherDevice.exists().markInterested(); + + final BooleanValueObject controlsAnalogLab = new BooleanValueObject(); + + controlsAnalogLab.addValueObserver(controlsLab -> sysExHandler.fireArturiaMode( + controlsLab ? SysExHandler.GeneralMode.ANALOG_LAB : SysExHandler.GeneralMode.DAW_MODE, + arturiaModeLayer.isActive())); + + final BooleanValue onArturiaDevice = cursorDevice.createEqualsValue(matcherDevice); + cursorTrack.arm().addValueObserver( armed -> controlsAnalogLab.set(armed && cursorDevice.exists().get() && onArturiaDevice.get())); - onArturiaDevice.addValueObserver( - onArturia -> controlsAnalogLab.set(cursorTrack.arm().get() && cursorDevice.exists().get() && onArturia)); - cursorDevice.exists() - .addValueObserver(cursorDeviceExists -> controlsAnalogLab.set( + onArturiaDevice.addValueObserver( + onArturia -> controlsAnalogLab.set(cursorTrack.arm().get() && cursorDevice.exists().get() && onArturia)); + cursorDevice.exists().addValueObserver(cursorDeviceExists -> controlsAnalogLab.set( cursorTrack.arm().get() && cursorDeviceExists && onArturiaDevice.get())); - } - - - private void setUpPreferences() { - DocumentState documentState = getHost().getDocumentState(); // THIS - 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); - updateTrackInfo(); - }); - Preferences preferences = getHost().getPreferences(); - final SettableEnumValue clipStopTiming = preferences.getEnumSetting("Long press to stop clip", // - "Clip", new String[]{"Fast", "Medium", "Standard"}, "Medium"); - clipStopTiming.addValueObserver(clipLaunchingLayer::setClipStopTiming); - } - - void bindEncoder(final Layer layer, final RelativeHardwareKnob encoder, final IntConsumer action) { - final HardwareActionBindable incAction = host.createAction(() -> action.accept(1), () -> "+"); - final HardwareActionBindable decAction = host.createAction(() -> action.accept(-1), () -> "-"); - layer.bind(encoder, host.createRelativeHardwareControlStepTarget(incAction, decAction)); - } - - private void initEncoders() { - bindEncoder(mainLayer, mainEncoder, this::mainEncoderAction); - bindEncoder(mainLayer, shiftMainEncoder, this::mainEncoderShiftAction); - - parameterBank.pageNames().addValueObserver(pages -> { - pageNames = pages; - showParameterPage(parameterBank.selectedPageIndex().get()); - }); - parameterBank.pageCount().markInterested(); - parameterBank.selectedPageIndex().addValueObserver(this::showParameterPage); - shiftEncoderPress.isPressed().addValueObserver(this::handleShiftEncoderPressed); - encoderPress.isPressed().addValueObserver(this::handleEncoderPressed); - } - - public RelativeHardwareKnob getMainEncoder() { - return mainEncoder; - } - - public RelativeHardwareKnob getShiftMainEncoder() { - return shiftMainEncoder; - } - - private void showParameterPage(final int index) { - if (parameterBank.pageCount().get() == 0) { - oled.sendTextInfo(DisplayMode.PARAM_PAGE, cursorDevice.name().get(), "", true); - } else if (index >= 0 && index < pageNames.length) { - oled.sendTextInfo(DisplayMode.PARAM_PAGE, cursorDevice.name().get(), pageNames[index], true); - } - } - - public OledDisplay getOled() { - return oled; - } - - public SysExHandler getSysExHandler() { - return sysExHandler; - } - - - public Layers getLayers() { - return layers; - } - - public PinnableCursorDevice getCursorDevice() { - return cursorDevice; - } - - public PinnableCursorDevice getPrimaryDevice() { - return primaryDevice; - } - - private void bindSliderValue(final Layer layer, final Parameter parameter, final AbsoluteHardwareControl slider, - final StringValue nameSource, final StringValue label) { - layer.bind(slider, parameter.value()); - layer.bind(slider, v -> oled.enableValues(DisplayMode.PARAM)); - label.addValueObserver( - v -> oled.sendSliderInfo(DisplayMode.PARAM, parameter.value().get(), v + " : " + nameSource.get(), - parameter.value().displayedValue().get())); - parameter.value() - .displayedValue() - .addValueObserver(displayedValue -> oled.sendSliderInfo(DisplayMode.PARAM, parameter.value().get(), - label.get() + " : " + nameSource.get(), displayedValue)); - parameter.value() - .addValueObserver(v -> oled.sendSliderInfo(DisplayMode.PARAM, v, label.get() + " : " + nameSource.get(), - parameter.value().displayedValue().get())); - } - - private void bindKnobValue(final int index, final Layer layer, final Parameter parameter, - final AbsoluteHardwareControl slider, final StringValue nameSource, - final StringValue label, final String type) { - final SettableRangedValue value = parameter.value(); - value.markInterested(); - parameter.exists().markInterested(); - nameSource.markInterested(); - layer.bind(slider, value); - layer.bind(slider, v -> oled.enableValues(DisplayMode.PARAM)); - value.displayedValue().markInterested(); - label.addValueObserver( - v -> oled.sendSliderInfo(DisplayMode.PARAM, value.get(), String.format("%s : %s", v, nameSource.get()), - value.displayedValue().get())); - value.displayedValue() - .addValueObserver(displayedValue -> oled.sendEncoderInfo(DisplayMode.PARAM, value.get(), + } + + + private void setUpPreferences() { + final DocumentState documentState = getHost().getDocumentState(); // THIS + 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); + updateTrackInfo(); + }); + final Preferences preferences = getHost().getPreferences(); + final SettableEnumValue clipStopTiming = preferences.getEnumSetting("Long press to stop clip", // + "Clip", new String[] {"Fast", "Medium", "Standard"}, "Medium"); + clipStopTiming.addValueObserver(clipLaunchingLayer::setClipStopTiming); + } + + void bindEncoder(final Layer layer, final RelativeHardwareKnob encoder, final IntConsumer action) { + final HardwareActionBindable incAction = host.createAction(() -> action.accept(1), () -> "+"); + final HardwareActionBindable decAction = host.createAction(() -> action.accept(-1), () -> "-"); + layer.bind(encoder, host.createRelativeHardwareControlStepTarget(incAction, decAction)); + } + + private void initEncoders() { + bindEncoder(mainLayer, mainEncoder, this::mainEncoderAction); + bindEncoder(mainLayer, shiftMainEncoder, this::mainEncoderShiftAction); + + parameterBank.pageNames().addValueObserver(pages -> { + pageNames = pages; + showParameterPage(parameterBank.selectedPageIndex().get()); + }); + parameterBank.pageCount().markInterested(); + parameterBank.selectedPageIndex().addValueObserver(this::showParameterPage); + shiftEncoderPress.isPressed().addValueObserver(this::handleShiftEncoderPressed); + encoderPress.isPressed().addValueObserver(this::handleEncoderPressed); + } + + public RelativeHardwareKnob getMainEncoder() { + return mainEncoder; + } + + public RelativeHardwareKnob getShiftMainEncoder() { + return shiftMainEncoder; + } + + private void showParameterPage(final int index) { + if (parameterBank.pageCount().get() == 0) { + oled.sendTextInfo(DisplayMode.PARAM_PAGE, cursorDevice.name().get(), "", true); + } else if (index >= 0 && index < pageNames.length) { + oled.sendTextInfo(DisplayMode.PARAM_PAGE, cursorDevice.name().get(), pageNames[index], true); + } + } + + public OledDisplay getOled() { + return oled; + } + + public SysExHandler getSysExHandler() { + return sysExHandler; + } + + + public Layers getLayers() { + return layers; + } + + public PinnableCursorDevice getCursorDevice() { + return cursorDevice; + } + + public PinnableCursorDevice getPrimaryDevice() { + return primaryDevice; + } + + private void bindSliderValue(final Layer layer, final Parameter parameter, final AbsoluteHardwareControl slider, + final StringValue nameSource, final StringValue label) { + layer.bind(slider, parameter.value()); + layer.bind(slider, v -> oled.enableValues(DisplayMode.PARAM)); + label.addValueObserver( + v -> oled.sendSliderInfo(DisplayMode.PARAM, parameter.value().get(), v + " : " + nameSource.get(), + parameter.value().displayedValue().get())); + parameter.value().displayedValue().addValueObserver( + displayedValue -> oled.sendSliderInfo(DisplayMode.PARAM, parameter.value().get(), + label.get() + " : " + nameSource.get(), displayedValue)); + parameter.value().addValueObserver( + v -> oled.sendSliderInfo(DisplayMode.PARAM, v, label.get() + " : " + nameSource.get(), + parameter.value().displayedValue().get())); + } + + private void bindKnobValue(final int index, final Layer layer, final Parameter parameter, + final AbsoluteHardwareControl slider, final StringValue nameSource, final StringValue label, + final String type) { + final SettableRangedValue value = parameter.value(); + value.markInterested(); + parameter.exists().markInterested(); + nameSource.markInterested(); + layer.bind(slider, value); + layer.bind(slider, v -> oled.enableValues(DisplayMode.PARAM)); + value.displayedValue().markInterested(); + label.addValueObserver( + v -> oled.sendSliderInfo(DisplayMode.PARAM, value.get(), String.format("%s : %s", v, nameSource.get()), + value.displayedValue().get())); + value.displayedValue().addValueObserver(displayedValue -> oled.sendEncoderInfo(DisplayMode.PARAM, value.get(), String.format("%s : %s", label.get(), nameSource.get()), displayedValue)); - value.addValueObserver( - v -> oled.sendEncoderInfo(DisplayMode.PARAM, v, String.format("%s : %s", label.get(), nameSource.get()), - value.displayedValue().get())); - if (type.equals("Fader")) { - slider.value().addValueObserver(v -> { - if (!parameter.exists().get()) { - oled.sendSliderInfo(DisplayMode.PARAM, v, String.format("%s : %d", type, index), - String.format("%d", (int) Math.round(v * 127))); - } - }); - } else { - slider.value().addValueObserver(v -> { - if (!parameter.exists().get()) { - oled.sendEncoderInfo(DisplayMode.PARAM, v, String.format("%s : %d", type, index), - String.format("%d", (int) Math.round(v * 127))); - } - }); - } - } - - private void setUpTransportControl() { - final RgbButton loopButton = new RgbButton(0x57, PadBank.TRANSPORT, RgbButton.Type.CC, 105, 0, true, this); - loopButton.bindToggle(shiftLayer, transport.isArrangerLoopEnabled(), RgbLightState.ORANGE, - RgbLightState.ORANGE_DIMMED); - transport.isArrangerLoopEnabled() - .addValueObserver( + value.addValueObserver( + v -> oled.sendEncoderInfo(DisplayMode.PARAM, v, String.format("%s : %s", label.get(), nameSource.get()), + value.displayedValue().get())); + if (type.equals("Fader")) { + slider.value().addValueObserver(v -> { + if (!parameter.exists().get()) { + oled.sendSliderInfo(DisplayMode.PARAM, v, String.format("%s : %d", type, index), + String.format("%d", (int) Math.round(v * 127))); + } + }); + } else { + slider.value().addValueObserver(v -> { + if (!parameter.exists().get()) { + oled.sendEncoderInfo(DisplayMode.PARAM, v, String.format("%s : %d", type, index), + String.format("%d", (int) Math.round(v * 127))); + } + }); + } + } + + private void setUpTransportControl() { + final RgbButton loopButton = new RgbButton(0x57, PadBank.TRANSPORT, RgbButton.Type.CC, 105, 0, true, this); + loopButton.bindToggle(mainLayer, transport.isArrangerLoopEnabled(), RgbLightState.ORANGE, + RgbLightState.ORANGE_DIMMED); + transport.isArrangerLoopEnabled().addValueObserver( loopEnabled -> oled.sendTextCond(DisplayMode.LOOP_VALUE, "Loop Mode", loopEnabled ? "ON" : "OFF")); - loopButton.bind(shiftLayer, () -> { - final boolean loopEnabled = transport.isArrangerLoopEnabled().get(); - oled.sendText(DisplayMode.LOOP_VALUE, "Loop Mode", loopEnabled ? "ON" : "OFF"); - }, () -> transport.isArrangerLoopEnabled().get() ? RgbLightState.ORANGE : RgbLightState.ORANGE_DIMMED); - - final RgbButton recordButton = new RgbButton(0x5A, PadBank.TRANSPORT, RgbButton.Type.CC, 108, 0, true, this); - transport.isArrangerRecordEnabled().markInterested(); - transport.isClipLauncherOverdubEnabled().markInterested(); - - recordButton.bind(shiftLayer, this::handleRecordPressed, this::getRecordingLightState); - - final RgbButton playButton = new RgbButton(0x59, PadBank.TRANSPORT, RgbButton.Type.CC, 107, 0, true, this); - playButton.bindToggle(shiftLayer, transport.isPlaying(), RgbLightState.GREEN, RgbLightState.GREEN_DIMMED); - - final RgbButton stopButton = new RgbButton(0x58, PadBank.TRANSPORT, RgbButton.Type.CC, 106, 0, true, this); - stopButton.bindPressed(shiftLayer, pressed -> { - if (pressed) { - transport.stop(); - } - }, () -> transport.isPlaying().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); - - final RgbButton tapButton = new RgbButton(0x5B, PadBank.TRANSPORT, RgbButton.Type.CC, 109, 0, true, this); - transport.tempo().value().addRawValueObserver(v -> { - final int tempo = (int) Math.round(v); - oled.sendTextCond(DisplayMode.TEMPO, "Tap Tempo", String.format("%d BPM", tempo)); - }); - tapButton.bind(shiftLayer, () -> { - transport.tapTempo(); - final int tempo = (int) Math.round(transport.tempo().value().getRaw()); - oled.sendText(DisplayMode.TEMPO, "Tap Tempo", String.format("%d BPM", tempo)); - }, RgbLightState.WHITE, RgbLightState.WHITE_DIMMED); - - transport.isArrangerRecordEnabled() - .addValueObserver(isRecording -> updateTrackInfo(transport.isPlaying().get(), + loopButton.bind(mainLayer, () -> { + final boolean loopEnabled = transport.isArrangerLoopEnabled().get(); + oled.sendText(DisplayMode.LOOP_VALUE, "Loop Mode", loopEnabled ? "ON" : "OFF"); + }, () -> transport.isArrangerLoopEnabled().get() ? RgbLightState.ORANGE : RgbLightState.ORANGE_DIMMED); + + final RgbButton recordButton = new RgbButton(0x5A, PadBank.TRANSPORT, RgbButton.Type.CC, 108, 0, true, this); + transport.isArrangerRecordEnabled().markInterested(); + transport.isClipLauncherOverdubEnabled().markInterested(); + + recordButton.bind(mainLayer, this::handleRecordPressed, this::getRecordingLightState); + + final RgbButton playButton = new RgbButton(0x59, PadBank.TRANSPORT, RgbButton.Type.CC, 107, 0, true, this); + playButton.bindToggle(mainLayer, transport.isPlaying(), RgbLightState.GREEN, RgbLightState.GREEN_DIMMED); + + final RgbButton stopButton = new RgbButton(0x58, PadBank.TRANSPORT, RgbButton.Type.CC, 106, 0, true, this); + stopButton.bindPressed(mainLayer, pressed -> { + if (pressed) { + transport.stop(); + } + }, () -> transport.isPlaying().get() ? RgbLightState.WHITE : RgbLightState.WHITE_DIMMED); + + final RgbButton tapButton = new RgbButton(0x5B, PadBank.TRANSPORT, RgbButton.Type.CC, 109, 0, true, this); + transport.tempo().value().addRawValueObserver(v -> { + final int tempo = (int) Math.round(v); + oled.sendTextCond(DisplayMode.TEMPO, "Tap Tempo", String.format("%d BPM", tempo)); + }); + tapButton.bind(mainLayer, () -> { + transport.tapTempo(); + final int tempo = (int) Math.round(transport.tempo().value().getRaw()); + oled.sendText(DisplayMode.TEMPO, "Tap Tempo", String.format("%d BPM", tempo)); + }, RgbLightState.WHITE, RgbLightState.WHITE_DIMMED); + + transport.isArrangerRecordEnabled().addValueObserver(isRecording -> updateTrackInfo(transport.isPlaying().get(), recordFocusMode == FocusMode.ARRANGER ? isRecording : recordFocusMode.getState(transport))); - transport.isClipLauncherOverdubEnabled() - .addValueObserver(isRecording -> updateTrackInfo(transport.isPlaying().get(), - recordFocusMode == FocusMode.LAUNCHER ? isRecording : recordFocusMode.getState(transport))); - - transport.isPlaying() - .addValueObserver(isPlaying -> updateTrackInfo(isPlaying, recordFocusMode.getState(transport))); - } - - private RgbLightState getRecordingLightState() { - if (recordFocusMode == FocusMode.ARRANGER) { - return transport.isArrangerRecordEnabled().get() ? RgbLightState.RED : RgbLightState.RED_DIMMED; - } else { - return transport.isClipLauncherOverdubEnabled().get() ? RgbLightState.RED : RgbLightState.RED_DIMMED; - } - } - - private void handleRecordPressed() { - if (recordFocusMode == FocusMode.ARRANGER) { - transport.isArrangerRecordEnabled().toggle(); - } else { - transport.isClipLauncherOverdubEnabled().toggle(); - } - } - - public void browserDisplayMode(final boolean browserModeActive) { - oled.setMainMode(browserModeActive ? DisplayMode.BROWSER : DisplayMode.TRACK, - browserModeActive ? browserLayer::updateInfo : this::updateTrackInfo); - if (!browserModeActive) { - updateTrackInfo(); - } - } - - private void updateTrackInfo(final boolean isPlaying, final boolean isRecording, final String trackName, - final String deviceName, final boolean deviceExists) { - oled.sendPictogramInfo(DisplayMode.TRACK, isRecording ? OledDisplay.Pict.REC : OledDisplay.Pict.NONE, - isPlaying ? OledDisplay.Pict.PLAY : OledDisplay.Pict.NONE, trackName, - deviceExists ? deviceName : ""); - } - - private void updateTrackInfo(final boolean playing, final boolean recording) { - updateTrackInfo(playing, recording, cursorTrack.name().get(), cursorDevice.name().get(), - cursorDevice.exists().get()); - } - - private void updateTrackInfo() { - updateTrackInfo(transport.isPlaying().get(), recordFocusMode.getState(transport), cursorTrack.name().get(), - cursorDevice.name().get(), cursorDevice.exists().get()); - } - - public ValueObject getPadBank() { - return padBank; - } - - private void toBankMode(final PadBank bankMode) { - padBank.set(bankMode); - } - - private RelativeHardwareKnob createMainEncoder(final int ccNr) { - final RelativeHardwareKnob mainEncoder = surface.createRelativeHardwareKnob("MAIN_ENCODER+_" + ccNr); - final RelativeHardwareValueMatcher stepUpMatcher = midiIn.createRelativeValueMatcher( - "(status == 176 && data1 == " + ccNr + " && data2 > 64)", 1); - final RelativeHardwareValueMatcher stepDownMatcher = midiIn.createRelativeValueMatcher( - "(status == 176 && data1 == " + ccNr + " && data2 < 63)", -1); - final RelativeHardwareValueMatcher matcher = host.createOrRelativeHardwareValueMatcher(stepDownMatcher, - stepUpMatcher); - mainEncoder.setAdjustValueMatcher(matcher); - mainEncoder.setStepSize(1); - return mainEncoder; - } - - private HardwareButton createEncoderPress(final int ccNr, final String name) { - final HardwareButton encoderButton = surface.createHardwareButton(name); - encoderButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(0, ccNr, 127)); - encoderButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(0, ccNr, 0)); - return encoderButton; - } - - private void handleShift(final boolean pressed) { - encoderStateMaschine.doTransition( - pressed ? EncoderStateMaschine.Event.SHIFT_DOWN : EncoderStateMaschine.Event.SHIFT_UP); - } - - /** - * when in mode HOLD+SHIFT and you release the Hold button, you of stay in the parameter/device mode. - * from now on, - */ - - private void handleEncoderPressed(final boolean down) { - if (browserLayer.isActive()) { - browserLayer.pressAction(down); - encoderStateMaschine.doTransition( - down ? EncoderStateMaschine.Event.ENCODER_DOWN : EncoderStateMaschine.Event.ENCODER_UP); - encoderStateMaschine.notifyTurn(false); - } else { - if (down && encoderStateMaschine.getState() == EncoderStateMaschine.State.INITIAL) { - oled.enableValues(DisplayMode.PARAM_PAGE); - } else { - if (encoderStateMaschine.getState() == EncoderStateMaschine.State.HOLD && !encoderStateMaschine.isTurnAction() && padBank.get() != PadBank.BANK_B) { - clipLaunchingLayer.launchScene(); + transport.isClipLauncherOverdubEnabled().addValueObserver( + isRecording -> updateTrackInfo(transport.isPlaying().get(), + recordFocusMode == FocusMode.LAUNCHER ? isRecording : recordFocusMode.getState(transport))); + + transport.isPlaying() + .addValueObserver(isPlaying -> updateTrackInfo(isPlaying, recordFocusMode.getState(transport))); + } + + private RgbLightState getRecordingLightState() { + if (recordFocusMode == FocusMode.ARRANGER) { + return transport.isArrangerRecordEnabled().get() ? RgbLightState.RED : RgbLightState.RED_DIMMED; + } else { + return transport.isClipLauncherOverdubEnabled().get() ? RgbLightState.RED : RgbLightState.RED_DIMMED; + } + } + + private void handleRecordPressed() { + if (recordFocusMode == FocusMode.ARRANGER) { + transport.isArrangerRecordEnabled().toggle(); + } else { + transport.isClipLauncherOverdubEnabled().toggle(); + } + } + + public void browserDisplayMode(final boolean browserModeActive) { + oled.setMainMode(browserModeActive ? DisplayMode.BROWSER : DisplayMode.TRACK, + browserModeActive ? browserLayer::updateInfo : this::updateTrackInfo); + if (!browserModeActive) { + updateTrackInfo(); + } + } + + private void updateTrackInfo(final boolean isPlaying, final boolean isRecording, final String trackName, + final String deviceName, final boolean deviceExists) { + oled.sendPictogramInfo(DisplayMode.TRACK, isRecording ? OledDisplay.Pict.REC : OledDisplay.Pict.NONE, + isPlaying ? OledDisplay.Pict.PLAY : OledDisplay.Pict.NONE, trackName, + deviceExists ? deviceName : ""); + } + + private void updateTrackInfo(final boolean playing, final boolean recording) { + updateTrackInfo(playing, recording, cursorTrack.name().get(), cursorDevice.name().get(), + cursorDevice.exists().get()); + } + + private void updateTrackInfo() { + updateTrackInfo(transport.isPlaying().get(), recordFocusMode.getState(transport), cursorTrack.name().get(), + cursorDevice.name().get(), cursorDevice.exists().get()); + } + + public ValueObject getPadBank() { + return padBank; + } + + private void toBankMode(final PadBank bankMode) { + padBank.set(bankMode); + } + + private RelativeHardwareKnob createMainEncoder(final int ccNr) { + final RelativeHardwareKnob mainEncoder = surface.createRelativeHardwareKnob("MAIN_ENCODER+_" + ccNr); + final RelativeHardwareValueMatcher stepUpMatcher = + midiIn.createRelativeValueMatcher("(status == 176 && data1 == " + ccNr + " && data2 > 64)", 1); + final RelativeHardwareValueMatcher stepDownMatcher = + midiIn.createRelativeValueMatcher("(status == 176 && data1 == " + ccNr + " && data2 < 63)", -1); + final RelativeHardwareValueMatcher matcher = + host.createOrRelativeHardwareValueMatcher(stepDownMatcher, stepUpMatcher); + mainEncoder.setAdjustValueMatcher(matcher); + mainEncoder.setStepSize(1); + return mainEncoder; + } + + private HardwareButton createEncoderPress(final int ccNr, final String name) { + final HardwareButton encoderButton = surface.createHardwareButton(name); + encoderButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(0, ccNr, 127)); + encoderButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(0, ccNr, 0)); + return encoderButton; + } + + private void handleShift(final boolean pressed) { + encoderStateMaschine.doTransition( + pressed ? EncoderStateMaschine.Event.SHIFT_DOWN : EncoderStateMaschine.Event.SHIFT_UP); + } + + /** + * when in mode HOLD+SHIFT and you release the Hold button, you of stay in the parameter/device mode. + * from now on, + */ + + private void handleEncoderPressed(final boolean down) { + if (browserLayer.isActive()) { + browserLayer.pressAction(down); + encoderStateMaschine.doTransition( + down ? EncoderStateMaschine.Event.ENCODER_DOWN : EncoderStateMaschine.Event.ENCODER_UP); + encoderStateMaschine.notifyTurn(false); + } else { + if (down && encoderStateMaschine.getState() == EncoderStateMaschine.State.INITIAL) { + oled.enableValues(DisplayMode.PARAM_PAGE); + } else { + if (encoderStateMaschine.getState() == EncoderStateMaschine.State.HOLD + && !encoderStateMaschine.isTurnAction() && padBank.get() != PadBank.BANK_B) { + clipLaunchingLayer.launchScene(); + } + } + updateTrackInfo(); + encoderStateMaschine.doTransition( + down ? EncoderStateMaschine.Event.ENCODER_DOWN : EncoderStateMaschine.Event.ENCODER_UP); + } + } + + private void handleShiftEncoderPressed(final boolean down) { + if (!down && encoderStateMaschine.getState() == EncoderStateMaschine.State.SHIFT_HOLD + && !encoderStateMaschine.isTurnAction()) { + if (!browserLayer.isActive()) { + browserLayer.shiftPressAction(encoderStateMaschine.getTimeSinceLastEvent()); + } else { + browserLayer.shiftPressAction(-1); } - } - updateTrackInfo(); - encoderStateMaschine.doTransition( + } + encoderStateMaschine.doTransition( down ? EncoderStateMaschine.Event.ENCODER_DOWN : EncoderStateMaschine.Event.ENCODER_UP); - } - } - - private void handleShiftEncoderPressed(final boolean down) { - if (!down && encoderStateMaschine.getState() == EncoderStateMaschine.State.SHIFT_HOLD && !encoderStateMaschine.isTurnAction()) { - if (!browserLayer.isActive()) { - browserLayer.shiftPressAction(encoderStateMaschine.getTimeSinceLastEvent()); - } else { - browserLayer.shiftPressAction(-1); - } - } - encoderStateMaschine.doTransition( - down ? EncoderStateMaschine.Event.ENCODER_DOWN : EncoderStateMaschine.Event.ENCODER_UP); - } - - private void mainEncoderAction(final int dir) { - encoderStateMaschine.notifyTurn(false); - oled.disableValues(); - switch (encoderStateMaschine.getState()) { - case INITIAL -> navigateScenesOrPads(dir); - case HOLD -> navigateParametersBanks(dir); - } - } - - private void mainEncoderShiftAction(final int dir) { - encoderStateMaschine.notifyTurn(true); - oled.disableValues(); - switch (encoderStateMaschine.getState()) { - case HOLD_SHIFT, SHIFT_HOLD -> navigateDevice(dir); - case SHIFT -> navigateTracks(dir); - } - } - - private void navigateScenesOrPads(int dir) { - if (padBank.get() == PadBank.BANK_A) { - oled.enableValues(DisplayMode.SCENE); - oled.sendTextInfo(DisplayMode.SCENE, cursorTrack.name().get(), sceneTrackItem.name().get(), true); - clipLaunchingLayer.navigateScenes(dir); - } else { - drumPadLayer.navigate(dir); - } - } - - private void navigateDevice(final int dir) { - if (!cursorDevice.exists().get() && !cursorDevice.hasNext().get() && !cursorDevice.hasPrevious().get()) { - cursorDevice.selectFirstInChannel(cursorTrack); - } else if (dir > 0) { - cursorDevice.selectNext(); - } else { - cursorDevice.selectPrevious(); - } - } - - private void navigateTracks(final int dir) { - if (dir > 0) { - cursorTrack.selectNext(); - } else { - cursorTrack.selectPrevious(); - } - } - - private void navigateParametersBanks(final int dir) { - oled.enableValues(DisplayMode.PARAM_PAGE); - showParameterPage(parameterBank.selectedPageIndex().get()); - if (dir > 0) { - parameterBank.selectNext(); - } else { - parameterBank.selectPrevious(); - } - } - - private void setUpHardware() { - mainEncoder = createMainEncoder(0x1C); - shiftMainEncoder = createMainEncoder(0x1D); - encoderPress = createEncoderPress(0x76, "ENCODER_PRESSED"); - shiftEncoderPress = createEncoderPress(0x77, "SHIFT_ENCODER_PRESSED"); - - for (int i = 0; i < ENCODER_CC_MAPPING.length; i++) { - knobs[i] = surface.createAbsoluteHardwareKnob("KNOB_" + (i + 1)); - knobs[i].setAdjustValueMatcher(midiIn.createAbsoluteCCValueMatcher(0, ENCODER_CC_MAPPING[i])); - - padBankAButtons[i] = new RgbButton(i + 0x34, PadBank.BANK_A, RgbButton.Type.NOTE, 0x24 + i, 9, false, this); - padBankBButtons[i] = new RgbButton(i + 0x44, PadBank.BANK_B, RgbButton.Type.NOTE, 0x2C + i, 9, false, this); - } - - for (int i = 0; i < SLIDER_CC_MAPPING.length; i++) { - sliders[i] = surface.createHardwareSlider("FADER_" + (i + 1)); - sliders[i].setAdjustValueMatcher(midiIn.createAbsoluteCCValueMatcher(0, SLIDER_CC_MAPPING[i])); - } - shiftButton = surface.createHardwareButton("SHIFT"); - shiftButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(0, 27, 127)); - shiftButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(0, 27, 0)); - } - - public RgbButton[] getPadBankAButtons() { - return padBankAButtons; - } - - public RgbButton[] getPadBankBButtons() { - return padBankBButtons; - } - - public CursorTrack getCursorTrack() { - return cursorTrack; - } - - public TrackBank getViewTrackBank() { - return viewTrackBank; - } - - public void updateBankState(final InternalHardwareLightState state) { - if (state instanceof RgbBankLightState) { - sysExHandler.sendBankState((RgbBankLightState) state); - } - } - - private void onMidi0(final ShortMidiMessage msg) { - final int channel = msg.getChannel(); - final int sb = msg.getStatusByte() & (byte) 0xF0; - if (channel == 9) { - drumPadLayer.notifyNote(sb, msg.getData1()); - } - } - - public HardwareSurface getSurface() { - return surface; - } - - public MidiIn getMidiIn() { - return midiIn; - } - - public MidiOut getMidiOut() { - return midiOut; - } - - @Override - public void exit() { - final CompletableFuture shutdown = new CompletableFuture<>(); - Executors.newSingleThreadExecutor().execute(() -> { - oled.clearText(); - sysExHandler.disconnectState(); - try { - Thread.sleep(100); - } catch (final InterruptedException e) { + } + + private void mainEncoderAction(final int dir) { + encoderStateMaschine.notifyTurn(false); + oled.disableValues(); + switch (encoderStateMaschine.getState()) { + case INITIAL -> navigateScenesOrPads(dir); + case HOLD -> navigateParametersBanks(dir); + } + } + + private void mainEncoderShiftAction(final int dir) { + encoderStateMaschine.notifyTurn(true); + oled.disableValues(); + switch (encoderStateMaschine.getState()) { + case HOLD_SHIFT, SHIFT_HOLD -> navigateDevice(dir); + case SHIFT -> navigateTracks(dir); + } + } + + private void navigateScenesOrPads(final int dir) { + if (padBank.get() == PadBank.BANK_A) { + oled.enableValues(DisplayMode.SCENE); + oled.sendTextInfo(DisplayMode.SCENE, cursorTrack.name().get(), sceneTrackItem.name().get(), true); + clipLaunchingLayer.navigateScenes(dir); + } else { + drumPadLayer.navigate(dir); + } + } + + private void navigateDevice(final int dir) { + if (!cursorDevice.exists().get() && !cursorDevice.hasNext().get() && !cursorDevice.hasPrevious().get()) { + cursorDevice.selectFirstInChannel(cursorTrack); + } else if (dir > 0) { + cursorDevice.selectNext(); + } else { + cursorDevice.selectPrevious(); + } + } + + private void navigateTracks(final int dir) { + if (dir > 0) { + cursorTrack.selectNext(); + } else { + cursorTrack.selectPrevious(); + } + } + + private void navigateParametersBanks(final int dir) { + oled.enableValues(DisplayMode.PARAM_PAGE); + showParameterPage(parameterBank.selectedPageIndex().get()); + if (dir > 0) { + parameterBank.selectNext(); + } else { + parameterBank.selectPrevious(); + } + } + + private void setUpHardware() { + mainEncoder = createMainEncoder(0x1C); + shiftMainEncoder = createMainEncoder(0x1D); + encoderPress = createEncoderPress(0x76, "ENCODER_PRESSED"); + shiftEncoderPress = createEncoderPress(0x77, "SHIFT_ENCODER_PRESSED"); + + for (int i = 0; i < ENCODER_CC_MAPPING.length; i++) { + knobs[i] = surface.createAbsoluteHardwareKnob("KNOB_" + (i + 1)); + knobs[i].setAdjustValueMatcher(midiIn.createAbsoluteCCValueMatcher(0, ENCODER_CC_MAPPING[i])); + + padBankAButtons[i] = new RgbButton(i + 0x34, PadBank.BANK_A, RgbButton.Type.NOTE, 0x24 + i, 9, false, this); + padBankBButtons[i] = new RgbButton(i + 0x44, PadBank.BANK_B, RgbButton.Type.NOTE, 0x2C + i, 9, false, this); + } + + for (int i = 0; i < SLIDER_CC_MAPPING.length; i++) { + sliders[i] = surface.createHardwareSlider("FADER_" + (i + 1)); + sliders[i].setAdjustValueMatcher(midiIn.createAbsoluteCCValueMatcher(0, SLIDER_CC_MAPPING[i])); + } + shiftButton = surface.createHardwareButton("SHIFT"); + shiftButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(0, 27, 127)); + shiftButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(0, 27, 0)); + } + + public RgbButton[] getPadBankAButtons() { + return padBankAButtons; + } + + public RgbButton[] getPadBankBButtons() { + return padBankBButtons; + } + + public CursorTrack getCursorTrack() { + return cursorTrack; + } + + public TrackBank getViewTrackBank() { + return viewTrackBank; + } + + public void updateBankState(final InternalHardwareLightState state) { + if (state instanceof final RgbBankLightState lightState) { + sysExHandler.sendBankState(lightState); + } + } + + private void onMidi0(final ShortMidiMessage msg) { + final int channel = msg.getChannel(); + final int sb = msg.getStatusByte() & (byte) 0xF0; + if (channel == 9) { + drumPadLayer.notifyNote(sb, msg.getData1()); + } + } + + public HardwareSurface getSurface() { + return surface; + } + + public MidiIn getMidiIn() { + return midiIn; + } + + public MidiOut getMidiOut() { + return midiOut; + } + + @Override + public void exit() { + final CompletableFuture shutdown = new CompletableFuture<>(); + Executors.newSingleThreadExecutor().execute(() -> { + oled.clearText(); + sysExHandler.disconnectState(); + try { + Thread.sleep(100); + } + catch (final InterruptedException e) { + e.printStackTrace(); + } + shutdown.complete(true); + }); + try { + shutdown.get(); + } + catch (final InterruptedException | ExecutionException e) { e.printStackTrace(); - } - shutdown.complete(true); - }); - try { - shutdown.get(); - } catch (final InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - } - - @Override - public void flush() { - surface.updateHardware(); - } - - /** - * Make sure no scene is launched upon release. - */ - public void notifyTurn(boolean shift) { - encoderStateMaschine.notifyTurn(shift); - } - - + } + } + + @Override + public void flush() { + surface.updateHardware(); + } + + /** + * Make sure no scene is launched upon release. + */ + public void notifyTurn(final boolean shift) { + encoderStateMaschine.notifyTurn(shift); + } + + } diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/RgbBankLightState.java b/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/RgbBankLightState.java index 7f3ea01b..f941eec1 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/RgbBankLightState.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/RgbBankLightState.java @@ -71,8 +71,7 @@ public HardwareLightVisualState getVisualState() { @Override public boolean equals(final Object obj) { - if (obj instanceof RgbBankLightState) { - final RgbBankLightState other = (RgbBankLightState) obj; + if (obj instanceof final RgbBankLightState other) { return other.bank == bank && Arrays.equals(other.colors, colors); } return false; diff --git a/src/main/java/com/bitwig/extensions/controllers/esi/Xjam.java b/src/main/java/com/bitwig/extensions/controllers/esi/Xjam.java index 05d716eb..afee8c2c 100644 --- a/src/main/java/com/bitwig/extensions/controllers/esi/Xjam.java +++ b/src/main/java/com/bitwig/extensions/controllers/esi/Xjam.java @@ -9,96 +9,186 @@ import com.bitwig.extension.controller.api.CursorTrack; import com.bitwig.extension.controller.api.HardwareSurface; import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MidiOut; import com.bitwig.extension.controller.api.NoteInput; -import com.bitwig.extensions.framework.Layer; -import com.bitwig.extensions.framework.Layers; +import com.bitwig.extension.controller.api.RelativeHardwareKnob; +import com.bitwig.extension.controller.api.RemoteControl; public class Xjam extends ControllerExtension { - protected Xjam(ControllerExtensionDefinition definition, ControllerHost host) { - super(definition, host); - } - - @Override - public void init() { - mHost = getHost(); - mHardwareSurface = mHost.createHardwareSurface(); - - mMidiIn = mHost.getMidiInPort(0); - - mNoteInput = mMidiIn.createNoteInput("pad", "9?????", "D?????", "A0????"); - mNoteInput.setShouldConsumeEvents(false); - - mCursorTrack = mHost.createCursorTrack(0, 0); - mCursorDevice = mCursorTrack.createCursorDevice(); - mCursorRemoteControls = mCursorDevice.createCursorRemoteControlsPage(6); - - for (int i = 0; i < 18; i++) { - int index = i + 10; - int channel = 0; - if (i == 0) - index = 3; - if (i == 1) - index = 9; - if (i > 5) - channel = 1; - if (i > 11) - channel = 2; - - final AbsoluteHardwareKnob knob = mHardwareSurface.createAbsoluteHardwareKnob("enc" + i); - knob.setLabel(String.valueOf(i + 1)); - knob.setIndexInGroup(i); - knob.setAdjustValueMatcher(mMidiIn.createAbsoluteCCValueMatcher(channel, index)); - - mAbsoluteKnobs[i] = knob; - } - initLayers(); - - mHost.showPopupNotification("Xjam initialized..."); - } - - public void initLayers() { - mMainLayer = new Layer(mLayers, "Main"); - initMainLayer(); - } - - private void initMainLayer() { - for (int i = 0; i < 6; i++) { - mCursorRemoteControls.getParameter(i).setIndication(true); - mMainLayer.bind(mAbsoluteKnobs[i], mCursorRemoteControls.getParameter(i)); - } - mMainLayer.activate(); - } - - @Override - public void exit() { - } - - @Override - public void flush() { - mHardwareSurface.updateHardware(); - } - - // API Elements - private HardwareSurface mHardwareSurface; - private ControllerHost mHost; - - private MidiIn mMidiIn; - - private NoteInput mNoteInput; - - private CursorTrack mCursorTrack; - - private CursorDevice mCursorDevice; - - private CursorRemoteControlsPage mCursorRemoteControls; - - - - // Hardware elements - private AbsoluteHardwareKnob[] mAbsoluteKnobs = new AbsoluteHardwareKnob[18]; - - private final Layers mLayers = new Layers(this); - private Layer mMainLayer; - + protected Xjam(ControllerExtensionDefinition definition, ControllerHost host) { + super(definition, host); + } + + @Override + public void init() { + mHost = getHost(); + mHardwareSurface = mHost.createHardwareSurface(); + + mMidiIn = mHost.getMidiInPort(0); + + mMidiOut = mHost.getMidiOutPort(0); + + mNoteInput = mMidiIn.createNoteInput("pad", "9?????", "D?????", "A0????"); + mNoteInput.setShouldConsumeEvents(false); + + mCursorTrack = mHost.createCursorTrack(0, 0); + mCursorDevice = mCursorTrack.createCursorDevice(); + mCursorRemoteControls = mCursorDevice.createCursorRemoteControlsPage(mRelativeKnobs.length); + + createRelativeEncoders(); + createAbsoluteEncoders(); + + initRemoteControls(); + + mMidiIn.setSysexCallback(this::sysexDataReceived); + + sendSysexCommand(0x7f); // Handshake to retrieve version number + } + + private void initAfterVersionCheck() + { + mMidiOut.sendMidi(0xC0, 0x7f, 0x00); + + mHost.showPopupNotification("Xjam initialized!"); + } + + private void reportVersionCheckFail() + { + mHost.showPopupNotification("Xjam not initialized: please update the device firmware to 1.55 or newer."); + } + + private void createRelativeEncoders() + { + final int[] controlNumbers = { 3, 9, 12, 13, 14, 15 }; + + for (int i = 0; i < 6; i++) + { + int controlNumber = controlNumbers[i]; + final RelativeHardwareKnob knob = mHardwareSurface.createRelativeHardwareKnob("enc" + i); + knob.setLabel(String.valueOf(i + 1)); + knob.setIndexInGroup(i); + knob.setAdjustValueMatcher(mMidiIn.createRelativeSignedBitCCValueMatcher(0, controlNumber, 120)); + + mRelativeKnobs[i] = knob; + } + } + + private void createAbsoluteEncoders() + { + createAbsoluteEncoders("Yellow", 6, 1, new int[]{ 1, 5, 7, 8, 70, 71 }); + createAbsoluteEncoders("Red", 12, 2, new int[]{ 72, 73, 74, 80, 81, 91 }); + } + + private void createAbsoluteEncoders(final String color, final int offset, final int channel, final int[] controlNumbers) + { + assert controlNumbers.length == 6; + + for (int i = 0; i < 6; i++) + { + final int index = i + offset; + int controlNumber = controlNumbers[i]; + final AbsoluteHardwareKnob knob = mHardwareSurface.createAbsoluteHardwareKnob("enc" + index); + knob.setLabel(color + " " + (i + 1)); + knob.setIndexInGroup(index); + knob.setAdjustValueMatcher(mMidiIn.createAbsoluteCCValueMatcher(channel, controlNumber)); + } + } + + public void initRemoteControls() { + for (int i = 0; i < mRelativeKnobs.length; i++) + { + final RemoteControl parameter = mCursorRemoteControls.getParameter(i); + parameter.setIndication(true); + parameter.addBinding(mRelativeKnobs[i]); + } + } + + @Override + public void exit() { + if (mSceneIndexToSwitchBackTo >= 0) + mMidiOut.sendMidi(0xC0, mSceneIndexToSwitchBackTo, 0x00); + } + + @Override + public void flush() { + mHardwareSurface.updateHardware(); + } + + private void sendSysexCommand(final int command) + { + assert command < 128; + + final String sysex = String.format("F0 00 20 54 30 %02X F7", command); + mMidiOut.sendSysex(sysex); + } + + private void sysexDataReceived(final String data) + { + final String expectedPrefix = "f0002054"; + if (!data.startsWith(expectedPrefix)) + return; + + final String expectedSuffix = "f7"; + if (!data.endsWith(expectedSuffix)) + return; + + final String payload = data.substring(expectedPrefix.length(), data.length() - expectedSuffix.length()); + + if (payload.startsWith("2901") && payload.length() == 8) + { + // Version number + try + { + final int versionMajor = Integer.parseInt(payload.substring(4, 6), 16); + final int versionMinor = Integer.parseInt(payload.substring(6, 8), 16); + getHost().println("Version: " + versionMajor + "." + versionMinor); + if (versionMajor > 1 || (versionMajor == 1 && versionMinor >= 85)) + initAfterVersionCheck(); + else + reportVersionCheckFail(); + } + catch (final NumberFormatException e) + { + reportVersionCheckFail(); + } + + } + else if (payload.startsWith("300008") && payload.length() == 8) + { + // This message contains the current scene index. We request it when starting the extension and store it so + // that we can switch back during exit. + final int sceneIndexToSwitchBackTo = Integer.parseInt(payload.substring(6, 8), 16) - 1; + if (0 <= sceneIndexToSwitchBackTo && sceneIndexToSwitchBackTo < 32) + { + mSceneIndexToSwitchBackTo = (byte) sceneIndexToSwitchBackTo; + getHost().println("Scene index before: " + (mSceneIndexToSwitchBackTo + 1)); + } + else + { + getHost().println("Warning: invalid scene index (" + sceneIndexToSwitchBackTo + ")"); + } + } + } + + // API Elements + private HardwareSurface mHardwareSurface; + private ControllerHost mHost; + + private MidiIn mMidiIn; + + private MidiOut mMidiOut; + + private NoteInput mNoteInput; + + private CursorTrack mCursorTrack; + + private CursorDevice mCursorDevice; + + private CursorRemoteControlsPage mCursorRemoteControls; + + // Hardware elements + private RelativeHardwareKnob[] mRelativeKnobs = new RelativeHardwareKnob[6]; + + private byte mSceneIndexToSwitchBackTo = -1; } diff --git a/src/main/java/com/bitwig/extensions/controllers/esi/XjamDefinition.java b/src/main/java/com/bitwig/extensions/controllers/esi/XjamDefinition.java index 18cc127c..33d5f702 100644 --- a/src/main/java/com/bitwig/extensions/controllers/esi/XjamDefinition.java +++ b/src/main/java/com/bitwig/extensions/controllers/esi/XjamDefinition.java @@ -49,7 +49,7 @@ public String getAuthor() { @Override public String getVersion() { - return "0.1"; + return "0.2"; } @Override diff --git a/src/main/java/com/bitwig/extensions/controllers/icon/VCast.java b/src/main/java/com/bitwig/extensions/controllers/icon/VCast.java index 38e8b68b..8e1d3a3d 100644 --- a/src/main/java/com/bitwig/extensions/controllers/icon/VCast.java +++ b/src/main/java/com/bitwig/extensions/controllers/icon/VCast.java @@ -1,7 +1,5 @@ package com.bitwig.extensions.controllers.icon; -import java.util.Arrays; -import java.util.List; import java.util.UUID; import java.util.function.Consumer; @@ -19,7 +17,6 @@ import com.bitwig.extension.controller.api.HardwareLightVisualState; import com.bitwig.extension.controller.api.HardwareSlider; import com.bitwig.extension.controller.api.HardwareSurface; -import com.bitwig.extension.controller.api.HardwareTextDisplay; import com.bitwig.extension.controller.api.InternalHardwareLightState; import com.bitwig.extension.controller.api.MidiIn; import com.bitwig.extension.controller.api.MidiOut; @@ -176,13 +173,8 @@ private void initVuMeter() mVuMeterLight = mHardwareSurface.createMultiStateHardwareLight("vuMeter"); mVuMeterLight.state().setValue(new VuMeterState()); mVuMeterLight.state().onUpdateHardware((state) -> { - if (!(state instanceof VuMeterState)) - { - return; - } - - final VuMeterState s = (VuMeterState) state; - mMidiOut.sendMidi(0xD0, 0x30 + s.value(), 0x00); + if (state instanceof final VuMeterState s) + mMidiOut.sendMidi(0xD0, 0x30 + s.value(), 0x00); }); } diff --git a/src/main/java/com/bitwig/extensions/controllers/intuitive_instruments/exquis/ExquisControllerExtension.java b/src/main/java/com/bitwig/extensions/controllers/intuitive_instruments/exquis/ExquisControllerExtension.java new file mode 100644 index 00000000..7e0d1dcd --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/intuitive_instruments/exquis/ExquisControllerExtension.java @@ -0,0 +1,98 @@ +package com.bitwig.extensions.controllers.intuitive_instruments.exquis; + +import com.bitwig.extension.controller.ControllerExtension; +import com.bitwig.extension.controller.api.AbsoluteHardwareKnob; +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.framework.Layer; +import com.bitwig.extensions.framework.Layers; + +public class ExquisControllerExtension extends ControllerExtension +{ + protected ExquisControllerExtension(ExquisControllerExtensionDefinition definition, ControllerHost host) + { + super(definition, host); + } + + @Override + public void init() + { + final var host = getHost(); + + mMidiIn = host.getMidiInPort(0); + mMidiOut = host.getMidiOutPort(0); + + mNoteInput = mMidiIn.createNoteInput("", "8?????", "9?????", "B?40??", "B?4A??", "D?????", "E?????"); + + final String[] bendRanges = { "12", "24", "36", "48", "60", "72", "84", "96" }; + final var bendRange = host.getPreferences().getEnumSetting("Bend Range", "MIDI", bendRanges, "48"); + bendRange.addValueObserver(this::setPitchBendRange); + + setPitchBendRange(bendRange.get()); + + final var cursorTrack = host.createCursorTrack(0, 0); + final var cursorDevice = cursorTrack.createCursorDevice(); + final var cursorRemoteControlsPage = cursorDevice.createCursorRemoteControlsPage(4); + + final var layers = new Layers(this); + final var mainLayer = new Layer(layers, "Main"); + + final var hardwareSurface = host.createHardwareSurface(); + final AbsoluteHardwareKnob[] knobs = new AbsoluteHardwareKnob[4]; + + for (int i = 0; i < 4; ++i) + { + final var parameter = cursorRemoteControlsPage.getParameter(i); + parameter.markInterested(); + parameter.setIndication(true); + + knobs[i] = hardwareSurface.createAbsoluteHardwareKnob(Integer.toString(i + 1)); + knobs[i].setIndexInGroup(i); + knobs[i].setAdjustValueMatcher(mMidiIn.createAbsoluteCCValueMatcher(41 + i)); + + mainLayer.bind(knobs[i], parameter); + } + + mainLayer.activate(); + } + + void setPitchBendRange(final String range) + { + final int pb = Integer.parseInt(range); + mNoteInput.setUseExpressiveMidi(true, 0, pb); + sendPitchBendRangeRPN(1, pb); + } + + void sendPitchBendRangeRPN(final int channel, final int range) + { + // Set up MPE mode: Zone 1 15 channels + mMidiOut.sendMidi(0xB0, 101, 0); // Registered Parameter Number (RPN) - MSB* + mMidiOut.sendMidi(0xB0, 100, 6); // Registered Parameter Number (RPN) - LSB* + mMidiOut.sendMidi(0xB0, 6, 15); + mMidiOut.sendMidi(0xB0, 38, 0); + + // Set up pitch bend sensitivity to "range" semitones (00/00) + mMidiOut.sendMidi(0xB1, 100, 0); // Registered Parameter Number (RPN) - LSB* + mMidiOut.sendMidi(0xB1, 101, 0); // Registered Parameter Number (RPN) - MSB* + mMidiOut.sendMidi(0xB1, 38, 0); + mMidiOut.sendMidi(0xB1, 6, range); + } + + @Override + public void exit() + { + } + + @Override + public void flush() + { + } + + private MidiIn mMidiIn; + + private MidiOut mMidiOut; + + private NoteInput mNoteInput; +} diff --git a/src/main/java/com/bitwig/extensions/controllers/intuitive_instruments/exquis/ExquisControllerExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/intuitive_instruments/exquis/ExquisControllerExtensionDefinition.java new file mode 100644 index 00000000..d2a151df --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/intuitive_instruments/exquis/ExquisControllerExtensionDefinition.java @@ -0,0 +1,100 @@ +package com.bitwig.extensions.controllers.intuitive_instruments.exquis; + +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 ExquisControllerExtensionDefinition extends ControllerExtensionDefinition +{ + private final static UUID ID = UUID.fromString("b694f535-f022-416c-b714-f0b5924128f9"); + + @Override + public String getHardwareVendor() + { + return "Intuitive Instruments"; + } + + @Override + public String getHardwareModel() + { + return "Exquis"; + } + + @Override + public int getNumMidiInPorts() + { + return 1; + } + + @Override + public int getNumMidiOutPorts() + { + return 1; + } + + @Override + public void listAutoDetectionMidiPortNames( + final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) + { + final String inputNames[] = new String[1]; + final String outputNames[] = new String[1]; + + switch (platformType) + { + case LINUX: + inputNames[0] = "Exquis MIDI 1"; + outputNames[0] = "Exquis MIDI 1"; + break; + + case WINDOWS: + case MAC: + inputNames[0] = "Exquis"; + outputNames[0] = "Exquis"; + break; + } + + list.add(inputNames, outputNames); + } + + @Override + public ControllerExtension createInstance(ControllerHost host) + { + return new ExquisControllerExtension(this, host); + } + + @Override + public String getName() + { + return "Exquis"; + } + + @Override + public String getAuthor() + { + return "Bitwig"; + } + + @Override + public String getVersion() + { + return "1.0"; + } + + @Override + public UUID getId() + { + return ID; + } + + @Override + public int getRequiredAPIVersion() + { + return 18; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mackie/devices/ParameterPage.java b/src/main/java/com/bitwig/extensions/controllers/mackie/devices/ParameterPage.java index 3be74b63..d53ead93 100644 --- a/src/main/java/com/bitwig/extensions/controllers/mackie/devices/ParameterPage.java +++ b/src/main/java/com/bitwig/extensions/controllers/mackie/devices/ParameterPage.java @@ -1,8 +1,23 @@ package com.bitwig.extensions.controllers.mackie.devices; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.DoubleConsumer; +import java.util.function.IntConsumer; + import com.bitwig.extension.callback.DoubleValueChangedCallback; import com.bitwig.extension.callback.IntegerValueChangedCallback; -import com.bitwig.extension.controller.api.*; +import com.bitwig.extension.controller.api.AbsoluteHardwareControl; +import com.bitwig.extension.controller.api.AbsoluteHardwareControlBinding; +import com.bitwig.extension.controller.api.DoubleValue; +import com.bitwig.extension.controller.api.HardwareSlider; +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extension.controller.api.RelativeHardwareControl; +import com.bitwig.extension.controller.api.RelativeHardwareControlToRangedValueBinding; +import com.bitwig.extension.controller.api.RelativeHardwareKnob; +import com.bitwig.extension.controller.api.SettableRangedValue; +import com.bitwig.extension.controller.api.StringValue; import com.bitwig.extensions.controllers.mackie.bindings.FaderParameterBankBinding; import com.bitwig.extensions.controllers.mackie.bindings.ResetableAbsoluteValueBinding; import com.bitwig.extensions.controllers.mackie.bindings.ResetableRelativeValueBinding; @@ -12,299 +27,291 @@ import com.bitwig.extensions.controllers.mackie.display.RingDisplayType; import com.bitwig.extensions.controllers.mackie.value.ModifierValueObject; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.DoubleConsumer; -import java.util.function.IntConsumer; - public class ParameterPage implements SettableRangedValue { - - private static final int RING_RANGE = 10; - private DeviceParameter currentParameter; - - private ResetableRelativeValueBinding relativeEncoderBinding; - private ResetableAbsoluteValueBinding absoluteEncoderBinding; - private RingParameterBankDisplayBinding ringBinding; - private FaderParameterBankBinding faderBinding; - - // listeners for the parameter Name - private final List> nameChangeCallbacks = new ArrayList<>(); - // listeners for the display values - private final List> valueChangeCallbacks = new ArrayList<>(); - // listeners for the ring display values - private final List intValueCallbacks = new ArrayList<>(); - // listeners for existence of value - private final List doulbeValueCallbacks = new ArrayList<>(); - - private final List pages = new ArrayList<>(); - - public ParameterPage(final int index, final SpecificDevice device) { - - for (int page = 0; page < device.getPages(); page++) { - final int pIndex = pages.size(); - final DeviceParameter deviceParameter = device.createDeviceParameter(page, index); - final Parameter param = deviceParameter.parameter; - - param.value().markInterested(); - param.value().addValueObserver(v -> { - if (pIndex == device.getCurrentPage()) { - final int intv = (int) (v * RING_RANGE); - notifyIntValueChanged(intv); - notifyValueChanged(v); - } - }); - if (deviceParameter.getCustomValueConverter() != null) { - final CustomValueConverter converter = deviceParameter.getCustomValueConverter(); - param.value().addValueObserver(converter.getIntRange(), v -> { - if (pIndex == device.getCurrentPage()) { - notifyValueChanged(converter.convert(v)); - } - }); - } else { - param.value().displayedValue().addValueObserver(v -> { - if (pIndex == device.getCurrentPage()) { - notifyValueChanged(v); - } + + private static final int RING_RANGE = 10; + private DeviceParameter currentParameter; + + private ResetableRelativeValueBinding relativeEncoderBinding; + private ResetableAbsoluteValueBinding absoluteEncoderBinding; + private RingParameterBankDisplayBinding ringBinding; + private FaderParameterBankBinding faderBinding; + + // listeners for the parameter Name + private final List> nameChangeCallbacks = new ArrayList<>(); + // listeners for the display values + private final List> valueChangeCallbacks = new ArrayList<>(); + // listeners for the ring display values + private final List intValueCallbacks = new ArrayList<>(); + // listeners for existence of value + private final List doulbeValueCallbacks = new ArrayList<>(); + + private final List pages = new ArrayList<>(); + + public ParameterPage(final int index, final SpecificDevice device) { + + for (int page = 0; page < device.getPages(); page++) { + final int pIndex = pages.size(); + final DeviceParameter deviceParameter = device.createDeviceParameter(page, index); + final Parameter param = deviceParameter.parameter; + + param.value().markInterested(); + param.value().addValueObserver(v -> { + if (pIndex == device.getCurrentPage()) { + final int intv = (int) (v * RING_RANGE); + notifyIntValueChanged(intv); + notifyValueChanged(v); + } }); - } - - pages.add(deviceParameter); - } - currentParameter = pages.get(device.getCurrentPage()); - } - - public Parameter getParameter(final int pageIndex) { - assert pageIndex < pages.size(); - return pages.get(pageIndex).parameter; - } - - public void triggerUpdate() { - if (ringBinding != null) { - ringBinding.update(); - } - if (faderBinding != null) { - faderBinding.update(); - } - } - - public RingParameterBankDisplayBinding createRingBinding(final RingDisplay display) { - ringBinding = new RingParameterBankDisplayBinding(this, display); - return ringBinding; - } - - public FaderParameterBankBinding createFaderBinding(final FaderResponse fader) { - faderBinding = new FaderParameterBankBinding(this, fader); - return faderBinding; - } - - public ResetableRelativeValueBinding getRelativeEncoderBinding(final RelativeHardwareKnob encoder) { - relativeEncoderBinding = new ResetableRelativeValueBinding(encoder, this); - return relativeEncoderBinding; - } - - public ResetableAbsoluteValueBinding getFaderBinding(final HardwareSlider fader) { - absoluteEncoderBinding = new ResetableAbsoluteValueBinding(fader, this); - return absoluteEncoderBinding; - } - - public void updatePage(final int currentPage) { - currentParameter = pages.get(currentPage); - resetBindings(); - } - - public void resetBindings() { - if (relativeEncoderBinding != null) { - relativeEncoderBinding.reset(); - } - if (absoluteEncoderBinding != null) { - absoluteEncoderBinding.reset(); - } - notifyValueChanged(getCurrentValue()); - notifyNameChanged(getCurrentName()); - } - - public Parameter getCurrentParameter() { - return currentParameter.parameter; - } - - @Override - public double get() { - return currentParameter.parameter.get(); - } - - @Override - public DoubleValue getOrigin() - { - return currentParameter.parameter.getOrigin(); - } - - @Override - public void markInterested() { - currentParameter.parameter.markInterested(); - } - - public void addStringValueObserver(final Consumer callback) { - valueChangeCallbacks.add(callback); - } - - @Override - public void addValueObserver(final DoubleValueChangedCallback callback) { - } - - @Override - public boolean isSubscribed() { - return currentParameter.parameter.isSubscribed(); - } - - @Override - public void setIsSubscribed(final boolean value) { - } - - @Override - public void subscribe() { - currentParameter.parameter.subscribe(); - } - - @Override - public void unsubscribe() { - currentParameter.parameter.unsubscribe(); - } - - @Override - public void set(final double value) { - currentParameter.parameter.set(value); - } - - @Override - public void inc(final double amount) { - currentParameter.parameter.inc(amount); - } - - @Override - public double getRaw() { - return currentParameter.parameter.getRaw(); - } - - @Override - public StringValue displayedValue() { - return currentParameter.parameter.displayedValue(); - } - - @Override - public void addValueObserver(final int range, final IntegerValueChangedCallback callback) { - // Not needed - } - - @Override - public void addRawValueObserver(final DoubleValueChangedCallback callback) { - // Not needed - } - - @Override - public void setImmediately(final double value) { - currentParameter.parameter.setImmediately(value); - } - - @Override - public void set(final Number value, final Number resolution) { - currentParameter.parameter.set(value, resolution); - } - - @Override - public void inc(final Number increment, final Number resolution) { - currentParameter.parameter.inc(increment, resolution); - } - - @Override - public void setRaw(final double value) { - currentParameter.parameter.setRaw(value); - } - - @Override - public void incRaw(final double delta) { - currentParameter.parameter.incRaw(delta); - } - - @Override - public AbsoluteHardwareControlBinding addBindingWithRange(final AbsoluteHardwareControl hardwareControl, - final double minNormalizedValue, - final double maxNormalizedValue) { - return currentParameter.parameter.addBindingWithRange(hardwareControl, minNormalizedValue, maxNormalizedValue); - } - - @Override - public RelativeHardwareControlToRangedValueBinding addBindingWithRangeAndSensitivity( - final RelativeHardwareControl hardwareControl, final double minNormalizedValue, final double maxNormalizedValue, - final double sensitivity) { - return currentParameter.parameter.addBindingWithRangeAndSensitivity(hardwareControl, minNormalizedValue, - maxNormalizedValue, currentParameter.getSensitivity()); - } - - private void notifyValueChanged(final String value) { - valueChangeCallbacks.forEach(callback -> callback.accept(value)); - } - - public String getCurrentValue() { - return currentParameter.getStringValue(); - } - - public void addNameObserver(final Consumer callback) { - nameChangeCallbacks.add(callback); - } - - private void notifyNameChanged(final String name) { - nameChangeCallbacks.forEach(callback -> callback.accept(currentParameter.getName())); - } - - public String getCurrentName() { - return currentParameter.getName(); - } - - public void addIntValueObserver(final IntConsumer listener) { - intValueCallbacks.add(listener); - } - - private void notifyIntValueChanged(final int value) { - intValueCallbacks.forEach(callback -> callback.accept(value)); - } - - private void notifyValueChanged(final double value) { - doulbeValueCallbacks.forEach(callback -> callback.accept(value)); - } - - public int getIntValue() { - return (int) (currentParameter.parameter.value().get() * RING_RANGE); - } - - public RingDisplayType getRingDisplayType() { - return currentParameter.getRingDisplayType(); - } - - public double getParamValue() { - return currentParameter.parameter.value().get(); - } - - public void addDoubleValueObserver(final DoubleConsumer listener) { - doulbeValueCallbacks.add(listener); - } - - public void doReset() { - currentParameter.parameter.reset(); - } - - public void resetAll() { - for (final DeviceParameter parameter : pages) { - parameter.doReset(); - } - } - - public void notifyEnablement(final int value) { - ringBinding.handleEnabled(value); - } - - public void doReset(final ModifierValueObject modifier) { - currentParameter.parameter.reset(); - } - + if (deviceParameter.getCustomValueConverter() != null) { + final CustomValueConverter converter = deviceParameter.getCustomValueConverter(); + param.value().addValueObserver(converter.getIntRange(), v -> { + if (pIndex == device.getCurrentPage()) { + notifyValueChanged(converter.convert(v)); + } + }); + } else { + param.value().displayedValue().addValueObserver(v -> { + if (pIndex == device.getCurrentPage()) { + notifyValueChanged(v); + } + }); + } + + pages.add(deviceParameter); + } + currentParameter = pages.get(device.getCurrentPage()); + } + + public Parameter getParameter(final int pageIndex) { + assert pageIndex < pages.size(); + return pages.get(pageIndex).parameter; + } + + public void triggerUpdate() { + if (ringBinding != null) { + ringBinding.update(); + } + if (faderBinding != null) { + faderBinding.update(); + } + } + + public RingParameterBankDisplayBinding createRingBinding(final RingDisplay display) { + ringBinding = new RingParameterBankDisplayBinding(this, display); + return ringBinding; + } + + public FaderParameterBankBinding createFaderBinding(final FaderResponse fader) { + faderBinding = new FaderParameterBankBinding(this, fader); + return faderBinding; + } + + public ResetableRelativeValueBinding getRelativeEncoderBinding(final RelativeHardwareKnob encoder) { + relativeEncoderBinding = new ResetableRelativeValueBinding(encoder, this); + return relativeEncoderBinding; + } + + public ResetableAbsoluteValueBinding getFaderBinding(final HardwareSlider fader) { + absoluteEncoderBinding = new ResetableAbsoluteValueBinding(fader, this); + return absoluteEncoderBinding; + } + + public void updatePage(final int currentPage) { + currentParameter = pages.get(currentPage); + resetBindings(); + } + + public void resetBindings() { + if (relativeEncoderBinding != null) { + relativeEncoderBinding.reset(); + } + if (absoluteEncoderBinding != null) { + absoluteEncoderBinding.reset(); + } + notifyValueChanged(getCurrentValue()); + notifyNameChanged(getCurrentName()); + } + + public Parameter getCurrentParameter() { + return currentParameter.parameter; + } + + @Override + public double get() { + return currentParameter.parameter.get(); + } + + @Override + public void markInterested() { + currentParameter.parameter.markInterested(); + } + + public void addStringValueObserver(final Consumer callback) { + valueChangeCallbacks.add(callback); + } + + @Override + public void addValueObserver(final DoubleValueChangedCallback callback) { + } + + @Override + public boolean isSubscribed() { + return currentParameter.parameter.isSubscribed(); + } + + @Override + public void setIsSubscribed(final boolean value) { + } + + @Override + public void subscribe() { + currentParameter.parameter.subscribe(); + } + + @Override + public void unsubscribe() { + currentParameter.parameter.unsubscribe(); + } + + @Override + public void set(final double value) { + currentParameter.parameter.set(value); + } + + @Override + public void inc(final double amount) { + currentParameter.parameter.inc(amount); + } + + @Override + public double getRaw() { + return currentParameter.parameter.getRaw(); + } + + @Override + public StringValue displayedValue() { + return currentParameter.parameter.displayedValue(); + } + + @Override + public void addValueObserver(final int range, final IntegerValueChangedCallback callback) { + // Not needed + } + + @Override + public void addRawValueObserver(final DoubleValueChangedCallback callback) { + // Not needed + } + + @Override + public void setImmediately(final double value) { + currentParameter.parameter.setImmediately(value); + } + + @Override + public void set(final Number value, final Number resolution) { + currentParameter.parameter.set(value, resolution); + } + + @Override + public void inc(final Number increment, final Number resolution) { + currentParameter.parameter.inc(increment, resolution); + } + + @Override + public void setRaw(final double value) { + currentParameter.parameter.setRaw(value); + } + + @Override + public void incRaw(final double delta) { + currentParameter.parameter.incRaw(delta); + } + + @Override + public AbsoluteHardwareControlBinding addBindingWithRange(final AbsoluteHardwareControl hardwareControl, + final double minNormalizedValue, final double maxNormalizedValue) { + return currentParameter.parameter.addBindingWithRange(hardwareControl, minNormalizedValue, maxNormalizedValue); + } + + @Override + public RelativeHardwareControlToRangedValueBinding addBindingWithRangeAndSensitivity( + final RelativeHardwareControl hardwareControl, final double minNormalizedValue, final double maxNormalizedValue, + final double sensitivity) { + return currentParameter.parameter.addBindingWithRangeAndSensitivity(hardwareControl, minNormalizedValue, + maxNormalizedValue, currentParameter.getSensitivity()); + } + + private void notifyValueChanged(final String value) { + valueChangeCallbacks.forEach(callback -> callback.accept(value)); + } + + public String getCurrentValue() { + return currentParameter.getStringValue(); + } + + public void addNameObserver(final Consumer callback) { + nameChangeCallbacks.add(callback); + } + + private void notifyNameChanged(final String name) { + nameChangeCallbacks.forEach(callback -> callback.accept(currentParameter.getName())); + } + + public String getCurrentName() { + return currentParameter.getName(); + } + + public void addIntValueObserver(final IntConsumer listener) { + intValueCallbacks.add(listener); + } + + private void notifyIntValueChanged(final int value) { + intValueCallbacks.forEach(callback -> callback.accept(value)); + } + + private void notifyValueChanged(final double value) { + doulbeValueCallbacks.forEach(callback -> callback.accept(value)); + } + + public int getIntValue() { + return (int) (currentParameter.parameter.value().get() * RING_RANGE); + } + + public RingDisplayType getRingDisplayType() { + return currentParameter.getRingDisplayType(); + } + + public double getParamValue() { + return currentParameter.parameter.value().get(); + } + + @Override + public DoubleValue getOrigin() { + return currentParameter.parameter.value().getOrigin(); + } + + public void addDoubleValueObserver(final DoubleConsumer listener) { + doulbeValueCallbacks.add(listener); + } + + public void doReset() { + currentParameter.parameter.reset(); + } + + public void resetAll() { + for (final DeviceParameter parameter : pages) { + parameter.doReset(); + } + } + + public void notifyEnablement(final int value) { + ringBinding.handleEnabled(value); + } + + public void doReset(final ModifierValueObject modifier) { + currentParameter.parameter.reset(); + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/mackie/display/LcdDisplay.java b/src/main/java/com/bitwig/extensions/controllers/mackie/display/LcdDisplay.java index f6c70dca..9e0e9e44 100644 --- a/src/main/java/com/bitwig/extensions/controllers/mackie/display/LcdDisplay.java +++ b/src/main/java/com/bitwig/extensions/controllers/mackie/display/LcdDisplay.java @@ -7,6 +7,7 @@ import com.bitwig.extensions.controllers.mackie.StringUtil; import com.bitwig.extensions.controllers.mackie.section.SectionType; +import java.nio.charset.StandardCharsets; import java.util.Arrays; /** @@ -233,9 +234,9 @@ public void sendToDisplay(final DisplaySource source, final int row, final Strin private void sendFullRow(final int row, final String text) { rowDisplayBuffer[6] = (byte) (row * LcdDisplay.ROW2_START); - final char[] ca = text.toCharArray(); + final byte[] ca = text.getBytes(StandardCharsets.US_ASCII); for (int i = 0; i < displayLen; i++) { - rowDisplayBuffer[i + 7] = i < ca.length ? (byte) ca[i] : 32; + rowDisplayBuffer[i + 7] = i < ca.length ? ca[i] : 32; } displayRep.line(row).text().setValue(text); midiOut.sendSysex(rowDisplayBuffer); @@ -263,9 +264,9 @@ public void sendToRowFull(final DisplaySource source, final int row, final int s private void sendTextSegFull(final DisplaySource source, final int row, final int segment, final String text) { segBuffer[6] = (byte) (row * LcdDisplay.ROW2_START + segment * segmentLength + segmentOffset); - final char[] ca = text.toCharArray(); + final byte[] ca = text.getBytes(StandardCharsets.US_ASCII); for (int i = 0; i < segmentLength; i++) { - segBuffer[i + 7] = i < ca.length ? (byte) ca[i] : 32; + segBuffer[i + 7] = i < ca.length ? ca[i] : 32; lines[row][segment * 7 + i] = (char) segBuffer[i + 7]; } midiOut.sendSysex(segBuffer); @@ -273,7 +274,7 @@ private void sendTextSegFull(final DisplaySource source, final int row, final in } private void sendTextSeg(final DisplaySource source, final int row, final int segment, final String text) { - final char[] ca = text.toCharArray(); + final byte[] ca = text.getBytes(StandardCharsets.US_ASCII); if (segment == 8) { if (isLowerDisplay()) { handleLastCell(row, segment, ca); @@ -292,10 +293,10 @@ private void sendTextSeg(final DisplaySource source, final int row, final int se } } - private void handleLastCell(final int row, final int segment, final char[] ca) { + private void handleLastCell(final int row, final int segment, final byte[] ca) { segBufferExp[6] = (byte) (row * LcdDisplay.ROW2_START + segment * segmentLength + segmentOffset); for (int i = 0; i < segmentLength; i++) { - segBufferExp[i + 7] = i < ca.length ? (byte) ca[i] : 32; + segBufferExp[i + 7] = i < ca.length ? ca[i] : 32; } if (segment < segmentLength + 1) { segBufferExp[6 + segmentLength] = ' '; diff --git a/src/main/java/com/bitwig/extensions/controllers/mackie/section/MixControl.java b/src/main/java/com/bitwig/extensions/controllers/mackie/section/MixControl.java index 98795521..53eff973 100644 --- a/src/main/java/com/bitwig/extensions/controllers/mackie/section/MixControl.java +++ b/src/main/java/com/bitwig/extensions/controllers/mackie/section/MixControl.java @@ -1,12 +1,30 @@ package com.bitwig.extensions.controllers.mackie.section; -import com.bitwig.extension.controller.api.*; +import com.bitwig.extension.controller.api.Channel; +import com.bitwig.extension.controller.api.CursorDeviceLayer; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.Device; +import com.bitwig.extension.controller.api.DrumPad; +import com.bitwig.extension.controller.api.DrumPadBank; +import com.bitwig.extension.controller.api.InsertionPoint; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extension.controller.api.MultiStateHardwareLight; +import com.bitwig.extension.controller.api.NoteInput; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extension.controller.api.SendBank; +import com.bitwig.extension.controller.api.TrackBank; import com.bitwig.extensions.controllers.mackie.ButtonViewState; import com.bitwig.extensions.controllers.mackie.MackieMcuProExtension; import com.bitwig.extensions.controllers.mackie.MixerMode; import com.bitwig.extensions.controllers.mackie.VPotMode; -import com.bitwig.extensions.controllers.mackie.configurations.*; +import com.bitwig.extensions.controllers.mackie.configurations.BrowserConfiguration; import com.bitwig.extensions.controllers.mackie.configurations.BrowserConfiguration.Type; +import com.bitwig.extensions.controllers.mackie.configurations.DeviceMenuConfiguration; +import com.bitwig.extensions.controllers.mackie.configurations.GlovalViewLayerConfiguration; +import com.bitwig.extensions.controllers.mackie.configurations.LayerConfiguration; +import com.bitwig.extensions.controllers.mackie.configurations.MixerLayerConfiguration; +import com.bitwig.extensions.controllers.mackie.configurations.TrackLayerConfiguration; import com.bitwig.extensions.controllers.mackie.devices.CursorDeviceControl; import com.bitwig.extensions.controllers.mackie.devices.DeviceManager; import com.bitwig.extensions.controllers.mackie.devices.DeviceTypeBank; @@ -15,7 +33,11 @@ import com.bitwig.extensions.controllers.mackie.display.LcdDisplay; import com.bitwig.extensions.controllers.mackie.display.RingDisplayType; import com.bitwig.extensions.controllers.mackie.display.VuMode; -import com.bitwig.extensions.controllers.mackie.layer.*; +import com.bitwig.extensions.controllers.mackie.layer.ClipLaunchButtonLayer; +import com.bitwig.extensions.controllers.mackie.layer.DrumMixerLayerGroup; +import com.bitwig.extensions.controllers.mackie.layer.HelperInfo; +import com.bitwig.extensions.controllers.mackie.layer.MixerLayerGroup; +import com.bitwig.extensions.controllers.mackie.layer.NotePlayingButtonLayer; import com.bitwig.extensions.controllers.mackie.seqencer.NoteSequenceLayer; import com.bitwig.extensions.controllers.mackie.value.BooleanValueObject; import com.bitwig.extensions.controllers.mackie.value.ModifierValueObject; @@ -71,8 +93,8 @@ public MixControl(final MackieMcuProExtension driver, final MidiIn midiIn, final if (hasTrackColoring) { backgroundColoring.state().onUpdateHardware(state -> { - if (state instanceof TrackColor) { - ((TrackColor) state).send(midiOut); + if (state instanceof final TrackColor trackColor) { + trackColor.send(midiOut); } }); } diff --git a/src/main/java/com/bitwig/extensions/controllers/maudio/oxygenpro/RgbColor.java b/src/main/java/com/bitwig/extensions/controllers/maudio/oxygenpro/RgbColor.java index 4400cb4a..98f2e9ca 100644 --- a/src/main/java/com/bitwig/extensions/controllers/maudio/oxygenpro/RgbColor.java +++ b/src/main/java/com/bitwig/extensions/controllers/maudio/oxygenpro/RgbColor.java @@ -1,10 +1,10 @@ package com.bitwig.extensions.controllers.maudio.oxygenpro; +import java.awt.Color; + import com.bitwig.extension.controller.api.HardwareLightVisualState; import com.bitwig.extension.controller.api.InternalHardwareLightState; -import java.awt.*; - public class RgbColor extends InternalHardwareLightState { private static final int BLINK = 64; @@ -24,15 +24,15 @@ public class RgbColor extends InternalHardwareLightState { public static final RgbColor MAGENTA = new RgbColor(51); public static final RgbColor ROSE = new RgbColor(35); - private int stateIndex; - private RgbColor blink; + private final int stateIndex; + private final RgbColor blink; - private RgbColor(int stateIndex) { + private RgbColor(final int stateIndex) { this.stateIndex = stateIndex; this.blink = new RgbColor(this); } - private RgbColor(RgbColor base) { + private RgbColor(final RgbColor base) { this.stateIndex = base.stateIndex + BLINK; this.blink = this; } @@ -47,32 +47,35 @@ public int getStateIndex() { @Override public HardwareLightVisualState getVisualState() { - return null; + if (stateIndex == 0) + return null; + + return HardwareLightVisualState.createForColor(com.bitwig.extension.api.Color.fromRGB(1, 1, 1)); } @Override - public boolean equals(Object obj) { + public boolean equals(final Object obj) { if (obj instanceof RgbColor) { - RgbColor other = (RgbColor) obj; + final RgbColor other = (RgbColor) obj; return other.stateIndex == stateIndex; } return false; } - public static RgbColor toColor(double red, double green, double blue) { + public static RgbColor toColor(final double red, final double green, final double 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); if (rv == 0 && gv == 0 && bv == 0) { return RgbColor.OFF; } - Hsb hsb = rgbToHsb(rv, gv, bv); - //DebugOutOxy.println("x Color %d %d %d %s", rv, gv, bv, hsb); + final Hsb hsb = rgbToHsb(rv, gv, bv); + // DebugOutOxy.println("x Color %d %d %d %s", rv, gv, bv, hsb); return toColor(hsb); } - private static RgbColor toColor(Hsb hsb) { + private static RgbColor toColor(final Hsb hsb) { if (hsb.sat < 4) { return RgbColor.WHITE; } @@ -109,11 +112,11 @@ private static RgbColor toColor(Hsb hsb) { } private static Hsb rgbToHsb(final int rv, final int gv, final int bv) { - float[] hsbValues = new float[3]; + final float[] hsbValues = new float[3]; Color.RGBtoHSB(rv, gv, bv, hsbValues); - int hr = Math.round(hsbValues[0] * 15) + 1; - int hg = Math.round(hsbValues[1] * 15); - int hb = Math.round(hsbValues[2] * 15); + final int hr = Math.round(hsbValues[0] * 15) + 1; + final int hg = Math.round(hsbValues[1] * 15); + final int hb = Math.round(hsbValues[2] * 15); return new Hsb(hr, hg, hb); } diff --git a/src/main/java/com/bitwig/extensions/controllers/maudio/oxygenpro/control/PadButton.java b/src/main/java/com/bitwig/extensions/controllers/maudio/oxygenpro/control/PadButton.java index c2ecf796..6c16092f 100644 --- a/src/main/java/com/bitwig/extensions/controllers/maudio/oxygenpro/control/PadButton.java +++ b/src/main/java/com/bitwig/extensions/controllers/maudio/oxygenpro/control/PadButton.java @@ -4,9 +4,9 @@ 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.apcmk2.led.RgbLightState; import com.bitwig.extensions.controllers.maudio.oxygenpro.MidiProcessor; import com.bitwig.extensions.controllers.maudio.oxygenpro.RgbColor; +import com.bitwig.extensions.controllers.nativeinstruments.maschinemikro.RgbLightState; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.values.Midi; @@ -16,27 +16,29 @@ public class PadButton extends OxygenButton { private final MultiStateHardwareLight light; - public PadButton(final int midiId, String name, HardwareSurface surface, MidiProcessor midiProcessor) { + public PadButton(final int midiId, final String name, final HardwareSurface surface, + final MidiProcessor midiProcessor) { super(midiId, midiProcessor); - MidiIn midiIn = midiProcessor.getMidiIn(); + final MidiIn midiIn = midiProcessor.getMidiIn(); hwButton = surface.createHardwareButton(name + "_" + midiId); hwButton.pressedAction().setPressureActionMatcher(midiIn.createNoteOnVelocityValueMatcher(0, midiId)); hwButton.releasedAction().setActionMatcher(midiIn.createNoteOffActionMatcher(0, midiId)); hwButton.isPressed().markInterested(); light = surface.createMultiStateHardwareLight(name + "_LIGHT_" + midiId); light.state().setValue(RgbLightState.OFF); + light.setColorToStateFunction(RgbLightState::forColor); hwButton.isPressed().markInterested(); light.state().onUpdateHardware(this::updateState); + hwButton.setBackgroundLight(light); } - private void updateState(InternalHardwareLightState state) { - if (state instanceof RgbColor rgbState) { - + private void updateState(final InternalHardwareLightState state) { + if (state instanceof final RgbColor rgbState) { midiProcessor.sendMidi(Midi.NOTE_ON, midiId, rgbState.getStateIndex()); } } - public void bindLight(Layer layer, final Supplier supplier) { + public void bindLight(final Layer layer, final Supplier supplier) { layer.bindLightState(supplier, this.light); } diff --git a/src/main/java/com/bitwig/extensions/controllers/maudio/oxygenpro/definition/OxygenProExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/maudio/oxygenpro/definition/OxygenProExtensionDefinition.java index 5380f94c..a142e2f3 100644 --- a/src/main/java/com/bitwig/extensions/controllers/maudio/oxygenpro/definition/OxygenProExtensionDefinition.java +++ b/src/main/java/com/bitwig/extensions/controllers/maudio/oxygenpro/definition/OxygenProExtensionDefinition.java @@ -15,7 +15,7 @@ public abstract class OxygenProExtensionDefinition extends ControllerExtensionDe private static final String KEY_FORMAT_WIN = "Oxygen Pro %s"; private static final String KEY_FORMAT_MAC = "Oxygen Pro %s USB MIDI"; - private static final String VERSION = "1.01"; + private static final String VERSION = "1.02"; public OxygenProExtensionDefinition() { } @@ -62,13 +62,13 @@ public int getNumMidiInPorts() { public int getNumMidiOutPorts() { return 2; } - + @Override public String getHelpFilePath() { return "Controllers/M-Audio/M-Audio Oxygen Pro 49-61-Hammer88.pdf"; } - - + + @Override public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, final PlatformType platformType) { @@ -80,7 +80,7 @@ public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList } private void addPorts(final AutoDetectionMidiPortNamesList list, final String inFormat, final String outFormat, - String keyFormat, final String keyVersion) { + final String keyFormat, final String keyVersion) { list.add(new String[]{String.format(inFormat, keyVersion), String.format(keyFormat, keyVersion)}, new String[]{String.format(outFormat, keyVersion), String.format(keyFormat, keyVersion)}); } diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/CursorDeviceControl.java b/src/main/java/com/bitwig/extensions/controllers/mcu/CursorDeviceControl.java new file mode 100644 index 00000000..0bd7b780 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/CursorDeviceControl.java @@ -0,0 +1,199 @@ +package com.bitwig.extensions.controllers.mcu; + +import com.bitwig.extension.controller.api.CursorDeviceFollowMode; +import com.bitwig.extension.controller.api.CursorDeviceLayer; +import com.bitwig.extension.controller.api.CursorRemoteControlsPage; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.Device; +import com.bitwig.extension.controller.api.DeviceBank; +import com.bitwig.extension.controller.api.DrumPadBank; +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extension.controller.api.PinnableCursorDevice; + +public class CursorDeviceControl { + private final DeviceBank deviceBank; + private final PinnableCursorDevice cursorDevice; + + private final CursorRemoteControlsPage remotes; + private final CursorTrack cursorTrack; + private final PinnableCursorDevice primaryDevice; + + private final DrumPadBank drumPadBank; + private final CursorDeviceLayer drumCursor; + private final DeviceBank drumDeviceBank; + private final CursorDeviceLayer cursorLayer; + private final Device cursorLayerItem; + private final DeviceBank trackDeviceBank; + + public CursorDeviceControl(final CursorTrack cursorTrack, final int size, final int totalChannelsAvailable) { + this.cursorTrack = cursorTrack; + cursorTrack.trackType().markInterested(); + cursorDevice = cursorTrack.createCursorDevice("main", "mmain", 1, CursorDeviceFollowMode.FOLLOW_SELECTION); + primaryDevice = + cursorTrack.createCursorDevice("drumdetection", "Pad Device", 8, CursorDeviceFollowMode.FIRST_INSTRUMENT); + primaryDevice.hasDrumPads().markInterested(); + primaryDevice.exists().markInterested(); + + drumPadBank = primaryDevice.createDrumPadBank(totalChannelsAvailable); + + drumPadBank.setSkipDisabledItems(false); + drumCursor = primaryDevice.createCursorLayer(); + + drumDeviceBank = drumCursor.createDeviceBank(8); + + cursorLayer = cursorDevice.createCursorLayer(); + final DeviceBank layerBank = cursorLayer.createDeviceBank(1); + cursorLayerItem = layerBank.getItemAt(0); + cursorLayerItem.name().markInterested(); + + markCursorDevice(); + + deviceBank = cursorDevice.deviceChain().createDeviceBank(8); + markDeviceBank(deviceBank); + trackDeviceBank = cursorTrack.createDeviceBank(8); + markDeviceBank(trackDeviceBank); + + remotes = cursorDevice.createCursorRemoteControlsPage(8); + remotes.pageCount().markInterested(); + + for (int i = 0; i < deviceBank.getSizeOfBank(); i++) { + final Device device = deviceBank.getDevice(i); + device.deviceType().markInterested(); + device.name().markInterested(); + device.position().markInterested(); + } + + cursorDevice.position().addValueObserver(cp -> { + if (cp >= 0) { + deviceBank.scrollPosition().set(cp - 1); + } + }); + + } + + private void markCursorDevice() { + cursorDevice.name().markInterested(); + cursorDevice.deviceType().markInterested(); + cursorDevice.isPinned().markInterested(); + cursorDevice.hasDrumPads().markInterested(); + cursorDevice.hasLayers().markInterested(); + cursorDevice.hasSlots().markInterested(); + cursorDevice.slotNames().markInterested(); + } + + private void markDeviceBank(final DeviceBank bank) { + bank.canScrollBackwards().markInterested(); + bank.canScrollForwards().markInterested(); + bank.itemCount().markInterested(); + bank.scrollPosition().markInterested(); + } + + public void moveDeviceLeft() { + final Device previousDevice = deviceBank.getDevice(0); + previousDevice.beforeDeviceInsertionPoint().moveDevices(cursorDevice); + cursorDevice.selectPrevious(); + cursorDevice.selectNext(); + } + + public void moveDeviceRight() { + final Device nextDevice = deviceBank.getDevice(2); + nextDevice.afterDeviceInsertionPoint().moveDevices(cursorDevice); + cursorDevice.selectPrevious(); + cursorDevice.selectNext(); + } + + public CursorDeviceLayer getDrumCursor() { + return drumCursor; + } + + public DrumPadBank getDrumPadBank() { + return drumPadBank; + } + + public DeviceBank getDrumDeviceBank() { + return drumDeviceBank; + } + + public PinnableCursorDevice getCursorDevice() { + return cursorDevice; + } + + public DeviceBank getDeviceBank() { + return deviceBank; + } + + public CursorTrack getCursorTrack() { + return cursorTrack; + } + + public CursorRemoteControlsPage getRemotes() { + return remotes; + } + + public Parameter getParameter(final int index) { + return remotes.getParameter(index); + } + + public void selectDevice(final Device device) { + cursorDevice.selectDevice(device); + cursorDevice.selectInEditor(); + } + + public void focusOnDrumDevice() { + cursorDevice.selectDevice(drumDeviceBank.getDevice(0)); + } + + public void focusOnPrimary() { + cursorDevice.selectDevice(primaryDevice); + } + + public void handleLayerSelection() { + cursorDevice.selectDevice(cursorLayerItem); + } + + public void navigateNextInLayer() { + cursorLayer.selectNext(); + } + + public void navigatePreviousInLayer() { + cursorLayer.selectPrevious(); + } + + public Device getCursorLayerItem() { + return cursorLayerItem; + } + + public String getLayerDeviceInfo() { + if (cursorDevice.hasLayers().get()) { + return "SEL=" + cursorLayerItem.name().get(); + } + return ""; + } + + public boolean cursorHasDrumPads() { + if (cursorTrack.trackType().get().equals("Instrument")) { + return primaryDevice.exists().get() && primaryDevice.hasDrumPads().get(); + } + return false; + } + + public boolean hasDrumPads() { + return cursorDevice.hasDrumPads().get(); + } + + public void navigateToPage(final int index) { + if (index < remotes.pageCount().get()) { + remotes.selectedPageIndex().set(index); + } + } + + public void navigateToDeviceInChain(final int index) { + if (index < trackDeviceBank.itemCount().get()) { + cursorDevice.selectDevice(trackDeviceBank.getDevice(index)); + } + } + + public void insertVst3Device(final String id) { + cursorDevice.afterDeviceInsertionPoint().insertVST3Device(id); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/GlobalStates.java b/src/main/java/com/bitwig/extensions/controllers/mcu/GlobalStates.java new file mode 100644 index 00000000..77dee723 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/GlobalStates.java @@ -0,0 +1,163 @@ +package com.bitwig.extensions.controllers.mcu; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.SettableBooleanValue; +import com.bitwig.extensions.controllers.mcu.display.VuMode; +import com.bitwig.extensions.framework.di.Activate; +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.ValueObject; + +@Component +public class GlobalStates { + private final BooleanValueObject shift = new BooleanValueObject(); + private final BooleanValueObject option = new BooleanValueObject(); + private final BooleanValueObject control = new BooleanValueObject(); + private final BooleanValueObject zoomMode = new BooleanValueObject(); + + private final BooleanValueObject flipped = new BooleanValueObject(); + private final BooleanValueObject nameValue = new BooleanValueObject(); + private final BooleanValueObject globalView = new BooleanValueObject(true); + private final BooleanValueObject clipLaunchingActive = new BooleanValueObject(); + private final BooleanValueObject duplicateHeld = new BooleanValueObject(); + private final BooleanValueObject clearHeld = new BooleanValueObject(); + private final BasicStringValue twoSegmentText = new BasicStringValue(" "); + private final ValueObject potMode; + private final ValueObject vuMode = new ValueObject<>(VuMode.LED); + private VPotMode lastSendsMode = VPotMode.SEND; + private final ControllerHost host; + private int heldSoloButtons = 0; + private String trackIndexMain = "01"; + private String trackIndexGlobal = "01"; + + public GlobalStates(final ControllerHost host) { + this.host = host; + potMode = new ValueObject<>(VPotMode.PAN); + potMode.addValueObserver((oldValue, newValue) -> { + if (newValue == VPotMode.ALL_SENDS || newValue == VPotMode.SEND) { + this.lastSendsMode = newValue; + } + }); + } + + @Activate + public void activate() { + clipLaunchingActive.addValueObserver(active -> updateSegmentText()); + getGlobalView().addValueObserver(active -> updateSegmentText()); + } + + private void updateSegmentText() { + if (clipLaunchingActive.get()) { + twoSegmentText.set("CL"); + } else { + if (globalView.get()) { + twoSegmentText.set(trackIndexGlobal); + } else { + twoSegmentText.set(trackIndexMain); + } + } + } + + public BooleanValueObject getGlobalView() { + return globalView; + } + + + public void toggleTrackMode() { + if (potMode.get() == VPotMode.SEND) { + potMode.set(VPotMode.ALL_SENDS); + } else if (potMode.get() == VPotMode.ALL_SENDS) { + potMode.set(VPotMode.SEND); + } + } + + public BooleanValueObject getFlipped() { + return flipped; + } + + public BooleanValueObject getShift() { + return shift; + } + + public BooleanValueObject getOption() { + return option; + } + + public BooleanValueObject getControl() { + return control; + } + + public ValueObject getPotMode() { + return potMode; + } + + public ValueObject getVuMode() { + return vuMode; + } + + public BooleanValueObject getNameValue() { + return nameValue; + } + + public BooleanValueObject getZoomMode() { + return zoomMode; + } + + public void soloPressed(final boolean pressed) { + if (pressed) { + heldSoloButtons++; + } else if (heldSoloButtons > 0) { + heldSoloButtons--; + } + } + + public boolean isSoloHeld() { + return heldSoloButtons > 0; + } + + public boolean isShiftSet() { + return shift.get(); + } + + public boolean isOptionSet() { + return option.get(); + } + + public boolean isControlSet() { + return control.get(); + } + + public boolean trackModeActive() { + return potMode.get().getAssign() != VPotMode.BitwigType.CHANNEL || potMode.get() == VPotMode.ALL_SENDS; + } + + public VPotMode getLastSendsMode() { + return lastSendsMode; + } + + public SettableBooleanValue getClipLaunchingActive() { + return clipLaunchingActive; + } + + public void notifySelectedTrackState(final String trackCode, final boolean fromExtended) { + if (fromExtended) { + trackIndexGlobal = trackCode; + } else { + trackIndexMain = trackCode; + } + updateSegmentText(); + } + + public BasicStringValue getTwoSegmentText() { + return twoSegmentText; + } + + public BooleanValueObject getDuplicateHeld() { + return duplicateHeld; + } + + public BooleanValueObject getClearHeld() { + return clearHeld; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/McuExtension.java b/src/main/java/com/bitwig/extensions/controllers/mcu/McuExtension.java new file mode 100644 index 00000000..da8a769f --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/McuExtension.java @@ -0,0 +1,128 @@ +package com.bitwig.extensions.controllers.mcu; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.AutoDetectionMidiPortNames; +import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; +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.mcu.config.ControllerConfig; +import com.bitwig.extensions.controllers.mcu.control.MainHardwareSection; +import com.bitwig.extensions.controllers.mcu.control.MixerSectionHardware; +import com.bitwig.extensions.controllers.mcu.definitions.AbstractMcuControllerExtensionDefinition; +import com.bitwig.extensions.controllers.mcu.layer.MainSection; +import com.bitwig.extensions.controllers.mcu.layer.MixerSection; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.di.Context; + +public class McuExtension extends ControllerExtension { + private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("hh:mm:ss SSS"); + private static ControllerHost debugHost; + private Layer mainLayer; + private HardwareSurface surface; + private final ControllerConfig controllerConfig; + private final List mixerHardwareSections = new ArrayList<>(); + private final List mainHardwareSections = new ArrayList<>(); + private final List midiProcessors = new ArrayList<>(); + + 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 McuExtension(final AbstractMcuControllerExtensionDefinition definition, final ControllerHost host, + final ControllerConfig controllerConfig) { + super(definition, host); + this.controllerConfig = controllerConfig; + this.controllerConfig.setNrOfExtenders(definition.getNrOfExtenders()); + } + + @Override + public void init() { + final ControllerHost host = getHost(); + debugHost = host; + //this.project = getHost().getProject(); + final Context diContext = new Context(this); + diContext.registerService(ControllerConfig.class, controllerConfig); + mainLayer = diContext.createLayer("MAIN_LAYER"); + surface = diContext.getService(HardwareSurface.class); + MainSection mainControl = null; + final List mainSections = new ArrayList<>(); + final List mixerSections = new ArrayList<>(); + + //showPortINfos(PlatformType.MAC); + showPortINfos(PlatformType.WINDOWS); + + for (int portIndex = 0; portIndex < controllerConfig.getNrOfExtenders() + 1; portIndex++) { + final MidiProcessor midiProcessor = new MidiProcessor(diContext, portIndex); + midiProcessors.add(midiProcessor); + + if (portIndex == 0 || !controllerConfig.isSingleMainUnit()) { + final MainHardwareSection mainHardwareSection = + new MainHardwareSection(diContext, midiProcessor, portIndex); + mainHardwareSections.add(mainHardwareSection); + mainControl = new MainSection(diContext, mainHardwareSection); + mainSections.add(mainControl); + } + + final MixerSectionHardware mixerSectionHardware = + new MixerSectionHardware(portIndex, diContext, midiProcessor, portIndex * 8); + final MixerSection mixerLayer = + new MixerSection(diContext, mixerSectionHardware, mainControl, portIndex, portIndex == 0); + mixerHardwareSections.add(mixerSectionHardware); + mixerSections.add(mixerLayer); + } + diContext.activate(); + mainSections.forEach(MainSection::activate); + mixerSections.forEach(MixerSection::activate); + if (controllerConfig.getForceUpdateOnStartup() != -1) { + host.scheduleTask(this::doForceUpdate, controllerConfig.getForceUpdateOnStartup()); + } + } + + private void doForceUpdate() { + println(" FORCE UPDATE "); + for (final MidiProcessor midiProcessor : midiProcessors) { + midiProcessor.forceUpdate(); + } + } + + private void showPortINfos(final PlatformType platformType) { + final AutoDetectionMidiPortNamesList portNames = + getExtensionDefinition().getAutoDetectionMidiPortNamesList(platformType); + for (int i = 0; i < portNames.getCount(); i++) { + println("PORTINFOS - %d LCL=%s", i + 1, Locale.getDefault()); + final AutoDetectionMidiPortNames adpm = portNames.getPortNamesAt(i); + final String[] inputNames = adpm.getInputNames(); + final String[] outputNames = adpm.getOutputNames(); + println(" ###### INPUTS #########"); + for (int j = 0; j < inputNames.length; j++) { + println(" [%s]", inputNames[j]); + } + println(" ###### OUTPUTS #########"); + for (int j = 0; j < outputNames.length; j++) { + println(" [%s]", outputNames[j]); + } + } + } + + @Override + public void exit() { + midiProcessors.forEach(MidiProcessor::exit); + mainHardwareSections.forEach(MainHardwareSection::clearAll); + mixerHardwareSections.forEach(MixerSectionHardware::clearAll); + } + + @Override + public void flush() { + surface.updateHardware(); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/MidiProcessor.java b/src/main/java/com/bitwig/extensions/controllers/mcu/MidiProcessor.java new file mode 100644 index 00000000..5e13efbd --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/MidiProcessor.java @@ -0,0 +1,293 @@ +package com.bitwig.extensions.controllers.mcu; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +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.HardwareSlider; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extension.controller.api.RelativeHardwareValueMatcher; +import com.bitwig.extensions.controllers.mcu.config.ControllerConfig; +import com.bitwig.extensions.controllers.mcu.display.ControllerDisplay; +import com.bitwig.extensions.controllers.mcu.display.DisplayPart; +import com.bitwig.extensions.controllers.mcu.display.LcdDisplay; +import com.bitwig.extensions.framework.di.Context; +import com.bitwig.extensions.framework.values.BooleanValueObject; +import com.bitwig.extensions.framework.values.Midi; + +public class MidiProcessor implements ControllerDisplay { + private final ControllerHost host; + private final MidiIn midiIn; + private final MidiOut midiOut; + private final int portIndex; + private final boolean has2ClickResolution; + private final int[] lightStatusMap = new int[127]; + private final LcdDisplay upperDisplay; + private final LcdDisplay lowerDisplay; + private final BooleanValueObject slidersTouched = new BooleanValueObject(); + private int touchCount = 0; + private boolean segmentedDisplay = false; + private long touchReleaseTime = -1; + private final TimedProcessor timedProcessor; + + public MidiProcessor(final Context context, final int portIndex) { + this.portIndex = portIndex; + this.host = context.getService(ControllerHost.class); + this.timedProcessor = context.getService(TimedProcessor.class); + final ControllerConfig controllerConfig = context.getService(ControllerConfig.class); + this.has2ClickResolution = controllerConfig.isHas2ClickResolution(); + this.midiIn = host.getMidiInPort(portIndex); + this.midiOut = host.getMidiOutPort(portIndex); + this.segmentedDisplay = controllerConfig.isDisplaySegmented(); + + midiIn.setMidiCallback(this::handleMidiIn); + midiIn.setSysexCallback(this::handleSysEx); + + final SectionType sectionType = + portIndex == 0 || !controllerConfig.isSingleMainUnit() ? SectionType.MAIN : SectionType.XTENDER; + + this.upperDisplay = + new LcdDisplay(context, portIndex, midiOut, sectionType, DisplayPart.UPPER, controllerConfig); + this.lowerDisplay = + controllerConfig.hasLowerDisplay() ? new LcdDisplay(context, portIndex, midiOut, sectionType, + DisplayPart.LOWER, controllerConfig) : null; + Arrays.fill(lightStatusMap, -1); + timedProcessor.addActionListener(() -> { + if (touchReleaseTime != -1 && (System.currentTimeMillis() - touchReleaseTime) > 400) { + slidersTouched.set(false); + touchReleaseTime = -1; + } + }); + } + + private void handleMidiIn(final int status, final int data1, final int data2) { + McuExtension.println("MIDI(%d) => %02X %02X %02X %03d %03d", portIndex, status, data1, data2, data1, data2); + } + + protected void handleSysEx(final String sysExString) { + McuExtension.println("SysEx = %s", sysExString); + } + + public int getPortIndex() { + return portIndex; + } + + public void handleTouch(final boolean touch) { + if (touch) { + if (touchCount == 0 && !slidersTouched.get()) { + slidersTouched.set(true); + } + touchReleaseTime = -1; + touchCount++; + } else { + touchCount = Math.max(0, touchCount - 1); + if (touchCount == 0) { + touchReleaseTime = System.currentTimeMillis(); + } + } + } + + public void exit() { + upperDisplay.exitMessage(); + if (lowerDisplay != null) { + lowerDisplay.clearAll(); + } + } + + public void sendMidi(final int status, final int data1, final int data2) { + midiOut.sendMidi(status, data1, data2); + } + + public void sendLedLightStatus(final int noteNr, final int channel, final int value) { + lightStatusMap[noteNr] = value; // TODO Consider Midi Channels + midiOut.sendMidi(Midi.NOTE_ON | channel, noteNr, value); + } + + public void forceUpdate() { + for (int i = 0; i < lightStatusMap.length; i++) { + if (lightStatusMap[i] != -1) { + midiOut.sendMidi(Midi.NOTE_ON, i, lightStatusMap[i]); + } + } + upperDisplay.refreshDisplay(); + if (lowerDisplay != null) { + lowerDisplay.refreshDisplay(); + } + } + + public void attachNoteOnOffMatcher(final HardwareButton button, final int channel, final int note) { + button.pressedAction().setActionMatcher(midiIn.createNoteOnActionMatcher(channel, note)); + button.releasedAction().setActionMatcher(midiIn.createNoteOffActionMatcher(channel, note)); + } + + public void attachPitchBendSliderValue(final HardwareSlider slider, final int channel) { + slider.setAdjustValueMatcher(midiIn.createAbsolutePitchBendValueMatcher(channel)); + } + + public RelativeHardwareValueMatcher createAcceleratedMatcher(final int ccNr) { + return midiIn.createRelativeSignedBitCCValueMatcher(0x0, ccNr, 200); + } + + public RelativeHardwareValueMatcher createNonAcceleratedMatcher(final int ccNr) { + final RelativeHardwareValueMatcher stepDownMatcher = + midiIn.createRelativeValueMatcher("(status == 176 && data1 == %d && data2 > 64)".formatted(ccNr), -1); + final RelativeHardwareValueMatcher stepUpMatcher = + midiIn.createRelativeValueMatcher("(status == 176 && data1 == %d && data2 < 65)".formatted(ccNr), 1); + return host.createOrRelativeHardwareValueMatcher(stepDownMatcher, stepUpMatcher); + } + + public void updateIconColors(final int[] colors) { + final StringBuilder sysEx = new StringBuilder("F0 00 00 68 16 14 "); + for (int i = 0; i < colors.length; i++) { + final int red = colors[i] >> 16; + final int green = colors[i] >> 8 & 0x7F; + final int blue = colors[i] & 0x7F; + sysEx.append(String.format("%02x %02x %02x ", red, green, blue)); + } + sysEx.append("F7"); + midiOut.sendSysex(sysEx.toString()); + } + + @Override + public void sendVuUpdate(final int index, final int value) { + midiOut.sendMidi(Midi.CHANNEL_AT, index << 4 | value, 0); + } + + @Override + public void sendMasterVuUpdateL(final int value) { + midiOut.sendMidi(Midi.CHANNEL_AT | 0x1, value, 0); + } + + @Override + public void sendMasterVuUpdateR(final int value) { + midiOut.sendMidi(Midi.CHANNEL_AT | 0x1, 0x10 | value, 0); + } + + public HardwareActionBindable createAction(final Runnable action) { + return host.createAction(action, null); + } + + public MidiIn getMidiIn() { + return midiIn; + } + + public MidiOut getMidiOut() { + return midiOut; + } + + public boolean isHas2ClickResolution() { + return has2ClickResolution; + } + + public void showText(final DisplayPart part, final int row, final int cell, final String text) { + if (part == DisplayPart.UPPER) { + upperDisplay.sendToRow(row, cell, text); + } else if (part == DisplayPart.LOWER && lowerDisplay != null) { + lowerDisplay.sendToRow(row, cell, text); + } + } + + public void showText(final DisplayPart part, final int row, final List texts) { + if (part == DisplayPart.UPPER) { + upperDisplay.sendSegmented(row, texts); + } else if (part == DisplayPart.LOWER && lowerDisplay != null) { + lowerDisplay.sendSegmented(row, texts); + } + } + + public void showText(final DisplayPart part, final int row, final String text) { + if (part == DisplayPart.UPPER) { + if (segmentedDisplay) { + final List segments = splitInSegments(text, 6); + for (int i = 0; i < 8; i++) { + final String value = i < segments.size() ? segments.get(i) : ""; + upperDisplay.sendToRow(row, i, value); + } + } else { + upperDisplay.sendToDisplay(row, text); + } + } else if (part == DisplayPart.LOWER && lowerDisplay != null) { + if (segmentedDisplay) { + final List segments = splitInSegments(text, 5); + for (int i = 0; i < 8; i++) { + final String value = i < segments.size() ? segments.get(i) : ""; + lowerDisplay.sendToRow(row, i, value); + } + } else { + lowerDisplay.sendToDisplay(row, text); + } + } + } + + private List splitInSegments(final String text, final int maxSegLen) { + final List result = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + final String[] split = text.split(" "); + + for (final String txt : split) { + if (txt.length() > 0) { + if (current.length() == 0) { + current.append(txt); + } else if (current.length() + txt.length() + 1 <= maxSegLen) { + current.append(" "); + current.append(txt); + } else { + if (current.length() > maxSegLen) { + result.add(current.substring(0, maxSegLen)); + final String restPart = current.substring(maxSegLen, current.length()); + if (restPart.length() + txt.length() > maxSegLen) { + result.add(restPart); + current = new StringBuilder(txt); + } else { + current = new StringBuilder(restPart + " " + txt); + } + } else { + result.add(current.toString()); + current = new StringBuilder(txt); + } + } + } + } + if (current.length() > 0) { + if (current.length() > maxSegLen) { + result.add(current.substring(0, maxSegLen)); + result.add(current.substring(maxSegLen, current.length())); + } else { + result.add(current.toString()); + } + } + + return result; + } + + @Override + public void refresh() { + if (lowerDisplay != null) { + lowerDisplay.refreshDisplay(); + } + if (upperDisplay != null) { + upperDisplay.refreshDisplay(); + } + } + + @Override + public void blockUpdate(final DisplayPart part, final int row) { + } + + @Override + public void enableUpdate(final DisplayPart part, final int row) { + } + + @Override + public boolean hasLower() { + return lowerDisplay != null; + } + + public BooleanValueObject getSlidersTouched() { + return slidersTouched; + } +} \ No newline at end of file diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/SectionType.java b/src/main/java/com/bitwig/extensions/controllers/mcu/SectionType.java new file mode 100644 index 00000000..a0e63234 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/SectionType.java @@ -0,0 +1,6 @@ +package com.bitwig.extensions.controllers.mcu; + +public enum SectionType { + MAIN, + XTENDER; +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/StringUtil.java b/src/main/java/com/bitwig/extensions/controllers/mcu/StringUtil.java new file mode 100644 index 00000000..69421387 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/StringUtil.java @@ -0,0 +1,138 @@ +package com.bitwig.extensions.controllers.mcu; + +public class StringUtil { + + private static final int PAN_RANGE = 50; + 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 StringUtil() { + } + + public static String toBarBeats(final double value) { + final int bars = (int) Math.floor(value); + final int beats = (int) Math.floor((value - bars) * 4); + return String.format("%02d:%02d", bars, beats); + } + + public static String panToString(final double v) { + final int intv = (int) (v * PAN_RANGE * 2); + if (intv == PAN_RANGE) { + return " C"; + } else if (intv < PAN_RANGE) { + return " " + (PAN_RANGE - intv) + "L"; + } + return " " + (intv - PAN_RANGE) + "R"; + } + + /** + * Tailored to condense Volume value strings. Removes leading + and spaces. + * + * @param valueText input text + * @param maxLen maximum output value + * @return condensed value string + */ + public static String condenseVolumeValue(final String valueText, final int maxLen) { + final StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < valueText.length() && sb.length() < maxLen) { + final char c = valueText.charAt(i++); + if (c != '+' && c != ' ') { + sb.append(c); + } + } + return sb.toString(); + } + + public static String toTwoCharVal(final int value) { + if (value < 10) { + return " " + value; + } + return Integer.toString(value); + } + + public static String toDisplayName(final String text) { + if (text.length() < 2) { + return text; + } + return text.charAt(0) + text.substring(1, Math.min(6, text.length())).toLowerCase(); + } + + + public static String padString(final String text, final int pad) { + return " ".repeat(Math.max(0, pad)) + text; + } + + public static String padEnd(final String text, final int paddingLength) { + if (text.length() == paddingLength) { + return text; + } + if (text.length() > paddingLength) { + return text.substring(0, paddingLength); + } + return text + " ".repeat(paddingLength - text.length()); + } + + public static String limit(final String value, final int max) { + return value.substring(0, Math.min(max, value.length())); + } + + public static String reduceAscii(final String name, final int maxLen) { + final String result = toAsciiDisplay(name, maxLen + 10); + if (result.length() <= maxLen) { + return result; + } + return result.replace(" ", ""); + } + + public static String toAscii(final String value) { + final StringBuilder b = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + final char c = value.charAt(i); + 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; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/TimedProcessor.java b/src/main/java/com/bitwig/extensions/controllers/mcu/TimedProcessor.java new file mode 100644 index 00000000..4743a348 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/TimedProcessor.java @@ -0,0 +1,93 @@ +package com.bitwig.extensions.controllers.mcu; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extensions.framework.di.Activate; +import com.bitwig.extensions.framework.di.Component; +import com.bitwig.extensions.framework.time.TimedDelayEvent; +import com.bitwig.extensions.framework.time.TimedEvent; + +@Component +public class TimedProcessor { + + private final ControllerHost host; + private final Queue timedEvents = new ConcurrentLinkedQueue<>(); + private final List timedAction = new ArrayList<>(); + private int blinkCounter = 0; + private TimedDelayEvent holdEvent = null; + + public TimedProcessor(final ControllerHost host) { + this.host = host; + } + + private void handlePing() { + blinkCounter = (blinkCounter + 1) % 8; + if (!timedEvents.isEmpty()) { + for (final TimedEvent event : timedEvents) { + event.process(); + if (event.isCompleted()) { + timedEvents.remove(event); + } + } + } + for (final Runnable action : timedAction) { + action.run(); + } + host.scheduleTask(this::handlePing, 50); + } + + public void addActionListener(final Runnable action) { + timedAction.add(action); + } + + @Activate + public void start() { + host.scheduleTask(this::handlePing, 100); + } + + public boolean blinkSlow() { + return blinkCounter % 8 < 4; + } + + public boolean blinkMid() { + return blinkCounter % 4 < 2; + } + + public boolean blinkFast() { + return blinkCounter % 2 == 0; + } + + public boolean blinkPeriodic() { + return blinkCounter == 0 || blinkCounter == 3; + } + + + public void delayTask(final Runnable action, final long delay) { + host.scheduleTask(action, delay); + } + + public void startHoldEvent(final Runnable delayedAction) { + if (holdEvent != null) { + holdEvent.cancel(); + } + holdEvent = new TimedDelayEvent(delayedAction, 600); + queueEvent(holdEvent); + } + + public void queueEvent(final TimedEvent event) { + timedEvents.add(event); + } + + public void completeHoldEvent(final Runnable releaseAction) { + if (holdEvent != null && !holdEvent.isCompleted()) { + holdEvent.cancel(); + holdEvent = null; + } else { + releaseAction.run(); + } + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/TrackBankView.java b/src/main/java/com/bitwig/extensions/controllers/mcu/TrackBankView.java new file mode 100644 index 00000000..8077564b --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/TrackBankView.java @@ -0,0 +1,147 @@ +package com.bitwig.extensions.controllers.mcu; + +import com.bitwig.extension.controller.api.SendBank; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; + +public class TrackBankView { + private final int[] trackColors; + + private final TrackBank trackBank; + private final int numberOfSends; + private int itemCount; + private final boolean isExtended; + private final GlobalStates globalStates; + private int selectedIndex; + private int numberOfSendsOverall = 0; + private String trackType = ""; + + public TrackBankView(final TrackBank trackBank, final GlobalStates globalStates, final boolean isExtended, + final int numberOfSends) { + this.trackBank = trackBank; + this.numberOfSends = numberOfSends; + this.isExtended = isExtended; + this.globalStates = globalStates; + trackColors = new int[trackBank.getSizeOfBank()]; + prepareTrackBank(); + trackBank.itemCount().addValueObserver(items -> { + this.itemCount = items; + updateSelectedTrackInfo(); + }); + this.globalStates.getGlobalView().addValueObserver(globalView -> updateSelectedTrackInfo()); + } + + private void prepareTrackBank() { + if (numberOfSends > 1) { + for (int i = 0; i < trackBank.getSizeOfBank(); i++) { + final Track track = trackBank.getItemAt(i); + track.sendBank().scrollPosition().markInterested(); + track.sendBank().itemCount().markInterested(); + } + } + trackBank.canScrollChannelsDown().markInterested(); + trackBank.canScrollChannelsUp().markInterested(); + for (int i = 0; i < trackBank.getSizeOfBank(); i++) { + final Track track = trackBank.getItemAt(i); + configureTrack(track, i); + } + } + + public void setCursorTrackPosition(final int trackPosition) { + this.selectedIndex = trackPosition; + updateSelectedTrackInfo(); + } + + public void setTrackType(final String trackType) { + this.trackType = trackType; + updateSelectedTrackInfo(); + } + + public void setNumberOfSends(final int numberOfSends) { + this.numberOfSendsOverall = numberOfSends; + updateSelectedTrackInfo(); + } + + private void configureTrack(final Track track, final int index) { + track.color().addValueObserver((r, g, b) -> trackColors[index] = toColor(r, g, b)); + } + + private static int toColor(final double r, final double g, final double b) { + final int red = (int) (Math.floor(r * 127)); + final int green = (int) (Math.floor(g * 127)); + final int blue = (int) (Math.floor(b * 127)); + return red << 16 | green << 8 | blue; + } + + private void updateSelectedTrackInfo() { + if (globalStates.getGlobalView().get() != isExtended) { + return; + } + if (trackType.equals("Master")) { + globalStates.notifySelectedTrackState("MT", isExtended); + } else if (trackType.equals("Effect")) { + final int sndPos = + isExtended ? numberOfSendsOverall - (itemCount - selectedIndex - 1) : selectedIndex - itemCount; + + globalStates.notifySelectedTrackState("F%01d".formatted(sndPos + 1), isExtended); + } else { + globalStates.notifySelectedTrackState("%02d".formatted(selectedIndex + 1), isExtended); + } + } + + public TrackBank getTrackBank() { + return trackBank; + } + + public int[] getColor(final int channelOffset) { + if (trackColors.length == 8) { + return trackColors; + } + final int[] result = new int[8]; + System.arraycopy(trackColors, 8, result, 0, 8); + return result; + } + + public void navigateChannels(final int dir) { + trackBank.scrollBy(dir); + } + + public void navigateToSends(final int index) { + for (int trackIndex = 0; trackIndex < trackBank.getSizeOfBank(); trackIndex++) { + final SendBank sendBank = trackBank.getItemAt(trackIndex).sendBank(); + if (index < sendBank.getSizeOfBank()) { + sendBank.scrollPosition().set(index); + } + } + } + + public void navigateSends(final int dir) { + if (numberOfSends == 1) { + for (int trackIndex = 0; trackIndex < trackBank.getSizeOfBank(); trackIndex++) { + final SendBank sendBank = trackBank.getItemAt(trackIndex).sendBank(); + if (dir > 0) { + sendBank.scrollForwards(); + } else { + sendBank.scrollBackwards(); + } + } + } else { + navigateSendsBank(dir); + } + } + + private void navigateSendsBank(final int dir) { + final SendBank firstBank = trackBank.getItemAt(0).sendBank(); + final int index = firstBank.scrollPosition().get(); + final int newIndex = index + dir; + + if (newIndex >= 0 && newIndex < firstBank.itemCount().get()) { + for (int i = 0; i < trackBank.getSizeOfBank(); i++) { + final SendBank bank = trackBank.getItemAt(i).sendBank(); + bank.scrollPosition().set(newIndex); + } + } + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/VPotMode.java b/src/main/java/com/bitwig/extensions/controllers/mcu/VPotMode.java new file mode 100644 index 00000000..5de7bb57 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/VPotMode.java @@ -0,0 +1,66 @@ +package com.bitwig.extensions.controllers.mcu; + +public enum VPotMode { + // TRACK(NoteOnAssignment.V_TRACK, Assign.MIXER), // + ALL_SENDS(BitwigType.CHANNEL), // + SEND(BitwigType.CHANNEL), // + PAN(BitwigType.CHANNEL), // + DEVICE(BitwigType.DEVICE), // Standard Device MODE + PLUGIN(BitwigType.DEVICE, "audio-effect", "DEVICE", "", "AudioFX"), + EQ(BitwigType.SPEC_DEVICE, "EQ+ device", "EQ", "EQ+", "EQ+"), // + INSTRUMENT(BitwigType.DEVICE, "instrument", "INSTRUMENT", "INSTRUMENT", "Synth"), + MIDI_EFFECT(BitwigType.DEVICE, "note-effect", "NOTEFX", "", "NoteFX"), + TRACK_REMOTE(BitwigType.REMOTE, "track-remotes", "TRACK_REMOTE", "", "TrackRemote"), + PROJECT_REMOTE(BitwigType.REMOTE, "project-remotes", "PROJECT_REMOTE", "", "ProjectRemote"), + ARPEGGIATOR(BitwigType.SPEC_DEVICE, "note-effect", "ALT+INSTRUMENT"); + + + private final BitwigType assign; + private final String typeName; + private final String deviceName; + private final String typeDisplayName; + private final String buttonDescription; + private final String description; + + public enum BitwigType { + CHANNEL, + DEVICE, + SPEC_DEVICE, + REMOTE + } + + VPotMode(final BitwigType assign) { + this(assign, null, null); + } + + VPotMode(final BitwigType assign, final String typeName, final String buttonDescription) { + this(assign, typeName, buttonDescription, null, null); + } + + VPotMode(final BitwigType assign, final String typeName, final String buttonDescription, final String deviceName, + final String description) { + this.assign = assign; + this.typeName = typeName; + this.deviceName = deviceName; + if (this.typeName != null && this.typeName.length() > 1) { + typeDisplayName = this.typeName.substring(0, 1).toUpperCase() + this.typeName.substring(1); + } else { + typeDisplayName = null; + } + this.buttonDescription = buttonDescription; + this.description = description; + } + + public BitwigType getAssign() { + return assign; + } + + public String getTypeName() { + return typeName; + } + + public String getDescription() { + return description; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/ViewControl.java b/src/main/java/com/bitwig/extensions/controllers/mcu/ViewControl.java new file mode 100644 index 00000000..8421de68 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/ViewControl.java @@ -0,0 +1,152 @@ +package com.bitwig.extensions.controllers.mcu; + +import com.bitwig.extension.controller.api.Arranger; +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.DetailEditor; +import com.bitwig.extension.controller.api.Mixer; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.controllers.mcu.config.ControllerConfig; +import com.bitwig.extensions.controllers.mcu.config.McuFunction; +import com.bitwig.extensions.framework.di.Component; + +@Component +public class ViewControl { + + private final TrackBankView globalTrackBank; + private final TrackBankView mainTrackBank; + private final CursorTrack cursorTrack; + + private final CursorDeviceControl cursorDeviceControl; + private final Track rootTrack; + private final CursorRemoteControlsPage projectRemotes; + private final CursorRemoteControlsPage trackRemotes; + private final int numberOfSends; + private final Mixer mixer; + private final Arranger arranger; + private final DetailEditor detailEditor; + + public ViewControl(final ControllerHost host, final ControllerConfig controllerConfig, + final GlobalStates globalStates) { + final int numberOfHwChannels = (controllerConfig.getNrOfExtenders() + 1) * 8; + final int nrOfScenes = controllerConfig.getAssignment(McuFunction.CLIP_LAUNCHER_MODE_2) != null ? 2 : 4; + cursorTrack = host.createCursorTrack(8, nrOfScenes); + cursorDeviceControl = new CursorDeviceControl(cursorTrack, 8, numberOfHwChannels); + numberOfSends = controllerConfig.hasDirectSelect() ? 8 : 1; + + mainTrackBank = + new TrackBankView(host.createMainTrackBank(numberOfHwChannels, numberOfSends, nrOfScenes), globalStates, + false, numberOfSends); + globalTrackBank = + new TrackBankView(host.createTrackBank(numberOfHwChannels, numberOfSends, nrOfScenes), globalStates, true, + numberOfSends); + + cursorTrack.position().addValueObserver(trackPosition -> { + mainTrackBank.setCursorTrackPosition(trackPosition); + globalTrackBank.setCursorTrackPosition(trackPosition); + }); + cursorTrack.trackType().addValueObserver(trackType -> { + mainTrackBank.setTrackType(trackType); + globalTrackBank.setTrackType(trackType); + }); + cursorTrack.sendBank().itemCount().addValueObserver(items -> { + mainTrackBank.setNumberOfSends(items); + globalTrackBank.setNumberOfSends(items); + }); + + mainTrackBank.getTrackBank().followCursorTrack(cursorTrack); + globalTrackBank.getTrackBank().followCursorTrack(cursorTrack); + + rootTrack = host.getProject().getRootTrackGroup(); + mixer = host.createMixer(); + arranger = host.createArranger(); + detailEditor = host.createDetailEditor(); + + trackRemotes = cursorTrack.createCursorRemoteControlsPage(8); + projectRemotes = rootTrack.createCursorRemoteControlsPage(8); + + trackRemotes.pageCount().markInterested(); + projectRemotes.pageCount().markInterested(); + } + + public Track getRootTrack() { + return rootTrack; + } + + public CursorRemoteControlsPage getTrackRemotes() { + return trackRemotes; + } + + public CursorRemoteControlsPage getProjectRemotes() { + return projectRemotes; + } + + public TrackBank getMainTrackBank() { + return mainTrackBank.getTrackBank(); + } + + public TrackBank getGlobalTrackBank() { + return globalTrackBank.getTrackBank(); + } + + public CursorTrack getCursorTrack() { + return cursorTrack; + } + + public CursorDeviceControl getCursorDeviceControl() { + return cursorDeviceControl; + } + + public int[] getColorMain(final int channelOffset) { + return mainTrackBank.getColor(channelOffset); + } + + public int[] getColorGlobal(final int channelOffset) { + return globalTrackBank.getColor(channelOffset); + } + + public void navigateToTrackRemotePage(final int index) { + if (index < trackRemotes.pageCount().get()) { + trackRemotes.selectedPageIndex().set(index); + } + } + + public void navigateToProjectRemotePage(final int index) { + if (index < projectRemotes.pageCount().get()) { + projectRemotes.selectedPageIndex().set(index); + } + } + + public void navigateSends(final int dir) { + mainTrackBank.navigateSends(dir); + globalTrackBank.navigateSends(dir); + } + + public void navigateChannels(final int dir) { + mainTrackBank.navigateChannels(dir); + globalTrackBank.navigateChannels(dir); + } + + public void navigateToSends(final int index) { + mainTrackBank.navigateToSends(index); + globalTrackBank.navigateToSends(index); + } + + public void navigateClipVertical(final int dir) { + mainTrackBank.getTrackBank().sceneBank().scrollBy(-dir); + } + + public Mixer getMixer() { + return mixer; + } + + public Arranger getArranger() { + return arranger; + } + + public DetailEditor getDetailEditor() { + return detailEditor; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/ButtonBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/ButtonBinding.java new file mode 100644 index 00000000..c268a5d8 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/ButtonBinding.java @@ -0,0 +1,25 @@ +package com.bitwig.extensions.controllers.mcu.bindings; + +import com.bitwig.extension.controller.api.HardwareAction; +import com.bitwig.extension.controller.api.HardwareActionBindable; +import com.bitwig.extension.controller.api.HardwareActionBinding; +import com.bitwig.extension.controller.api.HardwareButton; +import com.bitwig.extensions.framework.HardwareBinding; + +public class ButtonBinding extends HardwareBinding { + + public ButtonBinding(final HardwareButton exclusiveButtonSource, final HardwareActionBindable target) { + this(exclusiveButtonSource, exclusiveButtonSource.pressedAction(), target); + } + + public ButtonBinding(final HardwareButton exclusiveButtonSource, final HardwareAction action, + final HardwareActionBindable target) { + super(exclusiveButtonSource, action, target); + } + + @Override + protected HardwareActionBinding addHardwareBinding() { + return getSource().addBinding(getTarget()); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/FaderBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/FaderBinding.java new file mode 100644 index 00000000..612a9475 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/FaderBinding.java @@ -0,0 +1,32 @@ +package com.bitwig.extensions.controllers.mcu.bindings; + +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extensions.controllers.mcu.control.FaderResponse; +import com.bitwig.extensions.framework.Binding; + +public class FaderBinding extends Binding { + + private double lastValue = 0.0; + + public FaderBinding(final Parameter source, final FaderResponse target) { + super(target, source, target); + source.value().addValueObserver(this::valueChange); + } + + private void valueChange(final double value) { + lastValue = value; + if (isActive()) { + getTarget().sendValue(value); + } + } + + @Override + protected void deactivate() { + } + + @Override + protected void activate() { + getTarget().sendValue(lastValue); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/ResetableAbsoluteValueBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/ResetableAbsoluteValueBinding.java new file mode 100644 index 00000000..4f8ec888 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/ResetableAbsoluteValueBinding.java @@ -0,0 +1,48 @@ +package com.bitwig.extensions.controllers.mcu.bindings; + +import com.bitwig.extension.controller.api.AbsoluteHardwareControl; +import com.bitwig.extension.controller.api.AbsoluteHardwareControlBinding; +import com.bitwig.extension.controller.api.HardwareBinding; +import com.bitwig.extension.controller.api.SettableRangedValue; +import com.bitwig.extensions.framework.Binding; + +public class ResetableAbsoluteValueBinding extends Binding implements ResetableBinding { + + HardwareBinding hwBinding; + + public ResetableAbsoluteValueBinding(final AbsoluteHardwareControl source, final SettableRangedValue target) { + super(source, source, target); + } + + protected AbsoluteHardwareControlBinding getHardwareBinding() { + return getTarget().addBinding(getSource()); + } + + @Override + 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/mcu/bindings/ResetableBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/ResetableBinding.java new file mode 100644 index 00000000..be18dce7 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/ResetableBinding.java @@ -0,0 +1,5 @@ +package com.bitwig.extensions.controllers.mcu.bindings; + +public interface ResetableBinding { + void reset(); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/ResetableRelativeValueBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/ResetableRelativeValueBinding.java new file mode 100644 index 00000000..3ceb42ac --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/ResetableRelativeValueBinding.java @@ -0,0 +1,54 @@ +package com.bitwig.extensions.controllers.mcu.bindings; + +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 ResetableRelativeValueBinding extends Binding { + + private HardwareBinding hwBinding; + private final double sensitivity; + + public ResetableRelativeValueBinding(final RelativeHardwareKnob source, final SettableRangedValue target, + final double sensitivity) { + super(source, source, target); + this.sensitivity = sensitivity; + } + + public ResetableRelativeValueBinding(final RelativeHardwareKnob source, final SettableRangedValue target) { + this(source, target, 1.0); + } + + protected RelativeHardwareControlBinding getHardwareBinding() { + return getTarget().addBindingWithSensitivity(getSource(), sensitivity); + } + + 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/mcu/bindings/RingDisplayBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayBinding.java new file mode 100644 index 00000000..e3a5bb61 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayBinding.java @@ -0,0 +1,26 @@ +package com.bitwig.extensions.controllers.mcu.bindings; + +import com.bitwig.extensions.controllers.mcu.control.RingDisplay; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; +import com.bitwig.extensions.framework.Binding; + +public abstract class RingDisplayBinding extends Binding { + protected final RingDisplayType type; + + public RingDisplayBinding(final RingDisplay target, final T source, final RingDisplayType type) { + super(target, source, target); + this.type = type; + } + + @Override + protected void deactivate() { + } + + @Override + protected void activate() { + getTarget().sendValue(calcValue(), false); + } + + protected abstract int calcValue(); + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayDisabledBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayDisabledBinding.java new file mode 100644 index 00000000..06c18e99 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayDisabledBinding.java @@ -0,0 +1,16 @@ +package com.bitwig.extensions.controllers.mcu.bindings; + +import com.bitwig.extensions.controllers.mcu.control.RingDisplay; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; + +public class RingDisplayDisabledBinding extends RingDisplayBinding { + + public RingDisplayDisabledBinding(final RingDisplay target, final RingDisplayType type) { + super(target, type, type); + } + + @Override + protected int calcValue() { + return 0; + } +} 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 new file mode 100644 index 00000000..ae72ff10 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayIntValueBinding.java @@ -0,0 +1,28 @@ +package com.bitwig.extensions.controllers.mcu.bindings; + +import com.bitwig.extensions.controllers.mcu.control.RingDisplay; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; +import com.bitwig.extensions.framework.values.IntValueObject; + +public class RingDisplayIntValueBinding extends RingDisplayBinding { + + public RingDisplayIntValueBinding(final IntValueObject source, final RingDisplay target) { + super(target, source, RingDisplayType.FILL_LR); + source.addValueObserver(this::valueChanged); + } + + private void valueChanged(final int oldValue, final int newValue) { + if (isActive()) { + getTarget().sendValue(calcValue(), false); + } + } + + @Override + protected int calcValue() { + if (getSource().get() == -1) { + return 0; + } + return type.getOffset() + getSource().get(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayParameterBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayParameterBinding.java new file mode 100644 index 00000000..6ae87078 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayParameterBinding.java @@ -0,0 +1,36 @@ +package com.bitwig.extensions.controllers.mcu.bindings; + +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extensions.controllers.mcu.control.RingDisplay; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; + +public class RingDisplayParameterBinding extends RingDisplayBinding { + + public RingDisplayParameterBinding(final Parameter source, final RingDisplay target, final RingDisplayType type) { + super(target, source, type); + final int vintRange = type.getRange() + 1; + source.value().addValueObserver(vintRange, v -> valueChange(type.getOffset() + v)); + source.exists().addValueObserver(this::handleExists); + } + + public void handleExists(final boolean exist) { + if (isActive()) { + valueChange(calcValue(exist)); + } + } + + private void valueChange(final int value) { + if (isActive()) { + getTarget().sendValue(value, false); + } + } + + protected int calcValue(final boolean exists) { + return exists ? type.getOffset() + (int) (getSource().value().get() * type.getRange()) : 0; + } + + @Override + protected int calcValue() { + return getSource().exists().get() ? type.getOffset() + (int) (getSource().value().get() * type.getRange()) : 0; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayParameterBoolBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayParameterBoolBinding.java new file mode 100644 index 00000000..6189c339 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayParameterBoolBinding.java @@ -0,0 +1,26 @@ +package com.bitwig.extensions.controllers.mcu.bindings; + +import com.bitwig.extension.controller.api.BooleanValue; +import com.bitwig.extensions.controllers.mcu.control.RingDisplay; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; + +public class RingDisplayParameterBoolBinding extends RingDisplayBinding { + + public RingDisplayParameterBoolBinding(final BooleanValue source, final RingDisplay target) { + super(target, source, RingDisplayType.FILL_LR); + final int vintRange = type.getRange() + 1; + source.addValueObserver(this::valueChanged); + } + + private void valueChanged(final boolean b) { + if (isActive()) { + getTarget().sendValue(calcValue(), false); + } + } + + @Override + protected int calcValue() { + return getSource().get() ? type.getOffset() + type.getRange() : 0; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayValueBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayValueBinding.java new file mode 100644 index 00000000..09201494 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/RingDisplayValueBinding.java @@ -0,0 +1,36 @@ +package com.bitwig.extensions.controllers.mcu.bindings; + +import com.bitwig.extension.controller.api.SettableRangedValue; +import com.bitwig.extensions.controllers.mcu.control.RingDisplay; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; + +public class RingDisplayValueBinding extends RingDisplayBinding { + + public RingDisplayValueBinding(final SettableRangedValue source, final RingDisplay target, + final RingDisplayType type) { + super(target, source, type); + final int vintRange = type.getRange() + 1; + source.addValueObserver(vintRange, v -> valueChange(type.getOffset() + v)); + } + + public void handleExists(final boolean exist) { + if (isActive()) { + valueChange(calcValue(exist)); + } + } + + private void valueChange(final int value) { + if (isActive()) { + getTarget().sendValue(value, false); + } + } + + protected int calcValue(final boolean exists) { + return exists ? type.getOffset() + (int) (getSource().get() * type.getRange()) : 0; + } + + @Override + protected int calcValue() { + return type.getOffset() + (int) (getSource().get() * type.getRange()); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/VuMeterBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/VuMeterBinding.java new file mode 100644 index 00000000..5855e4eb --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/VuMeterBinding.java @@ -0,0 +1,40 @@ +package com.bitwig.extensions.controllers.mcu.bindings; + +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extensions.controllers.mcu.display.DisplayManager; +import com.bitwig.extensions.framework.Binding; + +public class VuMeterBinding extends Binding { + + private final int index; + private int lastValue; + + private record ExclusivityObject(int index, Track track) { + + } + + public VuMeterBinding(final DisplayManager target, final Track source, final int index) { + super(new ExclusivityObject(index, source), source, target); + this.index = index; + source.addVuMeterObserver(14, -1, true, value -> { + updateVuValue(value); + }); + } + + private void updateVuValue(final int value) { + if (isActive()) { + getTarget().sendVuUpdate(index, value); + } + lastValue = value; + } + + @Override + protected void deactivate() { + } + + @Override + protected void activate() { + getTarget().sendVuUpdate(index, lastValue); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/AbstractDisplayBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/AbstractDisplayBinding.java new file mode 100644 index 00000000..c944947e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/AbstractDisplayBinding.java @@ -0,0 +1,45 @@ +package com.bitwig.extensions.controllers.mcu.bindings.display; + +import com.bitwig.extensions.controllers.mcu.display.DisplayManager; +import com.bitwig.extensions.controllers.mcu.layer.ControlMode; +import com.bitwig.extensions.framework.Binding; + +public abstract class AbstractDisplayBinding extends Binding { + protected ControlMode controlMode; + protected String lastValue = ""; + protected boolean exists; + protected DisplayTarget targetIndex; + + public AbstractDisplayBinding(final DisplayManager target, final ControlMode mode, final DisplayTarget targetIndex, + final T value) { + super(targetIndex, value, target); + this.targetIndex = targetIndex; + this.controlMode = mode; + this.exists = true; + } + + @Override + protected void deactivate() { + } + + protected void handleExists(final boolean exists) { + this.exists = exists; + if (isActive()) { + updateDisplay(); + } + } + + protected void updateDisplay() { + if (exists) { + getTarget().sendText(controlMode, targetIndex.rowIndex(), targetIndex.cellIndex(), lastValue); + } else { + getTarget().sendText(controlMode, targetIndex.rowIndex(), targetIndex.cellIndex(), ""); + } + } + + @Override + protected void activate() { + updateDisplay(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/DisplayTarget.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/DisplayTarget.java new file mode 100644 index 00000000..6372568e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/DisplayTarget.java @@ -0,0 +1,28 @@ +package com.bitwig.extensions.controllers.mcu.bindings.display; + +import com.bitwig.extensions.controllers.mcu.display.DisplayRow; + +public record DisplayTarget(int rowIndex, int cellIndex, int sectionIndex, Object destination) { + public static DisplayTarget of(final int rowIndex, final int cellIndex, final int sectionIndex, + final Object destination) { + return new DisplayTarget(rowIndex, cellIndex, sectionIndex, destination); + } + + public static DisplayTarget of(final DisplayRow row, final int cellIndex, final int sectionIndex, + final Object destination) { + return new DisplayTarget(row.getRowIndex(), cellIndex, sectionIndex, destination); + } + + public static DisplayTarget of(final DisplayRow row, final int cellIndex, final Object destination) { + return new DisplayTarget(row.getRowIndex(), cellIndex, 0, destination); + } + + public static DisplayTarget of(final int rowIndex, final int cellIndex, final Object destination) { + return new DisplayTarget(rowIndex, cellIndex, 0, destination); + } + + public static DisplayTarget of(final int rowIndex, final Object destination) { + return new DisplayTarget(rowIndex, -1, 0, destination); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/ModelessDisplayBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/ModelessDisplayBinding.java new file mode 100644 index 00000000..4df8f039 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/ModelessDisplayBinding.java @@ -0,0 +1,42 @@ +package com.bitwig.extensions.controllers.mcu.bindings.display; + +import com.bitwig.extension.controller.api.StringValue; +import com.bitwig.extensions.controllers.mcu.display.DisplayManager; +import com.bitwig.extensions.framework.Binding; + +public class ModelessDisplayBinding extends Binding { + + private String lastValue = ""; + protected DisplayTarget targetIndex; + + public ModelessDisplayBinding(final DisplayManager target, final DisplayTarget targetIndex, + final StringValue value) { + super(targetIndex, value, target); + this.targetIndex = targetIndex; + value.addValueObserver(this::handleValueChange); + value.markInterested(); + this.lastValue = value.get(); + } + + private void handleValueChange(final String newValue) { + this.lastValue = newValue; + if (isActive()) { + updateDisplay(); + } + } + + @Override + protected void deactivate() { + } + + protected void updateDisplay() { + getTarget().sendText(targetIndex.rowIndex(), targetIndex.cellIndex(), lastValue); + } + + @Override + protected void activate() { + + updateDisplay(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/ParameterValueDisplayBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/ParameterValueDisplayBinding.java new file mode 100644 index 00000000..394c4970 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/ParameterValueDisplayBinding.java @@ -0,0 +1,28 @@ +package com.bitwig.extensions.controllers.mcu.bindings.display; + +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extensions.controllers.mcu.display.DisplayManager; +import com.bitwig.extensions.controllers.mcu.layer.ControlMode; +import com.bitwig.extensions.controllers.mcu.value.DoubleValueConverter; + +public class ParameterValueDisplayBinding extends AbstractDisplayBinding { + private final DoubleValueConverter converter; + + public ParameterValueDisplayBinding(final DisplayManager target, final ControlMode mode, + final DisplayTarget displayTargetIndex, final Parameter parameter, final DoubleValueConverter converter) { + super(target, mode, displayTargetIndex, parameter); + parameter.exists().addValueObserver(this::handleExists); + parameter.value().addValueObserver(this::handleValueChange); + this.converter = converter; + this.lastValue = converter.convert(parameter.value().get()); + } + + private void handleValueChange(final double newValue) { + this.lastValue = converter.convert(newValue); + if (isActive()) { + updateDisplay(); + } + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/StringDisplayBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/StringDisplayBinding.java new file mode 100644 index 00000000..68fa8583 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/StringDisplayBinding.java @@ -0,0 +1,48 @@ +package com.bitwig.extensions.controllers.mcu.bindings.display; + +import com.bitwig.extension.controller.api.BooleanValue; +import com.bitwig.extension.controller.api.StringValue; +import com.bitwig.extensions.controllers.mcu.StringUtil; +import com.bitwig.extensions.controllers.mcu.bindings.ResetableBinding; +import com.bitwig.extensions.controllers.mcu.display.DisplayManager; +import com.bitwig.extensions.controllers.mcu.layer.ControlMode; +import com.bitwig.extensions.controllers.mcu.value.StringValueConverter; + +public class StringDisplayBinding extends AbstractDisplayBinding implements ResetableBinding { + private final StringValueConverter stringConversion; + + public StringDisplayBinding(final DisplayManager target, final ControlMode mode, + final DisplayTarget displayTargetIndex, final StringValue stringValue, final BooleanValue existsValue, + final StringValueConverter stringConversion) { + super(target, mode, displayTargetIndex, stringValue); + if (existsValue != null) { + existsValue.addValueObserver(this::handleExists); + } + stringValue.addValueObserver(this::handleValueChange); + this.stringConversion = stringConversion; + this.lastValue = stringConversion.convert(stringValue.get()); + } + + public StringDisplayBinding(final DisplayManager target, final ControlMode mode, + final DisplayTarget displayTargetIndex, final StringValue stringValue, final BooleanValue existsValue) { + this(target, mode, displayTargetIndex, stringValue, existsValue, s -> StringUtil.toAsciiDisplay(s, 8)); + } + + private void handleValueChange(final String newValue) { + final String sendValue = stringConversion.convert(newValue); + if (!sendValue.equals(this.lastValue)) { + this.lastValue = sendValue; + if (isActive()) { + updateDisplay(); + } + } + } + + @Override + public void reset() { + this.lastValue = stringConversion.convert(getSource().get()); + if (isActive()) { + updateDisplay(); + } + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/StringRowDisplayBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/StringRowDisplayBinding.java new file mode 100644 index 00000000..a9b9e32a --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/display/StringRowDisplayBinding.java @@ -0,0 +1,35 @@ +package com.bitwig.extensions.controllers.mcu.bindings.display; + +import com.bitwig.extension.controller.api.StringValue; +import com.bitwig.extensions.controllers.mcu.StringUtil; +import com.bitwig.extensions.controllers.mcu.display.DisplayManager; +import com.bitwig.extensions.controllers.mcu.display.DisplayRow; +import com.bitwig.extensions.controllers.mcu.layer.ControlMode; + +public class StringRowDisplayBinding extends AbstractDisplayBinding { + + public StringRowDisplayBinding(final DisplayManager target, final ControlMode mode, final DisplayRow row, + final int sectionIndex, final StringValue stringValue) { + super(target, mode, DisplayTarget.of(row.getRowIndex(), -1, sectionIndex, stringValue), stringValue); + exists = true; + stringValue.addValueObserver(this::handleValueChange); + this.lastValue = StringUtil.toAscii(stringValue.get()); + } + + protected void updateDisplay() { + getTarget().sendText(controlMode, targetIndex.rowIndex(), lastValue); + } + + private void handleValueChange(final String newValue) { + this.lastValue = StringUtil.toAscii(newValue); + if (isActive()) { + updateDisplay(); + } + } + + @Override + protected void activate() { + updateDisplay(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/paramslots/FaderSlotBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/paramslots/FaderSlotBinding.java new file mode 100644 index 00000000..07ea668e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/paramslots/FaderSlotBinding.java @@ -0,0 +1,32 @@ +package com.bitwig.extensions.controllers.mcu.bindings.paramslots; + +import com.bitwig.extensions.controllers.mcu.control.FaderResponse; +import com.bitwig.extensions.controllers.mcu.devices.ParamPageSlot; +import com.bitwig.extensions.framework.Binding; + +public class FaderSlotBinding extends Binding { + + private double lastValue = 0.0; + + public FaderSlotBinding(final ParamPageSlot source, final FaderResponse target) { + super(target, source, target); + source.getValue().addValueObserver(this::valueChange); + } + + private void valueChange(final double value) { + lastValue = value; + if (isActive()) { + getTarget().sendValue(value); + } + } + + @Override + protected void deactivate() { + } + + @Override + protected void activate() { + getTarget().sendValue(lastValue); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/paramslots/ResetableAbsoluteValueSlotBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/paramslots/ResetableAbsoluteValueSlotBinding.java new file mode 100644 index 00000000..c58e692f --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/paramslots/ResetableAbsoluteValueSlotBinding.java @@ -0,0 +1,49 @@ +package com.bitwig.extensions.controllers.mcu.bindings.paramslots; + +import com.bitwig.extension.controller.api.AbsoluteHardwareControl; +import com.bitwig.extension.controller.api.AbsoluteHardwareControlBinding; +import com.bitwig.extension.controller.api.HardwareBinding; +import com.bitwig.extensions.controllers.mcu.bindings.ResetableBinding; +import com.bitwig.extensions.controllers.mcu.devices.ParamPageSlot; +import com.bitwig.extensions.framework.Binding; + +public class ResetableAbsoluteValueSlotBinding extends Binding implements ResetableBinding { + + HardwareBinding hwBinding; + + public ResetableAbsoluteValueSlotBinding(final AbsoluteHardwareControl source, final ParamPageSlot target) { + super(source, source, target); + } + + protected AbsoluteHardwareControlBinding getHardwareBinding() { + return getTarget().addBinding(getSource()); + } + + @Override + 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/mcu/bindings/paramslots/ResetableRelativeSlotBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/paramslots/ResetableRelativeSlotBinding.java new file mode 100644 index 00000000..e10a8067 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/paramslots/ResetableRelativeSlotBinding.java @@ -0,0 +1,56 @@ +package com.bitwig.extensions.controllers.mcu.bindings.paramslots; + +import com.bitwig.extension.controller.api.HardwareBinding; +import com.bitwig.extension.controller.api.RelativeHardwareControlBinding; +import com.bitwig.extension.controller.api.RelativeHardwareKnob; +import com.bitwig.extensions.controllers.mcu.bindings.ResetableBinding; +import com.bitwig.extensions.controllers.mcu.devices.ParamPageSlot; +import com.bitwig.extensions.framework.Binding; + +public class ResetableRelativeSlotBinding extends Binding implements ResetableBinding { + + private HardwareBinding hwBinding; + private final double sensitivity; + + public ResetableRelativeSlotBinding(final RelativeHardwareKnob source, final ParamPageSlot target, + final double sensitivity) { + super(source, source, target); + this.sensitivity = sensitivity; + } + + public ResetableRelativeSlotBinding(final RelativeHardwareKnob source, final ParamPageSlot target) { + this(source, target, 1.0); + } + + protected RelativeHardwareControlBinding getHardwareBinding() { + return getTarget().addBindingWithSensitivity(getSource(), sensitivity); + } + + @Override + 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/mcu/bindings/paramslots/RingParameterDisplaySlotBinding.java b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/paramslots/RingParameterDisplaySlotBinding.java new file mode 100644 index 00000000..27ac3970 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/bindings/paramslots/RingParameterDisplaySlotBinding.java @@ -0,0 +1,79 @@ +package com.bitwig.extensions.controllers.mcu.bindings.paramslots; + +import com.bitwig.extensions.controllers.mcu.bindings.ResetableBinding; +import com.bitwig.extensions.controllers.mcu.bindings.RingDisplayBinding; +import com.bitwig.extensions.controllers.mcu.control.RingDisplay; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; +import com.bitwig.extensions.controllers.mcu.devices.ParamPageSlot; + +/** + * Special binding for the encoder ring display, that also responds to being + * 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) -> { + valueChange(source.getRingDisplayType().getOffset() + newValue); + }); + source.getExistsValue().addValueObserver(exists -> { + this.exists = exists; + update(); + }); + 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(); + final int value = (lastEnableValue && exists) ? lastValue : 0; + getTarget().sendValue(value, false); + } + } + + private void valueChange(final int value) { + lastValue = value; + if (isActive()) { + final int newValue = (lastEnableValue && exists) ? lastValue : 0; + getTarget().sendValue(newValue, false); + } + } + + @Override + protected void activate() { + 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/mcu/config/ButtonAssignment.java b/src/main/java/com/bitwig/extensions/controllers/mcu/config/ButtonAssignment.java new file mode 100644 index 00000000..b5008355 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/config/ButtonAssignment.java @@ -0,0 +1,7 @@ +package com.bitwig.extensions.controllers.mcu.config; + +public interface ButtonAssignment { + int getNoteNo(); + + int getChannel(); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/config/ControllerConfig.java b/src/main/java/com/bitwig/extensions/controllers/mcu/config/ControllerConfig.java new file mode 100644 index 00000000..ab168784 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/config/ControllerConfig.java @@ -0,0 +1,302 @@ +package com.bitwig.extensions.controllers.mcu.config; + +import java.util.HashMap; +import java.util.Map; + +import com.bitwig.extensions.controllers.mcu.definitions.ManufacturerType; +import com.bitwig.extensions.controllers.mcu.definitions.SubType; +import com.bitwig.extensions.controllers.mcu.display.TimeCodeLed; + +public class ControllerConfig { + private boolean hasLowerDisplay; + private final ManufacturerType manufacturerType; + private final SubType subType; + private boolean hasDedicateVu; + private boolean hasMasterVu; + private boolean useClearDuplicateModifiers = false; + private boolean functionSectionLayered = false; + private boolean has2ClickResolution; + private boolean decelerateJogWheel = false; + private boolean noDedicatedZoom = false; + private boolean topDisplayRowsFlipped = false; + private boolean navigationWithJogWheel = false; + private int masterFaderChannel = -1; + private boolean hasIconTrackColoring = false; + private boolean displaySegmented = false; + private boolean singleMainUnit = true; + private int nrOfExtenders; + private long forceUpdateOnStartup = -1; + private final Map assignmentMap = new HashMap<>(); + private boolean hasTimeCodeLed; + private EncoderBehavior jogWheelBehavior = EncoderBehavior.STEP; + private TimeCodeLed.DisplayType displayType = TimeCodeLed.DisplayType.MCU; + + public ControllerConfig(final ManufacturerType manufacturerType, final SubType subType) { + this.manufacturerType = manufacturerType; + this.subType = subType; + createDefaultAssignmentMap(); + } + + public void createDefaultAssignmentMap() { + assignmentMap.put(McuFunction.PLAY, McuAssignments.PLAY); + assignmentMap.put(McuFunction.STOP, McuAssignments.STOP); + assignmentMap.put(McuFunction.RECORD, McuAssignments.RECORD); + assignmentMap.put(McuFunction.LOOP, McuAssignments.CYCLE); + assignmentMap.put(McuFunction.METRO, McuAssignments.CLICK); + assignmentMap.put(McuFunction.PUNCH_IN, McuAssignments.F6); + assignmentMap.put(McuFunction.PUNCH_OUT, McuAssignments.F7); + assignmentMap.put(McuFunction.FAST_FORWARD, McuAssignments.FFWD); + assignmentMap.put(McuFunction.FAST_REVERSE, McuAssignments.REWIND); + assignmentMap.put(McuFunction.SHIFT, McuAssignments.SHIFT); + assignmentMap.put(McuFunction.OPTION, McuAssignments.OPTION); + assignmentMap.put(McuFunction.ALT, McuAssignments.ALT); + assignmentMap.put(McuFunction.CONTROL, McuAssignments.CONTROL); + assignmentMap.put(McuFunction.GLOBAL_VIEW, McuAssignments.GLOBAL_VIEW); + assignmentMap.put(McuFunction.OVERDUB, McuAssignments.REPLACE); + assignmentMap.put(McuFunction.DISPLAY_SMPTE, McuAssignments.DISPLAY_SMPTE); + + assignmentMap.put(McuFunction.FLIP, McuAssignments.FLIP); + assignmentMap.put(McuFunction.NAME_VALUE, McuAssignments.DISPLAY_NAME); + + assignmentMap.put(McuFunction.AUTO_READ, McuAssignments.AUTO_READ_OFF); + assignmentMap.put(McuFunction.AUTO_WRITE, McuAssignments.AUTO_WRITE); + assignmentMap.put(McuFunction.AUTO_TOUCH, McuAssignments.TOUCH); + assignmentMap.put(McuFunction.AUTO_LATCH, McuAssignments.LATCH); + assignmentMap.put(McuFunction.CUE_MARKER, McuAssignments.MARKER); + + assignmentMap.put(McuFunction.MODE_ALL_SENDS, McuAssignments.V_TRACK); + assignmentMap.put(McuFunction.MODE_PAN, McuAssignments.V_PAN); + assignmentMap.put(McuFunction.MODE_EQ, McuAssignments.V_EQ); + assignmentMap.put(McuFunction.MODE_DEVICE, McuAssignments.V_PLUGIN); + assignmentMap.put(McuFunction.MODE_TRACK_REMOTE, McuAssignments.F2); + assignmentMap.put(McuFunction.MODE_PROJECT_REMOTE, McuAssignments.F3); + assignmentMap.put(McuFunction.MODE_SEND, McuAssignments.V_SEND); + assignmentMap.put(McuFunction.BANK_LEFT, McuAssignments.BANK_LEFT); + assignmentMap.put(McuFunction.BANK_RIGHT, McuAssignments.BANK_RIGHT); + assignmentMap.put(McuFunction.CHANNEL_LEFT, McuAssignments.TRACK_LEFT); + assignmentMap.put(McuFunction.CHANNEL_RIGHT, McuAssignments.TRACK_RIGHT); + assignmentMap.put(McuFunction.NAV_DOWN, McuAssignments.CURSOR_DOWN); + assignmentMap.put(McuFunction.NAV_UP, McuAssignments.CURSOR_UP); + assignmentMap.put(McuFunction.NAV_LEFT, McuAssignments.CURSOR_LEFT); + assignmentMap.put(McuFunction.NAV_RIGHT, McuAssignments.CURSOR_RIGHT); + assignmentMap.put(McuFunction.ZOOM, McuAssignments.ZOOM); + } + + + public boolean usesUnifiedDeviceControl() { + return assignmentMap.containsKey(McuFunction.MODE_DEVICE); + } + + public boolean hasDirectSelect() { + return assignmentMap.containsKey(McuFunction.SEND_SELECT_1); + } + + public ControllerConfig setAssignment(final McuFunction function, final ButtonAssignment assignment) { + assignmentMap.entrySet().stream().filter(entry -> entry.getValue().equals(assignment)).findFirst() + .ifPresent(entry -> assignmentMap.remove(entry.getKey())); + assignmentMap.put(function, assignment); + return this; + } + + public ControllerConfig setAssignment(final CustomAssignment assignment) { + assignmentMap.entrySet().stream().filter(entry -> entry.getValue().equals(assignment)).findFirst() + .ifPresent(entry -> assignmentMap.remove(entry.getKey())); + assignmentMap.put(assignment.getFunction(), assignment); + return this; + } + + public ControllerConfig removeAssignment(final McuFunction function) { + assignmentMap.remove(function); + return this; + } + + public boolean isHas2ClickResolution() { + return has2ClickResolution; + } + + public ControllerConfig setHas2ClickResolution(final boolean value) { + this.has2ClickResolution = value; + return this; + } + + public ControllerConfig setHasMasterVu(final boolean hasMasterVu) { + this.hasMasterVu = hasMasterVu; + return this; + } + + public ControllerConfig setJogWheelCoding(final EncoderBehavior behavior) { + this.jogWheelBehavior = behavior; + return this; + } + + public ControllerConfig setHasTimeCodeLed(final boolean hasTimeCodeLed) { + this.hasTimeCodeLed = hasTimeCodeLed; + return this; + } + + public ControllerConfig setHasIconTrackColoring(final boolean hasIconTrackColoring) { + this.hasIconTrackColoring = hasIconTrackColoring; + return this; + } + + public ControllerConfig setForceUpdateOnStartup(final long forceUpdateOnStartup) { + this.forceUpdateOnStartup = forceUpdateOnStartup; + return this; + } + + public long getForceUpdateOnStartup() { + return forceUpdateOnStartup; + } + + public boolean isNavigationWithJogWheel() { + return navigationWithJogWheel; + } + + public ControllerConfig setNavigationWithJogWheel(final boolean navigationWithJogWheel) { + this.navigationWithJogWheel = navigationWithJogWheel; + return this; + } + + public boolean isDecelerateJogWheel() { + return decelerateJogWheel; + } + + public TimeCodeLed.DisplayType getDisplayType() { + return displayType; + } + + public void setDisplayType(final TimeCodeLed.DisplayType displayType) { + this.displayType = displayType; + } + + public ControllerConfig setDecelerateJogWheel(final boolean decelerateJogWheel) { + this.decelerateJogWheel = decelerateJogWheel; + return this; + } + + public boolean isNoDedicatedZoom() { + return noDedicatedZoom; + } + + public ControllerConfig setNoDedicatedZoom(final boolean noDedicatedZoom) { + this.noDedicatedZoom = noDedicatedZoom; + return this; + } + + public boolean isSingleMainUnit() { + return singleMainUnit; + } + + public ControllerConfig setSingleMainUnit(final boolean singleMainUnit) { + this.singleMainUnit = singleMainUnit; + return this; + } + + public boolean hasNavigationWithJogWheel() { + return navigationWithJogWheel; + } + + public boolean hasIconTrackColoring() { + return hasIconTrackColoring; + } + + public boolean isDisplaySegmented() { + return displaySegmented; + } + + public ControllerConfig setDisplaySegmented(final boolean displaySegmented) { + this.displaySegmented = displaySegmented; + return this; + } + + public ControllerConfig setHasMasterFader(final int masterFaderChannel) { + this.masterFaderChannel = masterFaderChannel; + return this; + } + + public ControllerConfig setHasLowerDisplay(final boolean hasLowerDisplay) { + this.hasLowerDisplay = hasLowerDisplay; + return this; + } + + public int getNrOfExtenders() { + return nrOfExtenders; + } + + public ControllerConfig setNrOfExtenders(final int nrOfExtenders) { + this.nrOfExtenders = nrOfExtenders; + return this; + } + + public EncoderBehavior getJogWheelBehavior() { + return jogWheelBehavior; + } + + public boolean isFunctionSectionLayered() { + return functionSectionLayered; + } + + public ControllerConfig setFunctionSectionLayered(final boolean functionSectionLayer) { + functionSectionLayered = functionSectionLayer; + return this; + } + + public boolean isUseClearDuplicateModifiers() { + return useClearDuplicateModifiers; + } + + public ControllerConfig setUseClearDuplicateModifiers(final boolean useClearDuplicateModifiers) { + this.useClearDuplicateModifiers = useClearDuplicateModifiers; + return this; + } + + public boolean isTopDisplayRowsFlipped() { + return topDisplayRowsFlipped; + } + + public ControllerConfig setTopDisplayRowsFlipped(final boolean topDisplayRowsFlipped) { + this.topDisplayRowsFlipped = topDisplayRowsFlipped; + return this; + } + + public boolean isHasDedicateVu() { + return hasDedicateVu; + } + + public ControllerConfig setHasDedicateVu(final boolean hasDedicateVu) { + this.hasDedicateVu = hasDedicateVu; + return this; + } + + public boolean hasLowerDisplay() { + return hasLowerDisplay; + } + + public ManufacturerType getManufacturerType() { + return manufacturerType; + } + + public SubType getSubType() { + return subType; + } + + public boolean hasMasterVu() { + return hasMasterVu; + } + + public ButtonAssignment getAssignment(final McuFunction function) { + return assignmentMap.get(function); + } + + public int getMasterFaderChannel() { + return masterFaderChannel; + } + + public boolean hasTimecodeLed() { + return this.hasTimeCodeLed; + } + + public boolean hasAssignment(final McuFunction function) { + return assignmentMap.containsKey(function); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/config/CustomAssignment.java b/src/main/java/com/bitwig/extensions/controllers/mcu/config/CustomAssignment.java new file mode 100644 index 00000000..9fc98523 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/config/CustomAssignment.java @@ -0,0 +1,52 @@ +package com.bitwig.extensions.controllers.mcu.config; + +import java.util.Objects; + +public class CustomAssignment implements ButtonAssignment { + + private final int noteNo; + private final int channel; + private final McuFunction function; + + public CustomAssignment(final McuFunction function, final int noteNo, final int channel) { + this.function = function; + this.noteNo = noteNo; + this.channel = channel; + } + + @Override + public int getNoteNo() { + return noteNo; + } + + @Override + public int getChannel() { + return channel; + } + + public McuFunction getFunction() { + return function; + } + + @Override + public String toString() { + return function.toString(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final CustomAssignment that = (CustomAssignment) o; + return noteNo == that.noteNo && channel == that.channel; + } + + @Override + public int hashCode() { + return Objects.hash(noteNo, channel); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/config/EncoderBehavior.java b/src/main/java/com/bitwig/extensions/controllers/mcu/config/EncoderBehavior.java new file mode 100644 index 00000000..5fe9d401 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/config/EncoderBehavior.java @@ -0,0 +1,26 @@ +package com.bitwig.extensions.controllers.mcu.config; + +public enum EncoderBehavior { + ACCEL, // Signed bit + STEP, // 1 and 127 ; + STEP_1_65(1, 65); + private final int upValue; + private final int downValue; + + EncoderBehavior() { + this(1, -1); + } + + EncoderBehavior(final int upValue, final int downValue) { + this.upValue = upValue; + this.downValue = downValue; + } + + public int getDownValue() { + return downValue; + } + + public int getUpValue() { + return upValue; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/config/McuAssignments.java b/src/main/java/com/bitwig/extensions/controllers/mcu/config/McuAssignments.java new file mode 100644 index 00000000..6c6b1836 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/config/McuAssignments.java @@ -0,0 +1,106 @@ +package com.bitwig.extensions.controllers.mcu.config; + +public enum McuAssignments implements ButtonAssignment { + PLAY(94), // + STOP(93), // + RECORD(95), // + REWIND(91), // + FFWD(92), // + AUTO_WRITE(75), // + AUTO_READ_OFF(74), // + TRIM(76), + TOUCH(77), + LATCH(78), + GROUP(79), // + SOLO_BASE(8, true), + REC_BASE(0, true), + MUTE_BASE(16, true), + SELECT_BASE(24, true), // + ENC_PRESS_BASE(32, true), // + SIGNAL_BASE(104, true), // + TOUCH_VOLUME(104, true), // + SHIFT(70), // + OPTION(71), // + CONTROL(72), // + ALT(73), // + UNDO(81), // + SAVE(80), // ICON VST + CANCEL(82), + ENTER(83), // + MARKER(84), + NUDGE(85), + CYCLE(86), + DROP(87), // PUNCH IN + REPLACE(88), // PUNCH OUT + CLICK(89), + SOLO(90), // + FLIP(50), // + DISPLAY_NAME(52), + DISPLAY_SMPTE(53), // + BEATS_MODE(114), + SMPTE_MODE(113), // + V_TRACK(40), + V_SEND(41), + V_PAN(42), + V_PLUGIN(43), + V_EQ(44), + V_INSTRUMENT(45), // + F1(54), + F2(55), + F3(56), + F4(57), + F5(58), + F6(59), + F7(60), + F8(61), // + CURSOR_UP(96), + CURSOR_DOWN(97), + CURSOR_LEFT(98), + CURSOR_RIGHT(99), // + ZOOM(100), + SCRUB(101), // + BANK_LEFT(46), + BANK_RIGHT(47), // + TRACK_LEFT(48), // + TRACK_RIGHT(49), // + GLOBAL_VIEW(51), // + GV_MIDI_LF1(62), // + GV_INPUTS_LF2(63), // + GV_AUDIO_LF3(64), // + GV_INSTRUMENT_LF4(65), // + GV_AUX_LF5(66), // + GV_BUSSES_LF6(67), // + GV_OUTPUTS_LF7(68), // + GV_USER_LF8(69), // + GV_USER_LF8_G2(51), + REDO(71), // + AUTO_OVERRIDE(117), // is only overridden + MIXER(118), // is only overridden + STEP_SEQ(115), // is only overridden + CLIP_OVERDUB(116); // is only overridden + + private final int notNo; + private final boolean baseAssignment; + + McuAssignments(final int noteNo, final boolean baseAssignment) { + notNo = noteNo; + this.baseAssignment = baseAssignment; + } + + McuAssignments(final int notNo) { + this(notNo, false); + } + + public int getNoteNo() { + return notNo; + } + + public boolean isSingle() { + return !baseAssignment; + } + + @Override + public int getChannel() { + return 0; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/config/McuFunction.java b/src/main/java/com/bitwig/extensions/controllers/mcu/config/McuFunction.java new file mode 100644 index 00000000..6cab0ed7 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/config/McuFunction.java @@ -0,0 +1,70 @@ +package com.bitwig.extensions.controllers.mcu.config; + +public enum McuFunction { + PLAY, + FLIP, + RECORD, + STOP, + LOOP, + OVERDUB, + RESTORE_AUTOMATION, + FAST_FORWARD, + FAST_REVERSE, + TRACK_MODE, + MODE_TRACK, + MODE_SEND, + MODE_PAN, + MODE_EQ, + MODE_DEVICE, + MODE_TRACK_REMOTE, + MODE_PROJECT_REMOTE, + BANK_LEFT, + BANK_RIGHT, + CHANNEL_LEFT, + CHANNEL_RIGHT, + NAME_VALUE, + SHIFT, + CONTROL, + OPTION, + ALT, + READ, + AUTO_READ, + AUTO_WRITE, + AUTO_TOUCH, + AUTO_LATCH, + GLOBAL_VIEW, + METRO, + PUNCH_IN, + PUNCH_OUT, + MODE_ALL_SENDS, + DISPLAY_SMPTE, + NAV_LEFT, + NAV_RIGHT, + NAV_DOWN, + NAV_UP, + TEMPO, + UNDO, + CUE_MARKER, + SEND_SELECT_1, + SEND_SELECT_2, + SEND_SELECT_3, + SEND_SELECT_4, + SEND_SELECT_5, + SEND_SELECT_6, + SEND_SELECT_7, + SEND_SELECT_8, + CLIP_LAUNCHER_MODE_4, + CLIP_LAUNCHER_MODE_2, + SSL_PLUGINS_MENU, + GROOVE_MENU, + ZOOM, + ZOOM_MENU, + PAGE_LEFT, + PAGE_RIGHT, + ZOOM_OUT, + ZOOM_IN, + AUTOMATION_LAUNCHER, + ARRANGER, + DUPLICATE, + CLEAR +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/control/EncoderMode.java b/src/main/java/com/bitwig/extensions/controllers/mcu/control/EncoderMode.java new file mode 100644 index 00000000..d71039d0 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/control/EncoderMode.java @@ -0,0 +1,5 @@ +package com.bitwig.extensions.controllers.mcu.control; + +public enum EncoderMode { + ACCELERATED, NONACCELERATED +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/control/FaderResponse.java b/src/main/java/com/bitwig/extensions/controllers/mcu/control/FaderResponse.java new file mode 100644 index 00000000..d3f89e93 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/control/FaderResponse.java @@ -0,0 +1,35 @@ +package com.bitwig.extensions.controllers.mcu.control; + +import com.bitwig.extensions.controllers.mcu.MidiProcessor; + +public class FaderResponse { + private final MidiProcessor midiProcessor; + private final int aftertouchValue; + int lastValue = -1; + + public FaderResponse(final MidiProcessor midi, final int which) { + aftertouchValue = 0xE0 | which; + this.midiProcessor = midi; + } + + public void sendValue(final double v) { + final int value = (int) (v * 16383); + if (value != lastValue) { + lastValue = value; + final int lsb = value & 0x7F; + final int msb = value >> 7; + midiProcessor.sendMidi(aftertouchValue, lsb, msb); + } + } + + public int getWhich() { + return aftertouchValue & 0xF; + } + + public void refresh() { + final int lsb = lastValue & 0x7F; + final int msb = lastValue >> 7; + midiProcessor.sendMidi(aftertouchValue, lsb, msb); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/control/MainHardwareSection.java b/src/main/java/com/bitwig/extensions/controllers/mcu/control/MainHardwareSection.java new file mode 100644 index 00000000..77aa043f --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/control/MainHardwareSection.java @@ -0,0 +1,102 @@ +package com.bitwig.extensions.controllers.mcu.control; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.IntConsumer; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.RelativeHardwarControlBindable; +import com.bitwig.extension.controller.api.RelativeHardwareKnob; +import com.bitwig.extension.controller.api.RelativeHardwareValueMatcher; +import com.bitwig.extensions.controllers.mcu.MidiProcessor; +import com.bitwig.extensions.controllers.mcu.TimedProcessor; +import com.bitwig.extensions.controllers.mcu.config.ButtonAssignment; +import com.bitwig.extensions.controllers.mcu.config.ControllerConfig; +import com.bitwig.extensions.controllers.mcu.config.EncoderBehavior; +import com.bitwig.extensions.controllers.mcu.config.McuFunction; +import com.bitwig.extensions.controllers.mcu.display.TimeCodeLed; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.di.Context; + +public class MainHardwareSection { + + private static final int JOG_WHEEL_CC = 60; + private final RelativeHardwareKnob jogWheelEncoder; + private final ControllerHost host; + private final Map buttonMap = new HashMap<>(); + private final TimeCodeLed timeCodeLed; + private final MidiProcessor midiProcessor; + + public MainHardwareSection(final Context context, final MidiProcessor midiProcessor, final int subIndex) { + final HardwareSurface surface = context.getService(HardwareSurface.class); + final ControllerConfig config = context.getService(ControllerConfig.class); + final TimedProcessor timedProcessor = context.getService(TimedProcessor.class); + host = context.getService(ControllerHost.class); + this.midiProcessor = midiProcessor; + + for (final McuFunction function : McuFunction.values()) { + final ButtonAssignment assignment = config.getAssignment(function); + if (assignment != null) { + buttonMap.put(function, new McuButton(assignment, subIndex, surface, midiProcessor, timedProcessor)); + } + } + jogWheelEncoder = surface.createRelativeHardwareKnob("JOG_WHEEL_" + subIndex); + switch (config.getJogWheelBehavior()) { + case ACCEL -> setUpAccelerated(midiProcessor); + case STEP -> setUp2Complement(midiProcessor); + case STEP_1_65 -> setUpStepped(midiProcessor, config.getJogWheelBehavior()); + } + timeCodeLed = config.hasTimecodeLed() ? new TimeCodeLed(midiProcessor, config.getDisplayType()) : null; + } + + private void setUpAccelerated(final MidiProcessor midiProcessor) { + jogWheelEncoder.setAdjustValueMatcher( + midiProcessor.getMidiIn().createRelativeSignedBitCCValueMatcher(0, JOG_WHEEL_CC, 100)); + jogWheelEncoder.setStepSize(1.0 / 50.0); + } + + private void setUp2Complement(final MidiProcessor midiProcessor) { + jogWheelEncoder.setAdjustValueMatcher( + midiProcessor.getMidiIn().createRelative2sComplementCCValueMatcher(0, JOG_WHEEL_CC, 100)); + jogWheelEncoder.setStepSize(1.0); + } + + private void setUpStepped(final MidiProcessor midiProcessor, final EncoderBehavior behavior) { + final RelativeHardwareValueMatcher stepUpMatcher = midiProcessor.getMidiIn().createRelativeValueMatcher( + "(status == 176 && data1 == %d && data2 == %d)".formatted(JOG_WHEEL_CC, behavior.getUpValue()), 1); + final RelativeHardwareValueMatcher stepDownMatcher = midiProcessor.getMidiIn().createRelativeValueMatcher( + "(status == 176 && data1 == %d && data2 == %d)".formatted(JOG_WHEEL_CC, behavior.getDownValue()), -1); + final RelativeHardwareValueMatcher matcher = + host.createOrRelativeHardwareValueMatcher(stepDownMatcher, stepUpMatcher); + jogWheelEncoder.setAdjustValueMatcher(matcher); + } + + public MidiProcessor getMidiProcessor() { + return midiProcessor; + } + + public Optional getTimeCodeLed() { + return Optional.ofNullable(timeCodeLed); + } + + public void bindJogWheel(final Layer layer, final IntConsumer value) { + layer.bind(jogWheelEncoder, createIncrementBinder(value)); + } + + private RelativeHardwarControlBindable createIncrementBinder(final IntConsumer consumer) { + return host.createRelativeHardwareControlStepTarget(// + host.createAction(() -> consumer.accept(1), () -> "+"), + host.createAction(() -> consumer.accept(-1), () -> "-")); + } + + public Optional getButton(final McuFunction assignment) { + return Optional.ofNullable(buttonMap.get(assignment)); + } + + public void clearAll() { + timeCodeLed.clearAll(); + buttonMap.forEach((key, value) -> value.clear(midiProcessor)); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/control/McuButton.java b/src/main/java/com/bitwig/extensions/controllers/mcu/control/McuButton.java new file mode 100644 index 00000000..66b66385 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/control/McuButton.java @@ -0,0 +1,233 @@ +package com.bitwig.extensions.controllers.mcu.control; + +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +import com.bitwig.extension.callback.BooleanValueChangedCallback; +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.OnOffHardwareLight; +import com.bitwig.extension.controller.api.SettableBooleanValue; +import com.bitwig.extensions.controllers.mcu.MidiProcessor; +import com.bitwig.extensions.controllers.mcu.TimedProcessor; +import com.bitwig.extensions.controllers.mcu.config.ButtonAssignment; +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; +import com.bitwig.extensions.framework.values.BooleanValueObject; +import com.bitwig.extensions.framework.values.Midi; + +public class McuButton { + private final HardwareButton hwButton; + private final OnOffHardwareLight light; + private final int noteNr; + private final int channel; + private TimedEvent currentTimer; + private final TimedProcessor timedProcessor; + private long recordedDownTime; + public static final int FAST_ACTION_TIME = 5; + public static final int STD_REPEAT_DELAY = 400; + public static final int STD_REPEAT_FREQUENCY = 50; + + public McuButton(final int noteNr, final String name, final HardwareSurface surface, + final MidiProcessor midiProcessor, final TimedProcessor timedProcessor) { + this.timedProcessor = timedProcessor; + this.channel = 0; + hwButton = surface.createHardwareButton("B_%s".formatted(name)); + midiProcessor.attachNoteOnOffMatcher(hwButton, 0, noteNr); + this.noteNr = noteNr; + light = surface.createOnOffHardwareLight("BL_%s".formatted(name)); + hwButton.setBackgroundLight(light); + light.onUpdateHardware( + () -> midiProcessor.sendLedLightStatus(noteNr, 0, light.isOn().currentValue() ? 127 : 0)); + } + + public McuButton(final ButtonAssignment assignment, final int subIndex, final HardwareSurface surface, + final MidiProcessor midiProcessor, final TimedProcessor timedProcessor) { + this.timedProcessor = timedProcessor; + this.channel = assignment.getChannel(); + //McuExtension.println(" Create button %s %d %d", assignment, assignment.getNoteNo(), assignment.getChannel()); + hwButton = surface.createHardwareButton("B%d_%s_".formatted(subIndex, assignment)); + midiProcessor.attachNoteOnOffMatcher(hwButton, assignment.getChannel(), assignment.getNoteNo()); + this.noteNr = assignment.getNoteNo(); + light = surface.createOnOffHardwareLight("BL%d_%s_".formatted(subIndex, assignment)); + hwButton.setBackgroundLight(light); + light.onUpdateHardware( + () -> midiProcessor.sendLedLightStatus(this.noteNr, this.channel, light.isOn().currentValue() ? 127 : 0)); + } + + public int getNoteNr() { + return noteNr; + } + + public void bindToggle(final Layer layer, final SettableBooleanValue value) { + layer.bind(hwButton, hwButton.pressedAction(), () -> value.toggle()); + layer.bind(value, light.isOn()); + } + + public void bindIsPressed(final Layer layer, final BooleanValueChangedCallback callback) { + layer.bind(hwButton, hwButton.pressedAction(), () -> callback.valueChanged(true)); + layer.bind(hwButton, hwButton.releasedAction(), () -> callback.valueChanged(false)); + } + + public void bindLight(final Layer layer, final BooleanValue value) { + value.markInterested(); + layer.bind(value, light.isOn()); + } + + public void bindLight(final Layer layer, final BooleanSupplier value) { + layer.bind(value, light.isOn()); + } + + public void bindMomentary(final Layer layer, final BooleanValueObject value) { + layer.bind(hwButton, hwButton.pressedAction(), () -> value.set(true)); + layer.bind(hwButton, hwButton.releasedAction(), () -> value.set(false)); + layer.bind(value, light.isOn()); + } + + public void bindMode(final Layer layer, final Consumer consumer, final BooleanSupplier ledState) { + layer.bind(hwButton, hwButton.pressedAction(), () -> consumer.accept(true)); + layer.bind(hwButton, hwButton.releasedAction(), () -> consumer.accept(false)); + layer.bind(ledState, light.isOn()); + } + + public void bindRelease(final Layer layer, final Runnable action) { + layer.bind(hwButton, hwButton.releasedAction(), () -> action.run()); + } + + public void bindPressedLight(final Layer layer, final Runnable action) { + bindPressed(layer, action); + bindHeldLight(layer); + } + + public void bindPressed(final Layer layer, final Runnable action) { + layer.bind(hwButton, hwButton.pressedAction(), () -> action.run()); + } + + public void bindHeldLight(final Layer layer) { + layer.bind(hwButton.isPressed(), light.isOn()); + } + + public void bindPressed(final Layer layer, final HardwareActionBindable action) { + layer.bind(hwButton, hwButton.pressedAction(), action); + } + + 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); + timedProcessor.queueEvent(currentTimer); + } + + private void cancelEvent() { + if (currentTimer != null) { + currentTimer.cancel(); + currentTimer = null; + } + } + + public void bindRepeatHold(final Layer layer, final IntConsumer 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 IntConsumer action, final int repeatDelay, final int repeatFrequency) { + action.accept(0); + currentTimer = new TimeRepeatEvent(action, repeatDelay, repeatFrequency); + timedProcessor.queueEvent(currentTimer); + } + + public void bindRepeatHold(final Layer layer, final IntConsumer action, final Runnable fastCommandAction) { + layer.bind(hwButton, hwButton.pressedAction(), + () -> initiateRepeatSeq(action, STD_REPEAT_DELAY, STD_REPEAT_FREQUENCY)); + layer.bind(hwButton, hwButton.releasedAction(), () -> handleSeqRelease(fastCommandAction)); + } + + public void initiateRepeatSeq(final IntConsumer action, final int repeatDelay, final int repeatFrequency) { + recordedDownTime = System.currentTimeMillis(); + timedProcessor.delayTask(() -> { + if (recordedDownTime != -1 && (System.currentTimeMillis() - recordedDownTime) > FAST_ACTION_TIME) { + recordedDownTime = -1; + action.accept(0); + } + }, FAST_ACTION_TIME); + currentTimer = new TimeRepeatEvent(action, repeatDelay, repeatFrequency); + timedProcessor.queueEvent(currentTimer); + } + + public void handleSeqRelease(final Runnable fastCommandAction) { + if (recordedDownTime != -1 && (System.currentTimeMillis() - recordedDownTime) < FAST_ACTION_TIME) { + fastCommandAction.run(); + recordedDownTime = -1; + } + this.cancelEvent(); + } + + public void bindDelayedAction(final Layer layer, final Runnable baseAction, final Runnable delayedAction, + final Runnable releaseAction, final int time) { + layer.bind(hwButton, hwButton.pressedAction(), () -> { + baseAction.run(); + currentTimer = new TimedDelayEvent(delayedAction, time); + timedProcessor.queueEvent(currentTimer); + }); + layer.bind(hwButton, hwButton.releasedAction(), () -> { + if (currentTimer != null && !currentTimer.isCompleted()) { + currentTimer.cancel(); + } else { + releaseAction.run(); + } + }); + } + + public void bindDelayedAction(final Layer layer, final Consumer baseAction, final Runnable delayedAction, + final Runnable releaseAction, final int time) { + layer.bind(hwButton, hwButton.pressedAction(), () -> { + baseAction.accept(true); + currentTimer = new TimedDelayEvent(delayedAction, time); + timedProcessor.queueEvent(currentTimer); + }); + layer.bind(hwButton, hwButton.releasedAction(), () -> { + baseAction.accept(false); + if (currentTimer != null && !currentTimer.isCompleted()) { + currentTimer.cancel(); + } else { + releaseAction.run(); + } + }); + } + + public void bindClickAltMenu(final Layer layer, final Runnable clickAction, final Consumer holdFunction) { + layer.bind(hwButton, hwButton.pressedAction(), () -> handleClickDown(holdFunction)); + layer.bind(hwButton, hwButton.releasedAction(), () -> handleClickUp(clickAction, holdFunction)); + } + + private void handleClickDown(final Consumer holdFunction) { + cancelEvent(); + currentTimer = new TimedDelayEvent(() -> holdFunction.accept(true), 400); + timedProcessor.queueEvent(currentTimer); + recordedDownTime = System.currentTimeMillis(); + } + + private void handleClickUp(final Runnable clickAction, final Consumer holdFunction) { + if (currentTimer instanceof TimedDelayEvent && !currentTimer.isCompleted()) { + cancelEvent(); + clickAction.run(); + } else { + holdFunction.accept(false); + } + } + + public void clear(final MidiProcessor midiProcessor) { + midiProcessor.sendMidi(Midi.NOTE_ON | channel, noteNr, 0); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/control/MixerSectionHardware.java b/src/main/java/com/bitwig/extensions/controllers/mcu/control/MixerSectionHardware.java new file mode 100644 index 00000000..ee1654ba --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/control/MixerSectionHardware.java @@ -0,0 +1,145 @@ +package com.bitwig.extensions.controllers.mcu.control; + +import java.util.Optional; + +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.MultiStateHardwareLight; +import com.bitwig.extensions.controllers.mcu.MidiProcessor; +import com.bitwig.extensions.controllers.mcu.TimedProcessor; +import com.bitwig.extensions.controllers.mcu.config.ControllerConfig; +import com.bitwig.extensions.controllers.mcu.config.McuAssignments; +import com.bitwig.extensions.controllers.mcu.display.ControllerDisplay; +import com.bitwig.extensions.controllers.mcu.value.TrackColor; +import com.bitwig.extensions.framework.di.Context; + +public class MixerSectionHardware { + private final MidiProcessor midiProcessor; + private final int index; + private final MotorSlider[] sliders = new MotorSlider[8]; + private final RingEncoder[] encoders = new RingEncoder[8]; + private final McuButton[] armButtons = new McuButton[8]; + private final McuButton[] soloButtons = new McuButton[8]; + private final McuButton[] muteButtons = new McuButton[8]; + private final McuButton[] selectButtons = new McuButton[8]; + private final MotorSlider masterFader; + private MultiStateHardwareLight backgroundColoring; + + public MixerSectionHardware(final int index, final Context context, final MidiProcessor midiProcessor, + final int offset) { + final HardwareSurface surface = context.getService(HardwareSurface.class); + final ControllerConfig config = context.getService(ControllerConfig.class); + final TimedProcessor timedProcessor = context.getService(TimedProcessor.class); + this.index = index; + this.midiProcessor = midiProcessor; + + for (int i = 0; i < 8; i++) { + sliders[i] = new MotorSlider(surface, midiProcessor, i); + sliders[i].addTouchAction(midiProcessor::handleTouch); + encoders[i] = new RingEncoder(surface, midiProcessor, i); + armButtons[i] = + new McuButton(McuAssignments.REC_BASE.getNoteNo() + i, "ARM_%d".formatted(i + 1 + offset), surface, + midiProcessor, timedProcessor); + soloButtons[i] = + new McuButton(McuAssignments.SOLO_BASE.getNoteNo() + i, "SOLO_%d".formatted(i + 1 + offset), surface, + midiProcessor, timedProcessor); + muteButtons[i] = + new McuButton(McuAssignments.MUTE_BASE.getNoteNo() + i, "MUTE_%d".formatted(i + 1 + offset), surface, + midiProcessor, timedProcessor); + selectButtons[i] = + new McuButton(McuAssignments.SELECT_BASE.getNoteNo() + i, "SELECT_%d".formatted(i + 1 + offset), + surface, midiProcessor, timedProcessor); + } + masterFader = config.getMasterFaderChannel() > 0 + ? new MotorSlider(surface, midiProcessor, config.getMasterFaderChannel()) + : null; + if (masterFader != null) { + masterFader.addTouchAction(midiProcessor::handleTouch); + } + + if (config.hasIconTrackColoring()) { + backgroundColoring = surface.createMultiStateHardwareLight("BACKGROUND_COLOR_" + "%d".formatted(index)); + backgroundColoring.state().onUpdateHardware(state -> { + if (state instanceof TrackColor color) { + midiProcessor.updateIconColors(color.getColors()); + } + }); + } + } + + public void clearAll() { + for (int i = 0; i < 8; i++) { + armButtons[i].clear(midiProcessor); + soloButtons[i].clear(midiProcessor); + muteButtons[i].clear(midiProcessor); + selectButtons[i].clear(midiProcessor); + } + } + + public McuButton getButtonFromGridBy2Lane(final int row, final int column) { + if (column < 8) { + return switch (row) { + case 0 -> soloButtons[column]; + case 1 -> muteButtons[column]; + default -> null; + }; + } + return null; + } + + public McuButton getButtonFromGridBy4Lane(final int row, final int column) { + if (column < 8) { + return switch (row) { + case 0 -> armButtons[column]; + case 1 -> soloButtons[column]; + case 2 -> muteButtons[column]; + case 3 -> selectButtons[column]; + default -> null; + }; + } + return null; + } + + + public Optional getBackgroundColoring() { + return Optional.ofNullable(backgroundColoring); + } + + + public int getIndex() { + return index; + } + + public ControllerDisplay getDisplay() { + return midiProcessor; + } + + public MotorSlider getSlider(final int index) { + return sliders[index]; + } + + public McuButton getArmButton(final int index) { + return armButtons[index]; + } + + public McuButton getSoloButton(final int index) { + return soloButtons[index]; + } + + public McuButton getSelectButton(final int index) { + return selectButtons[index]; + } + + public McuButton getMuteButton(final int index) { + return muteButtons[index]; + } + + public RingEncoder getRingEncoder(final int index) { + return encoders[index]; + } + + public Optional getMasterFader() { + return Optional.ofNullable(masterFader); + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/control/MotorSlider.java b/src/main/java/com/bitwig/extensions/controllers/mcu/control/MotorSlider.java new file mode 100644 index 00000000..499ccf4b --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/control/MotorSlider.java @@ -0,0 +1,65 @@ +package com.bitwig.extensions.controllers.mcu.control; + +import com.bitwig.extension.callback.BooleanValueChangedCallback; +import com.bitwig.extension.controller.api.HardwareButton; +import com.bitwig.extension.controller.api.HardwareSlider; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extensions.controllers.mcu.MidiProcessor; +import com.bitwig.extensions.controllers.mcu.bindings.FaderBinding; +import com.bitwig.extensions.controllers.mcu.bindings.paramslots.FaderSlotBinding; +import com.bitwig.extensions.controllers.mcu.bindings.paramslots.ResetableAbsoluteValueSlotBinding; +import com.bitwig.extensions.controllers.mcu.config.McuAssignments; +import com.bitwig.extensions.controllers.mcu.devices.ParamPageSlot; +import com.bitwig.extensions.framework.AbsoluteHardwareControlBinding; +import com.bitwig.extensions.framework.Layer; + +public class MotorSlider { + + private final HardwareSlider fader; + private final FaderResponse response; + private final HardwareButton touchButton; + + public MotorSlider(final HardwareSurface surface, final MidiProcessor midiProcessor, final int channel) { + fader = surface.createHardwareSlider("FADER_%d_%d".formatted(midiProcessor.getPortIndex(), channel)); + midiProcessor.attachPitchBendSliderValue(fader, channel); + + response = new FaderResponse(midiProcessor, channel); + touchButton = surface.createHardwareButton( + "FADER_TOUCH_%d_%d".formatted(midiProcessor.getPortIndex(), channel)); + int touchNote = McuAssignments.TOUCH_VOLUME.getNoteNo() + channel; + midiProcessor.attachNoteOnOffMatcher(touchButton, 0, touchNote); + fader.setHardwareButton(touchButton); + } + + public void addTouchAction(BooleanValueChangedCallback touchAction) { + touchButton.isPressed().addValueObserver(touchAction); + } + + public void bindParameter(final Layer layer, final ParamPageSlot parameter) { + layer.addBinding(new FaderSlotBinding(parameter, response)); + layer.addBinding(new ResetableAbsoluteValueSlotBinding(fader, parameter)); + layer.bind(touchButton, touchButton.pressedAction(), () -> parameter.touch(true)); + layer.bind(touchButton, touchButton.releasedAction(), () -> parameter.touch(false)); + } + + public void bindParameter(final Layer layer, final Parameter parameter) { + layer.addBinding(new FaderBinding(parameter, response)); + layer.addBinding(new AbsoluteHardwareControlBinding(fader, parameter)); + layer.bind(touchButton, touchButton.pressedAction(), () -> parameter.touch(true)); + layer.bind(touchButton, touchButton.releasedAction(), () -> parameter.touch(false)); + } + + public ResetableAbsoluteValueSlotBinding createSlotBinding(ParamPageSlot paramPageSlot) { + return new ResetableAbsoluteValueSlotBinding(fader, paramPageSlot); + } + + public HardwareSlider getFader() { + return fader; + } + + public void sendValue(final int value) { + response.sendValue(0); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/control/RingDisplay.java b/src/main/java/com/bitwig/extensions/controllers/mcu/control/RingDisplay.java new file mode 100644 index 00000000..27df527b --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/control/RingDisplay.java @@ -0,0 +1,36 @@ +package com.bitwig.extensions.controllers.mcu.control; + +import com.bitwig.extensions.controllers.mcu.MidiProcessor; +import com.bitwig.extensions.framework.values.Midi; + +public class RingDisplay { + private final MidiProcessor midi; + private final int index; + private int lastValue = -1; + + public RingDisplay(final MidiProcessor midi, final int index) { + this.index = index; + this.midi = midi; + } + + public int getIndex() { + return index; + } + + public void sendValue(final int value, final boolean showDot) { + final int newValue = value | (showDot ? 0x40 : 0x00); + if (newValue != lastValue) { + midi.sendMidi(Midi.CC, 0x30 | index, newValue); + lastValue = value; + } + } + + public void refresh() { + midi.sendMidi(Midi.CC, 0x30 | index, lastValue); + } + + public void clear() { + midi.sendMidi(Midi.CC, 0x30 | index, 0); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/control/RingDisplayType.java b/src/main/java/com/bitwig/extensions/controllers/mcu/control/RingDisplayType.java new file mode 100644 index 00000000..76db6719 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/control/RingDisplayType.java @@ -0,0 +1,24 @@ +package com.bitwig.extensions.controllers.mcu.control; + +public enum RingDisplayType { + PAN_FILL(17, 10), // + FILL_LR(33, 10), // + SINGLE(1, 10), // + CENTER_FILL(49, 10), // + FILL_LR_0(32, 11); // + private final int offset; + private final int range; + + RingDisplayType(final int offset, final int range) { + this.offset = offset; + this.range = range; + } + + public int getOffset() { + return offset; + } + + public int getRange() { + return range; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/control/RingEncoder.java b/src/main/java/com/bitwig/extensions/controllers/mcu/control/RingEncoder.java new file mode 100644 index 00000000..0dfe004b --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/control/RingEncoder.java @@ -0,0 +1,176 @@ +package com.bitwig.extensions.controllers.mcu.control; + +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +import com.bitwig.extension.controller.api.BooleanValue; +import com.bitwig.extension.controller.api.HardwareButton; +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.SettableBooleanValue; +import com.bitwig.extension.controller.api.SettableRangedValue; +import com.bitwig.extensions.controllers.mcu.MidiProcessor; +import com.bitwig.extensions.controllers.mcu.bindings.ButtonBinding; +import com.bitwig.extensions.controllers.mcu.bindings.RingDisplayDisabledBinding; +import com.bitwig.extensions.controllers.mcu.bindings.RingDisplayIntValueBinding; +import com.bitwig.extensions.controllers.mcu.bindings.RingDisplayParameterBinding; +import com.bitwig.extensions.controllers.mcu.bindings.RingDisplayParameterBoolBinding; +import com.bitwig.extensions.controllers.mcu.bindings.RingDisplayValueBinding; +import com.bitwig.extensions.controllers.mcu.bindings.paramslots.ResetableRelativeSlotBinding; +import com.bitwig.extensions.controllers.mcu.bindings.paramslots.RingParameterDisplaySlotBinding; +import com.bitwig.extensions.controllers.mcu.config.McuAssignments; +import com.bitwig.extensions.controllers.mcu.devices.ParamPageSlot; +import com.bitwig.extensions.controllers.mcu.value.IncrementHolder; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.RelativeHardwareControlToRangedValueBinding; +import com.bitwig.extensions.framework.values.IntValueObject; + +public class RingEncoder { + private final RelativeHardwareKnob encoder; + private final HardwareButton encoderPress; + private final RingDisplay ringDisplay; + private final RelativeHardwareValueMatcher nonAcceleratedMatchers; + private final RelativeHardwareValueMatcher acceleratedMatchers; + private final EncoderMode encoderMode = EncoderMode.ACCELERATED; + private final MidiProcessor midiProcessor; + + public RingEncoder(final HardwareSurface surface, final MidiProcessor midiProcessor, final int index) { + this.midiProcessor = midiProcessor; + encoder = surface.createRelativeHardwareKnob("PAN_KNOB" + midiProcessor.getPortIndex() + "_" + index); + encoderPress = surface.createHardwareButton("ENCODER_PRESS_" + midiProcessor.getPortIndex() + "_" + index); + midiProcessor.attachNoteOnOffMatcher(encoderPress, 0, McuAssignments.ENC_PRESS_BASE.getNoteNo() + index); + ringDisplay = new RingDisplay(midiProcessor, index); + encoder.setHardwareButton(encoderPress); + nonAcceleratedMatchers = midiProcessor.createNonAcceleratedMatcher(0x10 + index); + acceleratedMatchers = midiProcessor.createAcceleratedMatcher(0x10 + index); + setEncoderBehavior(encoderMode, 128); + } + + 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.NONACCELERATED) { + encoder.setAdjustValueMatcher(nonAcceleratedMatchers); + encoder.setStepSize(1); + } + } + + public void bindParameter(final Layer layer, final ParamPageSlot slot) { + layer.addBinding(new ResetableRelativeSlotBinding(encoder, slot, 1.0)); + layer.addBinding(createDisplayBinding(slot)); + layer.addBinding(new ButtonBinding(encoderPress, midiProcessor.createAction(slot::parameterReset))); + } + + public RingParameterDisplaySlotBinding createDisplayBinding(final ParamPageSlot slot) { + return new RingParameterDisplaySlotBinding(slot, ringDisplay); + } + + public void bindIncrement(final Layer layer, final IntConsumer consumer, final double incrementMultiplier) { + final IncrementHolder incHolder = new IncrementHolder(consumer, incrementMultiplier); + layer.bind(encoder, v -> incHolder.increment(v)); + } + + public void bindIncrement(final Layer layer, final SettableBooleanValue value) { + final IncrementHolder incHolder = new IncrementHolder(v -> value.set(v > 0), 0.1); + layer.bind(encoder, v -> incHolder.increment(v)); + } + + public void bindIsPressed(final Layer layer, final Consumer pressAction) { + layer.bind(encoderPress, encoderPress.pressedAction(), () -> pressAction.accept(true)); + layer.bind(encoderPress, encoderPress.releasedAction(), () -> pressAction.accept(false)); + } + + public void bindIsPressed(final Layer layer, final SettableBooleanValue pressValue) { + layer.bind(encoderPress, encoderPress.pressedAction(), () -> pressValue.set(true)); + layer.bind(encoderPress, encoderPress.releasedAction(), () -> pressValue.set(false)); + } + + public void bindPressed(final Layer layer, final Runnable pressAction) { + layer.bind(encoderPress, encoderPress.pressedAction(), pressAction); + } + + public void bindRingValue(final Layer layer, final Parameter parameter, final RingDisplayType type) { + layer.addBinding(new RingDisplayParameterBinding(parameter, ringDisplay, type)); + } + + public void bindRingValue(final Layer layer, final BooleanValue value) { + layer.addBinding(new RingDisplayParameterBoolBinding(value, ringDisplay)); + } + + public void bindRingValue(final Layer layer, final IntValueObject value) { + layer.addBinding(new RingDisplayIntValueBinding(value, ringDisplay)); + } + + public void bindEmpty(final Layer layer) { + layer.addBinding(new RingDisplayDisabledBinding(ringDisplay, RingDisplayType.FILL_LR)); + layer.bind(encoder, v -> { + }); + layer.bindPressed(encoderPress, () -> { + }); + } + + public void bindRingEmpty(final Layer layer) { + layer.addBinding(new RingDisplayDisabledBinding(ringDisplay, RingDisplayType.FILL_LR)); + } + + public void bindRingPressed(final Layer layer) { + layer.addBinding(new RingDisplayParameterBoolBinding(encoderPress.isPressed(), ringDisplay)); + } + + public void bindValue(final Layer layer, final SettableRangedValue value, final RingDisplayType type) { + layer.addBinding(createEncoderToParamBinding(value)); + layer.addBinding(new RingDisplayValueBinding(value, ringDisplay, type)); + } + + private RelativeHardwareControlToRangedValueBinding createEncoderToParamBinding(final SettableRangedValue param) { + final RelativeHardwareControlToRangedValueBinding binding = + new RelativeHardwareControlToRangedValueBinding(encoder, param); + if (midiProcessor.isHas2ClickResolution()) { + binding.setSensitivity(2.0); + } + return binding; + } + + public void bindValue(final Layer layer, final SettableRangedValue value, final RingDisplayType type, + final double sensitivity) { + layer.addBinding(createEncoderToParamBinding(value, sensitivity)); + layer.addBinding(new RingDisplayValueBinding(value, ringDisplay, type)); + } + + private RelativeHardwareControlToRangedValueBinding createEncoderToParamBinding(final SettableRangedValue param, + final double sensitivity) { + final RelativeHardwareControlToRangedValueBinding binding = + new RelativeHardwareControlToRangedValueBinding(encoder, param); + // TODO CHECK 2click encoders + binding.setSensitivity(sensitivity); + return binding; + } + + public void bindParameter(final Layer layer, final Parameter parameter, final RingDisplayType type) { + layer.addBinding(createEncoderToParamBinding(parameter)); + layer.addBinding(new ButtonBinding(encoderPress, midiProcessor.createAction(parameter::reset))); + layer.addBinding(new RingDisplayParameterBinding(parameter, ringDisplay, type)); + } + + private RelativeHardwareControlToRangedValueBinding createEncoderToParamBinding(final Parameter param) { + final RelativeHardwareControlToRangedValueBinding binding = + new RelativeHardwareControlToRangedValueBinding(encoder, param); + if (midiProcessor.isHas2ClickResolution()) { + binding.setSensitivity(2.0); + } + return binding; + } + + public RelativeHardwareKnob getEncoder() { + return encoder; + } + + + public void bind(final Layer layer, final RelativeHardwarControlBindable bindable) { + layer.bind(encoder, bindable); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/AbstractMcuControllerExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/AbstractMcuControllerExtensionDefinition.java new file mode 100644 index 00000000..17ff4e8f --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/AbstractMcuControllerExtensionDefinition.java @@ -0,0 +1,61 @@ +package com.bitwig.extensions.controllers.mcu.definitions; + +import java.util.List; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; +import com.bitwig.extension.controller.ControllerExtensionDefinition; + +public abstract class AbstractMcuControllerExtensionDefinition extends ControllerExtensionDefinition { + protected static final int MCU_API_VERSION = 18; + protected static final String SOFTWARE_VERSION = "0.1"; + protected int nrOfExtenders; + + public AbstractMcuControllerExtensionDefinition(final int nrOfExtenders) { + super(); + this.nrOfExtenders = nrOfExtenders; + } + + @Override + public String getAuthor() { + return "Bitwig"; + } + + public static String getSoftwareVersion() { + return SOFTWARE_VERSION; + } + + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) { + final List inPorts = getInPorts(platformType); + final List outPorts = getOutPorts(platformType); + + for (int i = 0; i < inPorts.size(); i++) { + list.add(inPorts.get(i), outPorts.get(i)); + } + } + + protected abstract List getInPorts(final PlatformType platformType); + + protected abstract List getOutPorts(final PlatformType platformType); + + @Override + public int getRequiredAPIVersion() { + return MCU_API_VERSION; + } + + @Override + public int getNumMidiInPorts() { + return nrOfExtenders + 1; + } + + @Override + public int getNumMidiOutPorts() { + return nrOfExtenders + 1; + } + + public int getNrOfExtenders() { + return nrOfExtenders; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/ManufacturerType.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/ManufacturerType.java new file mode 100644 index 00000000..aabb175f --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/ManufacturerType.java @@ -0,0 +1,8 @@ +package com.bitwig.extensions.controllers.mcu.definitions; + +public enum ManufacturerType { + MACKIE, + ICON, + BEHRINGER, + SSL +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/SubType.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/SubType.java new file mode 100644 index 00000000..02395f29 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/SubType.java @@ -0,0 +1,11 @@ +package com.bitwig.extensions.controllers.mcu.definitions; + +public enum SubType { + UNSPECIFIED, + PRO_X, + G2, + V1M, + P1M, + UF8, + M_PLUS +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/AbstractIconExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/AbstractIconExtensionDefinition.java new file mode 100644 index 00000000..b0465b75 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/AbstractIconExtensionDefinition.java @@ -0,0 +1,155 @@ +package com.bitwig.extensions.controllers.mcu.definitions.icon; + +import java.util.ArrayList; +import java.util.List; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extensions.controllers.mcu.McuExtension; +import com.bitwig.extensions.controllers.mcu.config.ControllerConfig; +import com.bitwig.extensions.controllers.mcu.config.CustomAssignment; +import com.bitwig.extensions.controllers.mcu.config.McuAssignments; +import com.bitwig.extensions.controllers.mcu.config.McuFunction; +import com.bitwig.extensions.controllers.mcu.definitions.AbstractMcuControllerExtensionDefinition; + +public abstract class AbstractIconExtensionDefinition extends AbstractMcuControllerExtensionDefinition { + private static final String[] PORT_VARIANTS = new String[] {"Anschluss", "Port"}; + protected static final String SOFTWARE_VERSION = "1.0"; + + public AbstractIconExtensionDefinition() { + this(0); + } + + public AbstractIconExtensionDefinition(final int nrOfExtenders) { + super(nrOfExtenders); + } + + private String getInPortName(final PlatformType platformType, final String baseDevicePort, + final String portNameVariant, final int port) { + return switch (platformType) { + case WINDOWS -> port == 1 + ? baseDevicePort + getFiller() + : "MIDIIN%d (%s%s)".formatted(port, baseDevicePort, getFiller()); + case LINUX -> "%s MIDI %d".formatted(baseDevicePort, port); + case MAC -> "%s %s %d".formatted(baseDevicePort, portNameVariant, port); + }; + } + + private String getOutPortName(final PlatformType platformType, final String baseDevicePort, + final String portNameVariant, final int port) { + return switch (platformType) { + case WINDOWS -> port == 1 + ? baseDevicePort + getFiller() + : "MIDIOUT%d (%s%s)".formatted(port, baseDevicePort, getFiller()); + case LINUX -> "%s MIDI %d".formatted(baseDevicePort, port); + case MAC -> "%s %s %d".formatted(baseDevicePort, portNameVariant, port); + }; + } + + protected List getInPorts(final PlatformType platformType, final String baseModel, + final String[] versions) { + final List portList = new ArrayList<>(); + final String[] portVariants = getPortVariants(platformType); + for (final String portVariant : portVariants) { + for (final String version : versions) { + final String base = baseModel.formatted(version); + portList.add(getInPorts(platformType, base, portVariant)); + } + } + return portList; + } + + protected List getOutPorts(final PlatformType platformType, final String baseModel, + final String[] versions) { + final List portList = new ArrayList<>(); + + final String[] portVariants = getPortVariants(platformType); + for (final String portVariant : portVariants) { + for (final String version : versions) { + final String base = baseModel.formatted(version); + portList.add(getOutPorts(platformType, base, portVariant)); + } + } + return portList; + } + + protected String[] getInPorts(final PlatformType platformType, final String baseDevicePort, + final String portNameVariant) { + final String[] inPortNames = new String[nrOfExtenders + 1]; + for (int i = 0; i < nrOfExtenders + 1; i++) { + inPortNames[i] = getInPortName(platformType, baseDevicePort, portNameVariant, i + 1); + } + return inPortNames; + } + + protected String[] getOutPorts(final PlatformType platformType, final String baseDevicePort, + final String portNameVariant) { + final String[] outPortNames = new String[nrOfExtenders + 1]; + for (int i = 0; i < nrOfExtenders + 1; i++) { + outPortNames[i] = getOutPortName(platformType, baseDevicePort, portNameVariant, i + 1); + } + return outPortNames; + } + + @Override + protected List getInPorts(final PlatformType platformType) { + return getInPorts(platformType, getBasePortName(), getSupportedVersions()); + } + + @Override + protected List getOutPorts(final PlatformType platformType) { + return getOutPorts(platformType, getBasePortName(), getSupportedVersions()); + } + + protected String getFiller() { + return ""; + } + + protected abstract String getBasePortName(); + + protected abstract String[] getSupportedVersions(); + + protected String[] getPortVariants(final PlatformType platformType) { + return platformType == PlatformType.MAC ? PORT_VARIANTS : new String[] {""}; + } + + protected abstract ControllerConfig createControllerConfig(); + + public McuExtension createInstance(final ControllerHost host) { + final ControllerConfig controllerConfig = createControllerConfig(); + controllerConfig.setAssignment(McuFunction.CUE_MARKER, McuAssignments.SAVE); + controllerConfig.setAssignment(McuFunction.TEMPO, McuAssignments.GV_AUDIO_LF3); + controllerConfig.setAssignment(McuFunction.GROOVE_MENU, McuAssignments.GV_AUX_LF5); + controllerConfig.setAssignment(McuFunction.ZOOM_MENU, McuAssignments.GV_INSTRUMENT_LF4); + controllerConfig.setAssignment(McuFunction.AUTOMATION_LAUNCHER, McuAssignments.F2); + controllerConfig.setAssignment(McuFunction.TRACK_MODE, McuAssignments.V_TRACK); + controllerConfig.setAssignment(McuFunction.MODE_DEVICE, McuAssignments.V_PLUGIN); + controllerConfig.setAssignment(McuFunction.MODE_PROJECT_REMOTE, McuAssignments.NUDGE); + controllerConfig.setAssignment(McuFunction.MODE_TRACK_REMOTE, McuAssignments.V_INSTRUMENT); + controllerConfig.setAssignment(McuFunction.UNDO, McuAssignments.UNDO); + controllerConfig.setAssignment(McuFunction.CLIP_LAUNCHER_MODE_4, McuAssignments.GROUP); + controllerConfig.setAssignment( + McuFunction.RESTORE_AUTOMATION, McuAssignments.SOLO); //McuAssignments.AUTO_READ_OFF + controllerConfig.setAssignment(McuFunction.ARRANGER, McuAssignments.F1); + controllerConfig.setAssignment(McuFunction.DUPLICATE, new CustomAssignment(McuFunction.DUPLICATE, 54, 1)); + controllerConfig.setAssignment(McuFunction.CLEAR, new CustomAssignment(McuFunction.CLEAR, 55, 1)); + //initSimulationLayout(controllerConfig.getSimulationLayout()); + return new McuExtension(this, host, controllerConfig); + } + + @Override + public String getSupportFolderPath() { + return "Controllers/iCon/mappings"; + } + + @Override + public String getVersion() { + return SOFTWARE_VERSION; + } + + @Override + public String getHardwareVendor() { + return "iCON"; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1NanoExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1NanoExtensionDefinition.java new file mode 100644 index 00000000..9f14f2a2 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1NanoExtensionDefinition.java @@ -0,0 +1,77 @@ +package com.bitwig.extensions.controllers.mcu.definitions.icon; + +import java.util.UUID; + +import com.bitwig.extensions.controllers.mcu.config.ControllerConfig; +import com.bitwig.extensions.controllers.mcu.config.EncoderBehavior; +import com.bitwig.extensions.controllers.mcu.definitions.ManufacturerType; +import com.bitwig.extensions.controllers.mcu.definitions.SubType; + +public class IconP1NanoExtensionDefinition extends AbstractIconExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("36b7888b-28fb-4cce-b335-62b832449663"); + private static final String DEVICE_NAME = "iCON P1-Nano"; + protected static final String BASE_DEVICE_PORT = "iCON P1-Nano %s"; // + protected static final String[] VERSIONS = {"V1.21", "V1.22"}; + + public IconP1NanoExtensionDefinition() { + this(0); + } + + public IconP1NanoExtensionDefinition(final int nrOfExtenders) { + super(nrOfExtenders); + } + + @Override + protected String[] getSupportedVersions() { + return VERSIONS; + } + + @Override + protected String getBasePortName() { + return BASE_DEVICE_PORT; + } + + @Override + public String getHardwareModel() { + return "P1-Nano"; + } + + + @Override + public String getHelpFilePath() { + return "Controllers/iCon/P1-Nano.pdf"; + } + + @Override + protected ControllerConfig createControllerConfig() { + return new ControllerConfig(ManufacturerType.ICON, SubType.V1M) // + .setHasDedicateVu(true)// + .setHasLowerDisplay(true) // + .setHasIconTrackColoring(true) // + .setHas2ClickResolution(true) // + .setHasMasterFader(0x8)// + .setHasTimeCodeLed(true) // + .setJogWheelCoding(EncoderBehavior.ACCEL) // + .setDecelerateJogWheel(true) // + .setDisplaySegmented(true) // + .setHasMasterVu(true); + } + + @Override + public String getName() { + if (nrOfExtenders == 0) { + return IconP1NanoExtensionDefinition.DEVICE_NAME; + } + return String.format("%s +%d EXTENDER", IconP1NanoExtensionDefinition.DEVICE_NAME, nrOfExtenders); + } + + @Override + public String getVersion() { + return SOFTWARE_VERSION; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1NanoPlus1ExtenderExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1NanoPlus1ExtenderExtensionDefinition.java new file mode 100644 index 00000000..b76dec4f --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1NanoPlus1ExtenderExtensionDefinition.java @@ -0,0 +1,20 @@ +package com.bitwig.extensions.controllers.mcu.definitions.icon; + +import java.util.UUID; + +public class IconP1NanoPlus1ExtenderExtensionDefinition extends IconP1NanoExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("3fb9dd44-f934-41e4-b5c2-83842d1cf526"); + + public IconP1NanoPlus1ExtenderExtensionDefinition() { + this(1); + } + + public IconP1NanoPlus1ExtenderExtensionDefinition(final int nrOfExtenders) { + super(nrOfExtenders); + } + + @Override + public UUID getId() { + return DRIVER_ID; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1NanoPlus2ExtenderExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1NanoPlus2ExtenderExtensionDefinition.java new file mode 100644 index 00000000..b9f36b5a --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1NanoPlus2ExtenderExtensionDefinition.java @@ -0,0 +1,20 @@ +package com.bitwig.extensions.controllers.mcu.definitions.icon; + +import java.util.UUID; + +public class IconP1NanoPlus2ExtenderExtensionDefinition extends IconP1NanoExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("6208f5aa-017c-4d60-8635-1d1c4a04478a"); + + public IconP1NanoPlus2ExtenderExtensionDefinition() { + this(2); + } + + public IconP1NanoPlus2ExtenderExtensionDefinition(final int nrOfExtenders) { + super(nrOfExtenders); + } + + @Override + public UUID getId() { + return DRIVER_ID; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1NanoPlus3ExtenderExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1NanoPlus3ExtenderExtensionDefinition.java new file mode 100644 index 00000000..9c5e753e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1NanoPlus3ExtenderExtensionDefinition.java @@ -0,0 +1,20 @@ +package com.bitwig.extensions.controllers.mcu.definitions.icon; + +import java.util.UUID; + +public class IconP1NanoPlus3ExtenderExtensionDefinition extends IconP1NanoExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("a0b39436-f6b6-4662-b535-85de86626ad4"); + + public IconP1NanoPlus3ExtenderExtensionDefinition() { + this(3); + } + + public IconP1NanoPlus3ExtenderExtensionDefinition(final int nrOfExtenders) { + super(nrOfExtenders); + } + + @Override + public UUID getId() { + return DRIVER_ID; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1mExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1mExtensionDefinition.java new file mode 100644 index 00000000..d5f10802 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1mExtensionDefinition.java @@ -0,0 +1,81 @@ +package com.bitwig.extensions.controllers.mcu.definitions.icon; + +import java.util.UUID; + +import com.bitwig.extensions.controllers.mcu.config.ControllerConfig; +import com.bitwig.extensions.controllers.mcu.config.EncoderBehavior; +import com.bitwig.extensions.controllers.mcu.definitions.ManufacturerType; +import com.bitwig.extensions.controllers.mcu.definitions.SubType; +import com.bitwig.extensions.controllers.mcu.display.TimeCodeLed; + +public class IconP1mExtensionDefinition extends AbstractIconExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("e57683e3-3ff5-4f2b-bee3-6cade432a685"); + private static final String DEVICE_NAME = "iCON P1-M"; + protected static final String BASE_DEVICE_PORT = "iCON P1-M %s"; //iCON P1-Nano V1.19 + protected static final String[] VERSIONS = {"V1.06", "V1.07"}; + + public IconP1mExtensionDefinition() { + this(0); + } + + public IconP1mExtensionDefinition(final int nrOfExtenders) { + super(nrOfExtenders); + } + + @Override + public String getHardwareModel() { + return "P1-M"; + } + + @Override + protected String[] getSupportedVersions() { + return VERSIONS; + } + + @Override + protected String getBasePortName() { + return BASE_DEVICE_PORT; + } + + @Override + public String getHelpFilePath() { + return "Controllers/iCon/P1-M.pdf"; + } + + @Override + protected ControllerConfig createControllerConfig() { + final ControllerConfig config = new ControllerConfig(ManufacturerType.ICON, SubType.P1M) // + .setHasDedicateVu(true)// + .setHasLowerDisplay(true) // + .setHasMasterFader(0x8) // + .setHasIconTrackColoring(true) // + .setHas2ClickResolution(true) // + .setHasTimeCodeLed(true) // + .setDisplaySegmented(true) // + .setTopDisplayRowsFlipped(true) // + .setNavigationWithJogWheel(true) // + .setJogWheelCoding(EncoderBehavior.ACCEL) // + .setDecelerateJogWheel(true) // + .setHasMasterVu(true); + config.setDisplayType(TimeCodeLed.DisplayType.ICON); + return config; + } + + @Override + public String getName() { + if (nrOfExtenders == 0) { + return IconP1mExtensionDefinition.DEVICE_NAME + getFiller(); + } + return String.format("%s +%d EXTENDER", IconP1mExtensionDefinition.DEVICE_NAME, nrOfExtenders); + } + + @Override + protected String getFiller() { + return " "; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1mPlus1ExtenderExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1mPlus1ExtenderExtensionDefinition.java new file mode 100644 index 00000000..9284f037 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1mPlus1ExtenderExtensionDefinition.java @@ -0,0 +1,20 @@ +package com.bitwig.extensions.controllers.mcu.definitions.icon; + +import java.util.UUID; + +public class IconP1mPlus1ExtenderExtensionDefinition extends IconP1mExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("f7166b22-b3de-4c8f-8173-06e0995f102e"); + + public IconP1mPlus1ExtenderExtensionDefinition() { + this(1); + } + + public IconP1mPlus1ExtenderExtensionDefinition(final int nrOfExtenders) { + super(nrOfExtenders); + } + + @Override + public UUID getId() { + return DRIVER_ID; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1mPlus2ExtenderExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1mPlus2ExtenderExtensionDefinition.java new file mode 100644 index 00000000..b5896c1f --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1mPlus2ExtenderExtensionDefinition.java @@ -0,0 +1,20 @@ +package com.bitwig.extensions.controllers.mcu.definitions.icon; + +import java.util.UUID; + +public class IconP1mPlus2ExtenderExtensionDefinition extends IconP1mExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("aa9bdf05-be3f-45f1-b9d2-4ec2016c2851"); + + public IconP1mPlus2ExtenderExtensionDefinition() { + this(2); + } + + public IconP1mPlus2ExtenderExtensionDefinition(final int nrOfExtenders) { + super(nrOfExtenders); + } + + @Override + public UUID getId() { + return DRIVER_ID; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1mPlus3ExtenderExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1mPlus3ExtenderExtensionDefinition.java new file mode 100644 index 00000000..2c4ca80c --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconP1mPlus3ExtenderExtensionDefinition.java @@ -0,0 +1,20 @@ +package com.bitwig.extensions.controllers.mcu.definitions.icon; + +import java.util.UUID; + +public class IconP1mPlus3ExtenderExtensionDefinition extends IconP1mExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("358db9a4-b7e7-43f4-8a3d-76590c3a28e3"); + + public IconP1mPlus3ExtenderExtensionDefinition() { + this(3); + } + + public IconP1mPlus3ExtenderExtensionDefinition(final int nrOfExtenders) { + super(nrOfExtenders); + } + + @Override + public UUID getId() { + return DRIVER_ID; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconV1mExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconV1mExtensionDefinition.java new file mode 100644 index 00000000..dda38b74 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconV1mExtensionDefinition.java @@ -0,0 +1,77 @@ +package com.bitwig.extensions.controllers.mcu.definitions.icon; + +import java.util.UUID; + +import com.bitwig.extensions.controllers.mcu.config.ControllerConfig; +import com.bitwig.extensions.controllers.mcu.config.EncoderBehavior; +import com.bitwig.extensions.controllers.mcu.definitions.ManufacturerType; +import com.bitwig.extensions.controllers.mcu.definitions.SubType; + +public class IconV1mExtensionDefinition extends AbstractIconExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("ee3903ba-e348-40fc-9e52-45e28fb11703"); + private static final String DEVICE_NAME = "iCON V1-M"; + protected static final String BASE_DEVICE_PORT = "iCON V1-M %s"; + protected static final String[] VERSIONS = {"V1.16", "V1.17", "V1.18"}; + + public IconV1mExtensionDefinition() { + this(0); + } + + public IconV1mExtensionDefinition(final int nrOfExtenders) { + super(nrOfExtenders); + } + + + @Override + protected String[] getSupportedVersions() { + return VERSIONS; + } + + @Override + protected String getBasePortName() { + return BASE_DEVICE_PORT; + } + + @Override + public String getHardwareModel() { + return "V1-M"; + } + + @Override + protected ControllerConfig createControllerConfig() { + return new ControllerConfig(ManufacturerType.ICON, SubType.V1M) // + .setHasDedicateVu(true)// + .setHasLowerDisplay(true) // + .setHasMasterFader(0x8) // + .setHasIconTrackColoring(true) // + .setHas2ClickResolution(true) // + .setHasTimeCodeLed(true) // + .setDisplaySegmented(true) // + .setTopDisplayRowsFlipped(true) // + .setNavigationWithJogWheel(true) // + .setJogWheelCoding(EncoderBehavior.ACCEL) // + .setDecelerateJogWheel(true) // + .setForceUpdateOnStartup(5000) // + .setHasMasterVu(true); + } + + + @Override + public String getHelpFilePath() { + return "Controllers/iCon/V1-M.pdf"; + } + + + @Override + public String getName() { + if (nrOfExtenders == 0) { + return IconV1mExtensionDefinition.DEVICE_NAME; + } + return String.format("%s +%d EXTENDER", IconV1mExtensionDefinition.DEVICE_NAME, nrOfExtenders); + } + + @Override + public UUID getId() { + return DRIVER_ID; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconV1mPlus1ExtenderExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconV1mPlus1ExtenderExtensionDefinition.java new file mode 100644 index 00000000..2e748e63 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconV1mPlus1ExtenderExtensionDefinition.java @@ -0,0 +1,17 @@ +package com.bitwig.extensions.controllers.mcu.definitions.icon; + +import java.util.UUID; + +public class IconV1mPlus1ExtenderExtensionDefinition extends IconV1mExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("db102763-745b-476a-a6ee-f099c3ad1d5e"); + + public IconV1mPlus1ExtenderExtensionDefinition() { + super(1); + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconV1mPlus2ExtenderExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconV1mPlus2ExtenderExtensionDefinition.java new file mode 100644 index 00000000..c469e64e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconV1mPlus2ExtenderExtensionDefinition.java @@ -0,0 +1,17 @@ +package com.bitwig.extensions.controllers.mcu.definitions.icon; + +import java.util.UUID; + +public class IconV1mPlus2ExtenderExtensionDefinition extends IconV1mExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("ffc8800a-2801-45e0-80cb-928684d83cc9"); + + public IconV1mPlus2ExtenderExtensionDefinition() { + super(2); + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconV1mPlus3ExtenderExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconV1mPlus3ExtenderExtensionDefinition.java new file mode 100644 index 00000000..75fbf8d9 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/icon/IconV1mPlus3ExtenderExtensionDefinition.java @@ -0,0 +1,17 @@ +package com.bitwig.extensions.controllers.mcu.definitions.icon; + +import java.util.UUID; + +public class IconV1mPlus3ExtenderExtensionDefinition extends IconV1mExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("defe335e-5828-4c9c-a2f7-8824308c9d7a"); + + public IconV1mPlus3ExtenderExtensionDefinition() { + super(3); + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/mackie/MackieMcuPro1ExtenderExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/mackie/MackieMcuPro1ExtenderExtensionDefinition.java new file mode 100644 index 00000000..63e5b672 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/mackie/MackieMcuPro1ExtenderExtensionDefinition.java @@ -0,0 +1,17 @@ +package com.bitwig.extensions.controllers.mcu.definitions.mackie; + +import java.util.UUID; + +public class MackieMcuPro1ExtenderExtensionDefinition extends MackieMcuProExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("42133b66-3870-4e4c-a833-f3b1dd7389f4"); + + public MackieMcuPro1ExtenderExtensionDefinition() { + super(1); + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/mackie/MackieMcuProExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/mackie/MackieMcuProExtensionDefinition.java new file mode 100644 index 00000000..ed579566 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/mackie/MackieMcuProExtensionDefinition.java @@ -0,0 +1,97 @@ +package com.bitwig.extensions.controllers.mcu.definitions.mackie; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extensions.controllers.mcu.McuExtension; +import com.bitwig.extensions.controllers.mcu.config.ControllerConfig; +import com.bitwig.extensions.controllers.mcu.config.EncoderBehavior; +import com.bitwig.extensions.controllers.mcu.config.McuAssignments; +import com.bitwig.extensions.controllers.mcu.config.McuFunction; +import com.bitwig.extensions.controllers.mcu.definitions.AbstractMcuControllerExtensionDefinition; +import com.bitwig.extensions.controllers.mcu.definitions.ManufacturerType; +import com.bitwig.extensions.controllers.mcu.definitions.SubType; + +public class MackieMcuProExtensionDefinition extends AbstractMcuControllerExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("fa145533-5f45-45f9-81ad-2de77ffa2dab"); + private static final String DEVICE_NAME = "Mackie Control"; + protected static final String SOFTWARE_VERSION = "0.3"; + + public MackieMcuProExtensionDefinition() { + this(0); + } + + public MackieMcuProExtensionDefinition(final int nrOfExtenders) { + super(nrOfExtenders); + } + + @Override + protected List getInPorts(final PlatformType platformType) { + final String[] inPortNames = new String[nrOfExtenders + 1]; + inPortNames[0] = "MCU Pro USB v3.1"; + for (int i = 1; i < nrOfExtenders + 1; i++) { + inPortNames[i] = String.format("MIDIIN%d (MCU Pro USB v3.1)", i); + } + final List portList = new ArrayList<>(); + portList.add(inPortNames); + return portList; + } + + @Override + protected List getOutPorts(final PlatformType platformType) { + final String[] outPortNames = new String[nrOfExtenders + 1]; + outPortNames[0] = "MCU Pro USB v3.1"; + for (int i = 1; i < nrOfExtenders + 1; i++) { + outPortNames[i] = String.format("MIDIOUT%d (MCU Pro USB v3.1)", i); + } + final List portList = new ArrayList<>(); + portList.add(outPortNames); + return portList; + } + + @Override + public String getHardwareVendor() { + return "Mackie"; + } + + @Override + public String getHardwareModel() { + return "Mackie Control"; + } + + public McuExtension createInstance(final ControllerHost host) { + final ControllerConfig controllerConfig = new ControllerConfig(ManufacturerType.MACKIE, SubType.UNSPECIFIED) // + .setHasDedicateVu(false)// + .setJogWheelCoding(EncoderBehavior.ACCEL) // + .setHasMasterFader(0x8) // + .setHasTimeCodeLed(true) // + .setHasMasterVu(false); + //initSimulationLayout(controllerConfig.getSimulationLayout()); + controllerConfig.setAssignment(McuFunction.CLIP_LAUNCHER_MODE_4, McuAssignments.GROUP); + controllerConfig.setAssignment(McuFunction.MODE_DEVICE, McuAssignments.V_PLUGIN); + controllerConfig.setAssignment(McuFunction.MODE_TRACK_REMOTE, McuAssignments.V_INSTRUMENT); + controllerConfig.setAssignment(McuFunction.MODE_PROJECT_REMOTE, McuAssignments.GV_MIDI_LF1); + return new McuExtension(this, host, controllerConfig); + } + + @Override + public String getName() { + if (nrOfExtenders == 0) { + return MackieMcuProExtensionDefinition.DEVICE_NAME; + } + return String.format("%s +%d EXTENDER", MackieMcuProExtensionDefinition.DEVICE_NAME, nrOfExtenders); + } + + @Override + public String getVersion() { + return SOFTWARE_VERSION; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/ssl/SslUf8ExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/ssl/SslUf8ExtensionDefinition.java new file mode 100644 index 00000000..d8492848 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/ssl/SslUf8ExtensionDefinition.java @@ -0,0 +1,154 @@ +package com.bitwig.extensions.controllers.mcu.definitions.ssl; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extensions.controllers.mcu.McuExtension; +import com.bitwig.extensions.controllers.mcu.config.ControllerConfig; +import com.bitwig.extensions.controllers.mcu.config.EncoderBehavior; +import com.bitwig.extensions.controllers.mcu.config.McuAssignments; +import com.bitwig.extensions.controllers.mcu.config.McuFunction; +import com.bitwig.extensions.controllers.mcu.definitions.AbstractMcuControllerExtensionDefinition; +import com.bitwig.extensions.controllers.mcu.definitions.ManufacturerType; +import com.bitwig.extensions.controllers.mcu.definitions.SubType; + +public class SslUf8ExtensionDefinition extends AbstractMcuControllerExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("ebd7dcbe-7093-4f80-8a8a-a77308ab20a1"); + private final int layerIndex; + protected static final String DEVICE_NAME = "SSL UF8/UF1"; + protected static final String SOFTWARE_VERSION = "1.0"; + protected static final String BASE_DEVICE_PORT = "SSL V-MIDI Port %d"; + protected static final String BASE_DEVICE_PORT_MAC_IN = "SSL V-MIDI Port %d Source"; + protected static final String BASE_DEVICE_PORT_MAC_OUT = "SSL V-MIDI Port %d Destination"; + + public SslUf8ExtensionDefinition() { + this(0, 0); + } + + public SslUf8ExtensionDefinition(final int nrOfExtenders, final int layerIndex) { + super(nrOfExtenders); + this.layerIndex = layerIndex; + } + + @Override + protected List getInPorts(final PlatformType platformType) { + final String[] inPortNames = new String[nrOfExtenders + 1]; + final int portOffset = layerIndex * 4; + final String inPortNameFormat = + platformType == PlatformType.WINDOWS ? BASE_DEVICE_PORT : BASE_DEVICE_PORT_MAC_IN; + inPortNames[0] = inPortNameFormat.formatted(1 + portOffset); + for (int i = 1; i < nrOfExtenders + 1; i++) { + inPortNames[i] = inPortNameFormat.formatted(i + 1 + portOffset); + } + final List portList = new ArrayList<>(); + portList.add(inPortNames); + return portList; + } + + @Override + protected List getOutPorts(final PlatformType platformType) { + final String[] outPortNames = new String[nrOfExtenders + 1]; + final int portOffset = layerIndex * 4; + final String outPortNameFormat = + platformType == PlatformType.WINDOWS ? BASE_DEVICE_PORT : BASE_DEVICE_PORT_MAC_OUT; + outPortNames[0] = outPortNameFormat.formatted(1 + portOffset); + for (int i = 1; i < nrOfExtenders + 1; i++) { + outPortNames[i] = outPortNameFormat.formatted(i + 1 + portOffset); + } + final List portList = new ArrayList<>(); + portList.add(outPortNames); + return portList; + } + + @Override + public String getHardwareVendor() { + return "Solid State Logic"; + } + + @Override + public String getHardwareModel() { + if (layerIndex == 0) { + return "UF8/UF1"; + } else { + return "UF8/UF1 Layer %d".formatted(layerIndex + 1); + } + } + + public McuExtension createInstance(final ControllerHost host) { + return new McuExtension(this, host, createSslConfig()); + } + + protected ControllerConfig createSslConfig() { + final ControllerConfig controllerConfig = new ControllerConfig(ManufacturerType.SSL, SubType.UF8) // + .setDisplaySegmented(true)// + .setJogWheelCoding(EncoderBehavior.ACCEL) // + // .setSingleMainUnit(false) // + .setHasMasterFader(0x8) // + .setHasTimeCodeLed(true) // + .setHasDedicateVu(true); + controllerConfig.setAssignment(McuFunction.PUNCH_IN, McuAssignments.DROP); + controllerConfig.setAssignment(McuFunction.PUNCH_OUT, McuAssignments.REPLACE); + controllerConfig.setAssignment(McuFunction.OVERDUB, McuAssignments.TRIM); + controllerConfig.setAssignment(McuFunction.TEMPO, McuAssignments.GV_AUDIO_LF3); + controllerConfig.setAssignment(McuFunction.UNDO, McuAssignments.UNDO); + controllerConfig.setAssignment(McuFunction.SEND_SELECT_1, McuAssignments.F1); + controllerConfig.setAssignment(McuFunction.SEND_SELECT_2, McuAssignments.F2); + controllerConfig.setAssignment(McuFunction.SEND_SELECT_3, McuAssignments.F3); + controllerConfig.setAssignment(McuFunction.SEND_SELECT_4, McuAssignments.F4); + controllerConfig.setAssignment(McuFunction.SEND_SELECT_5, McuAssignments.F5); + controllerConfig.setAssignment(McuFunction.SEND_SELECT_6, McuAssignments.F6); + controllerConfig.setAssignment(McuFunction.SEND_SELECT_7, McuAssignments.F7); + controllerConfig.setAssignment(McuFunction.SEND_SELECT_8, McuAssignments.F8); + controllerConfig.setAssignment(McuFunction.TRACK_MODE, McuAssignments.V_TRACK); + controllerConfig.setAssignment(McuFunction.MODE_DEVICE, McuAssignments.V_PLUGIN); + controllerConfig.setAssignment(McuFunction.MODE_TRACK_REMOTE, McuAssignments.V_INSTRUMENT); + controllerConfig.setAssignment(McuFunction.MODE_PROJECT_REMOTE, McuAssignments.SAVE); + controllerConfig.setAssignment(McuFunction.CLIP_LAUNCHER_MODE_2, McuAssignments.GROUP); + controllerConfig.setAssignment(McuFunction.SSL_PLUGINS_MENU, McuAssignments.GV_BUSSES_LF6); + controllerConfig.setAssignment(McuFunction.GROOVE_MENU, McuAssignments.GV_AUX_LF5); + controllerConfig.setAssignment(McuFunction.ZOOM_MENU, McuAssignments.GV_INSTRUMENT_LF4); + controllerConfig.setAssignment( + McuFunction.RESTORE_AUTOMATION, McuAssignments.SOLO); //McuAssignments.AUTO_READ_OFF + controllerConfig.setAssignment(McuFunction.PAGE_LEFT, McuAssignments.GV_OUTPUTS_LF7); + controllerConfig.setAssignment(McuFunction.PAGE_RIGHT, McuAssignments.GV_USER_LF8); + controllerConfig.setAssignment(McuFunction.ZOOM_IN, McuAssignments.CANCEL); + controllerConfig.setAssignment(McuFunction.ZOOM_OUT, McuAssignments.NUDGE); + + return controllerConfig; + } + + @Override + public String getName() { + if (layerIndex == 0) { + return getBaseName(); + } + return "%s Layer %d".formatted(getBaseName(), layerIndex + 1); + } + + private String getBaseName() { + if (nrOfExtenders == 0) { + return SslUf8ExtensionDefinition.DEVICE_NAME; + } + return String.format("%s +%d EXTENDER", SslUf8ExtensionDefinition.DEVICE_NAME, nrOfExtenders); + } + + @Override + public String getVersion() { + return SOFTWARE_VERSION; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + + @Override + public String getHelpFilePath() { + return "Controllers/SSL/SSL UF8-UF1.pdf"; + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/ssl/SslUf8Plus1ExtenderExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/ssl/SslUf8Plus1ExtenderExtensionDefinition.java new file mode 100644 index 00000000..b47bdf91 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/ssl/SslUf8Plus1ExtenderExtensionDefinition.java @@ -0,0 +1,16 @@ +package com.bitwig.extensions.controllers.mcu.definitions.ssl; + +import java.util.UUID; + +public class SslUf8Plus1ExtenderExtensionDefinition extends SslUf8ExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("31c41848-541d-4119-8553-0c5690ea10e8"); + + public SslUf8Plus1ExtenderExtensionDefinition() { + super(1, 0); + } + + @Override + public UUID getId() { + return DRIVER_ID; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/ssl/SslUf8Plus2ExtenderExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/ssl/SslUf8Plus2ExtenderExtensionDefinition.java new file mode 100644 index 00000000..5d8aa17c --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/ssl/SslUf8Plus2ExtenderExtensionDefinition.java @@ -0,0 +1,16 @@ +package com.bitwig.extensions.controllers.mcu.definitions.ssl; + +import java.util.UUID; + +public class SslUf8Plus2ExtenderExtensionDefinition extends SslUf8ExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("984c4516-1b71-48ee-9ea3-f004662a904c"); + + public SslUf8Plus2ExtenderExtensionDefinition() { + super(2, 0); + } + + @Override + public UUID getId() { + return DRIVER_ID; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/ssl/SslUf8Plus3ExtenderExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/ssl/SslUf8Plus3ExtenderExtensionDefinition.java new file mode 100644 index 00000000..ca4a4ee7 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/definitions/ssl/SslUf8Plus3ExtenderExtensionDefinition.java @@ -0,0 +1,16 @@ +package com.bitwig.extensions.controllers.mcu.definitions.ssl; + +import java.util.UUID; + +public class SslUf8Plus3ExtenderExtensionDefinition extends SslUf8ExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("de4471bd-2eb7-4991-b74a-307b3d5d2338"); + + public SslUf8Plus3ExtenderExtensionDefinition() { + super(3, 0); + } + + @Override + public UUID getId() { + return DRIVER_ID; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/devices/CustomValueConverter.java b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/CustomValueConverter.java new file mode 100644 index 00000000..2a30754f --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/CustomValueConverter.java @@ -0,0 +1,6 @@ +package com.bitwig.extensions.controllers.mcu.devices; + +public interface CustomValueConverter { + String convert(double value); + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/devices/DeviceManager.java b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/DeviceManager.java new file mode 100644 index 00000000..55584c31 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/DeviceManager.java @@ -0,0 +1,33 @@ +package com.bitwig.extensions.controllers.mcu.devices; + +import com.bitwig.extension.controller.api.Parameter; + +public interface DeviceManager { + + //void initiateBrowsing(final BrowserConfiguration browser, Type type); + + //void addBrowsing(final BrowserConfiguration browser, boolean after); + + //void setInfoLayer(DisplayLayer infoLayer); + + //void enableInfo(InfoSource type); + + void disableInfo(); + + //InfoSource getInfoSource(); + + Parameter getParameter(int index); + + //ParameterPage getParameterPage(int index); + + void navigateDeviceParameters(final int direction); + + //void handleResetInvoked(final int index, final ModifierValueObject modifier); + + DeviceTypeFollower getDeviceFollower(); + + boolean isSpecificDevicePresent(); + + int getPageCount(); + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/devices/DeviceParameter.java b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/DeviceParameter.java new file mode 100644 index 00000000..e44de625 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/DeviceParameter.java @@ -0,0 +1,74 @@ +package com.bitwig.extensions.controllers.mcu.devices; + +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extensions.controllers.mcu.StringUtil; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; + +import java.util.function.Consumer; + +class DeviceParameter { + private final Parameter parameter; + private final double sensitivity; + private final String name; + private final RingDisplayType ringDisplayType; + private CustomValueConverter customValueConverter = null; + private Consumer customResetAction = null; + + public DeviceParameter(final String name, final Parameter parameter, final RingDisplayType ringDisplayType, + final double sensitivity) { + super(); + this.parameter = parameter; + this.parameter.displayedValue().markInterested(); + this.parameter.value().markInterested(); + this.name = StringUtil.toDisplayName(name); + this.ringDisplayType = ringDisplayType; + this.sensitivity = sensitivity; + } + + public String getName() { + return name; + } + + public double getSensitivity() { + return sensitivity; + } + + public RingDisplayType getRingDisplayType() { + return ringDisplayType; + } + + public void setCustomResetAction(final Consumer customResetAction) { + this.customResetAction = customResetAction; + } + + public Parameter getParameter() { + return parameter; + } + + public CustomValueConverter getCustomValueConverter() { + return customValueConverter; + } + + public void setCustomValueConverter(final CustomValueConverter customValueConverter) { + this.customValueConverter = customValueConverter; + } + + public void doReset() { + if (customResetAction != null) { + customResetAction.accept(parameter); + } else { + this.parameter.reset(); + } + } + + public String getStringValue() { + if (customValueConverter != null) { + return customValueConverter.convert(parameter.value().get()); + } + return parameter.displayedValue().get(); + } + + public int getRingValue() { + return (int) Math.round(parameter.get() * 10); + } +} \ No newline at end of file diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/devices/DeviceTypeBank.java b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/DeviceTypeBank.java new file mode 100644 index 00000000..4a3eac8d --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/DeviceTypeBank.java @@ -0,0 +1,170 @@ +package com.bitwig.extensions.controllers.mcu.devices; + +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.ControllerHost; +import com.bitwig.extension.controller.api.CursorRemoteControlsPage; +import com.bitwig.extension.controller.api.Device; +import com.bitwig.extension.controller.api.DeviceMatcher; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extensions.controllers.mcu.CursorDeviceControl; +import com.bitwig.extensions.controllers.mcu.GlobalStates; +import com.bitwig.extensions.controllers.mcu.VPotMode; +import com.bitwig.extensions.controllers.mcu.ViewControl; +import com.bitwig.extensions.framework.di.Component; + +@Component +public class DeviceTypeBank { + + public interface ExistenceChangedListener { + void changed(VPotMode typ, boolean exists); + } + + public enum GeneralDeviceType { + INSTRUMENT(VPotMode.INSTRUMENT), + AUDIO_EFFECT(VPotMode.PLUGIN), + NOTE_EFFECT(VPotMode.MIDI_EFFECT), + EQ_PLUS(VPotMode.EQ), + TRACK_REMOTE(VPotMode.TRACK_REMOTE), + PROJECT_REMOTE(VPotMode.PROJECT_REMOTE), + NONE(null); + + private final VPotMode mode; + + GeneralDeviceType(final VPotMode mode) { + this.mode = mode; + } + + public VPotMode getMode() { + return mode; + } + + public static GeneralDeviceType fromMode(final VPotMode mode) { + return Arrays.stream(GeneralDeviceType.values())// + .filter(type -> type.getMode().equals(mode)) // + .findFirst() // + .orElse(NONE); + } + } + + private final EqDevice eqDevice; + private final Map types = new HashMap<>(); + private final Map managers = new HashMap<>(); + private final Map focusStatus = new HashMap<>(); + private final Map typeExistsStatus = new HashMap<>(); + private final CursorDeviceControl cursorDeviceControl; + private final List listeners = new ArrayList<>(); + private final List> typeListeners = new ArrayList<>(); + private GeneralDeviceType deviceType = GeneralDeviceType.NONE; + private boolean deviceExists = false; + private String currentDeviceType = ""; + + public DeviceTypeBank(final ControllerHost host, final ViewControl viewControl, final GlobalStates states) { + cursorDeviceControl = viewControl.getCursorDeviceControl(); + Arrays.stream(VPotMode.values()).forEach(mode -> focusStatus.put(mode, false)); + + addFollower(cursorDeviceControl, VPotMode.INSTRUMENT, host.createInstrumentMatcher()); + addFollower(cursorDeviceControl, VPotMode.PLUGIN, host.createAudioEffectMatcher()); + addFollower(cursorDeviceControl, VPotMode.MIDI_EFFECT, host.createNoteEffectMatcher()); + + final DeviceMatcher eqDeviceMatcher = host.createBitwigDeviceMatcher(SpecialDevices.EQ_PLUS.getUuid()); + final DeviceTypeFollower eqFollower = addFollower(cursorDeviceControl, VPotMode.EQ, eqDeviceMatcher); + eqDevice = new EqDevice(cursorDeviceControl, eqFollower, states); + + final PinnableCursorDevice cursorDevice = cursorDeviceControl.getCursorDevice(); + cursorDevice.exists().addValueObserver(exist -> { + this.deviceExists = exist; + updateInstrumentType(); + }); + cursorDevice.deviceType().addValueObserver(type -> { + currentDeviceType = type; + updateInstrumentType(); + }); + } + + public void ensureDeviceSelection(final VPotMode mode) { + if (deviceType.getMode() == null) { + return; + } + if (deviceType.getMode() != mode) { + types.get(mode).ensurePosition(); + } + } + + public void addDeviceTypeListener(final Consumer listener) { + this.typeListeners.add(listener); + } + + public SpecificDevice getEqDevice() { + return eqDevice; + } + + public boolean hasDeviceType(final VPotMode mode) { + final Boolean exists = typeExistsStatus.get(mode); + return exists != null ? exists : false; + } + + private void updateInstrumentType() { + GeneralDeviceType type = GeneralDeviceType.NONE; + if (deviceExists) { + if (focusStatus.get(VPotMode.EQ)) { + type = GeneralDeviceType.EQ_PLUS; + } else { + for (final VPotMode mode : VPotMode.values()) { + if (focusStatus.get(mode)) { + type = GeneralDeviceType.fromMode(mode); + } + } + } + if (type == GeneralDeviceType.NONE && currentDeviceType.equals("audio_to_audio")) { + type = GeneralDeviceType.AUDIO_EFFECT; + } + } + if (type != deviceType) { + this.deviceType = type; + //McuExtension.println("### CURRENT TYPE = %s exist=%s", this.deviceType, deviceExists); + this.typeListeners.forEach(listener -> listener.accept(this.deviceType)); + } + } + + public CursorRemoteControlsPage getCursorRemotes() { + return cursorDeviceControl.getRemotes(); + } + + private DeviceTypeFollower addFollower(final CursorDeviceControl deviceControl, final VPotMode mode, + final DeviceMatcher matcher) { + final DeviceTypeFollower follower = new DeviceTypeFollower(deviceControl, matcher, mode); + types.put(mode, follower); + final Device device = follower.getFocusDevice(); + device.exists().addValueObserver(followExists -> { + typeExistsStatus.put(mode, followExists); + listeners.forEach(l -> l.changed(mode, followExists)); + }); + follower.addOnCursorListener(onCursor -> { + focusStatus.put(mode, onCursor); + updateInstrumentType(); + }); + return follower; + } + + public DeviceTypeFollower[] getStandardFollowers() { + final DeviceTypeFollower[] result = new DeviceTypeFollower[3]; + result[0] = types.get(VPotMode.INSTRUMENT); + result[1] = types.get(VPotMode.PLUGIN); + result[2] = types.get(VPotMode.MIDI_EFFECT); + return result; + } + + public void addExistenceListener(final ExistenceChangedListener listener) { + listeners.add(listener); + } + + public DeviceTypeFollower getFollower(final VPotMode mode) { + return types.get(mode); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/devices/DeviceTypeFollower.java b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/DeviceTypeFollower.java new file mode 100644 index 00000000..6fc66512 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/DeviceTypeFollower.java @@ -0,0 +1,109 @@ +package com.bitwig.extensions.controllers.mcu.devices; + +import com.bitwig.extension.callback.BooleanValueChangedCallback; +import com.bitwig.extension.controller.api.BooleanValue; +import com.bitwig.extension.controller.api.CursorTrack; +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.PinnableCursorDevice; +import com.bitwig.extensions.controllers.mcu.CursorDeviceControl; +import com.bitwig.extensions.controllers.mcu.VPotMode; +import com.bitwig.extensions.controllers.mcu.value.BasicIntValue; +import com.bitwig.extensions.framework.values.BooleanValueObject; + +public class DeviceTypeFollower { + private final DeviceBank deviceBank; + private final VPotMode potMode; + private final Device focusDevice; + private final CursorDeviceControl cursorDeviceControl; + private final BooleanValue cursorOnDevice; + private final BasicIntValue trackIndex = new BasicIntValue(); + + private int chainIndex = -1; + + public DeviceTypeFollower(final CursorDeviceControl cursorDeviceControl, final DeviceMatcher matcher, + final VPotMode potMode) { + this.cursorDeviceControl = cursorDeviceControl; + final CursorTrack cursorTrack = cursorDeviceControl.getCursorTrack(); + deviceBank = cursorTrack.createDeviceBank(1); + this.potMode = potMode; + deviceBank.setDeviceMatcher(matcher); + + focusDevice = deviceBank.getItemAt(0); + focusDevice.exists().markInterested(); + focusDevice.name().markInterested(); + + cursorTrack.position().addValueObserver(pos -> trackIndex.set(pos)); + + final PinnableCursorDevice cursorDevice = cursorDeviceControl.getCursorDevice(); + if (potMode.getAssign() == VPotMode.BitwigType.DEVICE) { + final BooleanValueObject matchesTyp = new BooleanValueObject(); + cursorOnDevice = matchesTyp; + cursorDevice.deviceType().addValueObserver(type -> { + matchesTyp.set(type.equals(potMode.getTypeName())); + if (matchesTyp.get()) { + chainIndex = cursorDevice.position().get(); + } + }); + } else { + cursorOnDevice = focusDevice.createEqualsValue(cursorDevice); + cursorOnDevice.addValueObserver(equalsCursor -> { + if (equalsCursor) { + chainIndex = cursorDevice.position().get(); + } + }); + } + cursorDevice.position().addValueObserver(position -> { + if (cursorOnDevice.get()) { + chainIndex = position; + } + }); + } + + public void addOnCursorListener(final BooleanValueChangedCallback booleanValueChangedCallback) { + cursorOnDevice.addValueObserver(booleanValueChangedCallback); + } + + public Device getFocusDevice() { + return focusDevice; + } + + public Device getCurrentDevice() { + return cursorDeviceControl.getCursorDevice(); + } + + public void addNewDeviceAfter() { + if (focusDevice.exists().get()) { + focusDevice.afterDeviceInsertionPoint().browse(); + } else { + deviceBank.browseToInsertDevice(0); + } + } + + public void addNewDeviceBefore() { + if (focusDevice.exists().get()) { + focusDevice.beforeDeviceInsertionPoint().browse(); + } else { + deviceBank.browseToInsertDevice(0); + } + } + + public void initiateBrowsing() { + if (focusDevice.exists().get()) { + focusDevice.replaceDeviceInsertionPoint().browse(); + } else { + deviceBank.browseToInsertDevice(0); + } + } + + public void ensurePosition() { + if (!cursorOnDevice.get()) { + cursorDeviceControl.getCursorDevice().selectDevice(focusDevice); + } + } + + public BasicIntValue getTrackIndex() { + return trackIndex; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/devices/EqDevice.java b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/EqDevice.java new file mode 100644 index 00000000..90f9b86c --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/EqDevice.java @@ -0,0 +1,162 @@ +package com.bitwig.extensions.controllers.mcu.devices; + +import java.util.ArrayList; +import java.util.List; + +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extensions.controllers.mcu.CursorDeviceControl; +import com.bitwig.extensions.controllers.mcu.GlobalStates; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; + +public class EqDevice extends SpecificDevice { + + private static final String[] TYPES = { + "*Off*", "LowC1", "LowC2", "LowC4", "LowC6", "LowC8", "LoShlv", "Bell", // + "HiC-1", "HiC-2", "HiC-4", "HiC-6", "HiC-8", "HiShlv", "Notch" + }; + private static final String[] PAGE_NAMES = {"Band 1&2", "Band 3&4", "Bands 5&6", "Bands 7&8", "General"}; + private static final int[] DEFAULT_BAND_TYPES = { + 3, 6, 7, 7, 7, 7, 9, 12 + }; + private static final ParameterSetting[] PAGE_5 = { // + new ParameterSetting("OUTPUT_GAIN", 0.125, RingDisplayType.FILL_LR), // + new ParameterSetting("GLOBAL_SHIFT", 0.125, RingDisplayType.FILL_LR), // + new ParameterSetting("BAND", 0.25, RingDisplayType.FILL_LR), // + new ParameterSetting("SOLO", 0.25, RingDisplayType.FILL_LR), // + new ParameterSetting("ADAPTIVE_Q", 0.25, RingDisplayType.FILL_LR), // + new ParameterSetting("DECIBEL_RANGE", 0.25, RingDisplayType.FILL_LR), // + new ParameterSetting("----", 1, RingDisplayType.FILL_LR), // + new ParameterSetting("----", 1, RingDisplayType.SINGLE), + }; + private final List PARAMS = List.of(new ParamSetup("TYPE%d", RingDisplayType.FILL_LR, 0.25), + new ParamSetup("FREQ%d", RingDisplayType.SINGLE, 1), new ParamSetup("GAIN%d", RingDisplayType.FILL_LR, 1), + new ParamSetup("Q%d", RingDisplayType.FILL_LR, 1)); + private final boolean[] enableValues = new boolean[8]; + private final boolean[] typeOn = new boolean[8]; + private final List enableParams = new ArrayList<>(); + private final GlobalStates states; + + private record ParamSetup(String nameFormat, RingDisplayType ringDisplayType, double sensitivites) { + + } + + public EqDevice(final CursorDeviceControl cursorDeviceControl, final DeviceTypeFollower deviceFollower, + final GlobalStates states) { + super(SpecialDevices.EQ_PLUS, cursorDeviceControl, deviceFollower); + this.states = states; + for (int slotIndex = 0; slotIndex < 8; slotIndex++) { + final ParamPageSlot parameterSlot = pageSlots.get(slotIndex); + for (int bandPage = 0; bandPage < 4; bandPage++) { + final DeviceParameter parameter = createDeviceParameter(bandPage, slotIndex); + parameterSlot.addParameter(parameter); + } + parameterSlot.addParameter(createDeviceParameter(4, slotIndex)); + } + + for (int i = 0; i < 8; i++) { + final int bandIndex = i; + final Parameter enableParam = bitwigDevice.createParameter("ENABLE%d".formatted(i + 1)); + enableParams.add(enableParam); + enableParam.value().addValueObserver(2, enabled -> updateEnablement(bandIndex, enabled)); + } + applyPageValues(0); + } + + private DeviceParameter createDeviceParameter(final int page, final int index) { + if (page < 4) { + return createBandParameter(page, index); + } else { + final ParameterSetting pageSetting = PAGE_5[index]; + final Parameter param = bitwigDevice.createParameter(pageSetting.getParameterName()); + final DeviceParameter parameter = + new DeviceParameter(pageSetting.getParameterName(), param, pageSetting.getRingType(), + pageSetting.getSensitivity()); + + return parameter; + } + } + + private void updateEnablement(final int bandIndex, final int enableValue) { + enableValues[bandIndex] = enableValue == 1; + final int pageIndex = bandIndex / 2; + if (this.pageIndex == pageIndex) { + notifyEnablement(bandIndex); + } + } + + @Override + public void applyPageValues(final int page) { + if (page < 4) { + for (int i = 0; i < 8; i++) { + final ParamPageSlot slot = pageSlots.get(i); + final int bandIndex = page * 2 + i / 4; + slot.notifyEnablement(enableValues[bandIndex] & typeOn[bandIndex]); + } + } + } + + private DeviceParameter createBandParameter(final int page, final int index) { + final String paramName = getParamName(page, index); + final Parameter param = bitwigDevice.createParameter(paramName); + final ParamSetup paramSetup = PARAMS.get(index % 4); + final DeviceParameter parameter = + new DeviceParameter(paramName, param, paramSetup.ringDisplayType(), paramSetup.sensitivites()); + if (index == 0 || index == 4) { + final int bandIndex = page * 2 + index / 4; + parameter.setCustomResetAction(p -> handleReset(bandIndex, p)); + parameter.setCustomValueConverter(value -> TYPES[(int) (value * 14)]); + parameter.getParameter().value().addValueObserver(14, v -> { + typeOn[bandIndex] = v > 0; + if (this.pageIndex == pageIndex) { + notifyEnablement(bandIndex); + } + }); + } + return parameter; + } + + private void notifyEnablement(final int bandIndex) { + final int offset = (bandIndex % 2) * 4; + for (int i = 0; i < 4; i++) { + final ParamPageSlot slot = pageSlots.get(i + offset); + slot.notifyEnablement(enableValues[bandIndex] & typeOn[bandIndex]); + } + } + + private String getParamName(final int page, final int index) { + return PARAMS.get(index % 4).nameFormat().formatted(1 + index / 4 + page * 2); + } + + private void handleReset(final int bandIndex, final Parameter p) { + if (states.isShiftSet()) { + if (enableValues[bandIndex]) { + enableParams.get(bandIndex).value().set(0); + } else { + enableParams.get(bandIndex).value().set(1); + } + } else { + p.value().set(DEFAULT_BAND_TYPES[bandIndex], 15); + } + } + + @Override + public Parameter getParameter(final int index) { + return null; + } + + @Override + public int getPageCount() { + return 5; + } + + @Override + public String getDeviceInfo() { + return "EQ+ Device"; + } + + @Override + public String getPageInfo() { + return PAGE_NAMES[pageIndex]; + } + +} \ No newline at end of file diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/devices/ParamPageSlot.java b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/ParamPageSlot.java new file mode 100644 index 00000000..8771f6ca --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/ParamPageSlot.java @@ -0,0 +1,185 @@ +package com.bitwig.extensions.controllers.mcu.devices; + +import com.bitwig.extension.controller.api.AbsoluteHardwareControl; +import com.bitwig.extension.controller.api.AbsoluteHardwareControlBinding; +import com.bitwig.extension.controller.api.DoubleValue; +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extension.controller.api.RelativeHardwareControl; +import com.bitwig.extension.controller.api.RelativeHardwareControlToRangedValueBinding; +import com.bitwig.extension.controller.api.RelativeHardwareKnob; +import com.bitwig.extensions.controllers.mcu.bindings.paramslots.ResetableAbsoluteValueSlotBinding; +import com.bitwig.extensions.controllers.mcu.bindings.paramslots.ResetableRelativeSlotBinding; +import com.bitwig.extensions.controllers.mcu.bindings.paramslots.RingParameterDisplaySlotBinding; +import com.bitwig.extensions.controllers.mcu.control.MotorSlider; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; +import com.bitwig.extensions.controllers.mcu.control.RingEncoder; +import com.bitwig.extensions.framework.values.BasicDoubleValue; +import com.bitwig.extensions.framework.values.BasicStringValue; +import com.bitwig.extensions.framework.values.BooleanValueObject; +import com.bitwig.extensions.framework.values.IntValueObject; + +import java.util.ArrayList; +import java.util.List; + +public class ParamPageSlot { + private static final int RING_RANGE = 10; + + private final List parameters = new ArrayList<>(); + private final int slotIndex; + private final SpecificDevice device; + private final BasicStringValue nameValue = new BasicStringValue(""); + private final BasicStringValue displayValue = new BasicStringValue(""); + private final IntValueObject ringValue = new IntValueObject(0, 0, 10); + private final BasicDoubleValue doubleValue = new BasicDoubleValue(); + private final BooleanValueObject existsValue = new BooleanValueObject(); + private final BooleanValueObject enabledValue = new BooleanValueObject(); + + public ParamPageSlot(final int index, final SpecificDevice device) { + this.slotIndex = index; + this.device = device; + device.addExistChangeListener(exists -> { + existsValue.set(exists); + }); + } + + + public void notifyEnablement(final boolean enabled) { + this.enabledValue.set(enabled); + } + + public void addParameter(final DeviceParameter deviceParameter) { + final Parameter param = deviceParameter.getParameter(); + final int runningIndex = parameters.size(); + param.value().addValueObserver(v -> handleValueChanged(runningIndex, v)); + param.displayedValue().addValueObserver(v -> handleDisplayValueChanged(runningIndex, v)); + parameters.add(deviceParameter); + if (runningIndex == 0) { + update(); + } + } + + public void update() { + final DeviceParameter currentParameter = getCurrentParameter(); + nameValue.set(currentParameter.getName()); + displayValue.set(currentParameter.getStringValue()); + ringValue.set(currentParameter.getRingValue()); + doubleValue.set(currentParameter.getParameter().getAsDouble()); + existsValue.set(true); // Need to deal with empty slots + } + + private void handleDisplayValueChanged(final int index, final String value) { + if (index != device.getCurrentPage()) { + return; + } + final CustomValueConverter valueConverter = getCurrentParameter().getCustomValueConverter(); + if (valueConverter == null) { + displayValue.set(value); + } + } + + private void handleValueChanged(final int index, final double value) { + if (index != device.getCurrentPage()) { + return; + } + ringValue.set((int) Math.round(value * RING_RANGE)); + doubleValue.set(value); + final CustomValueConverter valueConverter = getCurrentParameter().getCustomValueConverter(); + if (valueConverter != null) { + displayValue.set(valueConverter.convert(value)); + } + } + + public ResetableRelativeSlotBinding getRelativeEncoderBinding(final RelativeHardwareKnob encoder) { + return new ResetableRelativeSlotBinding(encoder, this, getCurrentParameter().getSensitivity()); + } + + public ResetableAbsoluteValueSlotBinding getFaderBinding(final MotorSlider fader) { + return fader.createSlotBinding(this); + } + + public AbsoluteHardwareControlBinding addBinding(final AbsoluteHardwareControl hardwareControl) { + return this.addBindingWithRange(hardwareControl, 0.0, 1.0); + } + + public AbsoluteHardwareControlBinding addBindingWithRange(final AbsoluteHardwareControl hardwareControl, + final double minNormalizedValue, + final double maxNormalizedValue) { + return getCurrentParameter().getParameter() + .addBindingWithRange(hardwareControl, minNormalizedValue, maxNormalizedValue); + } + + public RelativeHardwareControlToRangedValueBinding addBindingWithRangeAndSensitivity( + final RelativeHardwareControl hardwareControl, final double minNormalizedValue, + final double maxNormalizedValue, final double sensitivity) { + final DeviceParameter currentParameter = getCurrentParameter(); + return currentParameter.getParameter() + .addBindingWithRangeAndSensitivity(hardwareControl, minNormalizedValue, maxNormalizedValue, + currentParameter.getSensitivity()); + } + + public RelativeHardwareControlToRangedValueBinding addBindingWithSensitivity( + final RelativeHardwareControl hardwareControl, final double sensitivity) { + return this.addBindingWithRangeAndSensitivity(hardwareControl, 0.0, 1.0, sensitivity); + } + + public RingParameterDisplaySlotBinding createRingBinding(final RingEncoder encoder) { + final RingParameterDisplaySlotBinding ringBinding = encoder.createDisplayBinding(this); + return ringBinding; + } + + public String getCurrentValue() { + return getCurrentParameter().getStringValue(); + } + + public double getCurrentDoubleValue() { + return getCurrentParameter().getParameter().get(); + } + + public void parameterReset() { + getCurrentParameter().doReset(); + } + + public BooleanValueObject getEnabledValue() { + return enabledValue; + } + + private DeviceParameter getCurrentParameter() { + return parameters.get(device.getCurrentPage()); + } + + public BasicStringValue getDisplayValue() { + return displayValue; + } + + public BasicStringValue getNameValue() { + return nameValue; + } + + public IntValueObject getRingValue() { + return ringValue; + } + + public BooleanValueObject getExistsValue() { + return existsValue; + } + + public RingDisplayType getRingDisplayType() { + return getCurrentParameter().getRingDisplayType(); + } + + public String getCurrentName() { + return getCurrentParameter().getName(); + } + + public void touch(final boolean value) { + getCurrentParameter().getParameter().touch(value); + } + + public DoubleValue getValue() { + return doubleValue; + } + + public int getSlotIndex() { + return slotIndex; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/devices/ParameterSetting.java b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/ParameterSetting.java new file mode 100644 index 00000000..a94cfac2 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/ParameterSetting.java @@ -0,0 +1,29 @@ +package com.bitwig.extensions.controllers.mcu.devices; + + +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; + +public class ParameterSetting { + private final String parameterName; + private final double sensitivity; + private final RingDisplayType ringType; + + public ParameterSetting(final String name, final double sensitivity, final RingDisplayType ringType) { + this.parameterName = name; + this.sensitivity = sensitivity; + this.ringType = ringType; + } + + public String getParameterName() { + return parameterName; + } + + public double getSensitivity() { + return sensitivity; + } + + public RingDisplayType getRingType() { + return ringType; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/devices/SpecialDevices.java b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/SpecialDevices.java new file mode 100644 index 00000000..82da2a4e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/SpecialDevices.java @@ -0,0 +1,19 @@ +package com.bitwig.extensions.controllers.mcu.devices; + +import java.util.UUID; + +public enum SpecialDevices { + EQ_PLUS("e4815188-ba6f-4d14-bcfc-2dcb8f778ccb"), // + ARPEGGIATOR("4d407a2b-c91b-4e4c-9a89-c53c19fe6251"), // + DRUM("8ea97e45-0255-40fd-bc7e-94419741e9d1"); + + private UUID uuid; + + SpecialDevices(final String uuid) { + this.uuid = UUID.fromString(uuid); + } + + public UUID getUuid() { + return uuid; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/devices/SpecificDevice.java b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/SpecificDevice.java new file mode 100644 index 00000000..8a120bd3 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/SpecificDevice.java @@ -0,0 +1,102 @@ +package com.bitwig.extensions.controllers.mcu.devices; + +import java.util.ArrayList; +import java.util.List; + +import com.bitwig.extension.callback.BooleanValueChangedCallback; +import com.bitwig.extension.controller.api.BooleanValue; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extension.controller.api.SpecificBitwigDevice; +import com.bitwig.extensions.controllers.mcu.CursorDeviceControl; + +/** + * Fully Customized control of the Bitwig EQ+ Device Missing Parameters: + *

+ * OUTPUT_GAIN GLOBAL_SHIFT BAND SOLO + *

+ * ADAPTIVE_Q DECIBEL_RANGE + */ +public abstract class SpecificDevice implements DeviceManager { + + private final SpecialDevices deviceType; + private final List updateListeners = new ArrayList<>(); + protected final SpecificBitwigDevice bitwigDevice; + protected int pageIndex = 0; + protected final DeviceTypeFollower deviceFollower; + protected final CursorDeviceControl cursorDeviceControl; + protected final List pageSlots; + + public SpecificDevice(final SpecialDevices type, final CursorDeviceControl cursorDeviceControl, + final DeviceTypeFollower deviceFollower) { + this.deviceType = type; + this.cursorDeviceControl = cursorDeviceControl; + final PinnableCursorDevice cursorDevice = cursorDeviceControl.getCursorDevice(); + bitwigDevice = cursorDevice.createSpecificBitwigDevice(type.getUuid()); + this.deviceFollower = deviceFollower; + this.pageSlots = new ArrayList<>(); + for (int i = 0; i < 8; i++) { + this.pageSlots.add(new ParamPageSlot(i, this)); + } + } + + @Override + public void disableInfo() { + //infoSource = null; + } + + public void navigateDeviceParameters(final int direction) { + navigateToDeviceParameters(pageIndex + direction); + } + + public void navigateToDeviceParameters(final int index) { + if (index >= 0 && index < getPageCount()) { + pageIndex = index; + this.applyPageValues(pageIndex); + this.pageSlots.forEach(ParamPageSlot::update); + updateListeners.forEach(Runnable::run); + } + } + + public abstract void applyPageValues(int page); + + public ParamPageSlot getParamPageSlot(final int index) { + return pageSlots.get(index); + } + + public abstract String getDeviceInfo(); + + public abstract String getPageInfo(); + + public int getCurrentPage() { + return pageIndex; + } + + @Override + public boolean isSpecificDevicePresent() { + return deviceFollower.getFocusDevice().exists().get(); + } + + public BooleanValue getExists() { + return deviceFollower.getFocusDevice().exists(); + } + + @Override + public DeviceTypeFollower getDeviceFollower() { + return deviceFollower; + } + + public void addUpdateListeners(final Runnable callback) { + updateListeners.add(callback); + } + + public void addExistChangeListener(final BooleanValueChangedCallback callback) { + deviceFollower.getFocusDevice().exists().addValueObserver(callback); + } + + public void insertDevice() { + deviceFollower.getCurrentDevice()// + .afterDeviceInsertionPoint() // + .insertBitwigDevice(deviceType.getUuid()); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/devices/SslPlugins.java b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/SslPlugins.java new file mode 100644 index 00000000..1340d916 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/devices/SslPlugins.java @@ -0,0 +1,22 @@ +package com.bitwig.extensions.controllers.mcu.devices; + +public enum SslPlugins { + + S4K_B("56535453344B4273736C20346B206200"), + S4K_E("56535453344B4573736C20346B206500"), + METER("5653544D54524273736C206D65746572"), + BUS_COMPRESSOR("5653544E42433273736C206E61746976"), + CHANNEL_STRIP("5653544E43533273736C206E61746976"), + LINK_360("5653543336304C73736C20333630206C"); + + private final String id; + + SslPlugins(final String id) { + this.id = id; + } + + public String getId() { + return id; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/display/ControllerDisplay.java b/src/main/java/com/bitwig/extensions/controllers/mcu/display/ControllerDisplay.java new file mode 100644 index 00000000..b5a4446c --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/display/ControllerDisplay.java @@ -0,0 +1,30 @@ +package com.bitwig.extensions.controllers.mcu.display; + +import com.bitwig.extensions.framework.values.BooleanValueObject; + +import java.util.List; + +public interface ControllerDisplay { + + void showText(DisplayPart part, int row, int cell, String text); + + void showText(DisplayPart part, int row, List text); + + void showText(DisplayPart part, int row, String text); + + void refresh(); + + boolean hasLower(); + + BooleanValueObject getSlidersTouched(); + + void sendVuUpdate(final int index, final int value); + + void sendMasterVuUpdateL(final int value); + + void sendMasterVuUpdateR(final int value); + + void blockUpdate(DisplayPart part, int row); + + void enableUpdate(DisplayPart part, int row); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/display/DisplayManager.java b/src/main/java/com/bitwig/extensions/controllers/mcu/display/DisplayManager.java new file mode 100644 index 00000000..4560bfbe --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/display/DisplayManager.java @@ -0,0 +1,67 @@ +package com.bitwig.extensions.controllers.mcu.display; + +import java.util.List; +import java.util.Optional; + +import com.bitwig.extensions.controllers.mcu.layer.ControlMode; + +public class DisplayManager { + private final ControllerDisplay display; + private ControlMode upperMode = ControlMode.PAN; + private ControlMode lowerMode = ControlMode.VOLUME; + + public DisplayManager(final ControllerDisplay display) { + this.display = display; + } + + public void sendText(final int rowIndex, final int cellIndex, final String text) { + display.showText(DisplayPart.UPPER, rowIndex, cellIndex, text); + } + + public void sendText(final ControlMode mode, final int rowIndex, final int cellIndex, final String text) { + getTargetDisplay(mode).ifPresent(part -> { + // if (cellIndex == 0) { + // McuExtension.println(" DEBUG %d : %s mode=%s => part=%s", rowIndex, text, mode, part); + // } + display.showText(part, rowIndex, cellIndex, text); + }); + } + + public void sendText(final ControlMode mode, final int rowIndex, final String text) { + getTargetDisplay(mode).ifPresent(part -> { + display.showText(part, rowIndex, text); + }); + } + + public void sendText(final ControlMode mode, final int rowIndex, final List texts) { + getTargetDisplay(mode).ifPresent(part -> { + display.showText(part, rowIndex, texts); + }); + } + + public void registerModeAssignment(final ControlMode upperMode, final ControlMode lowerMode) { + //McuExtension.println("ASSIGNE UP = %s Low = %s", upperMode, lowerMode); + this.upperMode = upperMode; + this.lowerMode = lowerMode; + } + + private Optional getTargetDisplay(final ControlMode mode) { + if (mode == ControlMode.MENU) { + return Optional.of(DisplayPart.UPPER); + } + if (display.hasLower()) { + if (mode == upperMode) { + return Optional.of(DisplayPart.UPPER); + } else if (mode == lowerMode) { + return Optional.of(DisplayPart.LOWER); + } + } else { + return Optional.of(DisplayPart.UPPER); + } + return Optional.empty(); + } + + public void sendVuUpdate(final int index, final int value) { + display.sendVuUpdate(index, value); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/display/DisplayPart.java b/src/main/java/com/bitwig/extensions/controllers/mcu/display/DisplayPart.java new file mode 100644 index 00000000..8c8d8bb2 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/display/DisplayPart.java @@ -0,0 +1,6 @@ +package com.bitwig.extensions.controllers.mcu.display; + +public enum DisplayPart { + UPPER, + LOWER; +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/display/DisplayRow.java b/src/main/java/com/bitwig/extensions/controllers/mcu/display/DisplayRow.java new file mode 100644 index 00000000..59c45135 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/display/DisplayRow.java @@ -0,0 +1,16 @@ +package com.bitwig.extensions.controllers.mcu.display; + +public enum DisplayRow { + LABEL(0), + VALUE(1); + + final int rowIndex; + + DisplayRow(int rowIndex) { + this.rowIndex = rowIndex; + } + + public int getRowIndex() { + return rowIndex; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/display/LcdDisplay.java b/src/main/java/com/bitwig/extensions/controllers/mcu/display/LcdDisplay.java new file mode 100644 index 00000000..17904040 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/display/LcdDisplay.java @@ -0,0 +1,348 @@ +package com.bitwig.extensions.controllers.mcu.display; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.HardwareTextDisplay; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extensions.controllers.mcu.GlobalStates; +import com.bitwig.extensions.controllers.mcu.SectionType; +import com.bitwig.extensions.controllers.mcu.StringUtil; +import com.bitwig.extensions.controllers.mcu.config.ControllerConfig; +import com.bitwig.extensions.framework.di.Context; +import com.bitwig.extensions.framework.values.Midi; + +/** + * Represents 2x56 LCD display on the MCU or an extender. + */ +public class LcdDisplay { + private static final int DISPLAY_LEN = 55; + private static final int ROW2_START = 56; + + private final byte[] rowDisplayBuffer = { // + (byte) 0XF0, 0, 0, 0X66, 0x14, 0x12, 0, // z: the grid number Zone number 0-3 * 28 + 32, 32, 32, 0, 0, 0, 0, 0, 0, 0, // 7: 10 Chars + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 7: 10 Chars + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 7: 10 Chars + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 7: 10 Chars + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 7: 10 Chars + 0, 0, 0, 0, 0, 32, 32, (byte) 247 + }; + + private final byte[] segBuffer; + private final byte[] segBufferExp; + private final DisplayPart part; + private final HardwareTextDisplay displayRep; + private final boolean topRowFlipped; + private final String[][] lastSendGrids = new String[][] { + {"", "", "", "", "", "", "", "", ""}, // + {"", "", "", "", "", "", "", "", ""} + }; + private final String[] lastSentRows = new String[] {"", ""}; + private final boolean[] fullTextMode = new boolean[] {false, false}; + private final MidiOut midiOut; + private VuMode vuMode = VuMode.LED; + private boolean displayBarGraphEnabled = true; + private boolean isLowerDisplay; + private final int displayLen; + private final int segmentLength; + private final int segmentOffset; + private final boolean hasDedicatedVu; + private final char[][] lines = new char[2][60]; + public String sysHead; + + public LcdDisplay(final Context context, final int sectionIndex, final MidiOut midiOut, final SectionType type, + final DisplayPart part, final ControllerConfig controllerConfig) { + this.midiOut = midiOut; + this.hasDedicatedVu = controllerConfig.isHasDedicateVu(); + this.topRowFlipped = controllerConfig.isTopDisplayRowsFlipped(); + this.part = part; + final HardwareSurface surface = context.getService(HardwareSurface.class); + final GlobalStates states = context.getService(GlobalStates.class); + displayRep = surface.createHardwareTextDisplay("DISPLAY_SIMU_" + part + "_" + sectionIndex, 2); + + //initSimulation(driver, sectionIndex, part); + + if (part == DisplayPart.LOWER) { + isLowerDisplay = true; + rowDisplayBuffer[3] = 0X67; + rowDisplayBuffer[4] = 0x15; + rowDisplayBuffer[5] = 0x13; + sysHead = "f0 00 00 67 15 "; + segBuffer = new byte[] { // + (byte) 240, 0, 0, 0X67, 0x15, 0x13, 0, // z: the grid number Zone number 0-3 * 28 + 0, 0, 0, 0, 0, 32, // 7: 10 Chars + (byte) 247 + }; + segBufferExp = new byte[] { // + (byte) 240, 0, 0, 0X67, 0x15, 0x13, 0, // z: the grid number Zone number 0-3 * 28 + 0, 0, 0, 0, 0, 0, 32, // 7: 10 Chars + (byte) 247 + }; + } else { + segBuffer = new byte[] { // + (byte) 240, 0, 0, 102, 20, 18, 0, // z: the grid number Zone number 0-3 * 28 + 0, 0, 0, 0, 0, 0, 0, // 7: 10 Chars + (byte) 247 + }; + segBufferExp = new byte[] { // + (byte) 240, 0, 0, 102, 20, 18, 0, // z: the grid number Zone number 0-3 * 28 + 0, 0, 0, 0, 0, 0, 0, // 7: 10 Chars + (byte) 247 + }; + if (type == SectionType.XTENDER) { + rowDisplayBuffer[4] = 0x15; + segBuffer[4] = 0x15; + sysHead = "f0 00 00 66 15 "; + } else { + sysHead = "f0 00 00 66 14 "; + } + } + displayLen = LcdDisplay.DISPLAY_LEN + (part == DisplayPart.LOWER && type != SectionType.XTENDER ? 1 : 0); + + if (part == DisplayPart.LOWER) { + segmentLength = 6; + segmentOffset = 2; + } else { + segmentLength = 7; + segmentOffset = 0; + } + setVuMode(states.getVuMode().get()); + } + + // private void initSimulation(final MackieMcuProExtension driver, final int sectionIndex, final DisplayPart + // part) { + // driver.getControllerConfig().getSimulationLayout().layoutDisplay(part, sectionIndex, displayRep); + // for (int i = 0; i < 2; i++) { + // Arrays.fill(lines[i], ' '); + // } + // } + + public int getSegmentLength() { + return segmentLength; + } + + public boolean isLowerDisplay() { + return isLowerDisplay; + } + + public void setFullTextMode(final int row, final boolean fullTextMode) { + this.fullTextMode[row] = fullTextMode; + setDisplayBarGraphEnabled(!isFullModeActive()); + refreshDisplay(); + } + + public void setDisplayBarGraphEnabled(final boolean displayBarGraphEnabled) { + if (this.displayBarGraphEnabled == displayBarGraphEnabled) { + return; + } + this.displayBarGraphEnabled = displayBarGraphEnabled; + if (this.displayBarGraphEnabled) { // enable level metering in LCD + if (vuMode != VuMode.LED) { // action only need if overall Vu Mode is actually set to such + switchVuMode(vuMode); + } + } else if (vuMode != VuMode.LED) { // disable level metering in LCD + switchVuMode(VuMode.LED); + } + } + + private boolean isFullModeActive() { + return fullTextMode[0] | fullTextMode[1]; + } + + public void setVuMode(final VuMode mode) { + if (hasDedicatedVu) { + return; + } + vuMode = mode; + if (!isFullModeActive()) { + switchVuMode(mode); + refreshDisplay(); + } + } + + private void switchVuMode(final VuMode mode) { + switch (mode) { + case LED: + midiOut.sendSysex(sysHead + "21 01 f7"); // Vertical VU + for (int i = 0; i < 8; i++) { + midiOut.sendMidi(Midi.CHANNEL_AT, i << 4, 0); + midiOut.sendSysex(sysHead + "20 0" + i + " 01 f7"); + } + break; + case LED_LCD_VERTICAL: + midiOut.sendSysex(sysHead + "21 01 f7"); // Vertical VU + for (int i = 0; i < 8; i++) { + midiOut.sendSysex(sysHead + "20 0" + i + " 03 f7"); + midiOut.sendMidi(Midi.CHANNEL_AT, i << 4, 0); + } + midiOut.sendSysex(sysHead + "20 00 03 f7"); + break; + case LED_LCD_HORIZONTAL: + midiOut.sendSysex(sysHead + "21 00 f7"); // Horizontal VU + for (int i = 0; i < 8; i++) { + midiOut.sendSysex(sysHead + "20 0" + i + " 03 f7"); + midiOut.sendMidi(Midi.CHANNEL_AT, i << 4, 0); + } + break; + } + } + + private void resetGrids(final int row) { + Arrays.fill(lastSendGrids[row], " "); + } + + public void centerText(final int row, final String text) { + sendToDisplay(row, pad4Center(text)); + } + + @Override + public String toString() { + return "LcdDisplay " + part; + } + + private String pad4Center(final String text) { + final int fill = displayLen - text.length(); + if (fill < 0) { + return text.substring(0, displayLen); + } + if (fill < 2) { + return text; + } + return StringUtil.padString(text, fill / 2); + } + + public void sendDirect(final String topString, final String bottomString) { + lastSentRows[0] = topString; + lastSentRows[1] = bottomString; + resetGrids(0); + resetGrids(1); + sendFullRow(0, topString); + sendFullRow(1, bottomString); + } + + public void sendSegmented(final int row, final List texts) { + for (int i = 0; i < 8; i++) { + final String text = i < texts.size() ? texts.get(i) : ""; + sendToRowFull(row, i, text); + } + } + + public void sendToDisplay(final int row, final String text) { + // if (text.equals(lastSentRows[row])) { + // return; + // } + lastSentRows[row] = text; + resetGrids(row); + sendFullRow(row, text); + } + + public void sendFullRow(final int row, final String text) { + rowDisplayBuffer[6] = (byte) (row * LcdDisplay.ROW2_START); + final byte[] ca = text.getBytes(StandardCharsets.US_ASCII); + for (int i = 0; i < displayLen; i++) { + rowDisplayBuffer[i + 7] = i < ca.length ? ca[i] : 32; + } + displayRep.line(row).text().setValue(text); + midiOut.sendSysex(rowDisplayBuffer); + } + + public void sendToRow(final int row, final int segment, final String text) { + if (row > 1 || row < 0) { + return; + } + if (!text.equals(lastSendGrids[row][segment])) { + lastSendGrids[row][segment] = text; + sendTextSeg(row, segment, text); + } + } + + public void sendToRowFull(final int row, final int segment, final String text) { + if (row > 1 || row < 0) { + return; + } + if (!text.equals(lastSendGrids[row][segment])) { + lastSendGrids[row][segment] = text; + sendTextSegFull(row, segment, text); + } + } + + private void sendTextSegFull(final int row, final int segment, final String text) { + segBuffer[6] = (byte) (row * LcdDisplay.ROW2_START + segment * segmentLength + segmentOffset); + final byte[] ca = text.getBytes(StandardCharsets.US_ASCII); + for (int i = 0; i < segmentLength; i++) { + segBuffer[i + 7] = i < ca.length ? ca[i] : 32; + lines[row][segment * 7 + i] = (char) segBuffer[i + 7]; + } + midiOut.sendSysex(segBuffer); + displayRep.line(row).text().setValue(String.valueOf(lines[row])); + } + + private void sendTextSeg(final int row, final int segment, final String text) { + final byte[] ca = text.getBytes(StandardCharsets.US_ASCII); + if (segment == 8) { + if (isLowerDisplay()) { + handleLastCell(row, segment, ca); + } + } else { + segBuffer[6] = (byte) (row * LcdDisplay.ROW2_START + segment * segmentLength + segmentOffset); + for (int i = 0; i < segmentLength - 1; i++) { + segBuffer[i + 7] = i < ca.length ? (byte) ca[i] : 32; + lines[row][segment * 7 + i] = (char) segBuffer[i + 7]; + } + if (segment < segmentLength) { + segBuffer[6 + segmentLength] = ' '; + } + midiOut.sendSysex(segBuffer); + displayRep.line(row).text().setValue(String.valueOf(lines[row])); + } + } + + private void handleLastCell(final int row, final int segment, final byte[] ca) { + segBufferExp[6] = (byte) (row * LcdDisplay.ROW2_START + segment * segmentLength + segmentOffset); + for (int i = 0; i < segmentLength; i++) { + segBufferExp[i + 7] = i < ca.length ? ca[i] : 32; + } + if (segment < segmentLength + 1) { + segBufferExp[6 + segmentLength] = ' '; + } + midiOut.sendSysex(segBufferExp); + } + + public void refreshDisplay() { + for (int row = 0; row < 2; row++) { + if (fullTextMode[row]) { + sendFullRow(row, lastSentRows[row]); + } else { + for (int segment = 0; segment < 8; segment++) { // TODO segment 9 + sendTextSeg(row, segment, lastSendGrids[row][segment]); + } + } + } + } + + public void sendChar(final int index, final char cx) { + midiOut.sendMidi(Midi.CC, 0x30, cx); + } + + public void clearAll() { + midiOut.sendSysex(sysHead + "62 f7"); + sendToDisplay(0, ""); + sendToDisplay(1, ""); + } + + public void exitMessage() { + midiOut.sendSysex(sysHead + "62 f7"); + centerText(topRowFlipped ? 1 : 0, "Bitwig Studio"); + centerText(topRowFlipped ? 0 : 1, "... not running ..."); + } + + public void clearText() { + sendToDisplay(0, ""); + sendToDisplay(1, ""); + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/display/TimeCodeLed.java b/src/main/java/com/bitwig/extensions/controllers/mcu/display/TimeCodeLed.java new file mode 100644 index 00000000..cfc0e4f8 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/display/TimeCodeLed.java @@ -0,0 +1,337 @@ +package com.bitwig.extensions.controllers.mcu.display; + +import com.bitwig.extensions.controllers.mcu.McuExtension; +import com.bitwig.extensions.controllers.mcu.MidiProcessor; +import com.bitwig.extensions.controllers.mcu.config.McuAssignments; +import com.bitwig.extensions.framework.values.Midi; + +public class TimeCodeLed { + private static final int CODE_BLANK_OFFSET = -16; + private final MidiProcessor midiProcessor; + private double position; + private boolean preCountBeats = false; + private int bars = -1; + private int beats = -1; + private int subDivision = -1; + private int ticks = -1; + + private boolean preCountTime = false; + private int frames = -1; + private int seconds = -1; + private int minutes = -1; + private int hours = -1; + + private int tsMain; + private int tsDiv; + private int tsTicks = 16; + private Mode mode = Mode.BEATS; + private final DisplayType displayType; + + public enum Mode { + BEATS, + TIME + } + + public enum DisplayType { + MCU(-3), + ICON(11); + private final int minusOffset; + + DisplayType(final int minusOffset) { + this.minusOffset = minusOffset; + } + + public int getMinusOffset() { + return minusOffset; + } + + } + + public TimeCodeLed(final MidiProcessor midiProcessor, final DisplayType displayType) { + this.midiProcessor = midiProcessor; + this.displayType = displayType; + } + + public void toggleMode() { + if (mode == Mode.BEATS) { + setMode(Mode.TIME); + } else { + setMode(Mode.BEATS); + } + refreshMode(); + } + + public void refreshMode() { + if (mode == Mode.BEATS) { + midiProcessor.sendMidi(Midi.NOTE_ON, McuAssignments.BEATS_MODE.getNoteNo(), 127); + midiProcessor.sendMidi(Midi.NOTE_ON, McuAssignments.SMPTE_MODE.getNoteNo(), 0); + } else { + midiProcessor.sendMidi(Midi.NOTE_ON, McuAssignments.BEATS_MODE.getNoteNo(), 0); + midiProcessor.sendMidi(Midi.NOTE_ON, McuAssignments.SMPTE_MODE.getNoteNo(), 127); + } + } + + public void ensureMode() { + if (mode == Mode.BEATS) { + midiProcessor.sendMidi(Midi.NOTE_ON, McuAssignments.DISPLAY_SMPTE.getNoteNo(), 127); + } else { + midiProcessor.sendMidi(Midi.NOTE_ON, McuAssignments.DISPLAY_SMPTE.getNoteNo(), 0); + } + } + + private void refreshPosition() { + displayTicksBeat(ticks); + displaySubdivisionBeat(subDivision); + displayBeatsBeat(beats); + displayBarsBeat(bars, preCountBeats); + } + + private void refreshTime() { + displayTicks(frames); + displaySubdivision(seconds); + displayBeats(minutes); + displayBars(hours, preCountTime); + } + + private void displayTicksBeat(final int v) { + final int value = this.preCountBeats ? 99 - v : v; + final int seg2 = value / 10 % 10; + final int seg3 = value / 100 % 10; + final int v1 = value % 10; + final int v2 = zeroToBlank(seg2, seg3); + final int v3 = zeroToBlank(seg3, 0); + midiProcessor.sendMidi(Midi.CC, 64, v1 + 48); + midiProcessor.sendMidi(Midi.CC, 65, v2 + 48); + midiProcessor.sendMidi(Midi.CC, 66, v3 + 48); + } + + private int zeroToBlank(final int v, final int preDigit) { + if (preDigit == 0 && v == 0) { + return CODE_BLANK_OFFSET; + } + return v; + } + + private void displayTicks(final int value) { + final int v1 = value % 10; + final int v2 = value / 10 % 10; + final int v3 = value / 100 % 10; + midiProcessor.sendMidi(Midi.CC, 64, v1 + 48); + midiProcessor.sendMidi(Midi.CC, 65, v2 + 48); + midiProcessor.sendMidi(Midi.CC, 66, v3 + 48); + } + + private void displaySubdivision(final int value) { + final int v1 = value % 10; + final int v2 = value / 10 % 10; + midiProcessor.sendMidi(Midi.CC, 67, v1 + 48 + 64); + midiProcessor.sendMidi(Midi.CC, 68, v2 + 48); + } + + private void displaySubdivisionBeat(final int v) { + final int value = this.preCountBeats ? 5 - v : v; + final int v1 = value % 10; + final int v2 = value / 10 % 10; + midiProcessor.sendMidi(Midi.CC, 67, v1 + 48 + 64); + midiProcessor.sendMidi(Midi.CC, 68, zeroToBlank(v2, 0) + 48); + } + + private void displayBeats(final int value) { + final int v1 = value % 10; + final int v2 = value / 10 % 10; + midiProcessor.sendMidi(Midi.CC, 69, v1 + 48 + 64); + midiProcessor.sendMidi(Midi.CC, 70, v2 + 48); + } + + private void displayBeatsBeat(final int v) { + final int value = this.preCountBeats ? 5 - v : v; + final int v1 = value % 10; + final int v2 = zeroToBlank(value / 10 % 10, 0); + midiProcessor.sendMidi(Midi.CC, 69, v1 + 48 + 64); + midiProcessor.sendMidi(Midi.CC, 70, v2 + 48); + } + + private void displayBars(final int value, final boolean preCount) { + final int v1 = value % 10; + final int v2 = value / 10 % 10; + midiProcessor.sendMidi(Midi.CC, 71, v1 + 48 + 64); + midiProcessor.sendMidi(Midi.CC, 72, v2 + 48); + if (preCount) { + midiProcessor.sendMidi(Midi.CC, 73, 48 + displayType.getMinusOffset()); + } else { + final int v3 = value / 100 % 10; + midiProcessor.sendMidi(Midi.CC, 73, v3 + 48); + } + } + + private void displayBarsBeat(final int value, final boolean preCount) { + final int v1 = value % 10; + final int v2 = value / 10 % 10; + final int v3 = value / 100 % 10; + midiProcessor.sendMidi(Midi.CC, 71, v1 + 48 + 64); + midiProcessor.sendMidi(Midi.CC, 72, zeroToBlank(v2, v3) + 48); + if (preCount) { + midiProcessor.sendMidi(Midi.CC, 73, 48 + displayType.getMinusOffset()); + } else { + midiProcessor.sendMidi(Midi.CC, 73, zeroToBlank(v3, 0) + 48); + } + } + + public Mode getMode() { + return mode; + } + + public void setMode(final Mode mode) { + if (this.mode != mode) { + this.mode = mode; + if (mode == Mode.BEATS) { + refreshPosition(); + } else { + refreshTime(); + } + } + } + + public void setDivision(final String division) { + final String[] v = division.split("/"); + if (v.length == 2) { + tsMain = Integer.parseInt(v[0]); + if (v[1].indexOf(',') > 0) { + final String[] denominator = v[1].split(","); + if (denominator.length == 2) { + tsDiv = Integer.parseInt(denominator[0]); + tsTicks = Integer.parseInt(denominator[1]); + } + } else { + tsDiv = Integer.parseInt(v[1]); + tsTicks = 16; + } + updatePosition(position); + } + } + + public void updatePosition(final double pos) { + position = pos; + final boolean preCount = pos < 0; + McuExtension.println(" POS = %f %s %s", pos, preCount, this.preCountBeats); + final double positionAbs = Math.abs(pos); + final int totalBeats = (int) (positionAbs * tsDiv / 4); + final double rest = positionAbs - (int) positionAbs; + + final int bars = totalBeats / tsMain + 1; + final int beats = totalBeats % tsMain + 1; + final int sub = (int) (rest * 4 * tsTicks / 16) % (int) (16.0 / tsDiv) + 1; + final int ticks = (int) (rest * 400 * tsTicks / 16) % 100; + + if (this.preCountBeats != preCount && mode == Mode.BEATS) { + this.ticks = ticks; + subDivision = sub; + this.beats = beats; + this.bars = bars; + this.preCountBeats = preCount; + refreshPosition(); + } else { + if (ticks != this.ticks) { + this.ticks = ticks; + if (mode == Mode.BEATS) { + displayTicksBeat(ticks); + } + } + if (sub != subDivision) { + subDivision = sub; + if (mode == Mode.BEATS) { + displaySubdivisionBeat(sub); + } + } + if (beats != this.beats) { + this.beats = beats; + if (mode == Mode.BEATS) { + displayBeatsBeat(beats); + } + } + if (bars != this.bars) { + this.bars = bars; + if (mode == Mode.BEATS) { + displayBarsBeat(bars, preCount); + } + } + } + } + + public void updateTime(final double seconds) { + final boolean preCount = seconds < 0; + final int secondsTotal = (int) Math.abs(seconds); + final double rest = Math.abs(seconds) - secondsTotal; + final int secs = secondsTotal % 60; + final int minutes = secondsTotal / 60 % 60; + final int hours = secondsTotal / 60 / 60; + final int frames = (int) Math.round(rest * 24); + + if (frames != this.frames) { + this.frames = frames; + if (mode == Mode.TIME) { + displayTicks(frames); + } + } + if (secs != this.seconds) { + this.seconds = secs; + if (mode == Mode.TIME) { + displaySubdivision(secs); + } + } + if (minutes != this.minutes) { + this.minutes = minutes; + if (mode == Mode.TIME) { + displayBeats(minutes); + } + } + if (hours != this.hours || preCount != preCountTime) { + this.hours = hours; + this.preCountTime = preCount; + if (mode == Mode.TIME) { + displayBars(hours, preCount); + } + } + } + + public void setAssignment(final String ch, final boolean dotted) { + if (ch.length() != 2) { + return; + } + final char c1 = ch.charAt(0); + final char c2 = ch.charAt(1); + midiProcessor.sendMidi(Midi.CC, 75, toCharValue(c1)); + midiProcessor.sendMidi(Midi.CC, 74, toCharValue(c2) | (dotted ? 0x20 : 0x0)); + } + + private int toCharValue(final char c) { + if (c >= 97) { + return c - 96; + } + if (c >= 65) { + return c - 64; + } + if (c >= 48) { + return c; + } + return 0; + } + + public void setAssignment(final String ch) { + if (ch.length() != 2) { + return; + } + final char c1 = ch.charAt(0); + final char c2 = ch.charAt(1); + midiProcessor.sendMidi(Midi.CC, 75, toCharValue(c1)); + midiProcessor.sendMidi(Midi.CC, 74, toCharValue(c2)); + } + + public void clearAll() { + for (int cc = 64; cc < 76; cc++) { + midiProcessor.sendMidi(Midi.CC, cc, 0); + } + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/display/VuMode.java b/src/main/java/com/bitwig/extensions/controllers/mcu/display/VuMode.java new file mode 100644 index 00000000..3e4f5a54 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/display/VuMode.java @@ -0,0 +1,7 @@ +package com.bitwig.extensions.controllers.mcu.display; + +public enum VuMode { + LED, + LED_LCD_VERTICAL, + LED_LCD_HORIZONTAL +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/AllSendsModeLayer.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/AllSendsModeLayer.java new file mode 100644 index 00000000..b4a6dd76 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/AllSendsModeLayer.java @@ -0,0 +1,71 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +import com.bitwig.extensions.controllers.mcu.VPotMode; + +public class AllSendsModeLayer extends MixerModeLayer { + + public AllSendsModeLayer(final ControlMode mode, final MixerSection mixer) { + super(mode, mixer); + } + + public void handleModePress(final VPotMode mode, final boolean pressed, final boolean selection) { + if (!active) { + return; + } + if (!selection) { + if (pressed) { + mixer.activateSendPrePostMenu(); + } else { + mixer.releaseLayer(); + } + } + } + + public void assign() { + final boolean flipped = mixer.isFlipped(); + final boolean touched = mixer.isTouched(); + + final ControlMode mainMode = !flipped ? mode : ControlMode.VOLUME; + final ControlMode lowMode = flipped ? mode : ControlMode.VOLUME; + + encoderLayer = mixer.getLayerSource(mainMode).getEncoderLayer(); + faderLayer = mixer.getLayerSource(lowMode).getFaderLayer(); + if (mixer.hasLowerDisplay()) { + assignDualDisplay(); + } else { + final ControlMode displayMode = touched ? lowMode : mainMode; + final boolean nameValue = mixer.isNameValue(); + if (mixer.isFlipped()) { + displayLabelLayer = + nameValue ? mixer.getTrackDisplayLayer() : mixer.getLayerSource(displayMode).getDisplayLabelLayer(); + } else { + displayLabelLayer = + nameValue ? mixer.getTrackDisplayLayer() : mixer.getLayerSource(displayMode).getDisplayLabelLayer(); + } + displayValueLayer = mixer.getLayerSource(displayMode).getDisplayValueLayer(); + } + assignIfMenuModeActive(); + } + + private void assignDualDisplay() { + final boolean nameValue = mixer.isNameValue(); + if (mixer.isFlipped()) { + displayLabelLayer = nameValue + ? mixer.getLayerSource(ControlMode.VOLUME).getDisplayValueLayer() + : mixer.getTrackDisplayLayer(); + displayValueLayer = mixer.getLayerSource(ControlMode.VOLUME).getDisplayValueLayer(); + displayLowerValueLayer = mixer.getLayerSource(mode).getDisplayValueLayer(); + displayLowerLabelLayer = mixer.getLayerSource(mode).getDisplayLabelLayer(); + mixer.setUpperLowerDestination(ControlMode.VOLUME, mode); + } else { + displayLabelLayer = mixer.getLayerSource(mode).getDisplayValueLayer(); + displayValueLayer = mixer.getLayerSource(mode).getDisplayLabelLayer(); + displayLowerLabelLayer = nameValue + ? mixer.getLayerSource(ControlMode.VOLUME).getDisplayValueLayer() + : mixer.getTrackDisplayLayer(); + displayLowerValueLayer = mixer.getLayerSource(ControlMode.VOLUME).getDisplayValueLayer(); + mixer.setUpperLowerDestination(mode, ControlMode.VOLUME); + } + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/ClipLaunchingLayer.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/ClipLaunchingLayer.java new file mode 100644 index 00000000..3734d650 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/ClipLaunchingLayer.java @@ -0,0 +1,134 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +import com.bitwig.extension.controller.api.ClipLauncherSlot; +import com.bitwig.extension.controller.api.ClipLauncherSlotBank; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.controllers.mcu.GlobalStates; +import com.bitwig.extensions.controllers.mcu.TimedProcessor; +import com.bitwig.extensions.controllers.mcu.ViewControl; +import com.bitwig.extensions.controllers.mcu.config.ControllerConfig; +import com.bitwig.extensions.controllers.mcu.config.McuFunction; +import com.bitwig.extensions.controllers.mcu.control.McuButton; +import com.bitwig.extensions.controllers.mcu.control.MixerSectionHardware; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.di.Context; +import com.bitwig.extensions.framework.values.LayoutType; + +public class ClipLaunchingLayer extends Layer { + private final Layer launcherLayer; + //private final Layer arrangerLayer; + private final TrackBank trackBank; + private final LayoutType layoutType = LayoutType.LAUNCHER; + private final TimedProcessor timedProcessor; + private final int trackOffset; + private final GlobalStates gobalStates; + + public ClipLaunchingLayer(final Context diContext, final MixerSectionHardware hwElements, + final MixerSection mixerSection) { + super(diContext.getService(Layers.class), "CLIP_LAUNCH_%d".formatted(mixerSection.getSectionIndex() + 1)); + final ControllerConfig config = diContext.getService(ControllerConfig.class); + timedProcessor = diContext.getService(TimedProcessor.class); + launcherLayer = diContext.createLayer("VERTICAL_%d".formatted(mixerSection.getSectionIndex() + 1)); + //arrangerLayer = diContext.createLayer("HORIZONTAL_%d".formatted(mixerSection.getSectionIndex() + 1)); + trackOffset = mixerSection.getSectionIndex() * 8; + trackBank = diContext.getService(ViewControl.class).getMainTrackBank(); + this.gobalStates = diContext.getService(GlobalStates.class); + final boolean use2Lanes = config.getAssignment(McuFunction.CLIP_LAUNCHER_MODE_2) != null; + final int numberOfScenes = use2Lanes ? 2 : 4; + + for (int index = 0; index < 8; index++) { + final int trackIndex = index + trackOffset; + final Track track = trackBank.getItemAt(trackIndex); + final ClipLauncherSlotBank slotBank = track.clipLauncherSlotBank(); + for (int slotIndex = 0; slotIndex < numberOfScenes; slotIndex++) { + final ClipLauncherSlot slot = slotBank.getItemAt(slotIndex); + prepareSlot(slot); + final McuButton button = use2Lanes + ? hwElements.getButtonFromGridBy2Lane(slotIndex, index) + : hwElements.getButtonFromGridBy4Lane(slotIndex, index); + button.bindLight(launcherLayer, () -> getLightState(slot)); + button.bindIsPressed(launcherLayer, pressed -> handleSlotPressed(slot, pressed)); + } + } + } + + private static void prepareSlot(final ClipLauncherSlot slot) { + slot.exists().markInterested(); + slot.hasContent().markInterested(); + slot.isPlaying().markInterested(); + slot.isPlaybackQueued().markInterested(); + slot.isRecording().markInterested(); + slot.isRecordingQueued().markInterested(); + slot.isStopQueued().markInterested(); + } + + private boolean getLightState(final ClipLauncherSlot slot) { + if (!slot.exists().get()) { + return false; + } + if (slot.hasContent().get()) { + if (slot.isPlaybackQueued().get() || slot.isRecordingQueued().get() || slot.isPlaybackQueued().get() + || slot.isStopQueued().get()) { + return timedProcessor.blinkMid(); + } else if (slot.isRecording().get()) { + return timedProcessor.blinkPeriodic(); + } else if (slot.isPlaying().get()) { + return timedProcessor.blinkSlow(); + } + return true; + } + if (slot.isPlaybackQueued().get() || slot.isRecordingQueued().get()) { + return timedProcessor.blinkFast(); + } + + return false; + } + + private void handleSlotPressed(final ClipLauncherSlot slot, final boolean pressed) { + if (gobalStates.getClearHeld().get()) { + if (pressed) { + slot.deleteObject(); + } + } else if (gobalStates.getDuplicateHeld().get()) { + if (pressed) { + slot.duplicateClip(); + } + } else { + if (pressed) { + slot.launch(); + } else { + slot.launchRelease(); + } + } + } + + @Override + protected void onActivate() { + applyLayer(); + } + + private void applyLayer() { + if (!isActive()) { + return; + } + // if (layoutType == LayoutType.ARRANGER) { + // launcherLayer.deactivate(); + // arrangerLayer.activate(); + // } else { + // arrangerLayer.deactivate(); + // launcherLayer.activate(); + // } + launcherLayer.activate(); + trackBank.setShouldShowClipLauncherFeedback(true); + } + + @Override + protected void onDeactivate() { + trackBank.setShouldShowClipLauncherFeedback(false); + launcherLayer.deactivate(); + //arrangerLayer.deactivate(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/ControlMode.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/ControlMode.java new file mode 100644 index 00000000..71bd3d54 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/ControlMode.java @@ -0,0 +1,28 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +public enum ControlMode { + VOLUME(true), + PAN(true), + SENDS(true), + TRACK, + STD_PLUGIN, + EQ, + DEVICE, + TRACK_REMOTES, + PROJECT_REMOTES, + MENU; + + private final boolean isMixer; + + ControlMode() { + this(false); + } + + ControlMode(final boolean isMixer) { + this.isMixer = isMixer; + } + + public boolean isMixer() { + return isMixer; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/DeviceModeLayer.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/DeviceModeLayer.java new file mode 100644 index 00000000..e27005d2 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/DeviceModeLayer.java @@ -0,0 +1,197 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +import com.bitwig.extensions.controllers.mcu.StringUtil; +import com.bitwig.extensions.controllers.mcu.VPotMode; +import com.bitwig.extensions.controllers.mcu.bindings.display.StringRowDisplayBinding; +import com.bitwig.extensions.controllers.mcu.display.DisplayRow; +import com.bitwig.extensions.controllers.mcu.value.Orientation; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.values.BasicStringValue; + +public abstract class DeviceModeLayer extends MixerModeLayer { + protected final Layer topRowValueLayer; + protected final Layer bottomRowValueLayer; + + protected final BasicStringValue topRowInfoText; + protected final BasicStringValue bottomRowInfoText; + protected VPotMode potMode = VPotMode.PLUGIN; + protected String deviceName = ""; + protected String pageName = ""; + protected String deviceInfo = ""; + protected State matchState = State.TYPE_MATCH; + protected boolean justChangedTo = false; + protected String infoText = null; + + protected enum State { + TYPE_MATCH, + TYPE_MISMATCH, + TYPE_NOT_IN_CHAIN, + NO_PARAM_PAGES + } + + public DeviceModeLayer(final Layers layers, final ControlMode mode, final MixerSection mixer) { + super(mode, mixer); + this.topRowValueLayer = new Layer(layers, "INFO_LABEL_%s_%d".formatted(mode, mixer.getSectionIndex())); + this.bottomRowValueLayer = new Layer(layers, "INFO_VALUE_%s_%d".formatted(mode, mixer.getSectionIndex())); + topRowInfoText = new BasicStringValue(""); + bottomRowInfoText = new BasicStringValue(""); + this.topRowValueLayer.addBinding( + new StringRowDisplayBinding(mixer.getDisplayManager(), mode, DisplayRow.LABEL, mixer.getSectionIndex(), + topRowInfoText)); + this.bottomRowValueLayer.addBinding( + new StringRowDisplayBinding(mixer.getDisplayManager(), mode, DisplayRow.VALUE, mixer.getSectionIndex(), + bottomRowInfoText)); + } + + public void updateDeviceName(final String deviceName) { + this.deviceName = deviceName; + this.deviceInfo = "Device %s Page: %s".formatted(deviceName, 14, pageName); + if (matchState == State.TYPE_MATCH) { + bottomRowInfoText.set(deviceInfo); + } + } + + public void updateParameterPage(final String parameterPage) { + this.pageName = StringUtil.padEnd(parameterPage, 14); + this.deviceInfo = "Device %s Page: %s".formatted(deviceName, pageName); + if (matchState == State.TYPE_MATCH) { + bottomRowInfoText.set(deviceInfo); + } + } + + public void setPotMode(final VPotMode potMode) { + justChangedTo = this.potMode != potMode; + this.potMode = potMode; + if (active) { + evalInfoState(potMode); + } + } + + protected abstract void evalInfoState(final VPotMode potMode); + + @Override + public void handleModePress(final VPotMode mode, final boolean pressed, final boolean selection) { + if (!active) { + return; + } + if (pressed) { + evalInfoState(mode); + if (!justChangedTo && matchState == State.TYPE_MATCH) { + mixer.activateMenu(mixer.getDeviceModeLayer()); + } + reassign(); + justChangedTo = false; + } else { + mixer.releaseLayer(); + } + } + + @Override + public void handleInfoState(final boolean start, final Orientation orientation) { + if (active) { + if (start) { + infoText = "D: %s Page: %s".formatted(deviceName, pageName); + } else { + infoText = null; + } + reassign(); + } + } + + @Override + public void assign() { + final ControlMode mainMode = !mixer.isFlipped() ? mode : ControlMode.VOLUME; + final ControlMode lowMode = mixer.isFlipped() ? mode : ControlMode.VOLUME; + evalDeviceMatch(); + determineInfoState(mixer.isFlipped()); + encoderLayer = mixer.getLayerSource(mainMode).getEncoderLayer(); + faderLayer = mixer.getLayerSource(lowMode).getFaderLayer(); + + if (mixer.hasLowerDisplay()) { + assignDualDisplay(); + } else { + assignSingleDisplay(mainMode, lowMode); + } + assignIfMenuModeActive(); + } + + protected abstract void evalDeviceMatch(); + + protected abstract void determineInfoState(final boolean flipped); + + protected void assignDualDisplay() { + final boolean nameValue = mixer.isNameValue(); + final boolean isFlipped = mixer.isFlipped(); + if (isFlipped) { + mixer.setUpperLowerDestination(ControlMode.VOLUME, mode); + + displayLabelLayer = mixer.getTrackDisplayLayer(); + displayValueLayer = mixer.getLayerSource(ControlMode.VOLUME).getDisplayValueLayer(); + displayLowerLabelLayer = mixer.getLayerSource(mode).getDisplayLabelLayer(); + displayLowerValueLayer = mixer.getLayerSource(mode).getDisplayValueLayer(); + if (matchState != State.TYPE_MATCH) { + displayLowerLabelLayer = topRowValueLayer; + displayLowerValueLayer = bottomRowValueLayer; + } else if (nameValue) { + displayLabelLayer = mixer.getLayerSource(ControlMode.VOLUME).getDisplayLabelLayer(); + displayLowerValueLayer = bottomRowValueLayer; + } + if (infoText != null && matchState == State.TYPE_MATCH) { + topRowInfoText.set(infoText); + displayLowerLabelLayer = topRowValueLayer; + } + } else { + mixer.setUpperLowerDestination(mode, ControlMode.VOLUME); + if (matchState != State.TYPE_MATCH) { + displayLabelLayer = topRowValueLayer; + displayValueLayer = bottomRowValueLayer; + } else { + displayLabelLayer = mixer.getLayerSource(mode).getDisplayLabelLayer(); + displayValueLayer = mixer.getLayerSource(mode).getDisplayValueLayer(); + if (nameValue) { + displayValueLayer = bottomRowValueLayer; + } + } + displayLowerValueLayer = mixer.getLayerSource(ControlMode.VOLUME).getDisplayValueLayer(); + displayLowerLabelLayer = nameValue + ? mixer.getLayerSource(ControlMode.VOLUME).getDisplayLabelLayer() + : mixer.getTrackDisplayLayer(); + if (infoText != null && matchState == State.TYPE_MATCH) { + topRowInfoText.set(infoText); + displayLabelLayer = topRowValueLayer; + } + } + } + + protected void assignSingleDisplay(final ControlMode mainMode, final ControlMode lowMode) { + final boolean touched = mixer.isTouched(); + final boolean nameValue = mixer.isNameValue(); + final ControlMode displayMode = touched ? lowMode : mainMode; + + if (displayMode == ControlMode.STD_PLUGIN) { + displayLabelLayer = mixer.getLayerSource(displayMode).getDisplayLabelLayer(); + if (matchState != State.TYPE_MATCH) { + displayLabelLayer = topRowValueLayer; + displayValueLayer = bottomRowValueLayer; + encoderLayer = mixer.getEmptyEncoderLayer(); + } else { + if (nameValue) { + displayValueLayer = bottomRowValueLayer; + } else { + displayValueLayer = mixer.getLayerSource(displayMode).getDisplayValueLayer(); + } + } + } else { + displayLabelLayer = nameValue + ? mixer.getLayerSource(ControlMode.VOLUME).getDisplayLabelLayer() + : mixer.getTrackDisplayLayer(); + displayValueLayer = mixer.getLayerSource(displayMode).getDisplayValueLayer(); + } + if (infoText != null && matchState == State.TYPE_MATCH) { + topRowInfoText.set(infoText); + displayLabelLayer = topRowValueLayer; + } + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/JogWheelTransportHandler.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/JogWheelTransportHandler.java new file mode 100644 index 00000000..7f0a4386 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/JogWheelTransportHandler.java @@ -0,0 +1,139 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +import com.bitwig.extension.controller.api.Arranger; +import com.bitwig.extension.controller.api.Transport; +import com.bitwig.extensions.controllers.mcu.GlobalStates; +import com.bitwig.extensions.controllers.mcu.TimedProcessor; +import com.bitwig.extensions.controllers.mcu.config.McuFunction; +import com.bitwig.extensions.controllers.mcu.control.MainHardwareSection; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.di.Context; + +public class JogWheelTransportHandler { + private static final double[] FFWD_SPEEDS = {4.0, 8.0, 12.0, 16.0}; + private static final double[] FFWD_SPEEDS_SHIFT = {0.25, 1, 2.0, 4.0}; + private static final int[] STAGE_MULTIPLIER = {1, 2, 4, 8, 8, 16, 32, 64}; + private final MainSection mainSection; + private final TimedProcessor timedProcessor; + private final GlobalStates states; + private final Transport transport; + private long lastJogIncrement = 0L; + private int lastJogDir = 0; + private int jogWheelClickCount = 0; + private long timer; + private MarkerAction pendingMarkerAction = MarkerAction.NONE; + public static final int STAGE_HOLD_FACTOR = 20; + + private enum MarkerAction { + NEXT, + PREVIOUS, + NONE + } + + public JogWheelTransportHandler(final MainSection mainSection, final Context context, + final MainHardwareSection hwElements) { + final Layer layer = mainSection.getMainLayer(); + states = context.getService(GlobalStates.class); + transport = context.getService(Transport.class); + final Arranger arranger = context.getService(Arranger.class); + transport.playStartPosition().markInterested(); + this.mainSection = mainSection; + this.timedProcessor = context.getService(TimedProcessor.class); + + hwElements.bindJogWheel(layer, this::jogWheelPlayPosition); + hwElements.getButton(McuFunction.FAST_FORWARD).ifPresent(fastForwardButton -> { + fastForwardButton.bindHeldLight(layer); + fastForwardButton.bindRepeatHold(layer, + count -> movePlayPos(1, Math.min(count / STAGE_HOLD_FACTOR, FFWD_SPEEDS_SHIFT.length - 1)), + () -> pendingMarkerAction = MarkerAction.NEXT); + }); + hwElements.getButton(McuFunction.FAST_REVERSE).ifPresent(fastReverseButton -> { + fastReverseButton.bindHeldLight(layer); + fastReverseButton.bindRepeatHold(layer, // + count -> movePlayPos(-1, Math.min(count / STAGE_HOLD_FACTOR, FFWD_SPEEDS_SHIFT.length - 1)), + () -> pendingMarkerAction = MarkerAction.PREVIOUS); + }); + hwElements.getButton(McuFunction.CUE_MARKER) + .ifPresent(cueButton -> cueButton.bindIsPressed(layer, pressed -> handleMarkerPressed(pressed))); + } + + private void jogWheelPlayPosition(final int dir) { + double resolution = 0.25; + if (states.isOptionSet()) { + resolution = 4.0; + } else if (states.isShiftSet()) { + resolution = 0.125 / 4; + } + final int stage = getStage(dir, STAGE_MULTIPLIER.length - 1, 40); + + changePlayPosition(dir, resolution * STAGE_MULTIPLIER[stage], !states.isOptionSet(), !states.isControlSet()); + lastJogDir = dir; + lastJogIncrement = System.currentTimeMillis(); + } + + private void movePlayPos(final int dir, final int stageIndex) { + if (states.isShiftSet()) { + changePlayPosition(dir, FFWD_SPEEDS_SHIFT[stageIndex], true, true); + } else { + changePlayPosition(dir, FFWD_SPEEDS[stageIndex], true, true); + } + } + + private void handleMarkerPressed(final boolean pressed) { + if (pressed) { + timer = System.currentTimeMillis(); + timedProcessor.delayTask(() -> { + if (timer != -1) { + mainSection.invokeCueMenu(); + } + }, 5); + } else if (pendingMarkerAction != MarkerAction.NONE) { + if (pendingMarkerAction == MarkerAction.NEXT) { + transport.jumpToNextCueMarker(); + } else { + transport.jumpToPreviousCueMarker(); + } + pendingMarkerAction = MarkerAction.NONE; + timer = -1; + mainSection.releaseMenu(); + } else { + timer = -1; + mainSection.releaseMenu(); + } + } + + private int getStage(final int dir, final int nrOfStages, final int factor) { + final long timeLastChange = System.currentTimeMillis() - lastJogIncrement; + int stage = 0; + if (lastJogDir == dir && timeLastChange < 200) { + jogWheelClickCount++; + stage = Math.min(nrOfStages, jogWheelClickCount / factor); + } else { + jogWheelClickCount = 0; + } + return stage; + } + + private void changePlayPosition(final int inc, final double resolution, final boolean restrictToStart, + final boolean quantize) { + + final double position = transport.playStartPosition().get(); + double newPos = position + resolution * inc; + + if (restrictToStart && newPos < 0) { + newPos = 0; + } + + if (position != newPos) { + if (quantize) { + final double intPosition = Math.floor(newPos / resolution); + newPos = intPosition * resolution; + } + transport.playStartPosition().set(newPos); + if (transport.isPlaying().get()) { + transport.jumpToPlayStartPosition(); + } + } + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/LayerGroup.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/LayerGroup.java new file mode 100644 index 00000000..f025a394 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/LayerGroup.java @@ -0,0 +1,196 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +import com.bitwig.extension.controller.api.BooleanValue; +import com.bitwig.extension.controller.api.HardwareActionBindable; +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extension.controller.api.RelativeHardwarControlBindable; +import com.bitwig.extension.controller.api.SettableBooleanValue; +import com.bitwig.extension.controller.api.SettableRangedValue; +import com.bitwig.extension.controller.api.StringValue; +import com.bitwig.extensions.controllers.mcu.StringUtil; +import com.bitwig.extensions.controllers.mcu.bindings.display.DisplayTarget; +import com.bitwig.extensions.controllers.mcu.bindings.display.ModelessDisplayBinding; +import com.bitwig.extensions.controllers.mcu.bindings.display.StringDisplayBinding; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; +import com.bitwig.extensions.controllers.mcu.control.RingEncoder; +import com.bitwig.extensions.controllers.mcu.display.DisplayManager; +import com.bitwig.extensions.controllers.mcu.display.DisplayRow; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.di.Context; +import com.bitwig.extensions.framework.values.BasicStringValue; +import com.bitwig.extensions.framework.values.IntValueObject; + +public class LayerGroup { + private static final BasicStringValue EMPTY = new BasicStringValue(""); + + private final Layer labelLayer; + private final Layer valueLayer; + private final Layer encoderLayer; + private final String name; + + public LayerGroup(final Context context, final String name) { + this.name = name; + this.labelLayer = context.createLayer("LABEL_%s".formatted(name)); + this.valueLayer = context.createLayer("VALUE_%s".formatted(name)); + this.encoderLayer = context.createLayer("ENCODER_%s".formatted(name)); + } + + public String getName() { + return name; + } + + public Layer getLabelLayer() { + return labelLayer; + } + + public Layer getEncoderLayer() { + return encoderLayer; + } + + public Layer getValueLayer() { + return valueLayer; + } + + public void bindControls(final RingEncoder encoder, final RingDisplayType ringType, final Parameter parameter) { + encoder.bindParameter(this.encoderLayer, parameter, ringType); + } + + public void bindControls(final RingEncoder encoder, final RingDisplayType ringType, + final SettableRangedValue parameter) { + encoder.bindValue(this.encoderLayer, parameter, ringType); + } + + public void bindControls(final RingEncoder encoder, final RelativeHardwarControlBindable bindable) { + encoder.bind(this.encoderLayer, bindable); + } + + public void bindPressAction(final RingEncoder encoder, final Runnable action) { + encoder.bindPressed(this.encoderLayer, action); + } + + public void bindControls(final RingEncoder encoder, final RingDisplayType ringType, + final SettableRangedValue parameter, final double sensitivity) { + encoder.bindValue(this.encoderLayer, parameter, ringType, sensitivity); + } + + public void bindEncoderIsPressed(final RingEncoder encoder, final Consumer booleanConsumer) { + encoder.bindIsPressed(encoderLayer, booleanConsumer); + } + + public void bindEncoderIsPressed(final RingEncoder encoder, final SettableBooleanValue booleanValue) { + encoder.bindIsPressed(encoderLayer, booleanValue); + } + + public void bindEncoderPressed(final RingEncoder encoder, final Runnable action) { + encoder.bindPressed(encoderLayer, action); + } + + public void bindRingValue(final RingEncoder encoder, final BooleanValue value) { + encoder.bindRingValue(encoderLayer, value); + } + + public void bindRingValue(final RingEncoder encoder, final IntValueObject value) { + encoder.bindRingValue(encoderLayer, value); + } + + public void bindEncoderIncrement(final RingEncoder encoder, final IntConsumer incHandler, + final double incrementMultiplier) { + encoder.bindIncrement(encoderLayer, incHandler, incrementMultiplier); + } + + public void bindEncoderIncrement(final RingEncoder encoder, final HardwareActionBindable incAction, + final HardwareActionBindable decAction, final double incrementMultiplier) { + encoder.bindIncrement(encoderLayer, inc -> { + if (inc > 0) { + incAction.invoke(); + } else { + decAction.invoke(); + } + }, incrementMultiplier); + } + + + public void bindControlsInc(final RingEncoder encoder, final IntConsumer incHandler, final Parameter parameter, + final RingDisplayType type, final double incrementMultiplier) { + encoder.bindIncrement(encoderLayer, incHandler, incrementMultiplier); + encoder.bindRingValue(encoderLayer, parameter, type); + } + + public void bindEncoderTurnedBoolean(final RingEncoder encoder, final SettableBooleanValue value) { + encoder.bindIncrement(encoderLayer, value); + } + + public void bindDisplay(final DisplayManager displayManager, final Parameter parameter, final int index) { + final BooleanValue exists = parameter.exists(); + final StringValue labelValue = parameter.name(); + labelLayer.addBinding(new StringDisplayBinding(displayManager, ControlMode.MENU, + DisplayTarget.of(DisplayRow.LABEL, index, parameter), labelValue, exists, + name -> StringUtil.reduceAscii(name, 6))); + valueLayer.addBinding(new StringDisplayBinding(displayManager, ControlMode.MENU, + DisplayTarget.of(DisplayRow.VALUE, index, parameter), parameter.displayedValue(), exists)); + } + + public void bindDisplay(final DisplayManager displayManager, final String label, final SettableRangedValue value, + final int index) { + final StringValue labelValue = new BasicStringValue(label); + labelLayer.addBinding( + new ModelessDisplayBinding(displayManager, DisplayTarget.of(DisplayRow.LABEL, index, value), labelValue)); + valueLayer.addBinding( + new ModelessDisplayBinding(displayManager, DisplayTarget.of(DisplayRow.VALUE, index, value), + value.displayedValue())); + } + + public void bindDisplay(final DisplayManager displayManager, final StringValue label, final StringValue value, + final int index) { + labelLayer.addBinding( + new ModelessDisplayBinding(displayManager, DisplayTarget.of(DisplayRow.LABEL, index, value), label)); + valueLayer.addBinding( + new ModelessDisplayBinding(displayManager, DisplayTarget.of(DisplayRow.VALUE, index, value), value)); + } + + public void bindDisplay(final DisplayManager displayManager, final String label, final StringValue value, + final int index) { + final StringValue labelValue = new BasicStringValue(label); + labelLayer.addBinding( + new ModelessDisplayBinding(displayManager, DisplayTarget.of(DisplayRow.LABEL, index, value), labelValue)); + valueLayer.addBinding( + new ModelessDisplayBinding(displayManager, DisplayTarget.of(DisplayRow.VALUE, index, value), value)); + } + + public void bindDisplay(final DisplayManager displayManager, final String name, final BooleanValue value, + final int index) { + value.markInterested(); + final BasicStringValue onOffValue = new BasicStringValue(value.get() ? " ON" : " OFF"); + value.addValueObserver(val -> onOffValue.set(val ? " ON" : " OFF")); + labelLayer.addBinding( + new ModelessDisplayBinding(displayManager, DisplayTarget.of(DisplayRow.LABEL, index, value), + new BasicStringValue(name))); + valueLayer.addBinding( + new ModelessDisplayBinding(displayManager, DisplayTarget.of(DisplayRow.VALUE, index, value), onOffValue)); + } + + public void bindEncoderEmpty(final RingEncoder encoder) { + encoder.bindEmpty(encoderLayer); + } + + public void bindEmpty(final DisplayManager displayManager, final RingEncoder encoder, final int index) { + encoder.bindEmpty(encoderLayer); + final Object idObject = new Object(); + labelLayer.addBinding( + new ModelessDisplayBinding(displayManager, DisplayTarget.of(DisplayRow.LABEL, index, idObject), EMPTY)); + valueLayer.addBinding( + new ModelessDisplayBinding(displayManager, DisplayTarget.of(DisplayRow.VALUE, index, idObject), EMPTY)); + } + + public void bindRingToIsPressed(final RingEncoder ringEncoder) { + ringEncoder.bindRingPressed(encoderLayer); + } + + + public void bindRingEmpty(final RingEncoder ringEncoder) { + ringEncoder.bindRingEmpty(encoderLayer); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MainSection.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MainSection.java new file mode 100644 index 00000000..7af4f224 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MainSection.java @@ -0,0 +1,641 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +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.CueMarkerBank; +import com.bitwig.extension.controller.api.CursorRemoteControlsPage; +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.mcu.GlobalStates; +import com.bitwig.extensions.controllers.mcu.McuExtension; +import com.bitwig.extensions.controllers.mcu.TimedProcessor; +import com.bitwig.extensions.controllers.mcu.VPotMode; +import com.bitwig.extensions.controllers.mcu.ViewControl; +import com.bitwig.extensions.controllers.mcu.config.ControllerConfig; +import com.bitwig.extensions.controllers.mcu.config.McuFunction; +import com.bitwig.extensions.controllers.mcu.control.MainHardwareSection; +import com.bitwig.extensions.controllers.mcu.control.McuButton; +import com.bitwig.extensions.controllers.mcu.control.MixerSectionHardware; +import com.bitwig.extensions.controllers.mcu.devices.DeviceTypeBank; +import com.bitwig.extensions.controllers.mcu.display.DisplayManager; +import com.bitwig.extensions.controllers.mcu.display.TimeCodeLed; +import com.bitwig.extensions.controllers.mcu.value.Orientation; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.di.Context; +import com.bitwig.extensions.framework.time.TimeRepeatEvent; +import com.bitwig.extensions.framework.values.LayoutType; + +public class MainSection { + + private static final int SCROLL_REPEAT_INTERVAL = 100; + private static final long SCROLL_START_TIME = 600; + private final GlobalStates states; + private final Layer mainLayer; + private final Layer shiftLayer; + private final Layer zoomLayer; + private final ViewControl viewControl; + private final DeviceTypeBank deviceTypeBank; + private final List listeners = new ArrayList<>(); + private final List directNavigationListeners = new ArrayList<>(); + private final List navigationInfoListeners = new ArrayList<>(); + private String autoMode = "latch"; + private LayerGroup metroMenuLayer; + private LayerGroup tempoMenuLayer; + private LayerGroup cueMarkerMenuLayer; + private LayerGroup sslDeviceMenuLayer; + private LayerGroup loopMenuLayer; + private LayerGroup groveMenuLayer; + private LayerGroup zoomMenuLayer; + private MixerSection attachedMixerLayer; + private final Transport transport; + private final CueMarkerBank cueMarkerBank; + private final BeatTimeFormatter formatter; + private final Application application; + private final TimedProcessor timedProcessor; + private TimeRepeatEvent scrollEvent = null; + private LayoutType currentLayoutType; + public static final int DECELERATION_THRESHOLD_MS = 100; + long lastNavMessage = -1; + + + @FunctionalInterface + public interface ModeChangeListener { + void handleModeSelected(VPotMode potMode, boolean pressed, boolean selection); + } + + @FunctionalInterface + public interface DirectNavigationListener { + void handDirectNavigation(VPotMode mode, int index, boolean pressed); + } + + @FunctionalInterface + public interface NavigationInfoListener { + void handleNavigationInfo(boolean start, Orientation orientation); + } + + public MainSection(final Context context, final MainHardwareSection hwElements) { + mainLayer = context.createLayer("MAIN_CONTROL"); + shiftLayer = context.createLayer("MAIN_CONTROL_SHIFT"); + zoomLayer = context.createLayer("ZOOM"); + + this.states = context.getService(GlobalStates.class); + timedProcessor = context.getService(TimedProcessor.class); + this.viewControl = context.getService(ViewControl.class); + this.deviceTypeBank = context.getService(DeviceTypeBank.class); + this.application = context.getService(Application.class); + final ControllerHost host = context.getService(ControllerHost.class); + this.transport = context.getService(Transport.class); + transport.getPosition().markInterested(); + cueMarkerBank = viewControl.getArranger().createCueMarkerBank(8); + formatter = host.createBeatTimeFormatter(":", 2, 1, 1, 0); + final ControllerConfig config = context.getService(ControllerConfig.class); + initModifiers(hwElements); + initSendSelector(hwElements); + + hwElements.getButton(McuFunction.FLIP) + .ifPresent(button -> button.bindToggle(mainLayer, this.states.getFlipped())); + hwElements.getButton(McuFunction.NAME_VALUE) + .ifPresent(button -> button.bindMomentary(mainLayer, this.states.getNameValue())); + + if (config.isNoDedicatedZoom()) { + hwElements.getButton(McuFunction.ZOOM) + .ifPresent(button -> button.bindPressed(mainLayer, () -> this.states.getZoomMode().toggle())); + } else { + hwElements.getButton(McuFunction.ZOOM) + .ifPresent(button -> button.bindToggle(mainLayer, this.states.getZoomMode())); + } + this.states.getZoomMode().addValueObserver(zoomActive -> { + zoomLayer.setIsActive(zoomActive); + }); + potMode(hwElements); + initTransport(hwElements, context.getService(Transport.class), context.getService(Application.class)); + initNavigation(hwElements, config); + final JogWheelTransportHandler jogWheelTransportHandler = + new JogWheelTransportHandler(this, context, hwElements); + } + + private void initModifiers(final MainHardwareSection hwElements) { + hwElements.getButton(McuFunction.GLOBAL_VIEW) + .ifPresent(button -> button.bindToggle(mainLayer, this.states.getGlobalView())); + hwElements.getButton(McuFunction.SHIFT).ifPresent(button -> { + button.bindMomentary(mainLayer, this.states.getShift()); + this.states.getShift().addValueObserver(shiftLayer::setIsActive); + }); + hwElements.getButton(McuFunction.CONTROL) + .ifPresent(button -> button.bindMomentary(mainLayer, this.states.getControl())); + hwElements.getButton(McuFunction.OPTION) + .ifPresent(button -> button.bindMomentary(mainLayer, this.states.getOption())); + hwElements.getButton(McuFunction.CLIP_LAUNCHER_MODE_2).ifPresent(button -> { + button.bindToggle(mainLayer, states.getClipLaunchingActive()); + }); + hwElements.getButton(McuFunction.CLEAR) + .ifPresent(button -> button.bindMomentary(mainLayer, this.states.getClearHeld())); + hwElements.getButton(McuFunction.DUPLICATE) + .ifPresent(button -> button.bindMomentary(mainLayer, this.states.getDuplicateHeld())); + hwElements.getButton(McuFunction.CLIP_LAUNCHER_MODE_4).ifPresent(button -> { + button.bindToggle(mainLayer, states.getClipLaunchingActive()); + button.bindIsPressed(mainLayer, pressed -> McuExtension.println(" >> %s", pressed)); + }); + } + + private void initSendSelector(final MainHardwareSection hwElements) { + if (hwElements.getButton(McuFunction.SEND_SELECT_1).isPresent()) { + hwElements.getButton(McuFunction.SEND_SELECT_1) + .ifPresent(button -> button.bindIsPressed(mainLayer, pressed -> directSendSelect(0, pressed))); + hwElements.getButton(McuFunction.SEND_SELECT_2) + .ifPresent(button -> button.bindIsPressed(mainLayer, pressed -> directSendSelect(1, pressed))); + hwElements.getButton(McuFunction.SEND_SELECT_3) + .ifPresent(button -> button.bindIsPressed(mainLayer, pressed -> directSendSelect(2, pressed))); + hwElements.getButton(McuFunction.SEND_SELECT_4) + .ifPresent(button -> button.bindIsPressed(mainLayer, pressed -> directSendSelect(3, pressed))); + hwElements.getButton(McuFunction.SEND_SELECT_5) + .ifPresent(button -> button.bindIsPressed(mainLayer, pressed -> directSendSelect(4, pressed))); + hwElements.getButton(McuFunction.SEND_SELECT_6) + .ifPresent(button -> button.bindIsPressed(mainLayer, pressed -> directSendSelect(5, pressed))); + hwElements.getButton(McuFunction.SEND_SELECT_7) + .ifPresent(button -> button.bindIsPressed(mainLayer, pressed -> directSendSelect(6, pressed))); + hwElements.getButton(McuFunction.SEND_SELECT_8) + .ifPresent(button -> button.bindIsPressed(mainLayer, pressed -> directSendSelect(7, pressed))); + } + hwElements.getButton(McuFunction.TRACK_MODE).ifPresent(button -> { + button.bindPressed(mainLayer, states::toggleTrackMode); + button.bindLight(mainLayer, states::trackModeActive); + }); + + } + + public void potMode(final MainHardwareSection hwElements) { + //viewControl.getCursorDeviceControl(). + if (hwElements.getButton(McuFunction.TRACK_MODE).isPresent()) { + hwElements.getButton(McuFunction.MODE_SEND).ifPresent(this::bindSendsModeDual); + } else { + hwElements.getButton(McuFunction.MODE_SEND).ifPresent(button -> bindMode(button, VPotMode.SEND)); + hwElements.getButton(McuFunction.MODE_ALL_SENDS).ifPresent(button -> bindMode(button, VPotMode.ALL_SENDS)); + } + hwElements.getButton(McuFunction.MODE_PAN).ifPresent(button -> bindMode(button, VPotMode.PAN)); + hwElements.getButton(McuFunction.MODE_DEVICE).ifPresent(button -> bindMode(button, VPotMode.DEVICE)); + + hwElements.getButton(McuFunction.MODE_EQ).ifPresent(button -> bindMode(button, VPotMode.EQ)); + hwElements.getButton(McuFunction.MODE_TRACK_REMOTE) + .ifPresent(button -> bindMode(button, VPotMode.TRACK_REMOTE)); + hwElements.getButton(McuFunction.MODE_PROJECT_REMOTE) + .ifPresent(button -> bindMode(button, VPotMode.PROJECT_REMOTE)); + } + + public void initTransport(final MainHardwareSection hwElements, final Transport transport, + final Application application) { + transport.automationWriteMode().addValueObserver(v -> this.autoMode = v); + + hwElements.getButton(McuFunction.AUTO_LATCH) + .ifPresent(button -> bindAutoMode(button, transport.automationWriteMode(), "latch")); + hwElements.getButton(McuFunction.AUTO_WRITE) + .ifPresent(button -> bindAutoMode(button, transport.automationWriteMode(), "write")); + hwElements.getButton(McuFunction.AUTO_TOUCH) + .ifPresent(button -> bindAutoMode(button, transport.automationWriteMode(), "touch")); + hwElements.getButton(McuFunction.AUTO_READ) + .ifPresent(button -> button.bindToggle(mainLayer, transport.isArrangerAutomationWriteEnabled())); + hwElements.getButton(McuFunction.PUNCH_IN) + .ifPresent(button -> button.bindToggle(mainLayer, transport.isPunchInEnabled())); + hwElements.getButton(McuFunction.PUNCH_OUT) + .ifPresent(button -> button.bindToggle(mainLayer, transport.isPunchOutEnabled())); + + hwElements.getButton(McuFunction.PLAY).ifPresent(button -> { + button.bindLight(mainLayer, transport.isPlaying()); + button.bindPressed(mainLayer, transport.playAction()); + }); + + hwElements.getButton(McuFunction.STOP).ifPresent(stopButton -> { + stopButton.bindHeldLight(mainLayer); + stopButton.bindPressed(mainLayer, transport.stopAction()); + }); + hwElements.getButton(McuFunction.RECORD).ifPresent(recordButton -> { + recordButton.bindToggle(mainLayer, transport.isArrangerRecordEnabled()); + recordButton.bindToggle(shiftLayer, transport.isArrangerOverdubEnabled()); + }); + hwElements.getButton(McuFunction.OVERDUB).ifPresent(overdubButton -> { + overdubButton.bindToggle(mainLayer, transport.isArrangerOverdubEnabled()); + overdubButton.bindToggle(shiftLayer, transport.isClipLauncherOverdubEnabled()); + }); + hwElements.getButton(McuFunction.AUTOMATION_LAUNCHER).ifPresent(button -> { + button.bindToggle(mainLayer, transport.isClipLauncherAutomationWriteEnabled()); + }); + hwElements.getButton(McuFunction.RESTORE_AUTOMATION).ifPresent(restoreAutoButton -> { + restoreAutoButton.bindPressed(mainLayer, () -> transport.resetAutomationOverrides()); + restoreAutoButton.bindLight(mainLayer, transport.isAutomationOverrideActive()); + }); + hwElements.getButton(McuFunction.METRO).ifPresent(metroButton -> { + metroButton.bindClickAltMenu(mainLayer, () -> transport.isMetronomeEnabled().toggle(), + pressed -> handleMenuPressed(pressed, metroMenuLayer)); + metroButton.bindLight(mainLayer, transport.isMetronomeEnabled()); + }); + hwElements.getButton(McuFunction.TEMPO).ifPresent( + tempoButton -> tempoButton.bindIsPressed(mainLayer, pressed -> handleMenuPressed(pressed, tempoMenuLayer))); + hwElements.getButton(McuFunction.GROOVE_MENU).ifPresent(grooveButton -> grooveButton.bindIsPressed(mainLayer, + pressed -> handleMenuPressed(pressed, groveMenuLayer))); + hwElements.getButton(McuFunction.ZOOM_MENU).ifPresent( + zoomMenu -> zoomMenu.bindIsPressed(mainLayer, pressed -> handleMenuPressed(pressed, zoomMenuLayer))); + hwElements.getButton(McuFunction.UNDO).ifPresent(undoButton -> { + undoButton.bindPressed(mainLayer, application.undoAction()); + undoButton.bindLight(mainLayer, application.canUndo()); + undoButton.bindPressed(shiftLayer, application.redoAction()); + undoButton.bindLight(shiftLayer, application.canRedo()); + }); + hwElements.getButton(McuFunction.SSL_PLUGINS_MENU).ifPresent( + sslMenuButton -> sslMenuButton.bindIsPressed(mainLayer, + pressed -> handleMenuPressed(pressed, sslDeviceMenuLayer))); + + hwElements.getButton(McuFunction.LOOP).ifPresent(loopButton -> { + loopButton.bindClickAltMenu(mainLayer, () -> transport.isArrangerLoopEnabled().toggle(), + pressed -> handleMenuPressed(pressed, loopMenuLayer)); + loopButton.bindLight(mainLayer, transport.isArrangerLoopEnabled()); + }); + application.panelLayout().addValueObserver(v -> currentLayoutType = LayoutType.toType(v)); + hwElements.getButton(McuFunction.ARRANGER).ifPresent(button -> { + button.bindPressed(mainLayer, () -> { + this.application.setPanelLayout(currentLayoutType.other().getName()); + }); + button.bindLight(mainLayer, () -> currentLayoutType == LayoutType.ARRANGER); + }); + + hwElements.getTimeCodeLed().ifPresent(timeCodeLed -> assignTimeCodeDisplay(transport, timeCodeLed, hwElements)); + } + + private void initNavigation(final MainHardwareSection hwElements, final ControllerConfig config) { + final boolean withHold = !config.hasNavigationWithJogWheel(); + hwElements.getButton(McuFunction.ZOOM_IN).ifPresent(button -> bindZoom(button, -1)); + hwElements.getButton(McuFunction.ZOOM_OUT).ifPresent(button -> bindZoom(button, 1)); + hwElements.getButton(McuFunction.PAGE_LEFT).ifPresent(button -> bindPageNav(button, -1)); + hwElements.getButton(McuFunction.PAGE_RIGHT).ifPresent(button -> bindPageNav(button, 1)); + hwElements.getButton(McuFunction.CHANNEL_LEFT).ifPresent(button -> bindChannelNav(button, -1, withHold)); + hwElements.getButton(McuFunction.CHANNEL_RIGHT).ifPresent(button -> bindChannelNav(button, 1, withHold)); + hwElements.getButton(McuFunction.BANK_LEFT).ifPresent(button -> bindChannelNav(button, -8, withHold)); + hwElements.getButton(McuFunction.BANK_RIGHT).ifPresent(button -> bindChannelNav(button, 8, withHold)); + + if (config.isDecelerateJogWheel()) { + hwElements.getButton(McuFunction.NAV_LEFT).ifPresent(button -> bindDeceleratedHorizontalNav(button, -1)); + hwElements.getButton(McuFunction.NAV_RIGHT).ifPresent(button -> bindDeceleratedHorizontalNav(button, 1)); + hwElements.getButton(McuFunction.NAV_UP).ifPresent(button -> bindDeceleratedVerticalNav(button, -1)); + hwElements.getButton(McuFunction.NAV_DOWN).ifPresent(button -> bindDeceleratedVerticalNav(button, 1)); + } else { + hwElements.getButton(McuFunction.NAV_LEFT).ifPresent(button -> bindHorizontalNav(button, -1, withHold)); + hwElements.getButton(McuFunction.NAV_RIGHT).ifPresent(button -> bindHorizontalNav(button, 1, withHold)); + hwElements.getButton(McuFunction.NAV_UP).ifPresent(button -> bindVerticalNav(button, 1, withHold)); + hwElements.getButton(McuFunction.NAV_DOWN).ifPresent(button -> bindVerticalNav(button, -1, withHold)); + } + } + + private void directSendSelect(final int index, final boolean pressed) { + if (pressed) { + switch (states.getPotMode().get()) { + case SEND -> viewControl.navigateToSends(index); + case DEVICE, PLUGIN, INSTRUMENT, MIDI_EFFECT -> { + if (states.isShiftSet()) { + viewControl.getCursorDeviceControl().navigateToPage(index); + } else { + viewControl.getCursorDeviceControl().navigateToDeviceInChain(index); + } + } + case TRACK_REMOTE -> viewControl.navigateToTrackRemotePage(index); + case PROJECT_REMOTE -> viewControl.navigateToProjectRemotePage(index); + case EQ -> deviceTypeBank.getEqDevice().navigateToDeviceParameters(index); + } + } + directNavigationListeners.forEach( + listener -> listener.handDirectNavigation(states.getPotMode().get(), index, pressed)); + } + + private void bindSendsModeDual(final McuButton button) { + button.bindMode(mainLayer, this::handleSendsVPotModePressed, // + () -> states.getPotMode().get() == VPotMode.SEND + || states.getPotMode().get() == VPotMode.ALL_SENDS); // Maybe re propagate on release + } + + private void bindMode(final McuButton button, final VPotMode potMode) { + button.bindMode(mainLayer, pressed -> handleVPotModePressed(potMode, pressed), + () -> states.getPotMode().get() == potMode); // Maybe re propagate on release + } + + private void bindAutoMode(final McuButton button, final SettableEnumValue value, final String mode) { + button.bindLight(mainLayer, () -> this.autoMode.equals(mode)); + button.bindPressed(mainLayer, () -> value.set(mode)); + } + + private void handleMenuPressed(final boolean pressed, final LayerGroup menuLayer) { + if (pressed) { + attachedMixerLayer.activateMenu(menuLayer); + } else { + attachedMixerLayer.releaseLayer(); + } + } + + private void assignTimeCodeDisplay(final Transport transport, final TimeCodeLed timeCodeLed, + final MainHardwareSection hwElements) { + timeCodeLed.refreshMode(); + transport.timeSignature().addValueObserver(timeCodeLed::setDivision); + transport.playPosition().addValueObserver(timeCodeLed::updatePosition); + transport.playPositionInSeconds().addValueObserver(timeCodeLed::updateTime); + hwElements.getButton(McuFunction.DISPLAY_SMPTE).ifPresent(button -> { + button.bindLight(mainLayer, () -> timeCodeLed.getMode() == TimeCodeLed.Mode.BEATS); + button.bindPressed(mainLayer, timeCodeLed::toggleMode); + button.bindRelease(mainLayer, timeCodeLed::ensureMode); + }); + states.getTwoSegmentText().addValueObserver(v -> timeCodeLed.setAssignment(v)); + } + + private void bindZoom(final McuButton button, final int dir) { + button.bindPressed(mainLayer, () -> zoomLeftRight(dir)); + } + + private void bindPageNav(final McuButton button, final int dir) { + button.bindPressed(mainLayer, () -> handlePageNavigation(dir)); + } + + private void bindChannelNav(final McuButton button, final int dir, final boolean withHoldFunction) { + if (withHoldFunction) { + button.bindRepeatHold(mainLayer, () -> viewControl.navigateChannels(dir)); + } else { + button.bindPressed(mainLayer, () -> viewControl.navigateChannels(dir)); + } + } + + private void bindDeceleratedHorizontalNav(final McuButton button, final int dir) { + button.bindPressed(mainLayer, () -> { + if (needsToDecelerate()) { + return; + } + handleNavigationHorizontal(dir); + lastNavMessage = System.currentTimeMillis(); + }); + button.bindPressed(zoomLayer, () -> { + if (needsToDecelerate()) { + return; + } + zoomLeftRight(dir); + lastNavMessage = System.currentTimeMillis(); + }); + } + + private void bindHorizontalNav(final McuButton button, final int dir, final boolean withHold) { + if (withHold) { + button.bindDelayedAction(mainLayer, start -> handleNavigationHorizontal(dir, start), // + () -> notifyInfo(true, Orientation.HORIZONTAL), // + () -> notifyInfo(false, Orientation.HORIZONTAL), // + 600); + } else { + button.bindPressed(mainLayer, () -> handleNavigationHorizontal(dir)); + } + button.bindRepeatHold(zoomLayer, () -> zoomLeftRight(dir)); + } + + private void bindDeceleratedVerticalNav(final McuButton button, final int dir) { + button.bindPressed(mainLayer, () -> { + if (needsToDecelerate()) { + return; + } + handleNavigationVertical(-dir); + lastNavMessage = System.currentTimeMillis(); + }); + button.bindPressed(zoomLayer, () -> { + if (needsToDecelerate()) { + return; + } + zoomUpDown(-dir); + lastNavMessage = System.currentTimeMillis(); + }); + } + + private void bindVerticalNav(final McuButton button, final int dir, final boolean withHold) { + if (withHold) { + button.bindDelayedAction(mainLayer, start -> handleNavigationVertical(dir, start), // + () -> notifyInfo(true, Orientation.VERTICAL), // + () -> notifyInfo(false, Orientation.VERTICAL), // + 600); + } else { + button.bindPressed(mainLayer, () -> handleNavigationVertical(dir)); + } + button.bindRepeatHold(zoomLayer, () -> zoomUpDown(dir)); + } + + private void handleSendsVPotModePressed(final boolean pressed) { + final VPotMode current = states.getPotMode().get(); + final boolean changeByPress = current != VPotMode.ALL_SENDS && current != VPotMode.SEND; + if (pressed && changeByPress) { + states.getPotMode().set(states.getLastSendsMode()); + } + notifyMode(states.getPotMode().get(), pressed, changeByPress); + } + + private void handleVPotModePressed(final VPotMode potMode, final boolean pressed) { + final boolean selection = states.getPotMode().get() != potMode; + if (pressed) { + states.getPotMode().set(potMode); + } + notifyMode(states.getPotMode().get(), pressed, selection); + + if (Objects.requireNonNull(states.getPotMode().get()) == VPotMode.EQ) { + if (pressed && !selection && !deviceTypeBank.getEqDevice().isSpecificDevicePresent()) { + deviceTypeBank.getEqDevice().insertDevice(); + } + } + } + + private void handlePageNavigation(final int dir) { + switch (states.getPotMode().get()) { + case PLUGIN, INSTRUMENT, MIDI_EFFECT, DEVICE -> + selectRemotes(viewControl.getCursorDeviceControl().getRemotes(), dir); + case TRACK_REMOTE -> selectRemotes(viewControl.getTrackRemotes(), dir); + case PROJECT_REMOTE -> selectRemotes(viewControl.getProjectRemotes(), dir); + case EQ -> deviceTypeBank.getEqDevice().navigateDeviceParameters(dir); + case SEND -> viewControl.navigateSends(dir); + case PAN -> selectTracks(dir); + } + } + + private void handleNavigationHorizontal(final int dir) { + switch (states.getPotMode().get()) { + case PLUGIN, INSTRUMENT, MIDI_EFFECT, DEVICE -> selectDeviceInChain(dir); + case PAN, SEND, TRACK_REMOTE, PROJECT_REMOTE, EQ, ALL_SENDS -> selectTracks(dir); + } + } + + private void handleNavigationHorizontal(final int dir, final boolean start) { + switch (states.getPotMode().get()) { + case PLUGIN, INSTRUMENT, MIDI_EFFECT, DEVICE -> { + if (start) { + selectDeviceInChain(dir); + } + } + case PAN, SEND, TRACK_REMOTE, PROJECT_REMOTE, EQ, ALL_SENDS -> repeatEvent(() -> selectTracks(dir), start); + } + } + + public void handleNavigationVertical(final int dir, final boolean start) { + if (needsToDecelerate()) { + return; + } + if (states.getClipLaunchingActive().get()) { + repeatEvent(() -> viewControl.navigateClipVertical(dir), start); + } else if (states.getPotMode().get() == VPotMode.PAN || states.getPotMode().get() == VPotMode.ALL_SENDS) { + repeatEvent(() -> selectTracks(-dir), start); + } else if (start) { + handleNavigationVertical(dir); + } + } + + private boolean needsToDecelerate() { + final long diff = System.currentTimeMillis() - lastNavMessage; + if (diff < DECELERATION_THRESHOLD_MS) { + if (diff == 0) { + lastNavMessage = System.currentTimeMillis(); + } + return true; + } + return false; + } + + public void handleNavigationVertical(final int dir) { + if (states.getClipLaunchingActive().get()) { + viewControl.navigateClipVertical(dir); + } else { + switch (states.getPotMode().get()) { + case PLUGIN, INSTRUMENT, MIDI_EFFECT, DEVICE -> + selectRemotes(viewControl.getCursorDeviceControl().getRemotes(), -dir); + case EQ -> deviceTypeBank.getEqDevice().navigateDeviceParameters(-dir); + case TRACK_REMOTE -> selectRemotes(viewControl.getTrackRemotes(), -dir); + case PROJECT_REMOTE -> selectRemotes(viewControl.getProjectRemotes(), -dir); + case PAN, ALL_SENDS -> selectTracks(-dir); + case SEND -> viewControl.navigateSends(-dir); + } + } + } + + private void notifyInfo(final boolean start, final Orientation orientation) { + navigationInfoListeners.forEach(listener -> listener.handleNavigationInfo(start, orientation)); + } + + private void zoomLeftRight(final int dir) { + if (states.isShiftSet()) { + if (dir > 0) { + viewControl.getDetailEditor().zoomIn(); + } else { + viewControl.getDetailEditor().zoomOut(); + } + } else { + if (dir > 0) { + viewControl.getArranger().zoomIn(); + } else { + viewControl.getArranger().zoomOut(); + } + } + } + + private void zoomUpDown(final int dir) { + final Arranger arranger = viewControl.getArranger(); + if (states.isShiftSet()) { + if (dir > 0) { + arranger.zoomOutLaneHeightsSelected(); + } else { + arranger.zoomInLaneHeightsSelected(); + } + } else { + if (dir > 0) { + arranger.zoomOutLaneHeightsAll(); + } else { + arranger.zoomInLaneHeightsAll(); + } + } + } + + void notifyMode(final VPotMode mode, final boolean pressed, final boolean selection) { + listeners.forEach(listener -> listener.handleModeSelected(mode, pressed, selection)); + } + + private void selectRemotes(final CursorRemoteControlsPage remotes, final int dir) { + if (dir > 0) { + remotes.selectNext(); + } else { + remotes.selectPrevious(); + } + } + + private void repeatEvent(final Runnable action, final boolean start) { + if (scrollEvent != null) { + scrollEvent.cancel(); + } + if (start) { + scrollEvent = new TimeRepeatEvent(action, SCROLL_START_TIME, SCROLL_REPEAT_INTERVAL); + action.run(); + timedProcessor.queueEvent(scrollEvent); + } + } + + private void selectTracks(final int dir) { + if (dir > 0) { + viewControl.getCursorTrack().selectNext(); + } else { + viewControl.getCursorTrack().selectPrevious(); + } + } + + private void selectDeviceInChain(final int dir) { + final PinnableCursorDevice cursorDevice = viewControl.getCursorDeviceControl().getCursorDevice(); + if (dir > 0) { + cursorDevice.selectNext(); + } else { + cursorDevice.selectPrevious(); + } + } + + public Layer getMainLayer() { + return mainLayer; + } + + void addModeSelectListener(final ModeChangeListener listener) { + listeners.add(listener); + } + + void addDirectNavigationListeners(final DirectNavigationListener listener) { + directNavigationListeners.add(listener); + } + + void addNavigationInfoListener(final NavigationInfoListener listener) { + navigationInfoListeners.add(listener); + } + + public void activate() { + mainLayer.setIsActive(true); + } + + public void setupGlobalMenus(final Context context, final MixerSectionHardware hwElements, + final MixerSection attachedMixerLayer) { + final DisplayManager displayManager = attachedMixerLayer.getDisplayManager(); + final ControllerConfig config = context.getService(ControllerConfig.class); + this.attachedMixerLayer = attachedMixerLayer; + final MenuConfigure menuBuilder = new MenuConfigure(context, hwElements, displayManager); + metroMenuLayer = menuBuilder.createMetroMenu(); + tempoMenuLayer = menuBuilder.createTempoMenu(); + cueMarkerMenuLayer = menuBuilder.createCueMarkerMenu(cueMarkerBank); + loopMenuLayer = menuBuilder.createLoopMenu(); + groveMenuLayer = menuBuilder.createGrooveMenu(); + zoomMenuLayer = menuBuilder.createViewZoomMenu(); + if (config.hasAssignment(McuFunction.SSL_PLUGINS_MENU)) { + sslDeviceMenuLayer = menuBuilder.createSslMenu(); + } + } + + + public void invokeCueMenu() { + attachedMixerLayer.activateMenu(cueMarkerMenuLayer); + } + + public void releaseMenu() { + attachedMixerLayer.releaseLayer(); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MenuBuilder.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MenuBuilder.java new file mode 100644 index 00000000..f00a14ab --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MenuBuilder.java @@ -0,0 +1,254 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +import com.bitwig.extension.controller.api.BeatTimeFormatter; +import com.bitwig.extension.controller.api.CueMarker; +import com.bitwig.extension.controller.api.HardwareActionBindable; +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extension.controller.api.RelativeHardwarControlBindable; +import com.bitwig.extension.controller.api.SettableBeatTimeValue; +import com.bitwig.extension.controller.api.SettableBooleanValue; +import com.bitwig.extension.controller.api.SettableRangedValue; +import com.bitwig.extension.controller.api.StringValue; +import com.bitwig.extension.controller.api.Transport; +import com.bitwig.extensions.controllers.mcu.GlobalStates; +import com.bitwig.extensions.controllers.mcu.control.MixerSectionHardware; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; +import com.bitwig.extensions.controllers.mcu.control.RingEncoder; +import com.bitwig.extensions.controllers.mcu.display.DisplayManager; +import com.bitwig.extensions.controllers.mcu.value.IEnumDisplayValue; +import com.bitwig.extensions.framework.di.Context; +import com.bitwig.extensions.framework.values.BasicStringValue; +import com.bitwig.extensions.framework.values.BooleanValueObject; + +public class MenuBuilder { + private static final BasicStringValue EMPTY = new BasicStringValue(""); + private final LayerGroup layerGroup; + private final DisplayManager displayManager; + private final MixerSectionHardware hwElements; + private int index = 0; + private final GlobalStates globalStates; + + public MenuBuilder(final String name, final Context context, final MixerSectionHardware hwElements, + final DisplayManager displayManager) { + this.displayManager = displayManager; + this.layerGroup = new LayerGroup(context, name); + this.globalStates = context.getService(GlobalStates.class); + this.hwElements = hwElements; + } + + public LayerGroup getLayerGroup() { + fillRest(); + return layerGroup; + } + + private void fillRest() { + while (index < 8) { + fillNext(); + } + } + + public void fillNext() { + if (index >= 8) { + return; + } + layerGroup.bindEmpty(displayManager, hwElements.getRingEncoder(index), index); + index++; + } + + public void addLabelMenu(final StringValue label, final StringValue value) { + if (index >= 8) { + return; + } + layerGroup.bindDisplay(displayManager, label, value, index); + layerGroup.bindEncoderEmpty(hwElements.getRingEncoder(index)); + index++; + } + + public void addEnumValue(final String label, final IEnumDisplayValue enumValue) { + if (index >= 8) { + return; + } + layerGroup.bindDisplay(displayManager, label, enumValue.getDisplayValue(), index); + layerGroup.bindRingValue(hwElements.getRingEncoder(index), enumValue.getRingValue()); + layerGroup.bindEncoderIncrement(hwElements.getRingEncoder(index), enumValue::increment, 0.1); + // TODO Button Press Option need to be added + index++; + } + + public void addToggleParameterMenu(final String name, final SettableBooleanValue value) { + // Consider Ring encoder turn to set value + if (index >= 8) { + return; + } + layerGroup.bindDisplay(displayManager, name, value, index); + final RingEncoder encoder = hwElements.getRingEncoder(index); + layerGroup.bindEncoderPressed(encoder, value::toggle); + layerGroup.bindRingValue(encoder, value); + layerGroup.bindEncoderTurnedBoolean(encoder, value); + index++; + } + + public void addToggleParameterMenu(final String name, final Parameter parameter) { + if (index >= 8) { + return; + } + final BooleanValueObject booleanProxy = new BooleanValueObject(); + booleanProxy.addValueObserver(active -> { + parameter.value().setImmediately(active ? 1.0 : 0.0); + }); + parameter.value().addValueObserver(v -> { + booleanProxy.set(v == 1.0); + }); + booleanProxy.setDirect(parameter.value().get() == 1.0); + + layerGroup.bindDisplay(displayManager, name, parameter, index); + final RingEncoder encoder = hwElements.getRingEncoder(index); + layerGroup.bindEncoderPressed(encoder, () -> booleanProxy.toggle()); + layerGroup.bindRingValue(encoder, booleanProxy); + layerGroup.bindEncoderTurnedBoolean(encoder, booleanProxy); + index++; + } + + public void addPositionAdjustment(final String name, final SettableBeatTimeValue value, + final BeatTimeFormatter formatter) { + if (index >= 8) { + } + final BasicStringValue valueString = new BasicStringValue(""); + final int cueIndex = index; + final RingEncoder encoder = hwElements.getRingEncoder(cueIndex); + final BooleanValueObject encoderPressed = new BooleanValueObject(); + value.addValueObserver(position -> valueString.set(value.getFormatted(formatter))); + layerGroup.bindRingToIsPressed(encoder); + layerGroup.bindEncoderIsPressed(encoder, pressed -> encoderPressed.set(pressed)); + layerGroup.bindDisplay(displayManager, name, valueString, cueIndex); + layerGroup.bindEncoderIncrement(encoder, inc -> { + final double position = value.get(); + value.set(position + (encoderPressed.get() ? 0.25 : 1.0) * inc); + }, 0.25); + index++; + } + + public void addCueMenu(final CueMarker cueMarker, final Transport transport, final BeatTimeFormatter formatter) { + if (index >= 8) { + return; + } + // TODO ADD SHIFT Option + final BasicStringValue name = new BasicStringValue(""); + final BasicStringValue value = new BasicStringValue("---"); + final int cueIndex = index; + + cueMarker.exists().addValueObserver(exist -> { + updateCueMarkerValue(cueMarker, formatter, value, exist); + updateCueMarkerLabel(cueMarker, name, cueIndex, cueMarker.name().get(), exist); + }); + cueMarker.position().addValueObserver(position -> { + updateCueMarkerValue(cueMarker, formatter, value, cueMarker.exists().get()); + }); + cueMarker.name().addValueObserver( + newName -> updateCueMarkerLabel(cueMarker, name, cueIndex, newName, cueMarker.exists().get())); + + layerGroup.bindDisplay(displayManager, name, value, cueIndex); + final RingEncoder encoder = hwElements.getRingEncoder(cueIndex); + layerGroup.bindRingValue(encoder, cueMarker.exists()); + layerGroup.bindEncoderPressed(encoder, () -> { + if (cueMarker.exists().get()) { + transport.getPosition().set(cueMarker.position().get()); + } else { + transport.addCueMarkerAtPlaybackPosition(); + } + }); + layerGroup.bindEncoderIncrement(encoder, inc -> { + final double position = cueMarker.position().get(); + cueMarker.position().set(position + 0.25 * inc); + }, 0.25); + index++; + } + + private static void updateCueMarkerValue(final CueMarker cueMarker, final BeatTimeFormatter formatter, + final BasicStringValue value, final boolean exists) { + if (exists) { + value.set(cueMarker.position().getFormatted(formatter)); + } else { + value.set("----"); + } + } + + private static void updateCueMarkerLabel(final CueMarker cueMarker, final BasicStringValue value, final int index, + final String nameValue, final boolean exists) { + if (exists) { + if (nameValue.isEmpty() || nameValue.equals("Untitled")) { + value.set("".formatted(index + 1)); + } else { + value.set(nameValue); + } + } else { + value.set("[Cue%d]".formatted(index + 1)); + } + } + + public void addActionMenu(final String name, final Runnable action) { + if (index >= 8) { + return; + } + // Maybe Label with press + // Value shows state + layerGroup.bindDisplay(displayManager, name, EMPTY, index); + layerGroup.bindEncoderPressed(hwElements.getRingEncoder(index), action); + layerGroup.bindRingToIsPressed(hwElements.getRingEncoder(index)); + index++; + } + + public void addIncParameter(final Parameter parameter, final double incrementMultiplier) { + if (index >= 8) { + return; + } + final BooleanValueObject pressTurn = new BooleanValueObject(); + layerGroup.bindControlsInc(hwElements.getRingEncoder(index), dir -> modifyRaw(dir, pressTurn, parameter), + parameter, RingDisplayType.SINGLE, incrementMultiplier); + layerGroup.bindEncoderIsPressed(hwElements.getRingEncoder(index), pressTurn); + layerGroup.bindDisplay(displayManager, parameter, index); + index++; + } + + private void modifyRaw(final int diff, final BooleanValueObject modifier, final Parameter parameter) { + parameter.setRaw(parameter.getRaw() + (modifier.get() ? 0.1 * diff : diff)); + } + + public void addValue(final String label, final SettableRangedValue parameter, final RingDisplayType type, + final double sensitivity) { + if (index >= 8) { + return; + } + layerGroup.bindControls(hwElements.getRingEncoder(index), type, parameter, sensitivity); + layerGroup.bindEncoderPressed(hwElements.getRingEncoder(index), () -> parameter.set(0)); + layerGroup.bindDisplay(displayManager, label, parameter, index); + index++; + } + + public void addStepValue(final RelativeHardwarControlBindable value, final Runnable pressAction, + final String label) { + if (index >= 8) { + return; + } + layerGroup.bindControls(hwElements.getRingEncoder(index), value); + if (pressAction != null) { + layerGroup.bindPressAction(hwElements.getRingEncoder(index), pressAction); + } + layerGroup.bindDisplay(displayManager, label, EMPTY, index); + layerGroup.bindRingEmpty(hwElements.getRingEncoder(index)); + + index++; + } + + public void addStepIncDecValue(final HardwareActionBindable incAction, final HardwareActionBindable decAction, + final String label) { + if (index >= 8) { + return; + } + final RingEncoder ringEncoder = hwElements.getRingEncoder(index); + layerGroup.bindEncoderIncrement(ringEncoder, incAction, decAction, 1.0); + layerGroup.bindDisplay(displayManager, label, EMPTY, index); + layerGroup.bindRingEmpty(ringEncoder); + index++; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MenuConfigure.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MenuConfigure.java new file mode 100644 index 00000000..891ae788 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MenuConfigure.java @@ -0,0 +1,135 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +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.DetailEditor; +import com.bitwig.extension.controller.api.Groove; +import com.bitwig.extension.controller.api.SettableBeatTimeValue; +import com.bitwig.extension.controller.api.Transport; +import com.bitwig.extensions.controllers.mcu.ViewControl; +import com.bitwig.extensions.controllers.mcu.control.MixerSectionHardware; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; +import com.bitwig.extensions.controllers.mcu.devices.SslPlugins; +import com.bitwig.extensions.controllers.mcu.display.DisplayManager; +import com.bitwig.extensions.controllers.mcu.value.IEnumDisplayValue; +import com.bitwig.extensions.controllers.mcu.value.SettableEnumValueSelect; +import com.bitwig.extensions.framework.di.Context; + +public class MenuConfigure { + + private final MixerSectionHardware hwElements; + private final DisplayManager displayManager; + private final ViewControl viewControl; + private final Context context; + private final BeatTimeFormatter formatter; + private final Transport transport; + + public MenuConfigure(final Context context, final MixerSectionHardware hwElements, + final DisplayManager displayManager) { + this.context = context; + formatter = context.getService(ControllerHost.class).createBeatTimeFormatter(":", 2, 1, 1, 0); + this.viewControl = context.getService(ViewControl.class); + this.hwElements = hwElements; + this.transport = context.getService(Transport.class); + this.displayManager = displayManager; + } + + public LayerGroup createMetroMenu() { + final IEnumDisplayValue preRoll = new SettableEnumValueSelect( + transport.preRoll(), // + new SettableEnumValueSelect.Value("none", "None", 0), // + new SettableEnumValueSelect.Value("one_bar", "1bar", 3), // + new SettableEnumValueSelect.Value("two_bars", "2bar", 6), // + new SettableEnumValueSelect.Value("four_bars", "4bar", 11)); + + final MenuBuilder builder = new MenuBuilder("METRO", context, hwElements, displayManager); + builder.addToggleParameterMenu("Pre ->", transport.isMetronomeAudibleDuringPreRoll()); + builder.addEnumValue("Roll", preRoll); + builder.addValue("Clck.Vol", transport.metronomeVolume(), RingDisplayType.FILL_LR, 2.0); + builder.addToggleParameterMenu("M.Tick", transport.isMetronomeTickPlaybackEnabled()); + return builder.getLayerGroup(); + } + + public LayerGroup createSslMenu() { + final MenuBuilder builder = new MenuBuilder("SSL_DEVICES", context, hwElements, displayManager); + builder.addActionMenu("+Meter", () -> insertSslDevice(SslPlugins.METER)); + builder.addActionMenu("+4K B", () -> insertSslDevice(SslPlugins.S4K_B)); + builder.addActionMenu("+4K E", () -> insertSslDevice(SslPlugins.S4K_E)); + builder.addActionMenu("+Ch.Strip", () -> insertSslDevice(SslPlugins.CHANNEL_STRIP)); + builder.addActionMenu("+Comp", () -> insertSslDevice(SslPlugins.BUS_COMPRESSOR)); + builder.addActionMenu("+360Link", () -> insertSslDevice(SslPlugins.LINK_360)); + return builder.getLayerGroup(); + } + + private void insertSslDevice(final SslPlugins device) { + viewControl.getCursorDeviceControl().insertVst3Device(device.getId()); + } + + public LayerGroup createGrooveMenu() { + final Groove groove = context.getService(ControllerHost.class).createGroove(); + final MenuBuilder builder = new MenuBuilder("GROOVE", context, hwElements, displayManager); + + builder.addToggleParameterMenu("Groove", groove.getEnabled()); + builder.addValue("Sfl.Rt", groove.getShuffleRate().value(), RingDisplayType.FILL_LR, 0.1); + builder.addValue("Sfl.Am", groove.getShuffleAmount().value(), RingDisplayType.FILL_LR, 2); + builder.fillNext(); + builder.addValue("Acc.Rt", groove.getAccentRate().value(), RingDisplayType.FILL_LR, 0.1); + builder.addValue("Acc.Am", groove.getAccentAmount().value(), RingDisplayType.FILL_LR, 2); + builder.addValue("Acc.Ph", groove.getAccentPhase().value(), RingDisplayType.FILL_LR, 2); + builder.addToggleParameterMenu("Fill", transport.isFillModeActive()); + + return builder.getLayerGroup(); + } + + public LayerGroup createLoopMenu() { + final MenuBuilder builder = new MenuBuilder("LOOP", context, hwElements, displayManager); + + final SettableBeatTimeValue cycleStart = transport.arrangerLoopStart(); + final SettableBeatTimeValue cycleLength = transport.arrangerLoopDuration(); + + builder.addToggleParameterMenu("LOOP", transport.isArrangerLoopEnabled()); + builder.fillNext(); + builder.addPositionAdjustment("START", cycleStart, formatter); + builder.addPositionAdjustment("LENGTH", cycleLength, formatter); + return builder.getLayerGroup(); + } + + public LayerGroup createTempoMenu() { + final MenuBuilder builder = new MenuBuilder("TEMPO", context, hwElements, displayManager); + builder.addIncParameter(transport.tempo(), 1.0); + builder.addActionMenu("TAP", transport::tapTempo); + return builder.getLayerGroup(); + } + + public LayerGroup createCueMarkerMenu(final CueMarkerBank cueMarkerBank) { + final MenuBuilder builder = new MenuBuilder("CUE_MARKER", context, hwElements, displayManager); + for (int i = 0; i < 8; i++) { + final CueMarker cueMarker = cueMarkerBank.getItemAt(i); + cueMarker.exists().markInterested(); + builder.addCueMenu(cueMarker, transport, formatter); + } + return builder.getLayerGroup(); + } + + public LayerGroup createViewZoomMenu() { + final Arranger arranger = viewControl.getArranger(); + final DetailEditor detailEditor = viewControl.getDetailEditor(); + final MenuBuilder builder = new MenuBuilder("ZOOM", context, hwElements, displayManager); + + builder.addStepValue(arranger.zoomLevel(), () -> arranger.zoomToFit(), "ARRANGE"); + builder.addStepValue(detailEditor.zoomLevel(), () -> detailEditor.zoomToFit(), "DETAIL"); + builder.addStepValue(arranger.zoomLaneHeightsAllStepper(), () -> {}, "ALL.TR"); + builder.addStepIncDecValue( + arranger.zoomInLaneHeightsSelectedAction(), arranger.zoomOutLaneHeightsSelectedAction(), "SEl.TR"); + builder.addToggleParameterMenu("PB.Flw", arranger.isPlaybackFollowEnabled()); + builder.addToggleParameterMenu("CL.VIS", arranger.isClipLauncherVisible()); + builder.addToggleParameterMenu("FX.VIS", arranger.areEffectTracksVisible()); + builder.addToggleParameterMenu("TL.VIS", arranger.isTimelineVisible()); + + return builder.getLayerGroup(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MixerModeLayer.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MixerModeLayer.java new file mode 100644 index 00000000..db1c01b4 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MixerModeLayer.java @@ -0,0 +1,112 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +import com.bitwig.extensions.controllers.mcu.VPotMode; +import com.bitwig.extensions.controllers.mcu.value.Orientation; +import com.bitwig.extensions.framework.Layer; + +public class MixerModeLayer { + protected final ControlMode mode; + protected final MixerSection mixer; + + protected Layer faderLayer; + protected Layer encoderLayer; + protected Layer displayLabelLayer; + protected Layer displayValueLayer; + + protected Layer displayLowerLabelLayer; + protected Layer displayLowerValueLayer; + + protected boolean active = false; + + public MixerModeLayer(final ControlMode mode, final MixerSection mixer) { + this.mode = mode; + this.mixer = mixer; + } + + public void setIsActive(final boolean isActive) { + if (this.active == isActive) { + return; + } + this.active = isActive; + activateLayers(isActive); + } + + private void activateLayers(final boolean isActive) { + if (encoderLayer == null) { + this.assign(); + } + encoderLayer.setIsActive(isActive); + displayLabelLayer.setIsActive(isActive); + displayValueLayer.setIsActive(isActive); + faderLayer.setIsActive(isActive); + if (mixer.hasLowerDisplay()) { + displayLowerValueLayer.setIsActive(isActive); + displayLowerLabelLayer.setIsActive(isActive); + } + } + + public void assign() { + final boolean flipped = mixer.isFlipped(); + final boolean nameValue = mixer.isNameValue(); + + final ControlMode mainMode = !flipped ? mode : ControlMode.VOLUME; + final ControlMode lowMode = flipped ? mode : ControlMode.VOLUME; + + encoderLayer = mixer.getLayerSource(mainMode).getEncoderLayer(); + faderLayer = mixer.getLayerSource(lowMode).getFaderLayer(); + mixer.setUpperLowerDestination(mainMode, lowMode); + if (mixer.hasLowerDisplay()) { + displayValueLayer = mixer.getLayerSource(mainMode).getDisplayValueLayer(); + displayLowerValueLayer = mixer.getLayerSource(lowMode).getDisplayValueLayer(); + if (flipped) { + displayLabelLayer = nameValue + ? mixer.getLayerSource(ControlMode.VOLUME).getDisplayLabelLayer() + : mixer.getTrackDisplayLayer(); + displayLowerLabelLayer = mixer.getLayerSource(mainMode).getDisplayLabelLayer(); + } else { + displayLabelLayer = mixer.getLayerSource(mainMode).getDisplayLabelLayer(); + displayLowerLabelLayer = nameValue + ? mixer.getLayerSource(ControlMode.VOLUME).getDisplayLabelLayer() + : mixer.getTrackDisplayLayer(); + } + } else { + final ControlMode displayMode = mixer.isTouched() ? lowMode : mainMode; + displayLabelLayer = + nameValue ? mixer.getLayerSource(displayMode).getDisplayLabelLayer() : mixer.getTrackDisplayLayer(); + displayValueLayer = mixer.getLayerSource(displayMode).getDisplayValueLayer(); + } + assignIfMenuModeActive(); + } + + protected void assignIfMenuModeActive() { + if (mixer.getActiveLayerGroup() != null) { + encoderLayer = mixer.getActiveLayerGroup().getEncoderLayer(); + displayLabelLayer = mixer.getActiveLayerGroup().getLabelLayer(); + displayValueLayer = mixer.getActiveLayerGroup().getValueLayer(); + } + } + + public void reassign() { + activateLayers(false); + this.assign(); + activateLayers(true); + } + + public void handleInfoState(final boolean start, final Orientation orientation) { + // default does nothing + } + + public void handleModePress(final VPotMode mode, final boolean pressed, final boolean selection) { + if (!active) { + return; + } + + if (!selection && mode == VPotMode.SEND) { + if (pressed) { + mixer.activateSendPrePostMenu(); + } else { + mixer.releaseLayer(); + } + } + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MixerSection.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MixerSection.java new file mode 100644 index 00000000..4f0c4cb2 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MixerSection.java @@ -0,0 +1,424 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +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.RemoteControl; +import com.bitwig.extension.controller.api.Send; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extensions.controllers.mcu.CursorDeviceControl; +import com.bitwig.extensions.controllers.mcu.GlobalStates; +import com.bitwig.extensions.controllers.mcu.TimedProcessor; +import com.bitwig.extensions.controllers.mcu.VPotMode; +import com.bitwig.extensions.controllers.mcu.ViewControl; +import com.bitwig.extensions.controllers.mcu.config.ControllerConfig; +import com.bitwig.extensions.controllers.mcu.control.MixerSectionHardware; +import com.bitwig.extensions.controllers.mcu.control.MotorSlider; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; +import com.bitwig.extensions.controllers.mcu.control.RingEncoder; +import com.bitwig.extensions.controllers.mcu.devices.DeviceTypeBank; +import com.bitwig.extensions.controllers.mcu.devices.SpecificDevice; +import com.bitwig.extensions.controllers.mcu.display.ControllerDisplay; +import com.bitwig.extensions.controllers.mcu.display.DisplayManager; +import com.bitwig.extensions.controllers.mcu.value.Orientation; +import com.bitwig.extensions.controllers.mcu.value.TrackColor; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.di.Context; +import com.bitwig.extensions.framework.values.BasicStringValue; + +public class MixerSection { + private final Layer mainLayer; + private final Layer emptyEncoderLayer; + private LayerGroup activeLayerGroup; + + private LayerGroup deviceMenuLayer; + private LayerGroup trackSendsMenuLayer; + + private final GlobalStates globalStates; + private final ViewControl viewControl; + private final ControllerDisplay display; + private final DeviceModeLayer deviceModeLayer; + private final SpecialDeviceModeLayer eqDeviceModeLayer; + private VPotMode potMode = VPotMode.PAN; + private final Map layerSource = new HashMap<>(); + private final Map modeLayers = new HashMap<>(); + private final DisplayManager displayManager; + private final TrackColor trackColor = new TrackColor(); + private ControlMode controlMode = ControlMode.PAN; + private String[] remotePages; + private int remoteIndex; + private final int sectionIndex; + + private final MixingModeLayerCollection mainMixerLayerCollection; + private final MixingModeLayerCollection globalMixerLayerCollection; + private final ClipLaunchingLayer clipLaunchingLayer; + private final TimedProcessor timedProcessor; + + public MixerSection(final Context diContext, final MixerSectionHardware hwElements, final MainSection mainSection, + final int sectionIndex, final boolean isMain) { + final Layers layers = diContext.getService(Layers.class); + this.sectionIndex = sectionIndex; + viewControl = diContext.getService(ViewControl.class); + globalStates = diContext.getService(GlobalStates.class); + timedProcessor = diContext.getService(TimedProcessor.class); + final DeviceTypeBank deviceTypeBank = diContext.getService(DeviceTypeBank.class); + final ControllerConfig config = diContext.getService(ControllerConfig.class); + + display = hwElements.getDisplay(); + displayManager = new DisplayManager(hwElements.getDisplay()); + mainLayer = new Layer(layers, "MAIN_LAYER"); + emptyEncoderLayer = new Layer(layers, "EMPTY ENCODER LAYERS"); + if (sectionIndex == 0) { + mainSection.setupGlobalMenus(diContext, hwElements, this); + } + setupDeviceMenu(diContext, hwElements); + final int trackOffset = sectionIndex * 8; + + deviceModeLayer = config.usesUnifiedDeviceControl() + ? new UnifiedDeviceModeLayer(layers, ControlMode.STD_PLUGIN, this, viewControl.getCursorDeviceControl()) + : new SeparatedDeviceModeLayer(layers, ControlMode.STD_PLUGIN, this, deviceTypeBank); + eqDeviceModeLayer = + new SpecialDeviceModeLayer(layers, ControlMode.EQ, VPotMode.EQ, this, deviceTypeBank.getEqDevice()); + + mainMixerLayerCollection = + new MixingModeLayerCollection(diContext, viewControl.getMainTrackBank(), false, sectionIndex); + globalMixerLayerCollection = + new MixingModeLayerCollection(diContext, viewControl.getGlobalTrackBank(), true, sectionIndex); + + Arrays.stream(ControlMode.values()).filter(mode -> mode != ControlMode.MENU && !mode.isMixer()) + .forEach(mode -> layerSource.put(mode, new ModeLayerGroup(mode, layers, sectionIndex))); + Arrays.stream(ControlMode.values()).filter(mode -> mode != ControlMode.MENU).forEach(mode -> { + final MixerModeLayer modeLayer = create(mode, layers); + modeLayers.put(mode, modeLayer); + }); + + mainMixerLayerCollection.bind(hwElements, displayManager, trackOffset, config.isHasDedicateVu()); + globalMixerLayerCollection.bind(hwElements, displayManager, trackOffset, config.isHasDedicateVu()); + + bindSendsTrack(diContext, hwElements, viewControl.getCursorTrack()); + bindDevice(hwElements); + bindRemotes(hwElements, ControlMode.TRACK_REMOTES); + bindRemotes(hwElements, ControlMode.PROJECT_REMOTES); + bindSpecialDevice(hwElements, deviceTypeBank.getEqDevice()); + bindEmpty(hwElements); + hwElements.getMasterFader() + .ifPresent(slider -> slider.bindParameter(mainLayer, viewControl.getRootTrack().volume())); + + clipLaunchingLayer = new ClipLaunchingLayer(diContext, hwElements, this); + globalStates.getGlobalView().addValueObserver(this::handleGlobalStates); + globalStates.getPotMode().addValueObserver(this::handlePotMode); + globalStates.getFlipped().addValueObserver(this::handleFlipped); + if (!config.hasLowerDisplay()) { + display.getSlidersTouched().addValueObserver(this::handleTouched); + } + globalStates.getNameValue().addValueObserver(this::handleNameValue); + globalStates.getClipLaunchingActive().addValueObserver(this::handleClipLaunchingState); + + hwElements.getBackgroundColoring() + .ifPresent(backgroundColor -> mainLayer.bindLightState(() -> getTrackColor(trackOffset), backgroundColor)); + if (isMain && config.hasMasterVu()) { + setUpMasterVu(); + } + mainSection.addModeSelectListener((mode, pressed, selection) -> { + modeLayers.get(controlMode).handleModePress(mode, pressed, selection); + }); + mainSection.addDirectNavigationListeners(this::navigateDirect); + mainSection.addNavigationInfoListener(this::handleInfoInvoked); + } + + TrackColor getTrackColor(final int offset) { + if (globalStates.getGlobalView().get()) { + return trackColor.getState(viewControl.getColorGlobal(offset)); + } + return trackColor.getState(viewControl.getColorMain(offset)); + } + + private void setupDeviceMenu(final Context context, final MixerSectionHardware hwElements) { + final ViewControl viewControl = context.getService(ViewControl.class); + final CursorDeviceControl cursorDeviceControl = viewControl.getCursorDeviceControl(); + final PinnableCursorDevice cursorDevice = cursorDeviceControl.getCursorDevice(); + final MenuBuilder builder = new MenuBuilder("DEVICE_CONTROL", context, hwElements, displayManager); + builder.addLabelMenu(new BasicStringValue("Device"), cursorDevice.name()); + builder.addToggleParameterMenu("Active", cursorDevice.isEnabled()); + builder.addToggleParameterMenu("Pinned", cursorDevice.isPinned()); + builder.addToggleParameterMenu("Expanded", cursorDevice.isExpanded()); + builder.addActionMenu("", cursorDeviceControl::moveDeviceRight); + builder.addActionMenu("Remove", cursorDevice::deleteObject); + deviceMenuLayer = builder.getLayerGroup(); + } + + private MixerModeLayer create(final ControlMode mode, final Layers layers) { + return switch (mode) { + case STD_PLUGIN -> deviceModeLayer; + case EQ -> eqDeviceModeLayer; + case TRACK -> new AllSendsModeLayer(mode, this); + case TRACK_REMOTES -> new ParameterPageLayer(layers, mode, this, "Track", viewControl.getTrackRemotes()); + case PROJECT_REMOTES -> + new ParameterPageLayer(layers, mode, this, "Project", viewControl.getProjectRemotes()); + default -> new MixerModeLayer(mode, this); + }; + } + + private void bindSendsTrack(final Context context, final MixerSectionHardware hwElements, + final CursorTrack cursorTrack) { + trackSendsMenuLayer = new LayerGroup(context, "ALL_SENDS"); + final ModeLayerGroup layer = layerSource.get(ControlMode.TRACK); + for (int i = 0; i < 8; i++) { + final MotorSlider slider = hwElements.getSlider(i); + final RingEncoder encoder = hwElements.getRingEncoder(i); + final Send sendItem = cursorTrack.sendBank().getItemAt(i); + layer.bindControls(slider, encoder, RingDisplayType.FILL_LR, sendItem); + layer.bindDisplay(displayManager, sendItem.name(), sendItem.exists(), sendItem, i); + MixingModeLayerCollection.bindSendPrePost(trackSendsMenuLayer, hwElements, displayManager, i, sendItem); + } + } + + private void bindDevice(final MixerSectionHardware hwElements) { + final CursorDeviceControl deviceControl = viewControl.getCursorDeviceControl(); + final CursorRemoteControlsPage remotes = deviceControl.getRemotes(); + final DeviceModeLayer deviceLayer = (DeviceModeLayer) modeLayers.get(ControlMode.STD_PLUGIN); + for (int i = 0; i < 8; i++) { + final RemoteControl parameter = remotes.getParameter(i); + final RingEncoder encoder = hwElements.getRingEncoder(i); + final MotorSlider slider = hwElements.getSlider(i); + layerSource.get(ControlMode.STD_PLUGIN).bindControls(slider, encoder, RingDisplayType.FILL_LR, parameter); + layerSource.get(ControlMode.STD_PLUGIN) + .bindDisplay(displayManager, parameter.name(), parameter.exists(), parameter, i); + } + deviceControl.getCursorDevice().name().addValueObserver(deviceLayer::updateDeviceName); + remotes.pageNames().addValueObserver(pages -> { + this.remotePages = pages; + updateRemotePages(deviceLayer); + }); + remotes.selectedPageIndex().addValueObserver(pagesIndex -> { + this.remoteIndex = pagesIndex; + updateRemotePages(deviceLayer); + }); + } + + private void bindRemotes(final MixerSectionHardware hwElements, final ControlMode mode) { + final ParameterPageLayer layer = (ParameterPageLayer) modeLayers.get(mode); + final CursorRemoteControlsPage remotes = layer.getRemotePages(); + final ModeLayerGroup modeLayerGroup = layerSource.get(mode); + + for (int i = 0; i < 8; i++) { + final RemoteControl parameter = remotes.getParameter(i); + final RingEncoder encoder = hwElements.getRingEncoder(i); + final MotorSlider slider = hwElements.getSlider(i); + modeLayerGroup.bindControls(slider, encoder, RingDisplayType.FILL_LR, parameter); + modeLayerGroup.bindDisplay(displayManager, parameter.name(), parameter.exists(), parameter, i); + } + } + + private void bindSpecialDevice(final MixerSectionHardware hwElements, final SpecificDevice device) { + final ModeLayerGroup layer = layerSource.get(ControlMode.EQ); + for (int i = 0; i < 8; i++) { + final RingEncoder encoder = hwElements.getRingEncoder(i); + final MotorSlider slider = hwElements.getSlider(i); + layer.bindControls(device, slider, encoder, i); + layer.bindDisplay(device, displayManager, i); + } + } + + private void bindEmpty(final MixerSectionHardware hwElements) { + for (int i = 0; i < 8; i++) { + final RingEncoder encoder = hwElements.getRingEncoder(i); + encoder.bindEmpty(emptyEncoderLayer); + } + } + + private void handleGlobalStates(final boolean globalStatesActive) { + activateButtonLayer(); + modeLayers.get(controlMode).reassign(); + } + + public void activateButtonLayer() { + if (globalStates.getClipLaunchingActive().get()) { + clipLaunchingLayer.setIsActive(true); + globalMixerLayerCollection.setIsActive(false); + mainMixerLayerCollection.setIsActive(false); + } else { + clipLaunchingLayer.setIsActive(false); + if (globalStates.getGlobalView().get()) { + mainMixerLayerCollection.setIsActive(false); + globalMixerLayerCollection.setIsActive(true); + } else { + globalMixerLayerCollection.setIsActive(false); + mainMixerLayerCollection.setIsActive(true); + } + } + } + + private void handlePotMode(final VPotMode oldMode, final VPotMode newMode) { + potMode = newMode; + deviceModeLayer.setPotMode(potMode); + final ControlMode newControlMode = controlModeByVPotMode(); + if (newControlMode != controlMode) { + modeLayers.get(controlMode).setIsActive(false); + controlMode = newControlMode; + modeLayers.get(controlMode).assign(); + modeLayers.get(controlMode).setIsActive(true); + } + } + + private void handleFlipped(final boolean flipped) { + modeLayers.get(controlMode).reassign(); + } + + private void handleNameValue(final boolean value) { + modeLayers.get(controlMode).reassign(); + } + + private void handleTouched(final boolean touched) { + modeLayers.get(controlMode).reassign(); + } + + private void handleClipLaunchingState(final boolean active) { + activateButtonLayer(); + } + + private void setUpMasterVu() { + final Track rootTrack = viewControl.getRootTrack(); + rootTrack.addVuMeterObserver(14, 0, true, display::sendMasterVuUpdateL); + rootTrack.addVuMeterObserver(14, 1, true, display::sendMasterVuUpdateR); + } + + private void navigateDirect(final VPotMode mode, final int index, final boolean pressed) { + if (potMode.getAssign() != VPotMode.BitwigType.CHANNEL) { + if (pressed) { + timedProcessor.startHoldEvent( + () -> modeLayers.get(controlMode).handleInfoState(true, Orientation.HORIZONTAL)); + } else { + timedProcessor.completeHoldEvent( + () -> modeLayers.get(controlMode).handleInfoState(false, Orientation.HORIZONTAL)); + } + } + } + + private void handleInfoInvoked(final boolean start, final Orientation orientation) { + if (potMode.getAssign() != VPotMode.BitwigType.CHANNEL) { + modeLayers.get(controlMode).handleInfoState(start, orientation); + } + } + + private void updateRemotePages(final DeviceModeLayer deviceLayer) { + if (remoteIndex == -1) { + deviceLayer.updateParameterPage(""); + } else if (this.remotePages != null && remoteIndex < this.remotePages.length) { + deviceLayer.updateParameterPage(remotePages[this.remoteIndex]); + } + } + + private ControlMode controlModeByVPotMode() { + return switch (potMode) { + case ALL_SENDS -> ControlMode.TRACK; + case PAN -> ControlMode.PAN; + case SEND -> ControlMode.SENDS; + case PLUGIN, INSTRUMENT, MIDI_EFFECT, DEVICE -> ControlMode.STD_PLUGIN; + case EQ -> ControlMode.EQ; + case TRACK_REMOTE -> ControlMode.TRACK_REMOTES; + case PROJECT_REMOTE -> ControlMode.PROJECT_REMOTES; + default -> ControlMode.VOLUME; + }; + } + + public int getSectionIndex() { + return sectionIndex; + } + + public Layer getEmptyEncoderLayer() { + return emptyEncoderLayer; + } + + public boolean isFlipped() { + return globalStates.getFlipped().get(); + } + + public boolean isTouched() { + return display.getSlidersTouched().get(); + } + + public boolean isNameValue() { + return globalStates.getNameValue().get(); + } + + public boolean hasLowerDisplay() { + return display.hasLower(); + } + + public DisplayManager getDisplayManager() { + return displayManager; + } + + public LayerGroup getActiveLayerGroup() { + return activeLayerGroup; + } + + public void setUpperLowerDestination(final ControlMode upper, final ControlMode lower) { + displayManager.registerModeAssignment(upper, lower); + } + + public ModeLayerGroup getLayerSource(final ControlMode mode) { + if (mode.isMixer()) { + if (globalStates.getGlobalView().get()) { + return globalMixerLayerCollection.get(mode); + } + return mainMixerLayerCollection.get(mode); + } + return layerSource.get(mode); + } + + public Layer getTrackDisplayLayer() { + if (globalStates.getGlobalView().get()) { + return globalMixerLayerCollection.getTrackDisplayLayer(); + } + return mainMixerLayerCollection.getTrackDisplayLayer(); + } + + public void activate() { + mainLayer.setIsActive(true); + if (globalStates.getGlobalView().get()) { + mainMixerLayerCollection.setIsActive(false); + globalMixerLayerCollection.setIsActive(true); + } else { + globalMixerLayerCollection.setIsActive(false); + mainMixerLayerCollection.setIsActive(true); + } + modeLayers.get(controlMode).setIsActive(true); + display.refresh(); + } + + public void releaseLayer() { + if (activeLayerGroup != null) { + activeLayerGroup = null; + modeLayers.get(controlMode).reassign(); + } + } + + public LayerGroup getDeviceModeLayer() { + return deviceMenuLayer; + } + + public void activateSendPrePostMenu() { + if (globalStates.trackModeActive()) { + activateMenu(trackSendsMenuLayer); + } else { + final LayerGroup prePost = globalStates.getGlobalView().get() + ? globalMixerLayerCollection.getSendsPrePostLayer() + : mainMixerLayerCollection.getSendsPrePostLayer(); + activateMenu(prePost); + } + } + + public void activateMenu(final LayerGroup layerGroup) { + activeLayerGroup = layerGroup; + modeLayers.get(controlMode).reassign(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MixingModeLayerCollection.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MixingModeLayerCollection.java new file mode 100644 index 00000000..481b3658 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/MixingModeLayerCollection.java @@ -0,0 +1,196 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +import com.bitwig.extension.controller.api.Send; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.controllers.mcu.GlobalStates; +import com.bitwig.extensions.controllers.mcu.StringUtil; +import com.bitwig.extensions.controllers.mcu.bindings.VuMeterBinding; +import com.bitwig.extensions.controllers.mcu.bindings.display.DisplayTarget; +import com.bitwig.extensions.controllers.mcu.bindings.display.StringDisplayBinding; +import com.bitwig.extensions.controllers.mcu.control.MixerSectionHardware; +import com.bitwig.extensions.controllers.mcu.control.MotorSlider; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; +import com.bitwig.extensions.controllers.mcu.control.RingEncoder; +import com.bitwig.extensions.controllers.mcu.display.DisplayManager; +import com.bitwig.extensions.controllers.mcu.display.DisplayRow; +import com.bitwig.extensions.controllers.mcu.value.IEnumDisplayValue; +import com.bitwig.extensions.controllers.mcu.value.SettableEnumValueSelect; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.di.Context; +import com.bitwig.extensions.framework.values.BasicStringValue; + +public class MixingModeLayerCollection { + private final static SettableEnumValueSelect.Value[] PRE_POST_VALUES = new SettableEnumValueSelect.Value[] { + new SettableEnumValueSelect.Value("PRE", "PRE", 0), // + new SettableEnumValueSelect.Value("POST", "POST", 5), // + new SettableEnumValueSelect.Value("AUTO", "AUTO", 11) + }; + + private final ModeLayerGroup volumeLayer; + private final ModeLayerGroup panLayer; + private final ModeLayerGroup sendsLayer; + private final LayerGroup sendsPrePostLayer; + private final Layer trackDisplayLayer; + private final Layer buttonLayer; + private final Layer vuLayer; + private final GlobalStates globalStates; + private final TrackBank trackBank; + private int selectedTrackIndex; + private final boolean isExtended; + private final int trackOffset; + private final int sectionIndex; + + public MixingModeLayerCollection(final Context context, final TrackBank trackBank, final boolean extended, + final int sectionIndex) { + final Layers layers = context.getService(Layers.class); + this.globalStates = context.getService(GlobalStates.class); + this.isExtended = extended; + this.trackOffset = sectionIndex * 8; + this.sectionIndex = sectionIndex; + volumeLayer = new ModeLayerGroup(ControlMode.VOLUME, layers, sectionIndex); + panLayer = new ModeLayerGroup(ControlMode.PAN, layers, sectionIndex); + sendsLayer = new ModeLayerGroup(ControlMode.SENDS, layers, sectionIndex); + sendsPrePostLayer = new LayerGroup(context, "PRE_POST"); + trackDisplayLayer = new Layer(layers, "TRACK_DISPLAY"); + buttonLayer = new Layer(layers, "BUTTON_LAYER"); + vuLayer = new Layer(layers, "VU_LAYER"); + this.trackBank = trackBank; + trackBank.scrollPosition().markInterested(); + for (int i = 0; i < trackBank.getSizeOfBank(); i++) { + configureTrack(i); + } + } + + private void configureTrack(final int index) { + // if (usesColor) { + // track.color().addValueObserver((r, g, b) -> trackColors[index] = toColor(r, g, b)); + // } + // TODO needs to move to a different collection + final Track track = trackBank.getItemAt(index); + track.addIsSelectedInMixerObserver(select -> { + if (select) { + this.selectedTrackIndex = index; + } else if (this.selectedTrackIndex == index) { + this.selectedTrackIndex = -1; + } + }); + } + + public ModeLayerGroup get(final ControlMode mode) { + return switch (mode) { + case PAN -> panLayer; + case SENDS -> sendsLayer; + default -> volumeLayer; + }; + } + + public Layer getTrackDisplayLayer() { + return trackDisplayLayer; + } + + public void setIsActive(final boolean active) { + buttonLayer.setIsActive(active); + vuLayer.setIsActive(active); + } + + public void bind(final MixerSectionHardware hwElements, final DisplayManager displayManager, + final int channelOffset, final boolean hasDedicatedVu) { + for (int i = 0; i < 8; i++) { + final int index = i; + final Track track = trackBank.getItemAt(i + channelOffset); + + final MotorSlider slider = hwElements.getSlider(i); + final RingEncoder encoder = hwElements.getRingEncoder(i); + + volumeLayer.bindControls(slider, encoder, RingDisplayType.FILL_LR, track.volume()); + volumeLayer.bindDisplay( + displayManager, new BasicStringValue("LEVEL"), track.exists(), track.volume(), index); + panLayer.bindControls(slider, encoder, RingDisplayType.PAN_FILL, track.pan()); + panLayer.bindValue(displayManager, new BasicStringValue(" PAN"), track.exists(), track.pan(), index, + v -> StringUtil.panToString(v)); + final Send sendItem = track.sendBank().getItemAt(0); + + sendsLayer.bindControls(slider, encoder, RingDisplayType.FILL_LR, sendItem); + sendsLayer.bindDisplay(displayManager, sendItem.name(), track.exists(), sendItem, index); + + // ENUM track.monitorMode() + + bindSendPrePost(sendsPrePostLayer, hwElements, displayManager, index, sendItem); + + final BasicStringValue trackName = setUpTrackNameAggregate(track); + trackDisplayLayer.addBinding(new StringDisplayBinding(displayManager, ControlMode.VOLUME, + DisplayTarget.of(DisplayRow.LABEL, index, sectionIndex, trackName), trackName, track.exists(), + name -> StringUtil.reduceAscii(name, 7))); + + assignButtons(hwElements, i, track); + if (hasDedicatedVu) { + vuLayer.addBinding(new VuMeterBinding(displayManager, track, index)); + } + } + } + + public static void bindSendPrePost(final LayerGroup layerGroup, final MixerSectionHardware hwElements, + final DisplayManager displayManager, final int index, final Send sendItem) { + final IEnumDisplayValue sendMode = new SettableEnumValueSelect(sendItem.sendMode(), sendItem, PRE_POST_VALUES); + layerGroup.bindDisplay(displayManager, sendItem.name(), sendMode.getDisplayValue(), index); + layerGroup.bindEncoderIncrement(hwElements.getRingEncoder(index), sendMode::increment, 0.1); + layerGroup.bindEncoderPressed(hwElements.getRingEncoder(index), sendMode::stepRoundRobin); + layerGroup.bindRingValue(hwElements.getRingEncoder(index), sendMode.getRingValue()); + } + + private BasicStringValue setUpTrackNameAggregate(final Track track) { + final BasicStringValue name = new BasicStringValue(""); + track.name().addValueObserver( + trackName -> name.set(toTrackName(trackName, track.isGroup().get(), track.isGroupExpanded().get()))); + track.isGroup().addValueObserver( + isGroup -> name.set(toTrackName(track.name().get(), isGroup, track.isGroupExpanded().get()))); + track.isGroupExpanded() + .addValueObserver(expanded -> name.set((toTrackName(track.name().get(), track.isGroup().get(), expanded)))); + return name; + } + + private void assignButtons(final MixerSectionHardware hwElements, final int index, final Track track) { + hwElements.getMuteButton(index).bindToggle(buttonLayer, track.mute()); + hwElements.getArmButton(index).bindToggle(buttonLayer, track.arm()); + hwElements.getSoloButton(index).bindLight(buttonLayer, track.solo()); + hwElements.getSoloButton(index).bindIsPressed(buttonLayer, pressed -> handleSolo(pressed, track)); + hwElements.getSelectButton(index).bindPressed(buttonLayer, () -> handleSelect(track)); + hwElements.getSelectButton(index).bindLight(buttonLayer, () -> isTrackSelected(index + trackOffset)); + } + + private String toTrackName(final String name, final boolean isGroup, final boolean isExpanded) { + if (isGroup) { + return (isExpanded ? ">" : "_") + name; + } + return name; + } + + private void handleSolo(final boolean pressed, final Track track) { + if (pressed) { + track.solo().toggle(!(globalStates.isSoloHeld() || globalStates.getShift().get())); + } + globalStates.soloPressed(pressed); + } + + private void handleSelect(final Track track) { + if (globalStates.getClearHeld().get()) { + track.deleteObject(); + } else if (globalStates.getDuplicateHeld().get()) { + track.duplicate(); + } else if (globalStates.isShiftSet()) { + track.isGroupExpanded().toggle(); + } else { + track.selectInMixer(); + } + } + + private boolean isTrackSelected(final int index) { + return index == selectedTrackIndex; + } + + public LayerGroup getSendsPrePostLayer() { + return sendsPrePostLayer; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/ModeLayerGroup.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/ModeLayerGroup.java new file mode 100644 index 00000000..97b43496 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/ModeLayerGroup.java @@ -0,0 +1,98 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +import com.bitwig.extension.controller.api.BooleanValue; +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extension.controller.api.StringValue; +import com.bitwig.extensions.controllers.mcu.StringUtil; +import com.bitwig.extensions.controllers.mcu.bindings.display.DisplayTarget; +import com.bitwig.extensions.controllers.mcu.bindings.display.ParameterValueDisplayBinding; +import com.bitwig.extensions.controllers.mcu.bindings.display.StringDisplayBinding; +import com.bitwig.extensions.controllers.mcu.control.MotorSlider; +import com.bitwig.extensions.controllers.mcu.control.RingDisplayType; +import com.bitwig.extensions.controllers.mcu.control.RingEncoder; +import com.bitwig.extensions.controllers.mcu.devices.ParamPageSlot; +import com.bitwig.extensions.controllers.mcu.devices.SpecificDevice; +import com.bitwig.extensions.controllers.mcu.display.DisplayManager; +import com.bitwig.extensions.controllers.mcu.display.DisplayRow; +import com.bitwig.extensions.controllers.mcu.value.DoubleValueConverter; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; + + +public class ModeLayerGroup { + + private final ControlMode mode; + private final Layer faderLayer; + private final Layer encoderLayer; + private final Layer displayLabelLayer; + private final Layer displayValueLayer; + private final int sectionIndex; + + public ModeLayerGroup(final ControlMode mode, final Layers layers, final int index) { + this.mode = mode; + this.sectionIndex = index; + this.faderLayer = new Layer(layers, "%s_FADER_%d".formatted(mode, index)); + this.encoderLayer = new Layer(layers, "%s_ENCODER_%d".formatted(mode, index)); + this.displayLabelLayer = new Layer(layers, "%s_DISPLAY_LABEL_%d".formatted(mode, index)); + this.displayValueLayer = new Layer(layers, "%s_DISPLAY_VALUE_%d".formatted(mode, index)); + } + + public void bindControls(final MotorSlider slider, final RingEncoder encoder, final RingDisplayType ringType, + final Parameter parameter) { + slider.bindParameter(this.faderLayer, parameter); + encoder.bindParameter(this.encoderLayer, parameter, ringType); + } + + public void bindDisplay(final DisplayManager displayManager, final StringValue labelValue, + final BooleanValue exists, final Parameter parameter, final int index) { + displayLabelLayer.addBinding(new StringDisplayBinding(displayManager, mode, + DisplayTarget.of(DisplayRow.LABEL, index, sectionIndex, parameter), labelValue, exists, + name -> StringUtil.toAscii(name))); + displayValueLayer.addBinding(new StringDisplayBinding(displayManager, mode, + DisplayTarget.of(DisplayRow.VALUE, index, sectionIndex, parameter), parameter.displayedValue(), exists)); + } + + public void bindValue(final DisplayManager displayManager, final StringValue labelValue, final BooleanValue exists, + final Parameter parameter, final int index, final DoubleValueConverter valueConverter) { + displayLabelLayer.addBinding(new StringDisplayBinding(displayManager, mode, + DisplayTarget.of(DisplayRow.LABEL, index, sectionIndex, parameter), labelValue, exists, + name -> StringUtil.reduceAscii(name, 6))); + displayValueLayer.addBinding(new ParameterValueDisplayBinding(displayManager, mode, + DisplayTarget.of(DisplayRow.VALUE, index, sectionIndex, parameter), parameter, valueConverter)); + } + + + public void bindControls(final SpecificDevice device, final MotorSlider slider, final RingEncoder encoder, + final int paramIndex) { + final ParamPageSlot slot = device.getParamPageSlot(paramIndex); + slider.bindParameter(faderLayer, slot); + encoder.bindParameter(encoderLayer, slot); + } + + public void bindDisplay(final SpecificDevice device, final DisplayManager displayManager, final int index) { + final ParamPageSlot slot = device.getParamPageSlot(index); + displayLabelLayer.addBinding(new StringDisplayBinding(displayManager, mode, + DisplayTarget.of(DisplayRow.LABEL, index, sectionIndex, slot), slot.getNameValue(), slot.getExistsValue(), + name -> StringUtil.reduceAscii(name, 7))); + displayValueLayer.addBinding(new StringDisplayBinding(displayManager, mode, + DisplayTarget.of(DisplayRow.VALUE, index, sectionIndex, slot), slot.getDisplayValue(), + slot.getExistsValue(), name -> StringUtil.reduceAscii(name, 7))); + } + + + public Layer getFaderLayer() { + return faderLayer; + } + + public Layer getEncoderLayer() { + return encoderLayer; + } + + public Layer getDisplayLabelLayer() { + return displayLabelLayer; + } + + public Layer getDisplayValueLayer() { + return displayValueLayer; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/ParameterPageLayer.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/ParameterPageLayer.java new file mode 100644 index 00000000..de235332 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/ParameterPageLayer.java @@ -0,0 +1,168 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +import com.bitwig.extension.controller.api.CursorRemoteControlsPage; +import com.bitwig.extensions.controllers.mcu.bindings.display.StringRowDisplayBinding; +import com.bitwig.extensions.controllers.mcu.display.DisplayRow; +import com.bitwig.extensions.controllers.mcu.value.Orientation; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.values.BasicStringValue; + +public class ParameterPageLayer extends MixerModeLayer { + + private final CursorRemoteControlsPage remotePages; + private final Layer topRowValueLayer; + private final Layer bottomRowValueLayer; + private final BasicStringValue topRowInfoText; + private final BasicStringValue bottomRowInfoText; + private String[] pages = new String[0]; + private int remoteIndex; + private int pageCount; + private final String label; + private String currentPageName = ""; + protected String infoText = null; + + public ParameterPageLayer(final Layers layers, final ControlMode mode, final MixerSection mixer, final String label, + final CursorRemoteControlsPage remotePages) { + super(mode, mixer); + topRowInfoText = new BasicStringValue(""); + bottomRowInfoText = new BasicStringValue(label); + this.label = label; + this.topRowValueLayer = new Layer(layers, "INFO_LABEL_%s".formatted(mode)); + this.bottomRowValueLayer = new Layer(layers, "INFO_VALUE_%s".formatted(mode)); + this.remotePages = remotePages; + + this.topRowValueLayer.addBinding( + new StringRowDisplayBinding(mixer.getDisplayManager(), mode, DisplayRow.LABEL, mixer.getSectionIndex(), + topRowInfoText)); + this.bottomRowValueLayer.addBinding( + new StringRowDisplayBinding(mixer.getDisplayManager(), mode, DisplayRow.VALUE, mixer.getSectionIndex(), + bottomRowInfoText)); + remotePages.pageNames().addValueObserver(pages -> { + this.pages = pages; + update(); + }); + remotePages.selectedPageIndex().addValueObserver(pagesIndex -> { + this.remoteIndex = pagesIndex; + update(); + }); + remotePages.pageCount().addValueObserver(pages -> { + this.pageCount = pages; + update(); + }); + } + + private void update() { + updateRemotePageName(); + if (active) { + reassign(); + } + } + + private void updateRemotePageName() { + if (this.remoteIndex != -1 && this.remoteIndex < this.pages.length) { + this.currentPageName = pages[this.remoteIndex]; + } else { + this.currentPageName = ""; + } + if (pageCount == 0) { + topRowInfoText.set("No Parameter Pages set"); + bottomRowInfoText.set("Configure in Bitwig"); + } else if (remoteIndex == -1) { + topRowInfoText.set(""); + bottomRowInfoText.set("No Parameter Page selected"); + } else if (this.pages != null && remoteIndex < this.pages.length) { + topRowInfoText.set(""); + bottomRowInfoText.set("Page: %s".formatted(pages[this.remoteIndex])); + } + } + + public CursorRemoteControlsPage getRemotePages() { + return remotePages; + } + + @Override + public void handleInfoState(final boolean start, final Orientation orientation) { + if (active) { + if (start) { + infoText = "%s Remote Page: %s".formatted(label, currentPageName); + } else { + infoText = null; + } + reassign(); + } + } + + @Override + public void assign() { + final ControlMode mainMode = !mixer.isFlipped() ? mode : ControlMode.VOLUME; + final ControlMode lowMode = mixer.isFlipped() ? mode : ControlMode.VOLUME; + encoderLayer = mixer.getLayerSource(mainMode).getEncoderLayer(); + faderLayer = mixer.getLayerSource(lowMode).getFaderLayer(); + + if (mixer.hasLowerDisplay()) { + assignDualDisplay(); + } else { + assignSingleDisplay(mainMode, lowMode); + } + assignIfMenuModeActive(); + } + + private void assignDualDisplay() { + final boolean nameValue = mixer.isNameValue(); + final boolean isFlipped = mixer.isFlipped(); + if (isFlipped) { + mixer.setUpperLowerDestination(ControlMode.VOLUME, mode); + + displayLabelLayer = mixer.getTrackDisplayLayer(); + displayValueLayer = mixer.getLayerSource(ControlMode.VOLUME).getDisplayValueLayer(); + displayLowerLabelLayer = mixer.getLayerSource(mode).getDisplayLabelLayer(); + displayLowerValueLayer = mixer.getLayerSource(mode).getDisplayValueLayer(); + if (pageCount == 0 || remoteIndex == -1) { + displayLowerLabelLayer = topRowValueLayer; + displayLowerValueLayer = bottomRowValueLayer; + } + } else { + mixer.setUpperLowerDestination(mode, ControlMode.VOLUME); + displayLabelLayer = mixer.getLayerSource(mode).getDisplayLabelLayer(); + displayValueLayer = mixer.getLayerSource(mode).getDisplayValueLayer(); + if (nameValue) { + displayValueLayer = bottomRowValueLayer; + } + displayLowerValueLayer = mixer.getLayerSource(ControlMode.VOLUME).getDisplayValueLayer(); + displayLowerLabelLayer = mixer.getTrackDisplayLayer(); + if (pageCount == 0 || remoteIndex == -1) { + displayLabelLayer = topRowValueLayer; + displayValueLayer = bottomRowValueLayer; + } + } + } + + + private void assignSingleDisplay(final ControlMode mainMode, final ControlMode lowMode) { + final boolean touched = mixer.isTouched(); + final boolean nameValue = mixer.isNameValue(); + final ControlMode displayMode = touched ? lowMode : mainMode; + if (displayMode == mode) { + displayLabelLayer = mixer.getLayerSource(displayMode).getDisplayLabelLayer(); + if (nameValue) { + displayValueLayer = bottomRowValueLayer; + } else { + displayValueLayer = mixer.getLayerSource(displayMode).getDisplayValueLayer(); + } + if (pageCount == 0 || remoteIndex == -1) { + displayLabelLayer = topRowValueLayer; + displayValueLayer = bottomRowValueLayer; + } + } else { + displayLabelLayer = nameValue + ? mixer.getLayerSource(ControlMode.VOLUME).getDisplayLabelLayer() + : mixer.getTrackDisplayLayer(); + displayValueLayer = mixer.getLayerSource(displayMode).getDisplayValueLayer(); + } + if (infoText != null) { + topRowInfoText.set(infoText); + displayLabelLayer = topRowValueLayer; + } + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/SeparatedDeviceModeLayer.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/SeparatedDeviceModeLayer.java new file mode 100644 index 00000000..f0ece578 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/SeparatedDeviceModeLayer.java @@ -0,0 +1,120 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +import com.bitwig.extensions.controllers.mcu.StringUtil; +import com.bitwig.extensions.controllers.mcu.VPotMode; +import com.bitwig.extensions.controllers.mcu.devices.DeviceTypeBank; +import com.bitwig.extensions.framework.Layers; + +public class SeparatedDeviceModeLayer extends DeviceModeLayer { + private final DeviceTypeBank deviceTypeBank; + private int pageCount; + private DeviceTypeBank.GeneralDeviceType currentSelectedDeviceType = DeviceTypeBank.GeneralDeviceType.NONE; + + public SeparatedDeviceModeLayer(final Layers layers, final ControlMode mode, final MixerSection mixer, + final DeviceTypeBank deviceTypeBank) { + super(layers, mode, mixer); + this.deviceTypeBank = deviceTypeBank; + deviceTypeBank.addDeviceTypeListener(this::handleDeviceTypeChanged); + deviceTypeBank.getCursorRemotes().pageCount().addValueObserver(pageCount -> { + this.pageCount = pageCount; + if (active) { + evalDeviceMatch(); + reassign(); + } + }); + deviceTypeBank.addExistenceListener((potMode, exist) -> { + if (active && this.potMode == potMode) { + evalDeviceMatch(); + reassign(); + } + }); + } + + private void handleDeviceTypeChanged(final DeviceTypeBank.GeneralDeviceType type) { + currentSelectedDeviceType = type; + if (active) { + evalDeviceMatch(); + reassign(); + } + } + + @Override + protected void evalDeviceMatch() { + if (!deviceTypeBank.hasDeviceType(potMode)) { + matchState = State.TYPE_NOT_IN_CHAIN; + } else if (potMode != currentSelectedDeviceType.getMode()) { + matchState = State.TYPE_MISMATCH; + // McuExtension.println("MISMATCH %s %s = %s", currentSelectedDeviceType, + // currentSelectedDeviceType.getMode(), + // potMode); + } else if (pageCount == 0) { + matchState = State.NO_PARAM_PAGES; + } else { + matchState = State.TYPE_MATCH; + } + } + + public void updateDeviceName(final String deviceName) { + this.deviceName = deviceName; + this.deviceInfo = "Device %s Page: %s".formatted(deviceName, 14, pageName); + if (matchState == State.TYPE_MATCH) { + bottomRowInfoText.set(deviceInfo); + } + } + + public void updateParameterPage(final String parameterPage) { + this.pageName = StringUtil.padEnd(parameterPage, 14); + this.deviceInfo = "Device %s Page: %s".formatted(deviceName, pageName); + if (matchState == State.TYPE_MATCH) { + bottomRowInfoText.set(deviceInfo); + } + } + + public void handleModePress(final VPotMode mode, final boolean pressed) { + if (!active) { + return; + } + if (pressed) { + //McuExtension.println(" Force Mode " + mode + " > " + deviceTypeBank.hasDeviceType(mode)); + if (deviceTypeBank.hasDeviceType(mode)) { + //deviceTypeBank.ensureDeviceSelection(mode); + } + evalInfoState(mode); + if (!justChangedTo && matchState == State.TYPE_MATCH) { + mixer.activateMenu(mixer.getDeviceModeLayer()); + } + reassign(); + justChangedTo = false; + } else { + mixer.releaseLayer(); + } + } + + @Override + protected void evalInfoState(final VPotMode potMode) { + if (!deviceTypeBank.hasDeviceType(potMode)) { + matchState = State.TYPE_NOT_IN_CHAIN; + } else if (deviceTypeBank.hasDeviceType(potMode)) { + matchState = State.TYPE_MATCH; + deviceTypeBank.ensureDeviceSelection(potMode); + } + } + + protected void determineInfoState(final boolean flipTopBottom) { + String topLineText = deviceInfo; + String bottomLineText = ""; + if (matchState == State.TYPE_MISMATCH) { + topLineText = "No %s in focus".formatted(potMode.getDescription()); + bottomLineText = "Press %s to focus".formatted(this.potMode); + } else if (matchState == State.TYPE_NOT_IN_CHAIN) { + topLineText = "No %s in Chain".formatted(potMode.getDescription()); + bottomLineText = "Browse for Device in Bitwig"; + } else if (matchState == State.NO_PARAM_PAGES) { + topLineText = "Configure in Bitwig"; + bottomLineText = "%s has no Parameter Pages".formatted(deviceName); + } + bottomRowInfoText.set(flipTopBottom ? bottomLineText : topLineText); + topRowInfoText.set(flipTopBottom ? topLineText : bottomLineText); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/SpecialDeviceModeLayer.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/SpecialDeviceModeLayer.java new file mode 100644 index 00000000..c30b953f --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/SpecialDeviceModeLayer.java @@ -0,0 +1,195 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +import com.bitwig.extensions.controllers.mcu.McuExtension; +import com.bitwig.extensions.controllers.mcu.VPotMode; +import com.bitwig.extensions.controllers.mcu.bindings.ResetableBinding; +import com.bitwig.extensions.controllers.mcu.bindings.display.StringRowDisplayBinding; +import com.bitwig.extensions.controllers.mcu.devices.SpecificDevice; +import com.bitwig.extensions.controllers.mcu.display.DisplayRow; +import com.bitwig.extensions.controllers.mcu.value.Orientation; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.values.BasicStringValue; + +public class SpecialDeviceModeLayer extends MixerModeLayer { + + private final Layer topRowValueLayer; + private final Layer bottomRowValueLayer; + private final BasicStringValue topRowInfoText; + private final BasicStringValue bottomRowInfoText; + private String infoState = null; + private final SpecificDevice device; + private final VPotMode matchingPotMode; + private String infoText = null; + + public SpecialDeviceModeLayer(final Layers layers, final ControlMode mode, final VPotMode matchingPotMode, + final MixerSection mixer, final SpecificDevice specialDevice) { + super(mode, mixer); + this.device = specialDevice; + this.matchingPotMode = matchingPotMode; + this.topRowValueLayer = new Layer(layers, "INFO_LABEL_%s".formatted(mode)); + this.bottomRowValueLayer = new Layer(layers, "INFO_VALUE_%s".formatted(mode)); + topRowInfoText = new BasicStringValue(""); + bottomRowInfoText = new BasicStringValue(""); + this.topRowValueLayer.addBinding( + new StringRowDisplayBinding(mixer.getDisplayManager(), mode, DisplayRow.LABEL, mixer.getSectionIndex(), + topRowInfoText)); + this.bottomRowValueLayer.addBinding( + new StringRowDisplayBinding(mixer.getDisplayManager(), mode, DisplayRow.VALUE, mixer.getSectionIndex(), + bottomRowInfoText)); + device.addUpdateListeners(this::updateBindings); + device.addExistChangeListener(this::handleExistenceChanged); + device.getDeviceFollower().getTrackIndex().addListener(this::changeTrackIndex); + } + + private void updateBindings() { + if (!active) { + return; + } + resetBindings(); + } + + private void handleExistenceChanged(final boolean exists) { + if (!active) { + return; + } + reassign(); + } + + private void resetBindings() { + resetBindings(displayLabelLayer); + resetBindings(displayValueLayer); + resetBindings(faderLayer); + resetBindings(encoderLayer); + } + + private void resetBindings(final Layer layer) { + layer.getBindings().stream().filter(ResetableBinding.class::isInstance).map(ResetableBinding.class::cast) + .forEach(binding -> binding.reset()); + } + + private void changeTrackIndex(final int index) { + if (active) { + if (device.isSpecificDevicePresent()) { + device.getDeviceFollower().ensurePosition(); + } + } + } + + @Override + public void assign() { + final ControlMode mainMode = !mixer.isFlipped() ? mode : ControlMode.VOLUME; + final ControlMode lowMode = mixer.isFlipped() ? mode : ControlMode.VOLUME; + + encoderLayer = mixer.getLayerSource(mainMode).getEncoderLayer(); + faderLayer = mixer.getLayerSource(lowMode).getFaderLayer(); + + if (mixer.hasLowerDisplay()) { + assignDualDisplay(); + } else { + assignSingleDisplay(mainMode, lowMode); + } + assignIfMenuModeActive(); + } + + private void assignDualDisplay() { + final boolean nameValue = mixer.isNameValue(); + final boolean isFlipped = mixer.isFlipped(); + if (!device.isSpecificDevicePresent()) { + infoState = "No EQ+ in Device Chain press EQ+ to add"; + } else { + infoState = null; + } + if (isFlipped) { + mixer.setUpperLowerDestination(ControlMode.VOLUME, mode); + displayLabelLayer = mixer.getTrackDisplayLayer(); + displayValueLayer = mixer.getLayerSource(ControlMode.VOLUME).getDisplayValueLayer(); + + displayLowerLabelLayer = mixer.getLayerSource(mode).getDisplayLabelLayer(); + displayLowerValueLayer = mixer.getLayerSource(mode).getDisplayValueLayer(); + if (infoState != null) { + topRowInfoText.set(infoState); + bottomRowInfoText.set(""); + displayLowerLabelLayer = topRowValueLayer; + displayLowerValueLayer = bottomRowValueLayer; + } else if (nameValue) { + displayLabelLayer = mixer.getLayerSource(ControlMode.VOLUME).getDisplayLabelLayer(); + displayLowerValueLayer = bottomRowValueLayer; + } + } else { + mixer.setUpperLowerDestination(mode, ControlMode.VOLUME); + displayLabelLayer = mixer.getLayerSource(mode).getDisplayLabelLayer(); + displayValueLayer = mixer.getLayerSource(mode).getDisplayValueLayer(); + displayLowerValueLayer = mixer.getLayerSource(ControlMode.VOLUME).getDisplayValueLayer(); + displayLowerLabelLayer = mixer.getTrackDisplayLayer(); + if (infoState != null) { + topRowInfoText.set(infoState); + bottomRowInfoText.set(""); + displayLabelLayer = topRowValueLayer; + displayValueLayer = bottomRowValueLayer; + } else if (nameValue) { + bottomRowInfoText.set(device.getPageInfo()); + displayValueLayer = bottomRowValueLayer; + displayLowerLabelLayer = mixer.getLayerSource(ControlMode.VOLUME).getDisplayLabelLayer(); + } + } + } + + private void assignSingleDisplay(final ControlMode mainMode, final ControlMode lowMode) { + final boolean touched = mixer.isTouched(); + final boolean nameValue = mixer.isNameValue(); + final ControlMode displayMode = touched ? lowMode : mainMode; + + if (displayMode == this.mode) { + if (!device.isSpecificDevicePresent()) { + infoState = "No EQ+ in Device Chain press EQ+ to add"; + } else { + infoState = null; + } + + displayLabelLayer = mixer.getLayerSource(displayMode).getDisplayLabelLayer(); + if (infoState != null) { + topRowInfoText.set(infoState); + bottomRowInfoText.set(""); + displayLabelLayer = topRowValueLayer; + displayValueLayer = bottomRowValueLayer; + } else { + if (nameValue) { + displayValueLayer = bottomRowValueLayer; + } else { + displayValueLayer = mixer.getLayerSource(displayMode).getDisplayValueLayer(); + } + } + } else { + displayLabelLayer = nameValue + ? mixer.getLayerSource(ControlMode.VOLUME).getDisplayLabelLayer() + : mixer.getTrackDisplayLayer(); + displayValueLayer = mixer.getLayerSource(displayMode).getDisplayValueLayer(); + } + if (infoText != null && device.isSpecificDevicePresent()) { + topRowInfoText.set(infoText); + displayLabelLayer = topRowValueLayer; + } + } + + @Override + public void handleInfoState(final boolean start, final Orientation orientation) { + if (active) { + if (start) { + infoText = "EQ+ Page: %s".formatted(device.getPageInfo()); + } else { + infoText = null; + } + reassign(); + } + } + + @Override + public void handleModePress(final VPotMode mode, final boolean pressed, final boolean selection) { + McuExtension.println(" Pressed EQ+ %s present=%s", pressed, device.isSpecificDevicePresent()); + if (pressed) { + device.getDeviceFollower().ensurePosition(); + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/layer/UnifiedDeviceModeLayer.java b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/UnifiedDeviceModeLayer.java new file mode 100644 index 00000000..7cb48acb --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/layer/UnifiedDeviceModeLayer.java @@ -0,0 +1,64 @@ +package com.bitwig.extensions.controllers.mcu.layer; + +import com.bitwig.extensions.controllers.mcu.CursorDeviceControl; +import com.bitwig.extensions.controllers.mcu.VPotMode; +import com.bitwig.extensions.framework.Layers; + +public class UnifiedDeviceModeLayer extends DeviceModeLayer { + private int pageCount; + private final CursorDeviceControl cursorDeviceControl; + private boolean deviceExists; + + public UnifiedDeviceModeLayer(final Layers layers, final ControlMode mode, final MixerSection mixer, + final CursorDeviceControl cursorDeviceControl) { + super(layers, mode, mixer); + this.cursorDeviceControl = cursorDeviceControl; + cursorDeviceControl.getRemotes().pageCount().addValueObserver(pageCount -> { + this.pageCount = pageCount; + update(); + }); + cursorDeviceControl.getCursorDevice().exists().addValueObserver(exists -> { + this.deviceExists = exists; + update(); + }); + } + + private void update() { + evalDeviceMatch(); + if (active) { + reassign(); + } + } + + @Override + protected void evalDeviceMatch() { + if (!deviceExists) { + matchState = State.TYPE_NOT_IN_CHAIN; + } else if (pageCount == 0) { + matchState = State.NO_PARAM_PAGES; + } else { + matchState = State.TYPE_MATCH; + } + } + + @Override + protected void evalInfoState(final VPotMode potMode) { + + } + + @Override + protected void determineInfoState(final boolean flipped) { + String topLineText = deviceInfo; + String bottomLineText = ""; + if (matchState == State.NO_PARAM_PAGES) { + topLineText = "Configure in Bitwig"; + bottomLineText = "%s has no Parameter Pages".formatted(deviceName); + } else if (matchState == State.TYPE_NOT_IN_CHAIN) { + topLineText = "No Device in Chain"; + bottomLineText = ""; + } + bottomRowInfoText.set(flipped ? bottomLineText : topLineText); + topRowInfoText.set(flipped ? topLineText : bottomLineText); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/value/BasicIntValue.java b/src/main/java/com/bitwig/extensions/controllers/mcu/value/BasicIntValue.java new file mode 100644 index 00000000..d2b1ff7b --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/value/BasicIntValue.java @@ -0,0 +1,30 @@ +package com.bitwig.extensions.controllers.mcu.value; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.IntConsumer; + +public class BasicIntValue { + private int value; + private final List listeners = new ArrayList<>(); + + public void set(final int value) { + if (value != this.value) { + this.value = value; + this.listeners.forEach(listener -> listener.accept(this.value)); + } + } + + public void addListener(final IntConsumer listener) { + this.listeners.add(listener); + } + + public void setImmediately(final int value) { + this.value = value; + } + + public int get() { + return value; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/value/DoubleRangeValue.java b/src/main/java/com/bitwig/extensions/controllers/mcu/value/DoubleRangeValue.java new file mode 100644 index 00000000..d243a47a --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/value/DoubleRangeValue.java @@ -0,0 +1,17 @@ +package com.bitwig.extensions.controllers.mcu.value; + +import java.util.function.DoubleConsumer; + +public interface DoubleRangeValue { + double getMin(); + + double getMax(); + + void addDoubleValueObserver(DoubleConsumer callback); + + double getRawValue(); + + String displayedValue(); + + int scale(double v, int range); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/value/DoubleValueConverter.java b/src/main/java/com/bitwig/extensions/controllers/mcu/value/DoubleValueConverter.java new file mode 100644 index 00000000..bc0a1ef3 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/value/DoubleValueConverter.java @@ -0,0 +1,6 @@ +package com.bitwig.extensions.controllers.mcu.value; + +@FunctionalInterface +public interface DoubleValueConverter { + String convert(double v); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/value/IEnumDisplayValue.java b/src/main/java/com/bitwig/extensions/controllers/mcu/value/IEnumDisplayValue.java new file mode 100644 index 00000000..f1caa81e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/value/IEnumDisplayValue.java @@ -0,0 +1,20 @@ +package com.bitwig.extensions.controllers.mcu.value; + +import com.bitwig.extensions.framework.values.BasicStringValue; +import com.bitwig.extensions.framework.values.IntValueObject; + +public interface IEnumDisplayValue { + BasicStringValue getDisplayValue(); + + IntValueObject getRingValue(); + + void increment(int inc); + + void setIndex(int index); + + String getEnumValue(); + + void reset(); + + void stepRoundRobin(); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/value/IncrementHolder.java b/src/main/java/com/bitwig/extensions/controllers/mcu/value/IncrementHolder.java new file mode 100644 index 00000000..11012bdf --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/value/IncrementHolder.java @@ -0,0 +1,34 @@ +package com.bitwig.extensions.controllers.mcu.value; + +import java.util.function.IntConsumer; + +public class IncrementHolder { + + private double currentValue; + private final IntConsumer consumer; + private final double stepMultiplier; + private double lastValue; + + public IncrementHolder(final IntConsumer consumer, final double stepMultiplier) { + this.consumer = consumer; + this.stepMultiplier = stepMultiplier * 100; + } + + public void increment(final double value) { + final double increment = value * stepMultiplier; + + if (Math.signum(lastValue) != Math.signum(value)) { + currentValue = increment; + } else { + currentValue += increment; + if (currentValue >= 1.0) { + consumer.accept(1); + currentValue = 0; + } else if (currentValue <= -1.0) { + consumer.accept(-1); + currentValue = 0; + } + } + lastValue = value; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/value/IncrementalValue.java b/src/main/java/com/bitwig/extensions/controllers/mcu/value/IncrementalValue.java new file mode 100644 index 00000000..80028bda --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/value/IncrementalValue.java @@ -0,0 +1,7 @@ +package com.bitwig.extensions.controllers.mcu.value; + +public interface IncrementalValue { + void increment(int inc); + + String displayedValue(); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/value/IntRange.java b/src/main/java/com/bitwig/extensions/controllers/mcu/value/IntRange.java new file mode 100644 index 00000000..d3115d1c --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/value/IntRange.java @@ -0,0 +1,9 @@ +package com.bitwig.extensions.controllers.mcu.value; + +import java.util.function.IntConsumer; + +public interface IntRange { + void addRangeListener(int range, IntConsumer callback); + + int getIntValue(); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/value/IntValue.java b/src/main/java/com/bitwig/extensions/controllers/mcu/value/IntValue.java new file mode 100644 index 00000000..f53c5a52 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/value/IntValue.java @@ -0,0 +1,17 @@ +package com.bitwig.extensions.controllers.mcu.value; + +import java.util.function.IntConsumer; + +public interface IntValue { + int getMax(); + + int getMin(); + + void addIntValueObserver(IntConsumer callback); + + void addRangeObserver(RangeChangedCallback callback); + + int getIntValue(); + + String displayedValue(); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/value/IntValueConverter.java b/src/main/java/com/bitwig/extensions/controllers/mcu/value/IntValueConverter.java new file mode 100644 index 00000000..35036593 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/value/IntValueConverter.java @@ -0,0 +1,6 @@ +package com.bitwig.extensions.controllers.mcu.value; + +@FunctionalInterface +public interface IntValueConverter { + String convert(int value); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/value/Orientation.java b/src/main/java/com/bitwig/extensions/controllers/mcu/value/Orientation.java new file mode 100644 index 00000000..c4ddf870 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/value/Orientation.java @@ -0,0 +1,6 @@ +package com.bitwig.extensions.controllers.mcu.value; + +public enum Orientation { + HORIZONTAL, + VERTICAL +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/value/RangeChangedCallback.java b/src/main/java/com/bitwig/extensions/controllers/mcu/value/RangeChangedCallback.java new file mode 100644 index 00000000..182d2c2f --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/value/RangeChangedCallback.java @@ -0,0 +1,5 @@ +package com.bitwig.extensions.controllers.mcu.value; + +public interface RangeChangedCallback { + void rangeChanged(int min, int max); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/value/SettableEnumValueSelect.java b/src/main/java/com/bitwig/extensions/controllers/mcu/value/SettableEnumValueSelect.java new file mode 100644 index 00000000..24c04340 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/value/SettableEnumValueSelect.java @@ -0,0 +1,116 @@ +package com.bitwig.extensions.controllers.mcu.value; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.bitwig.extension.controller.api.ObjectProxy; +import com.bitwig.extension.controller.api.SettableEnumValue; +import com.bitwig.extensions.framework.values.BasicStringValue; +import com.bitwig.extensions.framework.values.IntValueObject; + +public class SettableEnumValueSelect implements IEnumDisplayValue { + + private final SettableEnumValue value; + private final List knownValues = new ArrayList<>(); + private final BasicStringValue displayValue = new BasicStringValue(""); + private final IntValueObject ringValue = new IntValueObject(0, 0, 11); + private boolean valueExists = false; + + public record Value(String value, String displayValue, int ringValue) { + // + } + + public SettableEnumValueSelect(final SettableEnumValue value, final ObjectProxy existsRef, + final Value... knownValues) { + this(value, knownValues); + existsRef.exists().addValueObserver(exists -> { + this.valueExists = exists; + updateDisplay(getIndex(value.get())); + }); + } + + public SettableEnumValueSelect(final SettableEnumValue value, final Value... knownValues) { + this.value = value; + value.markInterested(); + displayValue.set(value.get()); + Collections.addAll(this.knownValues, knownValues); + valueExists = true; + this.value.addValueObserver(newValue -> { + final Integer index = getIndex(newValue); + updateDisplay(index); + }); + } + + private void updateDisplay(final Integer index) { + if (valueExists) { + if (index != -1) { + displayValue.set(this.knownValues.get(index).displayValue); + ringValue.set(this.knownValues.get(index).ringValue); + } + } else { + displayValue.set(""); + ringValue.set(-1); + } + } + + public int getIndex(final String value) { + for (int i = 0; i < knownValues.size(); i++) { + if (knownValues.get(i).value.equals(value)) { + return i; + } + } + return -1; + } + + public SettableEnumValue getValue() { + return value; + } + + @Override + public BasicStringValue getDisplayValue() { + return displayValue; + } + + @Override + public IntValueObject getRingValue() { + return ringValue; + } + + @Override + public void stepRoundRobin() { + final int index = getIndex(value.get()); + if (index != -1) { + final int nextIndex = (index + 1) % knownValues.size(); + value.set(knownValues.get(nextIndex).value); + } + } + + @Override + public void increment(final int inc) { + final int index = getIndex(value.get()); + if (index != -1) { + final int nextIndex = index + inc; + if (nextIndex >= 0 && nextIndex < knownValues.size()) { + value.set(knownValues.get(nextIndex).value); + } + } + } + + @Override + public String getEnumValue() { + return value.get(); + } + + @Override + public void reset() { + setIndex(0); + } + + @Override + public void setIndex(final int index) { + if (index >= 0 && index < knownValues.size()) { + value.set(knownValues.get(index).value); + } + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/value/StringValueConverter.java b/src/main/java/com/bitwig/extensions/controllers/mcu/value/StringValueConverter.java new file mode 100644 index 00000000..8608045e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/value/StringValueConverter.java @@ -0,0 +1,8 @@ +package com.bitwig.extensions.controllers.mcu.value; + +/** + * Handles in converting a string into a different format. + */ +public interface StringValueConverter { + String convert(String displayValue); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/mcu/value/TrackColor.java b/src/main/java/com/bitwig/extensions/controllers/mcu/value/TrackColor.java new file mode 100644 index 00000000..985e8c11 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/mcu/value/TrackColor.java @@ -0,0 +1,68 @@ +package com.bitwig.extensions.controllers.mcu.value; + +import com.bitwig.extension.controller.api.HardwareLightVisualState; +import com.bitwig.extension.controller.api.InternalHardwareLightState; + +public class TrackColor extends InternalHardwareLightState { + + private final int[] colors = new int[8]; + + private TrackColor lastFetch = null; + + public TrackColor() { + } + + private TrackColor(int[] colors) { + System.arraycopy(colors, 0, this.colors, 0, 8); + } + + @Override + public HardwareLightVisualState getVisualState() { + return null; + } + + @Override + public boolean equals(final Object obj) { + if (obj instanceof TrackColor color) { + return compares(color); + } + return false; + } + + private boolean compares(final TrackColor other) { + for (int i = 0; i < colors.length; i++) { + if (other.colors[i] != colors[i]) { + return false; + } + } + return true; + } + + private boolean compares(int[] currentState) { + if (currentState.length != colors.length) { + return false; + } + for (int i = 0; i < colors.length; i++) { + if (currentState[i] != colors[i]) { + return false; + } + } + return true; + } + + public int[] getColors() { + if (lastFetch != null) { + return lastFetch.colors; + } + return this.colors; + } + + public TrackColor getState(int[] currentState) { + if (lastFetch != null && lastFetch.compares(currentState)) { + return lastFetch; + } + lastFetch = new TrackColor(currentState); + return lastFetch; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/midiplus/X2MiniControllerExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/midiplus/X2MiniControllerExtensionDefinition.java index f117d59f..6c5b3d72 100644 --- a/src/main/java/com/bitwig/extensions/controllers/midiplus/X2MiniControllerExtensionDefinition.java +++ b/src/main/java/com/bitwig/extensions/controllers/midiplus/X2MiniControllerExtensionDefinition.java @@ -44,11 +44,7 @@ public void listAutoDetectionMidiPortNames( list.add(new String[]{"X2mini MIDI 1"}, new String[]{"X2mini MIDI 1"}); break; - case WINDOWS: - list.add(new String[]{"X2mini"}, new String[]{"X2mini"}); - break; - - case MAC: + case WINDOWS, MAC: list.add(new String[]{"X2mini"}, new String[]{"X2mini"}); break; } @@ -90,12 +86,6 @@ public int getRequiredAPIVersion() return XControllerExtension.REQUIRED_API_VERSION; } - @Override - public boolean shouldFailOnDeprecatedUse() - { - return true; - } - public static X2MiniControllerExtensionDefinition getInstance() { return INSTANCE; diff --git a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/midi/NhiaSysexTextCommand.java b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/midi/NhiaSysexTextCommand.java index 914fbf46..28f32363 100644 --- a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/midi/NhiaSysexTextCommand.java +++ b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/midi/NhiaSysexTextCommand.java @@ -1,5 +1,8 @@ package com.bitwig.extensions.controllers.nativeinstruments.komplete.midi; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + import com.bitwig.extension.controller.api.MidiOut; /** @@ -34,8 +37,9 @@ private void send(final MidiOut midiOut, final int value, final int track, final dataArray[12] = (byte) track; final byte[] sendarray = new byte[dataArray.length + text.length()]; System.arraycopy(dataArray, 0, sendarray, 0, 13); - for (int i = 0; i < text.length(); i++) { - sendarray[13 + i] = (byte) text.charAt(i); + final byte[] chars = text.getBytes(StandardCharsets.US_ASCII); + for (int i = 0; i < chars.length; i++) { + sendarray[13 + i] = chars[i]; } sendarray[sendarray.length - 1] = SYSEX_END; midiOut.sendSysex(sendarray); diff --git a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschine/MaschineExtension.java b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschine/MaschineExtension.java index 1a8118c8..92ac9912 100644 --- a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschine/MaschineExtension.java +++ b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschine/MaschineExtension.java @@ -14,6 +14,7 @@ import com.bitwig.extensions.framework.time.TimedEvent; import com.bitwig.extensions.framework.values.BooleanValueObject; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Iterator; import java.util.LinkedList; @@ -961,9 +962,9 @@ public void sendToDisplay(final int grid, final String text) { } lastSenGrids[grid] = text; displayBuffer[6] = (byte) (Math.min(grid, 3) * 28); - final char[] ca = text.toCharArray(); + final byte[] ca = text.getBytes(StandardCharsets.US_ASCII); for (int i = 0; i < 28; i++) { - displayBuffer[i + 7] = i < ca.length ? (byte) ca[i] : 32; + displayBuffer[i + 7] = i < ca.length ? ca[i] : 32; } midiOut.sendSysex(displayBuffer); } diff --git a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschinemikro/RgbLightState.java b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschinemikro/RgbLightState.java new file mode 100644 index 00000000..0fc2419f --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschinemikro/RgbLightState.java @@ -0,0 +1,87 @@ +package com.bitwig.extensions.controllers.nativeinstruments.maschinemikro; + +import com.bitwig.extension.api.Color; +import com.bitwig.extension.controller.api.HardwareLightVisualState; +import com.bitwig.extension.controller.api.InternalHardwareLightState; +import com.bitwig.extensions.controllers.nativeinstruments.maschinemikro.layers.LedBehavior; +import com.bitwig.extensions.framework.values.Midi; + +import java.util.HashMap; +import java.util.Map; + +public class RgbLightState extends InternalHardwareLightState { + + private static final Map STATE_MAP = new HashMap<>(); + + public static final RgbLightState OFF = new RgbLightState(0); + public static final RgbLightState WHITE = RgbLightState.of(3); + public static final RgbLightState WHITE_DIM = RgbLightState.of(1); + public static final RgbLightState RED = new RgbLightState(5); + public static final RgbLightState GREEN = new RgbLightState(21); + public static final RgbLightState GREEN_PLAY = new RgbLightState(21, LedBehavior.PULSE_2); + + private final int colorIndex; + private final LedBehavior ledBehavior; + + public static RgbLightState of(final int colorIndex) { + return STATE_MAP.computeIfAbsent(colorIndex | LedBehavior.FULL.getCode() << 8, + index -> new RgbLightState(colorIndex)); + } + + public static RgbLightState of(final int colorIndex, final LedBehavior behavior) { + return STATE_MAP.computeIfAbsent(colorIndex | behavior.getCode() << 8, + index -> new RgbLightState(colorIndex, behavior)); + } + + public static RgbLightState forColor(final Color color) { + if (color == null || color.getAlpha() == 0 + || color.getRed() == 0 && color.getGreen() == 0 && color.getBlue() == 0) { + return OFF; + } + // TODO: Better color mapping here. + // This will be used for manual mapping feedback + return WHITE; + } + + public RgbLightState behavior(final LedBehavior behavior) { + if (this.ledBehavior == behavior) { + return this; + } + return of(this.colorIndex, behavior); + } + + private RgbLightState(final int colorIndex) { + this(colorIndex, LedBehavior.FULL); + } + + private RgbLightState(final int colorIndex, final LedBehavior ledBehavior) { + this.colorIndex = colorIndex; + this.ledBehavior = ledBehavior; + } + + public int getColorIndex() { + return colorIndex; + } + + public int getMidiCode() { + return Midi.NOTE_ON | ledBehavior.getCode(); + } + + @Override + public HardwareLightVisualState getVisualState() { + if (colorIndex == 0) { + return null; + } + + // TODO: Better visual representation + return HardwareLightVisualState.createForColor(Color.fromRGB(1, 1, 1)); + } + + @Override + public boolean equals(final Object o) { + if (o instanceof final RgbLightState other) { + return other.colorIndex == colorIndex && other.ledBehavior == ledBehavior; + } + return false; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschinemikro/buttons/RgbButton.java b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschinemikro/buttons/RgbButton.java index 51429d6d..e00cd14a 100644 --- a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschinemikro/buttons/RgbButton.java +++ b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschinemikro/buttons/RgbButton.java @@ -4,9 +4,9 @@ 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.apcmk2.led.RgbLightState; import com.bitwig.extensions.controllers.nativeinstruments.maschinemikro.MidiProcessor; import com.bitwig.extensions.controllers.nativeinstruments.maschinemikro.RgbColor; +import com.bitwig.extensions.controllers.nativeinstruments.maschinemikro.RgbLightState; import com.bitwig.extensions.framework.Layer; import java.util.function.Supplier; @@ -14,9 +14,10 @@ public class RgbButton extends GateButton { private final MultiStateHardwareLight light; - public RgbButton(final int midiId, String name, HardwareSurface surface, MidiProcessor midiProcessor) { + public RgbButton(final int midiId, final String name, final HardwareSurface surface, + final MidiProcessor midiProcessor) { super(midiId, midiProcessor); - MidiIn midiIn = midiProcessor.getMidiIn(); + final MidiIn midiIn = midiProcessor.getMidiIn(); hwButton = surface.createHardwareButton(name + "_" + midiId); hwButton.pressedAction().setPressureActionMatcher(midiIn.createNoteOnVelocityValueMatcher(0, midiId)); hwButton.releasedAction().setActionMatcher(midiIn.createNoteOffActionMatcher(0, midiId)); @@ -24,21 +25,22 @@ public RgbButton(final int midiId, String name, HardwareSurface surface, MidiPro hwButton.isPressed().markInterested(); light = surface.createMultiStateHardwareLight(name + "_LIGHT_" + midiId); light.state().setValue(RgbLightState.OFF); + light.setColorToStateFunction(RgbLightState::forColor); hwButton.isPressed().markInterested(); light.state().onUpdateHardware(this::updateState); } - private void updateState(InternalHardwareLightState state) { - if (state instanceof RgbColor rgbState) { + private void updateState(final InternalHardwareLightState state) { + if (state instanceof final RgbColor rgbState) { midiProcessor.updateColorPad(midiId, rgbState); } } - public void bindLight(Layer layer, final Supplier supplier) { + public void bindLight(final Layer layer, final Supplier supplier) { layer.bindLightState(supplier, this.light); } - public void bindDisabled(Layer layer) { + public void bindDisabled(final Layer layer) { this.bindLight(layer, () -> RgbColor.OFF); this.bindRelease(layer, () -> { }); diff --git a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschinemikro/layers/LedBehavior.java b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschinemikro/layers/LedBehavior.java new file mode 100644 index 00000000..05b6d4ab --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschinemikro/layers/LedBehavior.java @@ -0,0 +1,30 @@ +package com.bitwig.extensions.controllers.nativeinstruments.maschinemikro.layers; + +public enum LedBehavior { + LIGHT_10(0), + LIGHT_25(1), + LIGHT_50(2), + LIGHT_60(3), + LIGHT_75(4), + LIGHT_90(5), + FULL(6), + PULSE_16(7), + PULSE_8(8), + PULSE_4(9), + PULSE_2(10), + BLINK_24(11), + BLINK_16(12), + BLINK_8(13), + BLINK_4(14), + BLINK_2(15); + final int code; + + LedBehavior(final int code) { + this.code = code; + } + + public int getCode() { + return code; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschinemikro/layers/SceneLayer.java b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschinemikro/layers/SceneLayer.java index dfc33ed0..a3652845 100644 --- a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschinemikro/layers/SceneLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/maschinemikro/layers/SceneLayer.java @@ -40,7 +40,7 @@ public SceneLayer(Layers layers, HwElements hwElements, ControllerHost host, Mod sceneBank.scrollPosition().addValueObserver(value -> sceneOffset = value); List padButtons = hwElements.getPadButtons(); sceneBank.setIndication(true); - + for (int i = 0; i < 16; i++) { final int index = i; RgbButton button = padButtons.get(i); diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/ColorLookup.java b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/ColorLookup.java index 6d96433d..808a7533 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/ColorLookup.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/ColorLookup.java @@ -1,8 +1,17 @@ package com.bitwig.extensions.controllers.novation.commonsmk3; +import com.bitwig.extension.api.Color; + public class ColorLookup { private static final Hsb BLACK_HSB = new Hsb(0, 0, 0); + public static int toColor(final Color color) { + if (color == null || color.getAlpha() == 0) { + return 0; + } + return toColor((float) color.getRed(), (float) color.getGreen(), (float) color.getBlue()); + } + public static int toColor(final float r, final float g, final float b) { final int rv = (int) Math.floor(r * 255); final int gv = (int) Math.floor(g * 255); @@ -28,7 +37,7 @@ public static int toColor(final float r, final float g, final float b) { } else if (hsb.bright <= 8) { color += 2; } - //return color; + // return color; return adjust(color); } } @@ -41,7 +50,7 @@ private static int adjust(final int c) { return c; } - public static Hsb rgbToHsb(final float rv, final float gv, final float bv) { + private static Hsb rgbToHsb(final float rv, final float gv, final float bv) { final float rgb_max = Math.max(Math.max(rv, gv), bv); final float rgb_min = Math.min(Math.min(rv, gv), bv); final int bright = (int) rgb_max; @@ -80,13 +89,87 @@ public Hsb(final int hue, final int sat, final int bright) { @Override public String toString() { - final StringBuilder sb = new StringBuilder("Hsb{"); - sb.append("hue=").append(hue); - sb.append(", sat=").append(sat); - sb.append(", bright=").append(bright); - sb.append('}'); - return sb.toString(); + final String sb = "Hsb{" + "hue=" + hue + + ", sat=" + sat + + ", bright=" + bright + + '}'; + return sb; + } + } + + public static Color colorIndexToApiColor(final int colorIndex) { + if (colorIndex >= 0 && colorIndex < PALETTE.length) { + return PALETTE[colorIndex]; } + + return PALETTE[3]; } + private static final Color[] PALETTE = { + rgb(97, 97, 97), + rgb(179, 179, 179), + rgb(221, 221, 221), + rgb(255, 255, 255), + rgb(250, 179, 178), + rgb(246, 99, 102), + rgb(215, 98, 99), + rgb(188, 89, 101), + rgb(254, 242, 214), + rgb(250, 176, 112), + rgb(225, 135, 99), + rgb(175, 118, 100), + rgb(253, 252, 172), + rgb(253, 253, 104), + rgb(220, 221, 100), + rgb(180, 186, 97), + rgb(219, 255, 155), + rgb(194, 255, 104), + rgb(194, 255, 104), + rgb(135, 180, 94), + rgb(196, 252, 180), + rgb(117, 250, 110), + rgb(99, 228, 86), + rgb(104, 180, 96), + rgb(191, 255, 191), + rgb(109, 254, 142), + rgb(109, 219, 127), + rgb(102, 180, 106), + rgb(186, 255, 196), + rgb(125, 250, 205), + rgb(103, 226, 160), + rgb(117, 174, 132), + rgb(199, 255, 243), + rgb(112, 254, 229), + rgb(110, 222, 191), + rgb(105, 183, 146), + rgb(197, 244, 254), + rgb(117, 238, 252), + rgb(106, 202, 218), + rgb(106, 202, 218), + rgb(196, 221, 254), + rgb(113, 201, 245), + rgb(108, 161, 218), + rgb(103, 130, 178), + rgb(162, 137, 253), + rgb(105, 100, 243), + rgb(102, 97, 223), + rgb(99, 97, 179), + rgb(207, 178, 254), + rgb(165, 94, 253), + rgb(129, 98, 223), + rgb(120, 96, 185), + rgb(245, 185, 252), + rgb(242, 104, 247), + rgb(216, 100, 220), + rgb(172, 100, 180), + rgb(251, 180, 216), + rgb(249, 93, 193), + rgb(218, 101, 165), + rgb(178, 93, 137), + rgb(246, 119, 104) + }; + + private static Color rgb(final int r, final int g, final int b) { + return Color.fromRGB255(r, g, b); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/DrumButton.java b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/DrumButton.java index e3d3d6e9..d7fb99ea 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/DrumButton.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/DrumButton.java @@ -18,7 +18,6 @@ public DrumButton(final HardwareSurface surface, final MidiProcessor midiProcess initButtonNote(midiProcessor.getMidiIn(), notevalue); light.state().setValue(RgbState.of(0)); light.state().onUpdateHardware(this::updateState); - hwButton.setBackgroundLight(light); } private void updateState(final InternalHardwareLightState state) { diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/GridButton.java b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/GridButton.java index 9ad333a5..37cccad0 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/GridButton.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/GridButton.java @@ -14,7 +14,6 @@ public GridButton(final HardwareSurface surface, final MidiProcessor midiProcess initButtonNote(midiProcessor.getMidiIn(), notevalue); light.state().setValue(RgbState.of(0)); light.state().onUpdateHardware(this::updateState); - hwButton.setBackgroundLight(light); } private void updateState(final InternalHardwareLightState state) { diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/LabeledButton.java b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/LabeledButton.java index 3b9eb34e..0fbead9f 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/LabeledButton.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/LabeledButton.java @@ -11,7 +11,6 @@ public LabeledButton(final String name, final HardwareSurface surface, final Mid this.ccValue = ccValue; initButtonCc(midiProcessor.getMidiIn(), ccValue); light.state().onUpdateHardware(state -> midiProcessor.updatePadLed(state, ccValue)); - hwButton.setBackgroundLight(light); } public LabeledButton(final HardwareSurface surface, final MidiProcessor midiProcessor, @@ -20,7 +19,6 @@ public LabeledButton(final HardwareSurface surface, final MidiProcessor midiProc ccValue = ccAssignment.getCcValue(); initButtonCc(midiProcessor.getMidiIn(), ccAssignment); light.state().onUpdateHardware(state -> midiProcessor.updatePadLed(state, ccValue)); - hwButton.setBackgroundLight(light); } @Override diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/LaunchPadButton.java b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/LaunchPadButton.java index 5f217004..3106b4fb 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/LaunchPadButton.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/LaunchPadButton.java @@ -1,274 +1,321 @@ package com.bitwig.extensions.controllers.novation.commonsmk3; -import com.bitwig.extension.controller.api.*; -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; - import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -public abstract class LaunchPadButton { - public static final int STD_REPEAT_DELAY = 400; - public static final int STD_REPEAT_FREQUENCY = 50; - - protected HardwareButton hwButton; - protected MultiStateHardwareLight light; - protected final MidiProcessor midiProcessor; - protected final int channel; - private TimedEvent currentTimer; - private long recordedDownTime; - - protected LaunchPadButton(final String id, final HardwareSurface surface, final MidiProcessor midiProcessor, - final int channel) { - super(); - this.midiProcessor = midiProcessor; - this.channel = channel; - hwButton = surface.createHardwareButton(id); - light = surface.createMultiStateHardwareLight(id + "-light"); - light.state().setValue(RgbState.of(0)); - hwButton.isPressed().markInterested(); - } - - protected void initButtonNote(final MidiIn midiIn, final int notevalue) { - hwButton.pressedAction().setPressureActionMatcher(midiIn.createNoteOnVelocityValueMatcher(channel, notevalue)); - hwButton.releasedAction().setActionMatcher(midiIn.createNoteOffActionMatcher(channel, notevalue)); - } - - protected void initButtonCc(final MidiIn midiIn, final CCSource ccAssignment) { - hwButton.pressedAction().setActionMatcher(ccAssignment.createMatcher(midiIn, 127)); - hwButton.releasedAction().setActionMatcher(ccAssignment.createMatcher(midiIn, 0)); - } - - protected void initButtonCc(final MidiIn midiIn, final int ccValue) { - hwButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, ccValue, 127)); - hwButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, ccValue, 0)); - } - - public HardwareButton getHwButton() { - return hwButton; - } - - public MultiStateHardwareLight getLight() { - return light; - } - - public abstract void refresh(); - - public void bind(final Layer layer, final Runnable action, final BooleanSupplier state, final NovationColor color) { - final RgbState onState = color.getHiColor(); - final RgbState offState = color.getLowColor(); - if (state instanceof BooleanValue) { - ((BooleanValue) state).markInterested(); - } - layer.bind(hwButton, hwButton.pressedAction(), action); - layer.bindLightState(() -> pressedStateCombo(hwButton.isPressed(), state, onState, offState), light); - } - - private RgbState pressedStateCombo(final BooleanValue actionValue, final BooleanSupplier state, - final RgbState onState, final RgbState offState) { - if (!state.getAsBoolean()) { - return RgbState.of(0); - } - if (actionValue.get()) { - return onState; - } - return offState; - } - - public void disable(final Layer layer) { - bindPressed(layer, () -> { - }); - bindRelease(layer, () -> { - }); - bindLight(layer, () -> RgbState.OFF); - } - - public void bind(final Layer layer, final Runnable action, final NovationColor onColor) { - final RgbState onState = onColor.getHiColor(); - final RgbState offState = onColor.getLowColor(); - layer.bind(hwButton, hwButton.pressedAction(), action); - layer.bindLightState(() -> hwButton.isPressed().get() ? onState : offState, light); - } - - /** - * @param layer the layer - * @param downAction the press down action - * @param releaseAction the action to be invoked at release passing true if time was exceeded, false if not - * @param releaseTime the time the button needs to be down for the release action to be invoked upon release - */ - public void bindPressReleaseAfter(final Layer layer, final Runnable downAction, - final Consumer releaseAction, final long releaseTime) { - layer.bind(hwButton, hwButton.pressedAction(), () -> { - recordedDownTime = System.currentTimeMillis(); - downAction.run(); - }); - layer.bind(hwButton, hwButton.releasedAction(), () -> { - if (System.currentTimeMillis() - recordedDownTime > releaseTime) { - releaseAction.accept(true); - } else { - releaseAction.accept(false); - } - }); - } - - public void bindPressed(final Layer layer, final Runnable action) { - layer.bind(hwButton, hwButton.pressedAction(), action); - } - - public void bindRelease(final Layer layer, final Runnable action) { - layer.bind(hwButton, hwButton.releasedAction(), action); - } - - - public void bind(final Layer layer, final Runnable action, final Supplier supplier) { - layer.bind(hwButton, hwButton.pressedAction(), action); - layer.bindLightState(supplier, light); - } - - public void bindPressed(final Layer layer, final Consumer target, - final Function colorFunction) { - layer.bind(hwButton, hwButton.pressedAction(), () -> target.accept(true)); - layer.bind(hwButton, hwButton.releasedAction(), () -> target.accept(false)); - layer.bindLightState(() -> colorFunction.apply(hwButton.isPressed().get()), light); - } - - public void bindPressed(final Layer layer, final Consumer target, final NovationColor onColor) { - final RgbState onState = onColor.getHiColor(); - final RgbState offState = onColor.getLowColor(); - layer.bind(hwButton, hwButton.pressedAction(), () -> target.accept(true)); - layer.bind(hwButton, hwButton.releasedAction(), () -> target.accept(false)); - layer.bindLightState(() -> hwButton.isPressed().get() ? onState : offState, light); - } - - public void bindPressed(final Layer layer, final Consumer target) { - layer.bind(hwButton, hwButton.pressedAction(), () -> target.accept(true)); - layer.bind(hwButton, hwButton.releasedAction(), () -> target.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. - * - * @param layer the layer this is bound to - * @param action action to be invoked and after a delay repeat - * @param repeatDelay time in ms until the action gets repeated - * @param repeatFrequency time interval in ms between repeats, values should be >= 50ms - */ - public void bindRepeatHold(final Layer layer, final Runnable action, final int repeatDelay, - final int repeatFrequency) { - layer.bind(hwButton, hwButton.pressedAction(), () -> initiateRepeat(action, repeatDelay, repeatFrequency)); - layer.bind(hwButton, hwButton.releasedAction(), this::cancelEvent); - } - - /** - * Binds the given action to a button. Upon pressing the button the action is immediately executed. However while - * holding the button, the action repeats after an initial delay. 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); - } - - /** - * Creates a binding that invokes the press action after a delay. If the released before, the press action is not - * executed. - * - * @param layer the layer this is bound to - * @param pressAction the action to be invoked upon delay, if release before this is not invoked - * @param releaseAction the release action - * @param delayTime the time the button need to be held for the press action to be invoked - */ - public void bindDelayedAction(final Layer layer, final Runnable pressAction, final Runnable releaseAction, - final int delayTime) { - layer.bind(hwButton, hwButton.pressedAction(), () -> initiateTimed(pressAction, delayTime)); - layer.bind(hwButton, hwButton.releasedAction(), () -> { - cancelEvent(); - releaseAction.run(); - }); - } - - private void initiateTimed(final Runnable pressAction, final int delayTime) { - currentTimer = new TimedDelayEvent(pressAction, delayTime); - midiProcessor.queueEvent(currentTimer); - } - - public void bindLight(final Layer layer, final Supplier supplier) { - layer.bindLightState(supplier, 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 RgbState state, final RgbState holdState) { - layer.bindLightState(() -> hwButton.isPressed().get() ? holdState : state, light); - } - - /** - * A light state that is active or not. If not active, the light is off. If active which light depends if the - * button is being pressed or ot. - * - * @param layer the layer this is bound to - * @param isActiveState the active state - * @param active standard active color - * @param pressed active color when button iw being pressed - */ - public void bindHighlightButton(final Layer layer, final BooleanValue isActiveState, final RgbState active, - final RgbState pressed) { - layer.bindLightState(() -> { - if (isActiveState.getAsBoolean()) { - return hwButton.isPressed().get() ? pressed : active; - } - return RgbState.OFF; - }, light); - } - - public void bindHighlightButton(final Layer layer, final BooleanSupplier isActiveState, final RgbState active, - final RgbState pressed) { - layer.bindLightState(() -> { - if (isActiveState.getAsBoolean()) { - return hwButton.isPressed().get() ? pressed : active; - } - return RgbState.OFF; - }, light); - } - - public void bindPressed(final Layer layer, final Consumer target, final RgbState onState, - final RgbState offState) { - layer.bind(hwButton, hwButton.pressedAction(), () -> target.accept(true)); - layer.bind(hwButton, hwButton.releasedAction(), () -> target.accept(false)); - layer.bindLightState(() -> hwButton.isPressed().get() ? onState : offState, light); - } - - public void bindPressed(final Layer layer, final SettableBooleanValue value, final NovationColor color) { - final RgbState onState = color.getHiColor(); - final RgbState offState = color.getLowColor(); - layer.bind(hwButton, hwButton.pressedAction(), () -> value.set(true)); - layer.bind(hwButton, hwButton.releasedAction(), () -> value.set(false)); - layer.bindLightState(() -> value.get() ? onState : offState, light); - } - - private void cancelEvent() { - if (currentTimer != null) { - currentTimer.cancel(); - currentTimer = null; - } - } +import com.bitwig.extension.controller.api.BooleanValue; +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.framework.Layer; +import com.bitwig.extensions.framework.time.TimeRepeatEvent; +import com.bitwig.extensions.framework.time.TimedDelayEvent; +import com.bitwig.extensions.framework.time.TimedEvent; +public abstract class LaunchPadButton { + public static final int STD_REPEAT_DELAY = 400; + public static final int STD_REPEAT_FREQUENCY = 50; + public static final int TAP_INTERVAL = 200; + + protected final HardwareButton hwButton; + protected final MultiStateHardwareLight light; + protected final MidiProcessor midiProcessor; + protected final int channel; + private TimedEvent currentTimer; + private long recordedDownTime; + private DoubleTapState waitingForDouble = DoubleTapState.INIT; + + private enum DoubleTapState { + INIT, WAIT_FOR_DOUBLE, DOUBLE_OCCURRED + } + + protected LaunchPadButton(final String id, final HardwareSurface surface, final MidiProcessor midiProcessor, + final int channel) { + super(); + this.midiProcessor = midiProcessor; + this.channel = channel; + hwButton = surface.createHardwareButton(id); + light = surface.createMultiStateHardwareLight(id + "-light"); + light.state().setValue(RgbState.of(0)); + light.setColorToStateFunction(RgbState::forColor); + hwButton.setBackgroundLight(light); + hwButton.isPressed().markInterested(); + } + + protected void initButtonNote(final MidiIn midiIn, final int notevalue) { + hwButton.pressedAction().setPressureActionMatcher(midiIn.createNoteOnVelocityValueMatcher(channel, notevalue)); + hwButton.releasedAction().setActionMatcher(midiIn.createNoteOffActionMatcher(channel, notevalue)); + } + + protected void initButtonCc(final MidiIn midiIn, final CCSource ccAssignment) { + hwButton.pressedAction().setActionMatcher(ccAssignment.createMatcher(midiIn, 127)); + hwButton.releasedAction().setActionMatcher(ccAssignment.createMatcher(midiIn, 0)); + } + + protected void initButtonCc(final MidiIn midiIn, final int ccValue) { + hwButton.pressedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, ccValue, 127)); + hwButton.releasedAction().setActionMatcher(midiIn.createCCActionMatcher(channel, ccValue, 0)); + } + + public HardwareButton getHwButton() { + return hwButton; + } + + public MultiStateHardwareLight getLight() { + return light; + } + + public abstract void refresh(); + + public void bind(final Layer layer, final Runnable action, final BooleanSupplier state, final NovationColor color) { + final RgbState onState = color.getHiColor(); + final RgbState offState = color.getLowColor(); + if (state instanceof BooleanValue) { + ((BooleanValue) state).markInterested(); + } + layer.bind(hwButton, hwButton.pressedAction(), action); + layer.bindLightState(() -> pressedStateCombo(hwButton.isPressed(), state, onState, offState), light); + } + + private RgbState pressedStateCombo(final BooleanValue actionValue, final BooleanSupplier state, + final RgbState onState, final RgbState offState) { + if (!state.getAsBoolean()) { + return RgbState.of(0); + } + if (actionValue.get()) { + return onState; + } + return offState; + } + + public void disable(final Layer layer) { + bindPressed(layer, () -> { + }); + bindRelease(layer, () -> { + }); + bindLight(layer, () -> RgbState.OFF); + } + + public void bind(final Layer layer, final Runnable action, final NovationColor onColor) { + final RgbState onState = onColor.getHiColor(); + final RgbState offState = onColor.getLowColor(); + layer.bind(hwButton, hwButton.pressedAction(), action); + layer.bindLightState(() -> hwButton.isPressed().get() ? onState : offState, light); + } + + /** + * @param layer the layer + * @param downAction the press down action + * @param releaseAction the action to be invoked at release passing true if time was exceeded, false if not + * @param releaseTime the time the button needs to be down for the release action to be invoked upon release + */ + public void bindPressReleaseAfter(final Layer layer, final Runnable downAction, + final Consumer releaseAction, final long releaseTime) { + layer.bind(hwButton, hwButton.pressedAction(), () -> { + recordedDownTime = System.currentTimeMillis(); + downAction.run(); + }); + layer.bind(hwButton, hwButton.releasedAction(), () -> { + if (System.currentTimeMillis() - recordedDownTime > releaseTime) { + releaseAction.accept(true); + } else { + releaseAction.accept(false); + } + }); + } + + public void bindPressed(final Layer layer, final Runnable action) { + layer.bind(hwButton, hwButton.pressedAction(), action); + } + + public void bindDoubleTapCombo(final Layer layer, final Runnable singleAction, final Runnable doubleAction, + final BooleanSupplier doubleRequired) { + layer.bind( + hwButton, hwButton.pressedAction(), () -> handleDoubleTap(singleAction, doubleAction, doubleRequired)); + } + + private void handleDoubleTap(final Runnable singleAction, final Runnable doubleAction, + final BooleanSupplier doubleRequired) { + if (doubleRequired.getAsBoolean()) { + if (waitingForDouble != DoubleTapState.WAIT_FOR_DOUBLE) { + recordedDownTime = System.currentTimeMillis(); + midiProcessor.delayAction(TAP_INTERVAL + 5, () -> { + if (waitingForDouble == DoubleTapState.DOUBLE_OCCURRED) { + doubleAction.run(); + } else { + singleAction.run(); + } + waitingForDouble = DoubleTapState.INIT; + }); + waitingForDouble = DoubleTapState.WAIT_FOR_DOUBLE; + } else { + final long clickTime = System.currentTimeMillis() - recordedDownTime; + if (clickTime < TAP_INTERVAL) { + waitingForDouble = DoubleTapState.DOUBLE_OCCURRED; + } else { + waitingForDouble = DoubleTapState.INIT; + } + recordedDownTime = System.currentTimeMillis(); + } + } else { + singleAction.run(); + } + } + + public void bindRelease(final Layer layer, final Runnable action) { + layer.bind(hwButton, hwButton.releasedAction(), action); + } + + public void bind(final Layer layer, final Runnable action, final Supplier supplier) { + layer.bind(hwButton, hwButton.pressedAction(), action); + layer.bindLightState(supplier, light); + } + + public void bindPressed(final Layer layer, final Consumer target, + final Function colorFunction) { + layer.bind(hwButton, hwButton.pressedAction(), () -> target.accept(true)); + layer.bind(hwButton, hwButton.releasedAction(), () -> target.accept(false)); + layer.bindLightState(() -> colorFunction.apply(hwButton.isPressed().get()), light); + } + + public void bindPressed(final Layer layer, final Consumer target, final NovationColor onColor) { + final RgbState onState = onColor.getHiColor(); + final RgbState offState = onColor.getLowColor(); + layer.bind(hwButton, hwButton.pressedAction(), () -> target.accept(true)); + layer.bind(hwButton, hwButton.releasedAction(), () -> target.accept(false)); + layer.bindLightState(() -> hwButton.isPressed().get() ? onState : offState, light); + } + + public void bindPressed(final Layer layer, final Consumer target) { + layer.bind(hwButton, hwButton.pressedAction(), () -> target.accept(true)); + layer.bind(hwButton, hwButton.releasedAction(), () -> target.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. + * + * @param layer the layer this is bound to + * @param action action to be invoked and after a delay repeat + * @param repeatDelay time in ms until the action gets repeated + * @param repeatFrequency time interval in ms between repeats, values should be >= 50ms + */ + public void bindRepeatHold(final Layer layer, final Runnable action, final int repeatDelay, + final int repeatFrequency) { + layer.bind(hwButton, hwButton.pressedAction(), () -> initiateRepeat(action, repeatDelay, repeatFrequency)); + layer.bind(hwButton, hwButton.releasedAction(), this::cancelEvent); + } + + /** + * Binds the given action to a button. Upon pressing the button the action is immediately executed. However while + * holding the button, the action repeats after an initial delay. 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); + } + + /** + * Creates a binding that invokes the press action after a delay. If the released before, the press action is not + * executed. + * + * @param layer the layer this is bound to + * @param pressAction the action to be invoked upon delay, if release before this is not invoked + * @param releaseAction the release action + * @param delayTime the time the button need to be held for the press action to be invoked + */ + public void bindDelayedAction(final Layer layer, final Runnable pressAction, final Runnable releaseAction, + final int delayTime) { + layer.bind(hwButton, hwButton.pressedAction(), () -> initiateTimed(pressAction, delayTime)); + layer.bind(hwButton, hwButton.releasedAction(), () -> { + cancelEvent(); + releaseAction.run(); + }); + } + + private void initiateTimed(final Runnable pressAction, final int delayTime) { + currentTimer = new TimedDelayEvent(pressAction, delayTime); + midiProcessor.queueEvent(currentTimer); + } + + public void bindLight(final Layer layer, final Supplier supplier) { + layer.bindLightState(supplier, 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 RgbState state, final RgbState holdState) { + layer.bindLightState(() -> hwButton.isPressed().get() ? holdState : state, light); + } + + /** + * A light state that is active or not. If not active, the light is off. If active which light depends if the + * button is being pressed or ot. + * + * @param layer the layer this is bound to + * @param isActiveState the active state + * @param active standard active color + * @param pressed active color when button iw being pressed + */ + public void bindHighlightButton(final Layer layer, final BooleanValue isActiveState, final RgbState active, + final RgbState pressed) { + layer.bindLightState(() -> { + if (isActiveState.getAsBoolean()) { + return hwButton.isPressed().get() ? pressed : active; + } + return RgbState.OFF; + }, light); + } + + public void bindHighlightButton(final Layer layer, final BooleanSupplier isActiveState, final RgbState active, + final RgbState pressed) { + layer.bindLightState(() -> { + if (isActiveState.getAsBoolean()) { + return hwButton.isPressed().get() ? pressed : active; + } + return RgbState.OFF; + }, light); + } + + public void bindPressed(final Layer layer, final Consumer target, final RgbState onState, + final RgbState offState) { + layer.bind(hwButton, hwButton.pressedAction(), () -> target.accept(true)); + layer.bind(hwButton, hwButton.releasedAction(), () -> target.accept(false)); + layer.bindLightState(() -> hwButton.isPressed().get() ? onState : offState, light); + } + + public void bindPressed(final Layer layer, final SettableBooleanValue value, final NovationColor color) { + final RgbState onState = color.getHiColor(); + final RgbState offState = color.getLowColor(); + layer.bind(hwButton, hwButton.pressedAction(), () -> value.set(true)); + layer.bind(hwButton, hwButton.releasedAction(), () -> value.set(false)); + layer.bindLightState(() -> value.get() ? onState : offState, light); + } + + private void cancelEvent() { + if (currentTimer != null) { + currentTimer.cancel(); + currentTimer = null; + } + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/LpHwElements.java b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/LpHwElements.java new file mode 100644 index 00000000..761ab638 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/LpHwElements.java @@ -0,0 +1,15 @@ +package com.bitwig.extensions.controllers.novation.commonsmk3; + +import java.util.List; + +import com.bitwig.extensions.controllers.novation.commonsmk3.DrumButton; +import com.bitwig.extensions.controllers.novation.commonsmk3.GridButton; +import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; + +public interface LpHwElements { + GridButton getGridButton(int row, int col); + + List getDrumGridButtons(); + + List getSceneLaunchButtons(); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/MidiProcessor.java b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/MidiProcessor.java index 355ec06d..e7435cdd 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/MidiProcessor.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/MidiProcessor.java @@ -1,5 +1,8 @@ package com.bitwig.extensions.controllers.novation.commonsmk3; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + import com.bitwig.extension.controller.api.ControllerHost; import com.bitwig.extension.controller.api.InternalHardwareLightState; import com.bitwig.extension.controller.api.MidiIn; @@ -7,121 +10,120 @@ import com.bitwig.extensions.framework.time.TimedEvent; import com.bitwig.extensions.framework.values.Midi; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; - public class MidiProcessor { - protected static final String DEVICE_INQUIRY = "F0 7E 7F 06 01 F7"; - private static final String DAW_MODE = "F0 00 20 29 02 %02X 10 %02X F7"; - private static final String LAYOUT_COMMAND = "F0 00 20 29 02 %02X 00 %02X F7"; - private static final String NOTE_LAYOUT_COMMAND = "F0 00 20 29 02 %02X 0F %02X F7"; - private static final String BUTTON_ID_SYSEX = "F0 00 20 29 02 %02X %02X %02X 01 F7"; - private static final String FADER_SET = "%02X %02X %02X %02X "; - public static final String FADER_CONFIG = "F0 00 20 29 02 %02X 01 00 %02X "; - - private final MidiIn midiIn; - private final MidiOut midiOut; - private final Queue timedEvents = new ConcurrentLinkedQueue<>(); - private final ControllerHost host; - private final int deviceSysExCode; - private final int sliderValueStatus; - - public MidiProcessor(final ControllerHost host, final MidiIn midiIn, final MidiOut midiOut, - final LaunchpadDeviceConfig config) { - this.host = host; - this.midiIn = midiIn; - this.midiOut = midiOut; - deviceSysExCode = config.getSysExId(); - sliderValueStatus = config.getSliderValueStatus(); - //DebugMini.println(" MIDI PROC %02X", sliderValueStatus); - } - - public void queueEvent(final TimedEvent event) { - timedEvents.add(event); - } - - public MidiIn getMidiIn() { - return midiIn; - } - - public MidiOut getMidiOut() { - return midiOut; - } - - public void start() { - host.scheduleTask(this::handlePing, 50); - } - - private void handlePing() { - if (!timedEvents.isEmpty()) { - - for (final TimedEvent event : timedEvents) { - event.process(); - if (event.isCompleted()) { - timedEvents.remove(event); + protected static final String DEVICE_INQUIRY = "F0 7E 7F 06 01 F7"; + private static final String DAW_MODE = "F0 00 20 29 02 %02X 10 %02X F7"; + private static final String LAYOUT_COMMAND = "F0 00 20 29 02 %02X 00 %02X F7"; + private static final String NOTE_LAYOUT_COMMAND = "F0 00 20 29 02 %02X 0F %02X F7"; + private static final String BUTTON_ID_SYSEX = "F0 00 20 29 02 %02X %02X %02X 01 F7"; + private static final String FADER_SET = "%02X %02X %02X %02X "; + public static final String FADER_CONFIG = "F0 00 20 29 02 %02X 01 00 %02X "; + + private final MidiIn midiIn; + private final MidiOut midiOut; + private final Queue timedEvents = new ConcurrentLinkedQueue<>(); + private final ControllerHost host; + private final int deviceSysExCode; + private final int sliderValueStatus; + + public MidiProcessor(final ControllerHost host, final MidiIn midiIn, final MidiOut midiOut, + final LaunchpadDeviceConfig config) { + this.host = host; + this.midiIn = midiIn; + this.midiOut = midiOut; + deviceSysExCode = config.getSysExId(); + sliderValueStatus = config.getSliderValueStatus(); + //DebugMini.println(" MIDI PROC %02X", sliderValueStatus); + } + + public void queueEvent(final TimedEvent event) { + timedEvents.add(event); + } + + public MidiIn getMidiIn() { + return midiIn; + } + + public MidiOut getMidiOut() { + return midiOut; + } + + public void start() { + 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); - } - - public void sendMidi(final int status, final int val1, final int val2) { - midiOut.sendMidi(status, val1, val2); - } - - public void sendToSlider(final int ccNr, final int value) { - midiOut.sendMidi(sliderValueStatus, ccNr, value); - } - - public void setButtonLed(final int buttonId, final int color) { - midiOut.sendSysex(String.format(BUTTON_ID_SYSEX, deviceSysExCode, buttonId, color)); - } - - public void sendDeviceInquiry() { - midiOut.sendSysex(DEVICE_INQUIRY); - } - - public void enableDawMode(final boolean active) { - midiOut.sendSysex(String.format(DAW_MODE, deviceSysExCode, active ? 1 : 0)); - } - - public void toLayout(final int layoutId) { - midiOut.sendSysex(String.format(LAYOUT_COMMAND, deviceSysExCode, layoutId)); - } - - public void setNoteModeLayoutX(final int layoutId) { - final String format = String.format(NOTE_LAYOUT_COMMAND, deviceSysExCode, layoutId); - midiOut.sendSysex(format); - } - - public void setFaderBank(final int orient, final int[] colorIndex, final boolean uniPolar, final int ccNrOffset) { - final StringBuilder sysEx = new StringBuilder(String.format(FADER_CONFIG, deviceSysExCode, orient)); - for (int i = 0; i < 8; i++) { - sysEx.append(String.format(FADER_SET, i, uniPolar ? 0 : 1, ccNrOffset + i, colorIndex[i])); - } - sysEx.append("F7"); - midiOut.sendSysex(sysEx.toString()); - toLayout(0x0D); - } - - public void updatePadLed(final InternalHardwareLightState state, final int ccValue) { - if (state instanceof RgbState) { - final RgbState rgbState = (RgbState) state; - switch (rgbState.getState()) { - case NORMAL: - sendMidi(Midi.CC, ccValue, rgbState.getColorIndex()); - break; - case FLASHING: - sendMidi(Midi.CC, ccValue, rgbState.getAltColor()); - sendMidi(Midi.CC + 1, ccValue, rgbState.getColorIndex()); - break; - case PULSING: - sendMidi(Midi.CC + 2, ccValue, rgbState.getColorIndex()); - break; - } - } else { - sendMidi(Midi.NOTE_ON, ccValue, 0); - } - } - + } + host.scheduleTask(this::handlePing, 100); + } + + public void sendMidi(final int status, final int val1, final int val2) { + midiOut.sendMidi(status, val1, val2); + } + + public void sendToSlider(final int ccNr, final int value) { + midiOut.sendMidi(sliderValueStatus, ccNr, value); + } + + public void setButtonLed(final int buttonId, final int color) { + midiOut.sendSysex(String.format(BUTTON_ID_SYSEX, deviceSysExCode, buttonId, color)); + } + + public void sendDeviceInquiry() { + midiOut.sendSysex(DEVICE_INQUIRY); + } + + public void enableDawMode(final boolean active) { + midiOut.sendSysex(String.format(DAW_MODE, deviceSysExCode, active ? 1 : 0)); + } + + public void toLayout(final int layoutId) { + midiOut.sendSysex(String.format(LAYOUT_COMMAND, deviceSysExCode, layoutId)); + } + + public void setNoteModeLayoutX(final int layoutId) { + final String format = String.format(NOTE_LAYOUT_COMMAND, deviceSysExCode, layoutId); + midiOut.sendSysex(format); + } + + public void setFaderBank(final int orient, final int[] colorIndex, final boolean uniPolar, final int ccNrOffset) { + final StringBuilder sysEx = new StringBuilder(String.format(FADER_CONFIG, deviceSysExCode, orient)); + for (int i = 0; i < 8; i++) { + sysEx.append(String.format(FADER_SET, i, uniPolar ? 0 : 1, ccNrOffset + i, colorIndex[i])); + } + sysEx.append("F7"); + midiOut.sendSysex(sysEx.toString()); + toLayout(0x0D); + } + + public void updatePadLed(final InternalHardwareLightState state, final int ccValue) { + if (state instanceof final RgbState rgbState) { + switch (rgbState.getState()) { + case NORMAL: + sendMidi(Midi.CC, ccValue, rgbState.getColorIndex()); + break; + case FLASHING: + sendMidi(Midi.CC, ccValue, rgbState.getAltColor()); + sendMidi(Midi.CC + 1, ccValue, rgbState.getColorIndex()); + break; + case PULSING: + sendMidi(Midi.CC + 2, ccValue, rgbState.getColorIndex()); + break; + } + } else { + sendMidi(Midi.NOTE_ON, ccValue, 0); + } + } + + public void delayAction(final int delayTime, final Runnable action) { + this.host.scheduleTask(action, delayTime); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/OverviewGrid.java b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/OverviewGrid.java index 86961a7c..da26e4f7 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/OverviewGrid.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/OverviewGrid.java @@ -2,52 +2,92 @@ public class OverviewGrid { - private int numberOfScenes; - private int numberOfTracks; + private int sceneOffset; + private int trackOffset; + private int numberOfScenes; + private int numberOfTracks; - private int trackPosition; - private int scenePosition; + private int trackPosition; + private int scenePosition; - private int focusChanelPosition = 0; + private final int[][] hasClips = new int[8][8]; + private final int[] sceneQueuedClips = new int[64]; + public int getNumberOfScenes() { + return numberOfScenes; + } - public int getNumberOfScenes() { - return numberOfScenes; - } + public void setNumberOfScenes(final int numberOfScenes) { + this.numberOfScenes = numberOfScenes; + } - public void setNumberOfScenes(final int numberOfScenes) { - this.numberOfScenes = numberOfScenes; - } + public int getNumberOfTracks() { + return numberOfTracks; + } - public int getNumberOfTracks() { - return numberOfTracks; - } + public void setNumberOfTracks(final int numberOfTracks) { + this.numberOfTracks = numberOfTracks; + } - public void setNumberOfTracks(final int numberOfTracks) { - this.numberOfTracks = numberOfTracks; - } + public int getTrackPosition() { + return trackPosition - trackOffset; + } - public int getTrackPosition() { - return trackPosition; - } + public int getTrackOffset() { + return trackOffset; + } - public void setTrackPosition(final int trackPosition) { - this.trackPosition = trackPosition; - } + public void setTrackPosition(final int trackPosition) { + this.trackPosition = trackPosition; + this.trackOffset = (trackPosition / 64) * 64; + } - public int getScenePosition() { - return scenePosition; - } + public int getScenePosition() { + return scenePosition - sceneOffset; + } - public void setScenePosition(final int scenePosition) { - this.scenePosition = scenePosition; - } + public void setScenePosition(final int scenePosition) { + this.scenePosition = scenePosition; + this.sceneOffset = (scenePosition / 64) * 64; + } - public int getFocusChanelPosition() { - return focusChanelPosition; - } + public int getSceneOffset() { + return sceneOffset; + } - public void setFocusChanelPosition(final int focusChanelPosition) { - this.focusChanelPosition = focusChanelPosition; - } + public void markSceneQueued(final int sceneIndex, final boolean isQueued) { + if (isQueued) { + sceneQueuedClips[sceneIndex]++; + } else if (sceneQueuedClips[sceneIndex] > 0) { + sceneQueuedClips[sceneIndex]--; + } + } + + public void setHasClips(final int trackIndex, final int sceneIndex, final boolean hasClip) { + final int gridScene = (sceneIndex) / 8; + final int gridTrack = (trackIndex) / 8; + if (hasClip) { + this.hasClips[gridTrack][gridScene]++; + } else if (this.hasClips[gridTrack][gridScene] > 0) { + this.hasClips[gridTrack][gridScene]--; + } + } + + public boolean hasClips(final int trackIndex, final int sceneIndex) { + return this.hasClips[trackIndex][sceneIndex] > 0; + } + + public boolean hasQueuedScenes(final int sceneIndex) { + final int index = sceneIndex - sceneOffset; + if (index > 63) { + return false; + } + return this.sceneQueuedClips[sceneIndex - sceneOffset] > 0; + } + + public boolean inGrid(final int trackIndex, final int sceneIndex) { + final int posX = trackIndex * 8; + final int posY = sceneIndex * 8; + return posX < (numberOfTracks - trackOffset) && posY < (numberOfScenes - sceneOffset); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/OverviewLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/OverviewLayer.java similarity index 67% rename from src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/OverviewLayer.java rename to src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/OverviewLayer.java index 1dab7618..ee393df9 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/OverviewLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/OverviewLayer.java @@ -1,31 +1,26 @@ -package com.bitwig.extensions.controllers.novation.launchpadmini3.layers; +package com.bitwig.extensions.controllers.novation.commonsmk3; -import com.bitwig.extensions.controllers.novation.commonsmk3.GridButton; -import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; -import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; -import com.bitwig.extensions.controllers.novation.launchpadmini3.HwElements; -import com.bitwig.extensions.controllers.novation.launchpadmini3.ViewCursorControl; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.Inject; import com.bitwig.extensions.framework.di.PostConstruct; public class OverviewLayer extends Layer { - + @Inject private ViewCursorControl viewCursorControl; - + public OverviewLayer(final Layers layers) { super(layers, "SESSION_OVERVIEW_LAYER"); } - + @PostConstruct - public void initView(final HwElements hwElements, final ViewCursorControl viewCursorControl) { + public void initView(final LpHwElements hwElements, final ViewCursorControl viewCursorControl) { initClipControl(hwElements); initSceneButtons(hwElements); } - - private void initClipControl(final HwElements hwElements) { + + private void initClipControl(final LpHwElements hwElements) { for (int i = 0; i < 8; i++) { final int trackIndex = i; for (int j = 0; j < 8; j++) { @@ -36,26 +31,32 @@ private void initClipControl(final HwElements hwElements) { } } } - - private void initSceneButtons(final HwElements hwElements) { + + private void initSceneButtons(final LpHwElements hwElements) { for (int i = 0; i < 8; i++) { final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(i); sceneButton.disable(this); } } - + private void handleSelection(final int trackIndex, final int sceneIndex) { viewCursorControl.scrollToOverview(trackIndex, sceneIndex); } - + private RgbState getState(final int trackIndex, final int sceneIndex) { if (viewCursorControl.inOverviewGridFocus(trackIndex, sceneIndex)) { - return RgbState.of(21); + if (viewCursorControl.hasClips(trackIndex, sceneIndex)) { + return RgbState.of(21); + } + return RgbState.of(23); + } + if (viewCursorControl.hasClips(trackIndex, sceneIndex)) { + return RgbState.of(41); } if (viewCursorControl.inOverviewGrid(trackIndex, sceneIndex)) { - return RgbState.of(8); + return RgbState.of(1); } return RgbState.OFF; } - + } 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 ca74a13b..883455da 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 @@ -1,117 +1,176 @@ package com.bitwig.extensions.controllers.novation.commonsmk3; -import com.bitwig.extension.controller.api.HardwareLightVisualState; -import com.bitwig.extension.controller.api.InternalHardwareLightState; - import java.util.HashMap; import java.util.Objects; -public class RgbState extends InternalHardwareLightState { +import com.bitwig.extension.api.Color; +import com.bitwig.extension.controller.api.HardwareLightVisualState; +import com.bitwig.extension.controller.api.InternalHardwareLightState; +public class RgbState extends InternalHardwareLightState +{ private static final RgbState[] registry = new RgbState[128]; + private static final RgbState[] pulseRegistry = new RgbState[128]; + private static final HashMap flashingRegistry = new HashMap<>(); public static final RgbState OFF = RgbState.of(0); + public static final RgbState WHITE = RgbState.of(3); + public static final RgbState DIM_WHITE = RgbState.of(1); + public static final RgbState RED = RgbState.of(5); + public static final RgbState RED_LO = RgbState.of(7); + public static final RgbState YELLOW = RgbState.of(13); + public static final RgbState YELLOW_LO = RgbState.of(15); + public static final RgbState ORANGE = RgbState.of(9); + public static final RgbState ORANGE_LO = RgbState.of(11); + public static final RgbState BLUE = RgbState.of(41); + public static final RgbState GREEN = RgbState.of(21); + public static final RgbState BLUE_LO = RgbState.of(43); + public static final RgbState GREEN_FLASH = RgbState.flash(21, 0); + public static final RgbState SHIFT_INACTIVE = RgbState.of(12); + public static final RgbState SHIFT_ACTIVE = RgbState.of(3); + public static final RgbState BUTTON_INACTIVE = DIM_WHITE; + public static final RgbState BUTTON_ACTIVE = WHITE; + public static final RgbState TURQUOISE = RgbState.of(29); + public static final RgbState ORANGE_PULSE = RgbState.pulse(9); private final int colorIndex; + private final int altColorIndex; + private LightState state; - private RgbState(final int colorIndex, final LightState state) { + public static RgbState forColor(final Color color) + { + if (color == null || color.getAlpha() == 0) + return OFF; + + return of(ColorLookup.toColor(color)); + } + + private RgbState(final int colorIndex, final LightState state) + { super(); this.colorIndex = colorIndex; this.state = state; altColorIndex = 0; } - private RgbState(final int colorIndex, final LightState state, final int altColorIndex) { + private RgbState(final int colorIndex, final LightState state, final int altColorIndex) + { super(); this.colorIndex = colorIndex; this.state = state; this.altColorIndex = altColorIndex; } - public static RgbState flash(final int colorIndex, final int altColor) { + public static RgbState flash(final int colorIndex, final int altColor) + { final int index = Math.min(Math.max(0, colorIndex), 127); final int altInd = Math.min(Math.max(0, altColor), 127); final int lookUp = index << 8 | altInd; - return flashingRegistry.computeIfAbsent(lookUp, key -> new RgbState(index, LightState.FLASHING, altInd)); + return flashingRegistry.computeIfAbsent(lookUp, + key -> new RgbState(index, LightState.FLASHING, altInd)); } - public static RgbState pulse(final int colorIndex) { + public static RgbState pulse(final int colorIndex) + { final int index = Math.min(Math.max(0, colorIndex), 127); - if (pulseRegistry[index] == null) { + if (pulseRegistry[index] == null) + { pulseRegistry[index] = new RgbState(colorIndex, LightState.PULSING); } return pulseRegistry[index]; } - public static RgbState of(final int colorIndex) { + public static RgbState of(final int colorIndex) + { final int index = Math.min(Math.max(0, colorIndex), 127); - if (registry[index] == null) { + if (registry[index] == null) + { registry[index] = new RgbState(index, LightState.NORMAL); } return registry[index]; } @Override - public HardwareLightVisualState getVisualState() { - return null; + public HardwareLightVisualState getVisualState() + { + if (colorIndex == 0) + return null; + + final Color color = ColorLookup.colorIndexToApiColor(colorIndex); + + if (color == null) + return null; + + if (state == LightState.NORMAL) + return HardwareLightVisualState.createForColor(color); + + return HardwareLightVisualState.createBlinking(color, null, 0.3, 0.3); } - public int getColorIndex() { + public int getColorIndex() + { return colorIndex; } - public LightState getState() { + public LightState getState() + { return state; } - public void setState(final LightState state) { + public void setState(final LightState state) + { this.state = state; } @Override - public int hashCode() { + public int hashCode() + { return Objects.hash(colorIndex, state); } @Override - public boolean equals(final Object obj) { - if (this == obj) { + public boolean equals(final Object obj) + { + if (this == obj) + { return true; } - if (obj == null) { + if (obj == null) + { return false; } - if (getClass() != obj.getClass()) { + if (getClass() != obj.getClass()) + { return false; } - final RgbState other = (RgbState) obj; + final RgbState other = (RgbState)obj; return colorIndex == other.colorIndex && state == other.state && altColorIndex == other.altColorIndex; } - - public int getAltColor() { + public int getAltColor() + { return altColorIndex; } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/SliderBinding.java b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/SliderBinding.java index 03ec1ff0..822970df 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/SliderBinding.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/SliderBinding.java @@ -13,7 +13,7 @@ public class SliderBinding extends Binding { private int currentValue; private boolean process = true; int currSliderValue; - private int index; + private final int index; public SliderBinding(final int ccNr, final Parameter source, final HardwareSlider target, final int index, final MidiProcessor midiProcessor) { @@ -27,7 +27,7 @@ public SliderBinding(final int ccNr, final Parameter source, final HardwareSlide this.index = index; } - private void handleSliderChange(Parameter parameter, final double sliderValue) { + private void handleSliderChange(final Parameter parameter, final double sliderValue) { currSliderValue = (int) Math.round(sliderValue * 127); if (isActive()) { process = false; @@ -36,13 +36,13 @@ private void handleSliderChange(Parameter parameter, final double sliderValue) { } private void valueChanged(final int value) { - currentValue = value; - // This check against the last slider value and the process value seems redundant - // but is not. - if (isActive() && value != currSliderValue && process) { - midiProcessor.sendToSlider(ccNr, value); + if (value != currentValue) { + currentValue = value; + if (isActive() && process) { + midiProcessor.sendToSlider(ccNr, value); + } + process = true; } - process = true; } public void update() { 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 new file mode 100644 index 00000000..5090076a --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/novation/commonsmk3/ViewCursorControl.java @@ -0,0 +1,379 @@ +package com.bitwig.extensions.controllers.novation.commonsmk3; + +import java.util.Optional; + +import com.bitwig.extension.controller.api.Application; +import com.bitwig.extension.controller.api.BooleanValue; +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.CursorDeviceFollowMode; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.DeviceBank; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +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; + +@Component +public class ViewCursorControl { + public static final double MAX_LENGTH_FOR_DUPLICATE = 512 * 4.0; + + @Inject + Application application; + + private final CursorTrack cursorTrack; + private final DeviceBank deviceBank; + private final PinnableCursorDevice primaryDevice; + private final TrackBank trackBank; + private final PinnableCursorDevice cursorDevice; + private final Clip cursorClip; + + private final Track rootTrack; + private final ClipLauncherSlotBank mainTrackSlotBank; + private final Track largeFocusTrack; + private FocusSlot focusSlot; + private final TrackBank maxTrackBank; + + private final OverviewGrid overviewGrid = new OverviewGrid(); + + public ViewCursorControl(final ControllerHost host) { + rootTrack = host.getProject().getRootTrackGroup(); + rootTrack.arm().markInterested(); + + maxTrackBank = host.createTrackBank(64, 1, 64); + maxTrackBank.sceneBank().scrollPosition().markInterested(); + maxTrackBank.scrollPosition().markInterested(); + + + trackBank = host.createTrackBank(8, 1, 8); + + trackBank.sceneBank().itemCount().addValueObserver(overviewGrid::setNumberOfScenes); + trackBank.channelCount().addValueObserver(overviewGrid::setNumberOfTracks); + trackBank.scrollPosition().addValueObserver(pos -> { + overviewGrid.setTrackPosition(pos); + if (maxTrackBank.scrollPosition().get() != overviewGrid.getTrackOffset()) { + maxTrackBank.scrollPosition().set(overviewGrid.getTrackOffset()); + } + }); + trackBank.sceneBank().scrollPosition().addValueObserver(pos -> { + overviewGrid.setScenePosition(pos); + if (maxTrackBank.sceneBank().scrollPosition().get() != overviewGrid.getSceneOffset()) { + maxTrackBank.sceneBank().scrollPosition().set(overviewGrid.getSceneOffset()); + } + }); + setUpFocusScene(); + + cursorTrack = host.createCursorTrack(8, 8); + for (int i = 0; i < 8; i++) { + prepareTrack(trackBank.getItemAt(i)); + } + + cursorTrack.name().markInterested(); + cursorDevice = cursorTrack.createCursorDevice(); + cursorClip = host.createLauncherCursorClip(32 * 6, 127); + + cursorTrack.clipLauncherSlotBank().cursorIndex().addValueObserver(index -> { + // RemoteConsole.out.println(" => {}", index); + }); + prepareTrack(cursorTrack); + + deviceBank = cursorTrack.createDeviceBank(8); + primaryDevice = + cursorTrack.createCursorDevice("drumdetection", "Pad Device", 8, CursorDeviceFollowMode.FIRST_INSTRUMENT); + primaryDevice.hasDrumPads().markInterested(); + primaryDevice.exists().markInterested(); + + + final TrackBank singleTrackBank = host.createTrackBank(1, 0, 16); + singleTrackBank.scrollPosition().markInterested(); + singleTrackBank.followCursorTrack(cursorTrack); + largeFocusTrack = singleTrackBank.getItemAt(0); + prepareTrack(largeFocusTrack); + + mainTrackSlotBank = largeFocusTrack.clipLauncherSlotBank(); + final BooleanValue equalsToCursorTrack = largeFocusTrack.createEqualsValue(cursorTrack); + equalsToCursorTrack.markInterested(); + + + for (int i = 0; i < 16; i++) { + final int index = i; + final ClipLauncherSlot slot = mainTrackSlotBank.getItemAt(i); + prepareSlot(slot); + + slot.isSelected().addValueObserver(selected -> { + if (selected) { + focusSlot = new FocusSlot(largeFocusTrack, slot, index, equalsToCursorTrack); + } + }); + } + } + + private void setUpFocusScene() { + for (int i = 0; i < 64; i++) { + final int trackIndex = i; + final Track track = maxTrackBank.getItemAt(trackIndex); + for (int j = 0; j < 64; j++) { + final int sceneIndex = j; + final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); + slot.hasContent().addValueObserver(hasContent -> { + overviewGrid.setHasClips(trackIndex, sceneIndex, hasContent); + }); + slot.isPlaybackQueued().addValueObserver(isQueued -> { + overviewGrid.markSceneQueued(sceneIndex, isQueued); + }); + } + } + } + + public boolean hasQueuedForPlaying(final int sceneIndex) { + return overviewGrid.hasQueuedScenes(sceneIndex); + } + + private void prepareTrack(final Track track) { + track.arm().markInterested(); + track.monitorMode().markInterested(); + track.sourceSelector().hasAudioInputSelected().markInterested(); + track.sourceSelector().hasNoteInputSelected().markInterested(); + } + + private void prepareSlot(final ClipLauncherSlot slot) { + slot.isRecording().markInterested(); + slot.isRecordingQueued().markInterested(); + slot.hasContent().markInterested(); + slot.name().markInterested(); + slot.isPlaying().markInterested(); + slot.isSelected().markInterested(); + } + + public void scrollToOverview(final int trackIndex, final int sceneIndex) { + final int posX = trackIndex * 8; + final int posY = sceneIndex * 8; + if (posX < overviewGrid.getNumberOfTracks() && posY < overviewGrid.getNumberOfScenes()) { + trackBank.scrollPosition().set(posX); + trackBank.sceneBank().scrollPosition().set(posY); + } + } + + public boolean inOverviewGrid(final int trackIndex, final int sceneIndex) { + final int posX = trackIndex * 8; + final int posY = sceneIndex * 8; + return posX < overviewGrid.getNumberOfTracks() && posY < overviewGrid.getNumberOfScenes(); + } + + public boolean inOverviewGridFocus(final int trackIndex, final int sceneIndex) { + final int locX = overviewGrid.getTrackPosition() / 8; + final int locY = overviewGrid.getScenePosition() / 8; + return locX == trackIndex && locY == sceneIndex; + } + + public TrackBank getTrackBank() { + return trackBank; + } + + public CursorTrack getCursorTrack() { + return cursorTrack; + } + + public DeviceBank getDeviceBank() { + return deviceBank; + } + + public PinnableCursorDevice getPrimaryDevice() { + return primaryDevice; + } + + public PinnableCursorDevice getCursorDevice() { + return cursorDevice; + } + + public Clip getCursorClip() { + return cursorClip; + } + + public Track getRootTrack() { + return rootTrack; + } + + public void focusSlot(final FocusSlot slot) { + focusSlot = slot; + } + + public FocusSlot getFocusSlot() { + return focusSlot; + } + + public void createNewClip() { + if (focusSlot != null) { + final ClipLauncherSlot slot = focusSlot.getSlot(); + if (!slot.hasContent().get()) { + slot.createEmptyClip(4); + slot.select(); + } else { + cursorTrack.createNewLauncherClip(0); + // TODO try to move to next slot if possible + } + } else { + cursorTrack.createNewLauncherClip(0); + } + } + + private boolean trackOfFocusSlotArmed() { + return focusSlot != null && focusSlot.getTrack().arm().get(); + } + + public void globalRecordAction(final Transport transport) { + if (largeFocusTrack.arm().get() || trackOfFocusSlotArmed()) { + if (focusSlot != null) { + handleFocusedSlotOnArmedTrack(transport); + } else { + handleNoFocusSlotOnArmedTrack(transport); + } + } else if (focusSlot != null) { + handleRecordFocusSlotNotArmed(transport); + } else { + transport.isClipLauncherOverdubEnabled().toggle(); + } + } + + private void handleNoFocusSlotOnArmedTrack(final Transport transport) { + final Optional playingSlot = findPlayingSlot(); + if (playingSlot.isPresent()) { + toggleRecording(playingSlot.get(), transport); + } else { + findEmptySlotAndLaunch(transport, -1); + } + } + + private void handleFocusedSlotOnArmedTrack(final Transport transport) { + if (focusSlot.getTrack().arm().get()) { + if (focusSlot.isEmpty()) { + recordToEmptySlot(focusSlot.getSlot(), transport); + } else { + toggleRecording(focusSlot.getSlot(), transport); + } + } else { + findEmptySlotAndLaunch(transport, focusSlot.getSlotIndex()); + } + } + + private void handleRecordFocusSlotNotArmed(final Transport transport) { + final Track track = focusSlot.getTrack(); + if (canRecord(focusSlot.getTrack())) { + track.arm().set(true); + track.selectInEditor(); + if (!focusSlot.isEmpty()) { + toggleRecording(focusSlot.getSlot(), transport); + } else { + recordToEmptySlot(focusSlot.getSlot(), transport); + } + } else { + transport.isClipLauncherOverdubEnabled().toggle(); + } + } + + private void findEmptySlotAndLaunch(final Transport transport, final int slotIndex) { + final Optional slot = findCursorFirstEmptySlot(slotIndex); + if (slot.isPresent()) { + recordToEmptySlot(slot.get(), transport); + } else { + transport.isClipLauncherOverdubEnabled().toggle(); + } + } + + private boolean canRecord(final Track track) { + return track.sourceSelector().hasNoteInputSelected().get() || track.sourceSelector().hasAudioInputSelected() + .get(); + } + + public Optional findCursorFirstEmptySlot(final int firstIndex) { + if (firstIndex >= 0 && firstIndex < 16) { + final ClipLauncherSlot slot = mainTrackSlotBank.getItemAt(firstIndex); + if (!slot.hasContent().get()) { + return Optional.of(slot); + } + } + for (int i = 0; i < 16; i++) { + final ClipLauncherSlot slot = mainTrackSlotBank.getItemAt(i); + if (!slot.hasContent().get()) { + return Optional.of(slot); + } + } + return Optional.empty(); + } + + private void toggleRecording(final ClipLauncherSlot slot, final Transport transport) { + if (slot.isRecordingQueued().get() || slot.isRecording().get()) { + slot.launch(); + transport.isClipLauncherOverdubEnabled().set(false); + } else if (!slot.isPlaying().get()) { + slot.launch(); + transport.isClipLauncherOverdubEnabled().set(true); + } else if (transport.isPlaying().get()) { + transport.isClipLauncherOverdubEnabled().toggle(); + } else { + transport.restart(); + slot.launch(); + transport.isClipLauncherOverdubEnabled().set(true); + } + } + + private void recordToEmptySlot(final ClipLauncherSlot slot, final Transport transport) { + if (!transport.isPlaying().get()) { + transport.restart(); + } + slot.select(); + slot.launch(); + transport.isClipLauncherOverdubEnabled().set(true); + } + + public Optional findPlayingSlot() { + for (int i = 0; i < 16; i++) { + final ClipLauncherSlot slot = mainTrackSlotBank.getItemAt(i); + if (slot.hasContent().get() && slot.isPlaying().get()) { + return Optional.of(slot); + } + } + return Optional.empty(); + } + + public void handleQuantize(final boolean shift) { + cursorClip.quantize(1.0); + final ClipLauncherSlot slot = cursorClip.clipLauncherSlot(); + slot.showInEditor(); + } + + public void handleDuplication(final boolean shift) { + if (focusSlot == null || focusSlot.isEmpty() || !focusSlot.isCursorTrack()) { + return; + } + if (!shift) { + cursorClip.duplicate(); + } else { + if (cursorClip.getLoopLength().get() < MAX_LENGTH_FOR_DUPLICATE) { + cursorClip.duplicateContent(); + cursorClip.clipLauncherSlot().showInEditor(); + } + } + } + + public void handleClear(final boolean shift) { + if (focusSlot == null || focusSlot.isEmpty() || !focusSlot.isCursorTrack()) { + return; + } + if (!shift) { + cursorClip.clearSteps(); + cursorClip.clipLauncherSlot().showInEditor(); + } else { + focusSlot.getSlot().deleteObject(); + } + } + + public boolean hasClips(final int trackIndex, final int sceneIndex) { + return overviewGrid.hasClips(trackIndex, sceneIndex); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/Button.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/Button.java index 7919f455..37c09a5c 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/Button.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/Button.java @@ -4,20 +4,11 @@ import com.bitwig.extension.controller.api.ControllerHost; import com.bitwig.extension.controller.api.HardwareButton; import com.bitwig.extension.controller.api.HardwareSurface; -import com.bitwig.extension.controller.api.InternalHardwareLightState; import com.bitwig.extension.controller.api.MidiIn; import com.bitwig.extension.controller.api.MultiStateHardwareLight; -import com.bitwig.extension.controller.api.ObjectHardwareProperty; final class Button { - static final int NO_PULSE = 0; - static final int PULSE_PLAYING = 88; - static final int PULSE_RECORDING = 72; - static final int PULSE_PLAYBACK_QUEUED = 89; - static final int PULSE_RECORDING_QUEUED = 56; - static final int PULSE_STOP_QUEUED = 118; - enum State { RELEASED, PRESSED, HOLD, @@ -63,7 +54,8 @@ enum State final MultiStateHardwareLight light = hardwareSurface.createMultiStateHardwareLight(id + "-light"); light.state().setValue(LedState.OFF); - light.state().onUpdateHardware(internalHardwareLightState -> mDriver.updateButtonLed(Button.this)); + light.setColorToStateFunction(color -> new LedState(color)); + light.state().onUpdateHardware(internalHardwareLightState -> mDriver.updateButtonLed(Button.this, (LedState)internalHardwareLightState)); bt.setBackgroundLight(light); mButton = bt; @@ -100,23 +92,16 @@ boolean isPressed() return mButtonState == State.PRESSED || mButtonState == State.HOLD; } - public void appendLedUpdate( + public void appendLedUpdate(LedState ledState, final StringBuilder ledClear, final StringBuilder ledUpdate, final StringBuilder ledPulseUpdate) { - final ObjectHardwareProperty state = mLight.state(); - LedState currentState = (LedState)state.currentValue(); - final LedState lastSent = (LedState)state.lastSentValue(); - - if (currentState == null) - currentState = LedState.OFF; - - if (lastSent != null && currentState.equals(lastSent)) - return; + if (ledState == null) + ledState = LedState.OFF; - final Color color = currentState.mColor; - final int pulse = currentState.mPulse; + final Color color = ledState.mColor; + final int pulse = ledState.mPulse; - if (pulse == NO_PULSE) + if (pulse == LedState.NO_PULSE) { if (color.isBlack()) ledClear.append(String.format(" %02x 00", mIndex)); diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/Color.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/Color.java index 1c9a52d7..d43c65e1 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/Color.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/Color.java @@ -126,6 +126,11 @@ public Color(final ColorValue value) this(value.red(), value.green(), value.blue()); } + public Color(final com.bitwig.extension.api.Color color) + { + this((float)color.getRed(), (float)color.getGreen(), (float)color.getBlue()); + } + public Color(final Color color, final float scale) { mRed = (byte) (color.mRed * scale); diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/LaunchpadProControllerExtension.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/LaunchpadProControllerExtension.java index 040e68e7..ca1dd344 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/LaunchpadProControllerExtension.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/LaunchpadProControllerExtension.java @@ -560,9 +560,9 @@ public void exit() mMidiOut.sendSysex("F0 00 20 29 02 10 0E 00 F7"); } - public void updateButtonLed(final Button button) + public void updateButtonLed(final Button button, final LedState ledState) { - button.appendLedUpdate(mLedClearSysexBuffer, mLedColorUpdateSysexBuffer, mLedPulseUpdateSysexBuffer); + button.appendLedUpdate(ledState, mLedClearSysexBuffer, mLedColorUpdateSysexBuffer, mLedPulseUpdateSysexBuffer); // Let's not send sysex that are too big if (mLedColorUpdateSysexBuffer.length() >= 4 * 3 * 48) diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/LedState.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/LedState.java index 36b70410..dee7fbcb 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/LedState.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/LedState.java @@ -6,77 +6,77 @@ final class LedState extends InternalHardwareLightState { - public static final LedState OFF = new LedState(Color.OFF, 0); + static final LedState OFF = new LedState(Color.OFF, 0); - public static final LedState SESSION_MODE_ON = new LedState(Color.SESSION_MODE_ON); - public static final LedState SESSION_MODE_OFF = new LedState(Color.SESSION_MODE_OFF); + static final LedState SESSION_MODE_ON = new LedState(Color.SESSION_MODE_ON); + static final LedState SESSION_MODE_OFF = new LedState(Color.SESSION_MODE_OFF); - public static final LedState PAN_MODE = new LedState(Color.PAN_MODE); - public static final LedState PAN_MODE_LOW = new LedState(Color.PAN_MODE_LOW); + static final LedState PAN_MODE = new LedState(Color.PAN_MODE); + static final LedState PAN_MODE_LOW = new LedState(Color.PAN_MODE_LOW); - public static final LedState SENDS_MODE = new LedState(Color.SENDS_MODE); - public static final LedState SENDS_MODE_LOW = new LedState(Color.SENDS_MODE_LOW); + static final LedState SENDS_MODE = new LedState(Color.SENDS_MODE); + static final LedState SENDS_MODE_LOW = new LedState(Color.SENDS_MODE_LOW); - public static final LedState VOLUME_MODE = new LedState(Color.VOLUME_MODE); - public static final LedState VOLUME_MODE_LOW = new LedState(Color.VOLUME_MODE_LOW); + static final LedState VOLUME_MODE = new LedState(Color.VOLUME_MODE); + static final LedState VOLUME_MODE_LOW = new LedState(Color.VOLUME_MODE_LOW); - public static final LedState PLAY_MODE = new LedState(Color.PLAY_MODE); - public static final LedState PLAY_MODE_OFF = new LedState(Color.PLAY_MODE_OFF); + static final LedState PLAY_MODE = new LedState(Color.PLAY_MODE); + static final LedState PLAY_MODE_OFF = new LedState(Color.PLAY_MODE_OFF); - public static final LedState DRUM_SEQ_MODE = new LedState(Color.DRUM_SEQ_MODE); - public static final LedState DRUM_SEQ_MODE_OFF = new LedState(Color.DRUM_SEQ_MODE_OFF); + static final LedState DRUM_SEQ_MODE = new LedState(Color.DRUM_SEQ_MODE); + static final LedState DRUM_SEQ_MODE_OFF = new LedState(Color.DRUM_SEQ_MODE_OFF); - public static final LedState STEP_SEQ_MODE = new LedState(Color.STEP_SEQ_MODE); - public static final LedState STEP_SEQ_MODE_OFF = new LedState(Color.STEP_SEQ_MODE_OFF); + static final LedState STEP_SEQ_MODE = new LedState(Color.STEP_SEQ_MODE); + static final LedState STEP_SEQ_MODE_OFF = new LedState(Color.STEP_SEQ_MODE_OFF); - public static final LedState TRACK = new LedState(Color.TRACK); - public static final LedState TRACK_LOW = new LedState(Color.TRACK_LOW); + static final LedState TRACK = new LedState(Color.TRACK); + static final LedState TRACK_LOW = new LedState(Color.TRACK_LOW); - public static final LedState SCENE = new LedState(Color.SCENE); - public static final LedState SCENE_LOW = new LedState(Color.SCENE_LOW); + static final LedState SCENE = new LedState(Color.SCENE); + static final LedState SCENE_LOW = new LedState(Color.SCENE_LOW); - public static final LedState SHIFT_ON = new LedState(Color.SHIFT_ON); - public static final LedState SHIFT_OFF = new LedState(Color.SHIFT_OFF); + static final LedState SHIFT_ON = new LedState(Color.SHIFT_ON); + static final LedState SHIFT_OFF = new LedState(Color.SHIFT_OFF); - public static final LedState CLICK_ON = new LedState(Color.CLICK_ON); - public static final LedState CLICK_OFF = new LedState(Color.CLICK_OFF); + static final LedState CLICK_ON = new LedState(Color.CLICK_ON); + static final LedState CLICK_OFF = new LedState(Color.CLICK_OFF); - public static final LedState UNDO_ON = new LedState(Color.UNDO_ON); - public static final LedState UNDO_OFF = new LedState(Color.UNDO_OFF); + static final LedState UNDO_ON = new LedState(Color.UNDO_ON); + static final LedState UNDO_OFF = new LedState(Color.UNDO_OFF); - public static final LedState REC_ON = new LedState(Color.REC_ON); - public static final LedState REC_OFF = new LedState(Color.REC_OFF); + static final LedState REC_ON = new LedState(Color.REC_ON); + static final LedState REC_OFF = new LedState(Color.REC_OFF); - public static final LedState PLAY_ON = new LedState(Color.PLAY_ON); - public static final LedState PLAY_OFF = new LedState(Color.PLAY_OFF); + static final LedState PLAY_ON = new LedState(Color.PLAY_ON); + static final LedState PLAY_OFF = new LedState(Color.PLAY_OFF); - public static final LedState DELETE_ON = new LedState(Color.DELETE_ON); - public static final LedState DELETE_OFF = new LedState(Color.DELETE_OFF); + static final LedState DELETE_ON = new LedState(Color.DELETE_ON); + static final LedState DELETE_OFF = new LedState(Color.DELETE_OFF); - public static final LedState QUANTIZE_ON = new LedState(Color.QUANTIZE_ON); - public static final LedState QUANTIZE_OFF = new LedState(Color.QUANTIZE_OFF); + static final LedState QUANTIZE_ON = new LedState(Color.QUANTIZE_ON); + static final LedState QUANTIZE_OFF = new LedState(Color.QUANTIZE_OFF); - public static final LedState DUPLICATE_ON = new LedState(Color.DUPLICATE_ON); - public static final LedState DUPLICATE_OFF = new LedState(Color.DUPLICATE_OFF); + static final LedState DUPLICATE_ON = new LedState(Color.DUPLICATE_ON); + static final LedState DUPLICATE_OFF = new LedState(Color.DUPLICATE_OFF); - public static final LedState MUTE = new LedState(Color.MUTE); - public static final LedState MUTE_LOW = new LedState(Color.MUTE_LOW); + static final LedState MUTE = new LedState(Color.MUTE); + static final LedState MUTE_LOW = new LedState(Color.MUTE_LOW); - public static final LedState SOLO = new LedState(Color.SOLO); - public static final LedState SOLO_LOW = new LedState(Color.SOLO_LOW); + static final LedState SOLO = new LedState(Color.SOLO); + static final LedState SOLO_LOW = new LedState(Color.SOLO_LOW); - public static final LedState STOP_CLIP_ON = new LedState(Color.STOP_CLIP_ON); - public static final LedState STOP_CLIP_OFF = new LedState(Color.STOP_CLIP_OFF); + static final LedState STOP_CLIP_ON = new LedState(Color.STOP_CLIP_ON); + static final LedState STOP_CLIP_OFF = new LedState(Color.STOP_CLIP_OFF); - public static final LedState STEP_HOLD = new LedState(Color.STEP_HOLD); - public static final LedState STEP_PLAY_HEAD = new LedState(Color.STEP_PLAY_HEAD); - public static final LedState STEP_PLAY = new LedState(Color.STEP_PLAY); - public static final LedState STEP_ON = new LedState(Color.STEP_ON); - public static final LedState STEP_SUSTAIN = new LedState(Color.STEP_SUSTAIN); - public static final LedState STEP_OFF = new LedState(Color.STEP_OFF); + static final LedState STEP_HOLD = new LedState(Color.STEP_HOLD); + static final LedState STEP_PLAY_HEAD = new LedState(Color.STEP_PLAY_HEAD); + static final LedState STEP_PLAY = new LedState(Color.STEP_PLAY); + static final LedState STEP_ON = new LedState(Color.STEP_ON); + static final LedState STEP_SUSTAIN = new LedState(Color.STEP_SUSTAIN); + static final LedState STEP_OFF = new LedState(Color.STEP_OFF); - public static final LedState PITCH = new LedState(Color.PITCH); - public static final LedState PITCH_LOW = new LedState(Color.PITCH_LOW); + static final LedState PITCH = new LedState(Color.PITCH); + static final LedState PITCH_LOW = new LedState(Color.PITCH_LOW); final static LedState ROOT_KEY_COLOR = new LedState(Color.fromRgb255(11, 100, 63)); final static LedState USED_KEY_COLOR = new LedState(Color.fromRgb255(255, 240, 240)); @@ -84,15 +84,26 @@ final class LedState extends InternalHardwareLightState final static LedState SCALE_ON_COLOR = new LedState(Color.fromRgb255(50, 167, 202)); final static LedState SCALE_OFF_COLOR = new LedState(Color.scale(SCALE_ON_COLOR.mColor, 0.2f)); + static final int NO_PULSE = 0; + static final int PULSE_PLAYING = 88; + static final int PULSE_RECORDING = 72; + static final int PULSE_PLAYBACK_QUEUED = 89; + static final int PULSE_RECORDING_QUEUED = 56; + static final int PULSE_STOP_QUEUED = 118; - public LedState(final ColorValue color) + LedState(final ColorValue color) { this(new Color(color)); } LedState(final Color color) { - this(color, 0); + this(color, NO_PULSE); + } + + LedState(final com.bitwig.extension.api.Color color) + { + this(new Color(color)); } LedState(final Color color, final int pulse) @@ -113,7 +124,7 @@ public boolean equals(final Object obj) return obj instanceof LedState && equals((LedState) obj); } - public boolean equals(final LedState obj) + boolean equals(final LedState obj) { if (obj == this) return true; diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/SessionMode.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/SessionMode.java index 26e94813..5445b92f 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/SessionMode.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpad_pro/SessionMode.java @@ -97,17 +97,17 @@ private InternalHardwareLightState computeGridLedState(final ClipLauncherSlot sl final int pulse; if (slot.isStopQueued().get()) - pulse = Button.PULSE_STOP_QUEUED; + pulse = LedState.PULSE_STOP_QUEUED; else if (slot.isRecordingQueued().get()) - pulse = Button.PULSE_RECORDING_QUEUED; + pulse = LedState.PULSE_RECORDING_QUEUED; else if (slot.isPlaybackQueued().get()) - pulse = Button.PULSE_PLAYBACK_QUEUED; + pulse = LedState.PULSE_PLAYBACK_QUEUED; else if (slot.isRecording().get()) - pulse = Button.PULSE_RECORDING; + pulse = LedState.PULSE_RECORDING; else if (slot.isPlaying().get()) - pulse = Button.PULSE_PLAYING; + pulse = LedState.PULSE_PLAYING; else - pulse = Button.NO_PULSE; + pulse = LedState.NO_PULSE; return new LedState(color, pulse); } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/AbstractLaunchpadMk3Extension.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/AbstractLaunchpadMk3Extension.java index 9afe5f18..1d0c3148 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/AbstractLaunchpadMk3Extension.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/AbstractLaunchpadMk3Extension.java @@ -1,5 +1,9 @@ package com.bitwig.extensions.controllers.novation.launchpadmini3; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; + import com.bitwig.extension.controller.ControllerExtension; import com.bitwig.extension.controller.ControllerExtensionDefinition; import com.bitwig.extension.controller.api.ControllerHost; @@ -7,113 +11,129 @@ import com.bitwig.extension.controller.api.MidiIn; import com.bitwig.extension.controller.api.MidiOut; import com.bitwig.extensions.controllers.novation.commonsmk3.LaunchpadDeviceConfig; +import com.bitwig.extensions.controllers.novation.commonsmk3.LpHwElements; import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; -import com.bitwig.extensions.controllers.novation.launchpadmini3.layers.*; +import com.bitwig.extensions.controllers.novation.commonsmk3.OverviewLayer; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; +import com.bitwig.extensions.controllers.novation.launchpadmini3.layers.ControlMode; +import com.bitwig.extensions.controllers.novation.launchpadmini3.layers.DeviceSliderLayer; +import com.bitwig.extensions.controllers.novation.launchpadmini3.layers.NotePlayingLayer; +import com.bitwig.extensions.controllers.novation.launchpadmini3.layers.PanSliderLayer; +import com.bitwig.extensions.controllers.novation.launchpadmini3.layers.SendsSliderLayer; +import com.bitwig.extensions.controllers.novation.launchpadmini3.layers.SessionLayer; +import com.bitwig.extensions.controllers.novation.launchpadmini3.layers.VolumeSliderLayer; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.di.Context; import com.bitwig.extensions.framework.di.TrackerRegistration; import com.bitwig.extensions.framework.di.ViewTracker; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; - public abstract class AbstractLaunchpadMk3Extension extends ControllerExtension { - - protected HardwareSurface surface; - protected MidiOut midiOut; - protected SessionLayer sessionLayer; - protected OverviewLayer overviewLayer; - protected LpMode mode = LpMode.SESSION; - protected final LaunchpadDeviceConfig deviceConfig; - protected Layer currentLayer; - protected MidiProcessor midiProcessor; - protected HwElements hwElements; - private TrackerRegistration trackerRegistration; - - protected AbstractLaunchpadMk3Extension(final ControllerExtensionDefinition definition, final ControllerHost host, - final LaunchpadDeviceConfig deviceConfig) { - super(definition, host); - this.deviceConfig = deviceConfig; - } - - protected Context initContext() { - DebugMini.registerHost(getHost()); - final Context diContext = new Context(this); - surface = diContext.getService(HardwareSurface.class); - diContext.registerService(LaunchpadDeviceConfig.class, deviceConfig); - final ControllerHost host = diContext.getService(ControllerHost.class); - initMidi(diContext, host); - sessionLayer = diContext.create(SessionLayer.class); - overviewLayer = diContext.create(OverviewLayer.class); - hwElements = diContext.getService(HwElements.class); - final LaunchPadPreferences preferences = diContext.getService(LaunchPadPreferences.class); - preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> sessionLayer.setLayout(newValue))); - diContext.create(NotePlayingLayer.class); - createControlLayers(diContext); - setUpTracking(diContext); - return diContext; - } - - private void setUpTracking(final Context diContext) { - final ViewTracker tracker = diContext.getViewTracker(); - final ViewCursorControl viewCursor = diContext.getService(ViewCursorControl.class); - trackerRegistration = tracker.registerViewPositionListener(deviceConfig.getDeviceId(), - (src, trackIndex) -> viewCursor.getTrackBank().scrollPosition().set(trackIndex)); - viewCursor.getTrackBank() - .scrollPosition() - .addValueObserver(trackIndex -> tracker.fireViewChanged(deviceConfig.getDeviceId(), trackIndex)); - } - - private void createControlLayers(final Context diContext) { - sessionLayer.registerControlLayer(ControlMode.VOLUME, diContext.create(VolumeSliderLayer.class)); - sessionLayer.registerControlLayer(ControlMode.PAN, diContext.create(PanSliderLayer.class)); - sessionLayer.registerControlLayer(ControlMode.SENDS, diContext.create(SendsSliderLayer.class)); - sessionLayer.registerControlLayer(ControlMode.DEVICE, diContext.create(DeviceSliderLayer.class)); - } - - protected void initMidi(final Context diContext, final ControllerHost host) { - final MidiIn midiIn = host.getMidiInPort(0); - final MidiIn midiIn2 = host.getMidiInPort(1); - - midiOut = host.getMidiOutPort(0); - midiProcessor = new MidiProcessor(host, midiIn, midiOut, deviceConfig); - diContext.registerService(MidiProcessor.class, midiProcessor); - midiIn2.createNoteInput("MIDI", "8?????", "9?????", "A?????", "D?????"); - midiIn.setSysexCallback(this::handleSysEx); - midiProcessor.start(); - } - - protected abstract void handleSysEx(String sysEx); - - private void shutDownController(final CompletableFuture shutdown) { - midiProcessor.enableDawMode(false); - try { - Thread.sleep(300); - } catch (final InterruptedException e) { - e.printStackTrace(); - } - shutdown.complete(true); - } - - @Override - public void flush() { - surface.updateHardware(); - } - - @Override - public void exit() { - if (trackerRegistration != null) { - trackerRegistration.unregister(); - } - final CompletableFuture shutdown = new CompletableFuture<>(); - Executors.newSingleThreadExecutor().execute(() -> shutDownController(shutdown)); - try { - shutdown.get(); - } catch (final InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - } - - + + protected HardwareSurface surface; + protected MidiOut midiOut; + protected SessionLayer sessionLayer; + protected OverviewLayer overviewLayer; + protected LpMode mode = LpMode.SESSION; + protected final LaunchpadDeviceConfig deviceConfig; + protected Layer currentLayer; + protected MidiProcessor midiProcessor; + protected LpMiniHwElements hwElements; + private TrackerRegistration trackerRegistration; + + private static ControllerHost debugHost; + + public static void println(final String format, final Object... args) { + if (debugHost != null) { + debugHost.println(format.formatted(args)); + } + } + + protected AbstractLaunchpadMk3Extension(final ControllerExtensionDefinition definition, final ControllerHost host, + final LaunchpadDeviceConfig deviceConfig) { + super(definition, host); + this.deviceConfig = deviceConfig; + } + + protected Context initContext() { + debugHost = getHost(); + final Context diContext = new Context(this, ViewCursorControl.class.getPackage()); + surface = diContext.getService(HardwareSurface.class); + diContext.registerService(LaunchpadDeviceConfig.class, deviceConfig); + final ControllerHost host = diContext.getService(ControllerHost.class); + initMidi(diContext, host); + sessionLayer = diContext.create(SessionLayer.class); + overviewLayer = diContext.create(OverviewLayer.class); + hwElements = diContext.getService(LpMiniHwElements.class); + diContext.registerService(LpHwElements.class, hwElements); + final LaunchPadPreferences preferences = diContext.getService(LaunchPadPreferences.class); + preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> sessionLayer.setLayout(newValue))); + diContext.create(NotePlayingLayer.class); + createControlLayers(diContext); + setUpTracking(diContext); + return diContext; + } + + private void setUpTracking(final Context diContext) { + final ViewTracker tracker = diContext.getViewTracker(); + final ViewCursorControl viewCursor = diContext.getService(ViewCursorControl.class); + trackerRegistration = tracker.registerViewPositionListener( + deviceConfig.getDeviceId(), + (src, trackIndex) -> viewCursor.getTrackBank().scrollPosition().set(trackIndex)); + viewCursor.getTrackBank().scrollPosition() + .addValueObserver(trackIndex -> tracker.fireViewChanged(deviceConfig.getDeviceId(), trackIndex)); + } + + private void createControlLayers(final Context diContext) { + sessionLayer.registerControlLayer(ControlMode.VOLUME, diContext.create(VolumeSliderLayer.class)); + sessionLayer.registerControlLayer(ControlMode.PAN, diContext.create(PanSliderLayer.class)); + sessionLayer.registerControlLayer(ControlMode.SENDS, diContext.create(SendsSliderLayer.class)); + sessionLayer.registerControlLayer(ControlMode.DEVICE, diContext.create(DeviceSliderLayer.class)); + } + + protected void initMidi(final Context diContext, final ControllerHost host) { + final MidiIn midiIn = host.getMidiInPort(0); + final MidiIn midiIn2 = host.getMidiInPort(1); + + midiOut = host.getMidiOutPort(0); + midiProcessor = new MidiProcessor(host, midiIn, midiOut, deviceConfig); + diContext.registerService(MidiProcessor.class, midiProcessor); + midiIn2.createNoteInput("MIDI", "8?????", "9?????", "A?????", "D?????"); + midiIn.setSysexCallback(this::handleSysEx); + midiProcessor.start(); + } + + protected abstract void handleSysEx(String sysEx); + + private void shutDownController(final CompletableFuture shutdown) { + midiProcessor.enableDawMode(false); + try { + Thread.sleep(300); + } + catch (final InterruptedException e) { + e.printStackTrace(); + } + shutdown.complete(true); + } + + @Override + public void flush() { + surface.updateHardware(); + } + + @Override + public void exit() { + if (trackerRegistration != null) { + trackerRegistration.unregister(); + } + final CompletableFuture shutdown = new CompletableFuture<>(); + Executors.newSingleThreadExecutor().execute(() -> shutDownController(shutdown)); + try { + shutdown.get(); + } + catch (final InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/DebugMini.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/DebugMini.java deleted file mode 100644 index 5d887e05..00000000 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/DebugMini.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.bitwig.extensions.controllers.novation.launchpadmini3; - -import com.bitwig.extension.controller.api.ControllerHost; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - -public class DebugMini { - - private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("hh:mm:ss SSS"); - private static ControllerHost host; - - public static void println(final String format, final Object... args) { - if (host != null) { - final LocalDateTime now = LocalDateTime.now(); - host.println(now.format(DF) + " > " + String.format(format, args)); - } - } - - public static void registerHost(final ControllerHost host) { - DebugMini.host = host; - } -} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchPadMiniMk3ExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchPadMiniMk3ExtensionDefinition.java index 9114d38f..1c53887a 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchPadMiniMk3ExtensionDefinition.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchPadMiniMk3ExtensionDefinition.java @@ -71,13 +71,13 @@ public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList final String[] outputNames = new String[1]; switch (platformType) { - case LINUX: case WINDOWS: inputNames[0] = "LPMiniMK3 MIDI"; inputNames[1] = "MIDIIN2 (LPMiniMK3 MIDI)"; outputNames[0] = "LPMiniMK3 MIDI"; break; case MAC: + case LINUX: inputNames[0] = "Launchpad Mini MK3 LPMiniMK3 DAW Out"; inputNames[1] = "Launchpad Mini MK3 LPMiniMK3 MIDI Out"; outputNames[0] = "Launchpad Mini MK3 LPMiniMK3 DAW In"; diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchPadXExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchPadXExtensionDefinition.java index 92bbe955..aa1d55cc 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchPadXExtensionDefinition.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchPadXExtensionDefinition.java @@ -66,12 +66,12 @@ public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList final String[] outputNames = new String[1]; switch (platformType) { - case LINUX: case WINDOWS: inputNames[0] = "LPX MIDI"; inputNames[1] = "MIDIIN2 (LPX MIDI)"; outputNames[0] = "LPX MIDI"; break; + case LINUX: case MAC: inputNames[0] = "Launchpad X LPX DAW Out"; inputNames[1] = "Launchpad X LPX MIDI Out"; diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchpadMiniMk3ControllerExtension.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchpadMiniMk3ControllerExtension.java index ef909950..2cb427f6 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchpadMiniMk3ControllerExtension.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchpadMiniMk3ControllerExtension.java @@ -10,95 +10,96 @@ import com.bitwig.extensions.framework.di.Context; public class LaunchpadMiniMk3ControllerExtension extends AbstractLaunchpadMk3Extension { - - private static final String DEVICE_RESPONSE = "f07e00060200202913010000"; - private long modeButtonDownTime = 0; - - protected LaunchpadMiniMk3ControllerExtension(final ControllerExtensionDefinition definition, - final ControllerHost host) { - super(definition, host, new LaunchpadDeviceConfig("LAUNCHPADMINIMK3", 0xD, 0xB5, 0xB4, true)); - } - - @Override - public void init() { - final Context diContext = super.initContext(); - // Main Grid Buttons counting from top to bottom - final Layer mainLayer = diContext.createLayer("MainLayer"); - - initModeButtons(diContext, mainLayer); - currentLayer = sessionLayer; - mainLayer.activate(); - - diContext.activate(); - midiProcessor.sendDeviceInquiry(); - } - - private void initModeButtons(final Context diContext, final Layer layer) { - final HwElements hwElements = diContext.getService(HwElements.class); - final LabeledButton sessionButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.SESSION); - sessionButton.bindPressed(layer, () -> { - modeSwitch(); - modeButtonDownTime = System.currentTimeMillis(); - }); - - final LabeledButton drumsButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.DRUMS); - drumsButton.bindPressed(layer, () -> mode = LpMode.DRUMS); - - final LabeledButton keysButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.KEYS); - keysButton.bindPressed(layer, () -> mode = LpMode.KEYS); - - final LabeledButton userButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.USER); - userButton.bindPressed(layer, () -> mode = LpMode.CUSTOM); - final MultiStateHardwareLight novationLight = hwElements.getNovationLight(); - layer.bindLightState(() -> RgbState.of(43), novationLight); - } - - private void modeSwitch() { - final long clickTime = (System.currentTimeMillis() - modeButtonDownTime); - if (mode == LpMode.SESSION) { - if (clickTime < 500) { - changeMode(LpMode.OVERVIEW); - midiProcessor.setButtonLed(0x14, 0x29); + + private static final String DEVICE_RESPONSE = "f07e00060200202913010000"; + private long modeButtonDownTime = 0; + + protected LaunchpadMiniMk3ControllerExtension(final ControllerExtensionDefinition definition, + final ControllerHost host) { + super(definition, host, new LaunchpadDeviceConfig("LAUNCHPADMINIMK3", 0xD, 0xB5, 0xB4, true)); + } + + @Override + public void init() { + final Context diContext = super.initContext(); + // Main Grid Buttons counting from top to bottom + final Layer mainLayer = diContext.createLayer("MainLayer"); + + initModeButtons(diContext, mainLayer); + currentLayer = sessionLayer; + mainLayer.activate(); + + diContext.activate(); + midiProcessor.sendDeviceInquiry(); + } + + private void initModeButtons(final Context diContext, final Layer layer) { + final LpMiniHwElements hwElements = diContext.getService(LpMiniHwElements.class); + final LabeledButton sessionButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.SESSION); + sessionButton.bindPressed(layer, () -> { + modeSwitch(); + modeButtonDownTime = System.currentTimeMillis(); + }); + + final LabeledButton drumsButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.DRUMS); + drumsButton.bindPressed(layer, () -> mode = LpMode.DRUMS); + + final LabeledButton keysButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.KEYS); + keysButton.bindPressed(layer, () -> mode = LpMode.KEYS); + + final LabeledButton userButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.USER); + userButton.bindPressed(layer, () -> mode = LpMode.CUSTOM); + final MultiStateHardwareLight novationLight = hwElements.getNovationLight(); + layer.bindLightState(() -> RgbState.of(43), novationLight); + } + + private void modeSwitch() { + final long clickTime = (System.currentTimeMillis() - modeButtonDownTime); + if (mode == LpMode.SESSION) { + if (clickTime < 500) { + changeMode(LpMode.OVERVIEW); + midiProcessor.setButtonLed(0x14, 0x29); + modeButtonDownTime = 0; + } + } else { + changeMode(LpMode.SESSION); + midiProcessor.setButtonLed(0x14, 0x18); modeButtonDownTime = 0; - } - } else { - changeMode(LpMode.SESSION); - midiProcessor.setButtonLed(0x14, 0x18); - modeButtonDownTime = 0; - } - } - - private void changeMode(final LpMode mode) { - if (this.mode == mode) { - return; - } - if (this.mode == LpMode.OVERVIEW) { - overviewLayer.setIsActive(false); - } - if (mode == LpMode.OVERVIEW) { - overviewLayer.setIsActive(true); - } - sessionLayer.setMode(mode); - this.mode = mode; - } - - @Override - protected void handleSysEx(final String sysExString) { - if (sysExString.startsWith(DEVICE_RESPONSE)) { - midiProcessor.enableDawMode(true); - midiProcessor.toLayout(0); - pause(20); - hwElements.refresh(); - midiProcessor.setButtonLed(0x14, 0x18); - } - } - - private void pause(int time) { - try { - Thread.sleep(time); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - + } + } + + private void changeMode(final LpMode mode) { + if (this.mode == mode) { + return; + } + if (this.mode == LpMode.OVERVIEW) { + overviewLayer.setIsActive(false); + } + if (mode == LpMode.OVERVIEW) { + overviewLayer.setIsActive(true); + } + sessionLayer.setMode(mode); + this.mode = mode; + } + + @Override + protected void handleSysEx(final String sysExString) { + if (sysExString.startsWith(DEVICE_RESPONSE)) { + midiProcessor.enableDawMode(true); + midiProcessor.toLayout(0); + pause(20); + hwElements.refresh(); + midiProcessor.setButtonLed(0x14, 0x18); + } + } + + private void pause(int time) { + try { + Thread.sleep(time); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchpadXControllerExtension.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchpadXControllerExtension.java index 670cf0ef..3c6e11ca 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchpadXControllerExtension.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LaunchpadXControllerExtension.java @@ -1,150 +1,177 @@ package com.bitwig.extensions.controllers.novation.launchpadmini3; import com.bitwig.extension.controller.ControllerExtensionDefinition; -import com.bitwig.extension.controller.api.*; +import com.bitwig.extension.controller.api.BooleanValue; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.MultiStateHardwareLight; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extension.controller.api.Preferences; +import com.bitwig.extension.controller.api.Transport; import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; import com.bitwig.extensions.controllers.novation.commonsmk3.LaunchpadDeviceConfig; import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; import com.bitwig.extensions.controllers.novation.launchpadmini3.layers.DrumLayer; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.di.Context; public class LaunchpadXControllerExtension extends AbstractLaunchpadMk3Extension { - - private static final String DEVICE_RESPONSE = "f07e000602002029"; - private static final String MODE_CHANGE_PREFIX = "f0002029020c00"; - private DrumLayer drumLayer; - private ViewCursorControl viewControl; - private Transport transport; - private BooleanValue recordButtonAsAlt; - private Layer altModeLayer; - - protected LaunchpadXControllerExtension(final ControllerExtensionDefinition definition, final ControllerHost host) { - super(definition, host, new LaunchpadDeviceConfig("LAUNCHPAD_X_MK3", 0xC, 0xB4, 0xB5, false)); - } - - @Override - public void init() { - final Context diContext = super.initContext(); - - drumLayer = diContext.create(DrumLayer.class); - final Layer mainLayer = diContext.createLayer("MainLayer"); - altModeLayer = diContext.createLayer("AltModeLayer"); - viewControl = diContext.getService(ViewCursorControl.class); - transport = diContext.getService(Transport.class); - initPreferences(); - initModeButtons(diContext, mainLayer); - initViewControlListeners(); - mainLayer.activate(); - diContext.activate(); - altModeLayer.setIsActive(recordButtonAsAlt.get()); - midiProcessor.sendDeviceInquiry(); - } - - private void initPreferences() { - final Preferences preferences = getHost().getPreferences(); // THIS - recordButtonAsAlt = preferences.getBooleanSetting("Use as ALT trigger modifier", "Record Button", false); - recordButtonAsAlt.markInterested(); - } - - private void initModeButtons(final Context diContext, final Layer layer) { - final HwElements hwElements = diContext.getService(HwElements.class); - final LabeledButton sessionButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.SESSION); - sessionButton.bindPressed(layer, this::toggleMode); - - final LabeledButton keysButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.DRUMS); - keysButton.bindPressed(layer, () -> { - if (mode == LpMode.KEYS) { - return; - } - mode = LpMode.KEYS; - changeDrumMode(); - if (recordButtonAsAlt.get()) { - altModeLayer.setIsActive(false); - } - }); - - final LabeledButton userButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.KEYS); - userButton.bindPressed(layer, () -> mode = LpMode.CUSTOM); - - transport.isPlaying().markInterested(); - transport.isClipLauncherOverdubEnabled().markInterested(); - - recordButtonAsAlt.addValueObserver(recAsAlt -> { - altModeLayer.setIsActive(recAsAlt); - if (!recAsAlt) { - sessionLayer.setShiftHeld(false); - } - }); - - final LabeledButton recButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.USER); - recButton.bindPressed(layer, () -> viewControl.globalRecordAction(transport)); - recButton.bindLight(layer, sessionLayer::getRecordButtonColorRegular); - - recButton.bindPressed(altModeLayer, pressed -> sessionLayer.setShiftHeld(pressed), RgbState.WHITE, - RgbState.pulse(2)); - - final MultiStateHardwareLight novationLight = hwElements.getNovationLight(); - layer.bindLightState(() -> RgbState.DIM_WHITE, novationLight); - getHost().scheduleTask(recButton::refresh, 50); - - } - - private void initViewControlListeners() { - final PinnableCursorDevice primaryDevice = viewControl.getPrimaryDevice(); - primaryDevice.hasDrumPads().addValueObserver(hasDrumPads -> { - drumModeActive = hasDrumPads; - if (mode == LpMode.KEYS) { + + private static final String DEVICE_RESPONSE = "f07e000602002029"; + private static final String MODE_CHANGE_PREFIX = "f0002029020c00"; + private DrumLayer drumLayer; + private ViewCursorControl viewControl; + private Transport transport; + private BooleanValue recordButtonAsAlt; + private Layer altModeLayer; + private LpMode preOverviewMode; + + protected LaunchpadXControllerExtension(final ControllerExtensionDefinition definition, final ControllerHost host) { + super(definition, host, new LaunchpadDeviceConfig("LAUNCHPAD_X_MK3", 0xC, 0xB4, 0xB5, false)); + } + + @Override + public void init() { + final Context diContext = super.initContext(); + + drumLayer = diContext.create(DrumLayer.class); + final Layer mainLayer = diContext.createLayer("MainLayer"); + altModeLayer = diContext.createLayer("AltModeLayer"); + viewControl = diContext.getService(ViewCursorControl.class); + transport = diContext.getService(Transport.class); + initPreferences(); + initModeButtons(diContext, mainLayer); + initViewControlListeners(); + mainLayer.activate(); + diContext.activate(); + altModeLayer.setIsActive(recordButtonAsAlt.get()); + midiProcessor.sendDeviceInquiry(); + } + + private void initPreferences() { + final Preferences preferences = getHost().getPreferences(); // THIS + recordButtonAsAlt = preferences.getBooleanSetting("Use as ALT trigger modifier", "Record Button", false); + recordButtonAsAlt.markInterested(); + } + + private void initModeButtons(final Context diContext, final Layer layer) { + final LpMiniHwElements hwElements = diContext.getService(LpMiniHwElements.class); + final LabeledButton sessionButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.SESSION); + //sessionButton.bindPressed(layer, this::toggleMode); + sessionButton.bindDoubleTapCombo( + layer, this::toggleMode, this::toOverviewMode, () -> this.mode != LpMode.OVERVIEW); + // Double Press to Overview Mode + + final LabeledButton keysButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.DRUMS); + keysButton.bindPressed(layer, () -> { + if (mode == LpMode.KEYS) { + return; + } + mode = LpMode.KEYS; changeDrumMode(); - } - }); - } - - private boolean drumModeActive = false; - - private void changeDrumMode() { - midiProcessor.setNoteModeLayoutX(drumModeActive ? 0x1 : 0x0); - drumLayer.setIsActive(drumModeActive); - } - - private void toggleMode() { - if (mode == LpMode.SESSION) { - changeMode(LpMode.MIXER); - midiProcessor.setButtonLed(0x14, 0x3E); - - } else { - changeMode(LpMode.SESSION); - midiProcessor.setButtonLed(0x14, 0x14); - } - } - - private void changeMode(final LpMode mode) { - if (this.mode == mode) { - return; - } - if (recordButtonAsAlt.get()) { - altModeLayer.setIsActive(true); - } - if (this.mode == LpMode.OVERVIEW) { - overviewLayer.setIsActive(false); - } - if (mode == LpMode.OVERVIEW) { - overviewLayer.setIsActive(true); - } - sessionLayer.setMode(mode); - this.mode = mode; - } - - @Override - public void handleSysEx(final String sysExString) { - if (sysExString.startsWith(DEVICE_RESPONSE)) { - midiProcessor.enableDawMode(true); - midiProcessor.toLayout(0); - hwElements.refresh(); - } else if (sysExString.startsWith(MODE_CHANGE_PREFIX)) { - final String modeString = sysExString.substring(MODE_CHANGE_PREFIX.length(), sysExString.length() - 2); - DebugMini.println("MODE = %s", modeString); - } - } - + if (recordButtonAsAlt.get()) { + altModeLayer.setIsActive(false); + } + }); + + final LabeledButton userButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.KEYS); + userButton.bindPressed(layer, () -> mode = LpMode.CUSTOM); + + transport.isPlaying().markInterested(); + transport.isClipLauncherOverdubEnabled().markInterested(); + + recordButtonAsAlt.addValueObserver(recAsAlt -> { + altModeLayer.setIsActive(recAsAlt); + if (!recAsAlt) { + sessionLayer.setShiftHeld(false); + } + }); + + final LabeledButton recButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.USER); + recButton.bindPressed(layer, () -> viewControl.globalRecordAction(transport)); + recButton.bindLight(layer, sessionLayer::getRecordButtonColorRegular); + + recButton.bindPressed(altModeLayer, pressed -> sessionLayer.setShiftHeld(pressed), RgbState.WHITE, + RgbState.pulse(2)); + + final MultiStateHardwareLight novationLight = hwElements.getNovationLight(); + layer.bindLightState(() -> RgbState.DIM_WHITE, novationLight); + getHost().scheduleTask(recButton::refresh, 50); + + } + + private void initViewControlListeners() { + final PinnableCursorDevice primaryDevice = viewControl.getPrimaryDevice(); + primaryDevice.hasDrumPads().addValueObserver(hasDrumPads -> { + drumModeActive = hasDrumPads; + if (mode == LpMode.KEYS) { + changeDrumMode(); + } + }); + } + + private boolean drumModeActive = false; + + private void changeDrumMode() { + midiProcessor.setNoteModeLayoutX(drumModeActive ? 0x1 : 0x0); + drumLayer.setIsActive(drumModeActive); + } + + private void toOverviewMode() { + if (mode != LpMode.OVERVIEW) { + this.preOverviewMode = this.mode;// == LpMode.MIXER ? LpMode.SESSION : LpMode.MIXER; + changeMode(LpMode.OVERVIEW); + updateSessionButtonColor(); + } + } + + private void toggleMode() { + if (mode == LpMode.OVERVIEW) { + changeMode(this.preOverviewMode); + } else if (mode == LpMode.SESSION) { + changeMode(LpMode.MIXER); + } else { + changeMode(LpMode.SESSION); + } + updateSessionButtonColor(); + } + + private void updateSessionButtonColor() { + midiProcessor.setButtonLed(0x14, this.mode.getButtonColor()); + } + + private void changeMode(final LpMode mode) { + if (this.mode == mode) { + return; + } + if (recordButtonAsAlt.get()) { + altModeLayer.setIsActive(true); + } + if (this.mode == LpMode.OVERVIEW) { + overviewLayer.setIsActive(false); + } + if (mode == LpMode.OVERVIEW) { + if (this.mode == LpMode.MIXER) { + sessionLayer.setMode(LpMode.SESSION); + } + sessionLayer.setIsActive(false); + overviewLayer.setIsActive(true); + } else { + sessionLayer.setIsActive(true); + sessionLayer.setMode(mode); + } + this.mode = mode; + } + + @Override + public void handleSysEx(final String sysExString) { + if (sysExString.startsWith(DEVICE_RESPONSE)) { + midiProcessor.enableDawMode(true); + midiProcessor.toLayout(0); + hwElements.refresh(); + } else if (sysExString.startsWith(MODE_CHANGE_PREFIX)) { + final String modeString = sysExString.substring(MODE_CHANGE_PREFIX.length(), sysExString.length() - 2); + } + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/HwElements.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LpMiniHwElements.java similarity index 90% rename from src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/HwElements.java rename to src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LpMiniHwElements.java index cd2c4fd5..38e9952e 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/HwElements.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LpMiniHwElements.java @@ -1,34 +1,35 @@ package com.bitwig.extensions.controllers.novation.launchpadmini3; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import com.bitwig.extension.controller.api.HardwareSurface; import com.bitwig.extension.controller.api.MultiStateHardwareLight; import com.bitwig.extensions.controllers.novation.commonsmk3.DrumButton; import com.bitwig.extensions.controllers.novation.commonsmk3.GridButton; import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; +import com.bitwig.extensions.controllers.novation.commonsmk3.LpHwElements; import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; import com.bitwig.extensions.framework.di.Component; import com.bitwig.extensions.framework.di.Inject; import com.bitwig.extensions.framework.di.PostConstruct; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - @Component -public class HwElements { +public class LpMiniHwElements implements LpHwElements { private final GridButton[][] gridButtons = new GridButton[8][8]; private final Map labeledButtons = new HashMap<>(); private final List sceneLaunchButtons = new ArrayList<>(); private final List drumGridButtons = new ArrayList<>(); - + private MultiStateHardwareLight novationLight; - + private final int[] SCENE_CCS = {0x59, 0x4f, 0x45, 0x3B, 0x31, 0x27, 0x1D, 0x13}; - + @Inject private MidiProcessor midiProcessor; - + @PostConstruct public void init(final HardwareSurface surface) { initGridButtons(surface, midiProcessor); @@ -39,14 +40,14 @@ public void init(final HardwareSurface surface) { } } for (int i = 0; i < 8; i++) { - final LabeledButton sceneButton = new LabeledButton("SCENE_LAUNCH_" + (i + 1), surface, midiProcessor, - SCENE_CCS[i]); + final LabeledButton sceneButton = + new LabeledButton("SCENE_LAUNCH_" + (i + 1), surface, midiProcessor, SCENE_CCS[i]); sceneLaunchButtons.add(sceneButton); } novationLight = surface.createMultiStateHardwareLight("NovationLight"); novationLight.state().onUpdateHardware(hwState -> midiProcessor.updatePadLed(hwState, 0x63)); } - + public void refresh() { sceneLaunchButtons.forEach(LabeledButton::refresh); for (int row = 0; row < 8; row++) { @@ -55,7 +56,7 @@ public void refresh() { } } } - + private void initGridButtons(final HardwareSurface surface, final MidiProcessor midiProcessor) { for (int row = 0; row < 8; row++) { for (int col = 0; col < 8; col++) { @@ -67,25 +68,25 @@ private void initGridButtons(final HardwareSurface surface, final MidiProcessor drumGridButtons.add(new DrumButton(surface, midiProcessor, 8, noteValue)); } } - + public List getDrumGridButtons() { return drumGridButtons; } - + public GridButton getGridButton(final int row, final int col) { return gridButtons[row][col]; } - + public LabeledButton getLabeledButton(final LabelCcAssignmentsMini assignment) { return labeledButtons.get(assignment); } - + public List getSceneLaunchButtons() { return sceneLaunchButtons; } - + public MultiStateHardwareLight getNovationLight() { return novationLight; } - + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LpMode.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LpMode.java index 597423d5..4b4cfb4f 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LpMode.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/LpMode.java @@ -1,20 +1,21 @@ package com.bitwig.extensions.controllers.novation.launchpadmini3; public enum LpMode { - SESSION(0), - KEYS(0x5), - DRUMS(0x4), - CUSTOM(0x6), - MIXER(0x17), - OVERVIEW(0x18); - + SESSION(0, 0x14), KEYS(0x5, 0), DRUMS(0x4, 0), CUSTOM(0x6, 0), MIXER(0x17, 0x3E), OVERVIEW(0x18, 0x26); + + private final int buttonColor; final int modeId; - - LpMode(final int modeId) { + + LpMode(final int modeId, final int buttonColor) { this.modeId = modeId; + this.buttonColor = buttonColor; } - + public int getModeId() { return modeId; } + + public int getButtonColor() { + return buttonColor; + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/TrackState.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/TrackState.java index 12cba34c..5e9b2635 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/TrackState.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/TrackState.java @@ -1,35 +1,36 @@ package com.bitwig.extensions.controllers.novation.launchpadmini3; +import java.util.ArrayList; +import java.util.List; + import com.bitwig.extension.controller.api.CursorTrack; import com.bitwig.extension.controller.api.Track; import com.bitwig.extension.controller.api.TrackBank; import com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; import com.bitwig.extensions.framework.di.Component; -import java.util.ArrayList; -import java.util.List; - @Component public class TrackState { private final int[] colorIndex; private final boolean[] exists; private int cursorColor = ColorLookup.toColor(0, 0, 0); - + private final List existsListeners = new ArrayList<>(); private final List colorListeners = new ArrayList<>(); - + public interface BoolStateListener { void changed(int trackIndex, boolean state); } - + public interface ColorStateListener { void changed(int trackIndex, int color); } - + public TrackState(final ViewCursorControl viewCursorControl) { final TrackBank trackBank = viewCursorControl.getTrackBank(); final int sizeOfBank = trackBank.getSizeOfBank(); - + final CursorTrack cursorTrack = viewCursorControl.getCursorTrack(); cursorTrack.color().addValueObserver((r, g, b) -> cursorColor = ColorLookup.toColor(r, g, b)); colorIndex = new int[sizeOfBank]; @@ -41,39 +42,39 @@ public TrackState(final ViewCursorControl viewCursorControl) { track.exists().addValueObserver(exists -> handleExistsChanged(trackIndex, exists)); } } - + public void addColorStateListener(final ColorStateListener colorStateListener) { colorListeners.add(colorStateListener); } - + public void addExistsListener(final BoolStateListener listener) { existsListeners.add(listener); } - + private void handleColorChanged(final int trackIndex, final int color) { colorIndex[trackIndex] = color; colorListeners.forEach(l -> l.changed(trackIndex, color)); } - + private void handleExistsChanged(final int trackIndex, final boolean exists) { this.exists[trackIndex] = exists; existsListeners.forEach(l -> l.changed(trackIndex, exists)); } - + public int[] getColorIndex() { return colorIndex; } - + public boolean[] getExists() { return exists; } - + public int getCursorColor() { return cursorColor; } - + public int getColorOfTrack(final int index) { return colorIndex[index]; } - + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/ViewCursorControl.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/ViewCursorControl.java deleted file mode 100644 index a3b459c6..00000000 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/ViewCursorControl.java +++ /dev/null @@ -1,300 +0,0 @@ -package com.bitwig.extensions.controllers.novation.launchpadmini3; - -import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.novation.commonsmk3.OverviewGrid; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.DebugOutLp; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.FocusSlot; -import com.bitwig.extensions.framework.di.Component; -import com.bitwig.extensions.framework.di.Inject; - -import java.util.Optional; - -@Component -public class ViewCursorControl { - @Inject - Application application; - - private final CursorTrack cursorTrack; - private final PinnableCursorDevice primaryDevice; - private final TrackBank trackBank; - private final PinnableCursorDevice cursorDevice; - private final Clip cursorClip; - - private final Track rootTrack; - private final ClipLauncherSlotBank mainTrackSlotBank; - private final Track largeFocusTrack; - private FocusSlot focusSlot; - private final TrackBank maxTrackBank; - private int queuedForPlaying = 0; - private int focusSceneIndex; - - private final OverviewGrid overviewGrid = new OverviewGrid(); - - public ViewCursorControl(final ControllerHost host) { - rootTrack = host.getProject().getRootTrackGroup(); - rootTrack.arm().markInterested(); - - maxTrackBank = host.createTrackBank(64, 1, 1); - - setUpFocusScene(); - - trackBank = host.createTrackBank(8, 1, 8); - - trackBank.sceneBank().itemCount().addValueObserver(overviewGrid::setNumberOfScenes); - trackBank.channelCount().addValueObserver(overviewGrid::setNumberOfTracks); - trackBank.scrollPosition().addValueObserver(overviewGrid::setTrackPosition); - trackBank.sceneBank().scrollPosition().addValueObserver(overviewGrid::setScenePosition); - - cursorTrack = host.createCursorTrack(8, 8); - for (int i = 0; i < 8; i++) { - prepareTrack(trackBank.getItemAt(i)); - } - - cursorDevice = cursorTrack.createCursorDevice(); - cursorClip = host.createLauncherCursorClip(32 * 6, 127); - - cursorTrack.clipLauncherSlotBank().cursorIndex().addValueObserver(index -> { - // RemoteConsole.out.println(" => {}", index); - }); - prepareTrack(cursorTrack); - - primaryDevice = cursorTrack.createCursorDevice("drumdetection", "Pad Device", 8, - CursorDeviceFollowMode.FIRST_INSTRUMENT); - primaryDevice.hasDrumPads().markInterested(); - primaryDevice.exists().markInterested(); - - - final TrackBank singleTrackBank = host.createTrackBank(1, 0, 16); - singleTrackBank.scrollPosition().markInterested(); - singleTrackBank.followCursorTrack(cursorTrack); - largeFocusTrack = singleTrackBank.getItemAt(0); - prepareTrack(largeFocusTrack); - - mainTrackSlotBank = largeFocusTrack.clipLauncherSlotBank(); - final BooleanValue equalsToCursorTrack = largeFocusTrack.createEqualsValue(cursorTrack); - equalsToCursorTrack.markInterested(); - - - for (int i = 0; i < 16; i++) { - final int index = i; - final ClipLauncherSlot slot = mainTrackSlotBank.getItemAt(i); - prepareSlot(slot); - - slot.isSelected().addValueObserver(selected -> { - if (selected) { - DebugOutLp.println("Select Slot MAIN %d", index); - focusSlot = new FocusSlot(largeFocusTrack, slot, index, equalsToCursorTrack); - } - }); - } - } - - private void setUpFocusScene() { - maxTrackBank.sceneBank().scrollPosition().addValueObserver(scrollPos -> focusSceneIndex = scrollPos); - for (int i = 0; i < 64; i++) { - final Track track = maxTrackBank.getItemAt(i); - final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(0); - slot.isPlaybackQueued().addValueObserver(queued -> { - if (queued) { - queuedForPlaying++; - } else if (queuedForPlaying > 0) { - queuedForPlaying--; - } - }); - } - } - - public boolean hasQueuedForPlaying() { - return queuedForPlaying > 0; - } - - public void focusScene(final int sceneIndex) { - maxTrackBank.sceneBank().scrollPosition().set(sceneIndex); - } - - private void prepareTrack(final Track track) { - track.arm().markInterested(); - track.monitorMode().markInterested(); - track.sourceSelector().hasAudioInputSelected().markInterested(); - track.sourceSelector().hasNoteInputSelected().markInterested(); - } - - private void prepareSlot(final ClipLauncherSlot slot) { - slot.isRecording().markInterested(); - slot.isRecordingQueued().markInterested(); - slot.hasContent().markInterested(); - slot.name().markInterested(); - slot.isPlaying().markInterested(); - slot.isSelected().markInterested(); - } - - public void scrollToOverview(final int trackIndex, final int sceneIndex) { - final int posX = trackIndex * 8; - final int posY = sceneIndex * 8; - if (posX < overviewGrid.getNumberOfTracks() && posY < overviewGrid.getNumberOfScenes()) { - trackBank.scrollPosition().set(posX); - trackBank.sceneBank().scrollPosition().set(posY); - } - } - - public boolean inOverviewGrid(final int trackIndex, final int sceneIndex) { - final int posX = trackIndex * 8; - final int posY = sceneIndex * 8; - return posX < overviewGrid.getNumberOfTracks() && posY < overviewGrid.getNumberOfScenes(); - } - - public boolean inOverviewGridFocus(final int trackIndex, final int sceneIndex) { - final int locX = overviewGrid.getTrackPosition() / 8; - final int locY = overviewGrid.getScenePosition() / 8; - return locX == trackIndex && locY == sceneIndex; - } - - public TrackBank getTrackBank() { - return trackBank; - } - - public CursorTrack getCursorTrack() { - return cursorTrack; - } - - public PinnableCursorDevice getPrimaryDevice() { - return primaryDevice; - } - - public PinnableCursorDevice getCursorDevice() { - return cursorDevice; - } - - public Clip getCursorClip() { - return cursorClip; - } - - public FocusSlot getFocusSlot() { - return focusSlot; - } - - private boolean trackOfFocusSlotArmed() { - return focusSlot != null && focusSlot.getTrack().arm().get(); - } - - public void globalRecordAction(final Transport transport) { - if (largeFocusTrack.arm().get() || trackOfFocusSlotArmed()) { - if (focusSlot != null) { - handleFocusedSlotOnArmedTrack(transport); - } else { - handleNoFocusSlotOnArmedTrack(transport); - } - } else if (focusSlot != null) { - handleRecordFocusSlotNotArmed(transport); - } else { - transport.isClipLauncherOverdubEnabled().toggle(); - } - } - - private void handleNoFocusSlotOnArmedTrack(final Transport transport) { - final Optional playingSlot = findPlayingSlot(); - if (playingSlot.isPresent()) { - toggleRecording(playingSlot.get(), transport); - } else { - findEmptySlotAndLaunch(transport, -1); - } - } - - private void handleFocusedSlotOnArmedTrack(final Transport transport) { - if (focusSlot.getTrack().arm().get()) { - if (focusSlot.isEmpty()) { - recordToEmptySlot(focusSlot.getSlot(), transport); - } else { - toggleRecording(focusSlot.getSlot(), transport); - } - } else { - findEmptySlotAndLaunch(transport, focusSlot.getSlotIndex()); - } - } - - private void handleRecordFocusSlotNotArmed(final Transport transport) { - final Track track = focusSlot.getTrack(); - if (canRecord(focusSlot.getTrack())) { - track.arm().set(true); - track.selectInEditor(); - if (!focusSlot.isEmpty()) { - toggleRecording(focusSlot.getSlot(), transport); - } else { - recordToEmptySlot(focusSlot.getSlot(), transport); - } - } else { - transport.isClipLauncherOverdubEnabled().toggle(); - } - } - - private void findEmptySlotAndLaunch(final Transport transport, final int slotIndex) { - final Optional slot = findCursorFirstEmptySlot(slotIndex); - if (slot.isPresent()) { - recordToEmptySlot(slot.get(), transport); - } else { - transport.isClipLauncherOverdubEnabled().toggle(); - } - } - - private boolean canRecord(final Track track) { - return track.sourceSelector().hasNoteInputSelected().get() || track.sourceSelector() - .hasAudioInputSelected() - .get(); - } - - public int getFocusSceneIndex() { - return focusSceneIndex; - } - - public Optional findCursorFirstEmptySlot(final int firstIndex) { - if (firstIndex >= 0 && firstIndex < 16) { - final ClipLauncherSlot slot = mainTrackSlotBank.getItemAt(firstIndex); - if (!slot.hasContent().get()) { - return Optional.of(slot); - } - } - for (int i = 0; i < 16; i++) { - final ClipLauncherSlot slot = mainTrackSlotBank.getItemAt(i); - if (!slot.hasContent().get()) { - return Optional.of(slot); - } - } - return Optional.empty(); - } - - private void toggleRecording(final ClipLauncherSlot slot, final Transport transport) { - if (slot.isRecordingQueued().get() || slot.isRecording().get()) { - slot.launch(); - transport.isClipLauncherOverdubEnabled().set(false); - } else if (!slot.isPlaying().get()) { - slot.launch(); - transport.isClipLauncherOverdubEnabled().set(true); - } else if (transport.isPlaying().get()) { - transport.isClipLauncherOverdubEnabled().toggle(); - } else { - transport.restart(); - slot.launch(); - transport.isClipLauncherOverdubEnabled().set(true); - } - } - - private void recordToEmptySlot(final ClipLauncherSlot slot, final Transport transport) { - if (!transport.isPlaying().get()) { - transport.restart(); - } - slot.select(); - slot.launch(); - transport.isClipLauncherOverdubEnabled().set(true); - } - - public Optional findPlayingSlot() { - for (int i = 0; i < 16; i++) { - final ClipLauncherSlot slot = mainTrackSlotBank.getItemAt(i); - if (slot.hasContent().get() && slot.isPlaying().get()) { - return Optional.of(slot); - } - } - return Optional.empty(); - } - -} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/DeviceSliderLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/DeviceSliderLayer.java index 4cab6b7a..bbebc242 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/DeviceSliderLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/DeviceSliderLayer.java @@ -1,19 +1,24 @@ package com.bitwig.extensions.controllers.novation.launchpadmini3.layers; -import com.bitwig.extension.controller.api.*; +import com.bitwig.extension.controller.api.CursorDeviceLayer; +import com.bitwig.extension.controller.api.CursorRemoteControlsPage; +import com.bitwig.extension.controller.api.DeviceBank; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extension.controller.api.RemoteControl; import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; import com.bitwig.extensions.controllers.novation.commonsmk3.SliderBinding; -import com.bitwig.extensions.controllers.novation.launchpadmini3.HwElements; +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.ViewCursorControl; import com.bitwig.extensions.framework.Layers; public class DeviceSliderLayer extends AbstractSliderLayer { - + private static final int[] PARAM_COLORS = {5, 9, 13, 25, 29, 41, 49, 57}; - + private final CursorRemoteControlsPage parameterBank; private final DeviceBank drumDeviceBank; final PinnableCursorDevice device; @@ -24,9 +29,9 @@ public class DeviceSliderLayer extends AbstractSliderLayer { private boolean hasLayers; private boolean hasSlots; private String[] deviceSlotNames = new String[0]; - + public DeviceSliderLayer(final ViewCursorControl viewCursorControl, final HardwareSurface controlSurface, - final Layers layers, final MidiProcessor midiProcessor, final HwElements hwElements) { + final Layers layers, final MidiProcessor midiProcessor, final LpMiniHwElements hwElements) { super("DEVICE", controlSurface, layers, midiProcessor, 40, 31); device = viewCursorControl.getCursorDevice(); parameterBank = device.createCursorRemoteControlsPage(8); @@ -35,7 +40,7 @@ public DeviceSliderLayer(final ViewCursorControl viewCursorControl, final Hardwa final PinnableCursorDevice primary = viewCursorControl.getPrimaryDevice(); final CursorDeviceLayer drumCursor = primary.createCursorLayer(); drumDeviceBank = drumCursor.createDeviceBank(8); - + //initSceneButtons(hwElements); for (int i = 0; i < 8; i++) { final RemoteControl parameter = parameterBank.getParameter(i); @@ -45,43 +50,43 @@ public DeviceSliderLayer(final ViewCursorControl viewCursorControl, final Hardwa } //initNavigation(hwElements); } - - private void initNavigation(final HwElements hwElements) { + + private void initNavigation(final LpMiniHwElements hwElements) { final LabeledButton upButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.UP); final LabeledButton downButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.DOWN); final LabeledButton leftButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.LEFT); final LabeledButton rightButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.RIGHT); parameterBank.hasNext().markInterested(); parameterBank.hasPrevious().markInterested(); - + device.hasNext().markInterested(); device.hasPrevious().markInterested(); - + device.isNested().addValueObserver(nested -> isNested = nested); device.slotNames().addValueObserver(slotNames -> deviceSlotNames = slotNames); device.hasSlots().addValueObserver(hasSlots -> this.hasSlots = hasSlots); device.hasLayers().addValueObserver(hasLayers -> this.hasLayers = hasLayers); device.hasDrumPads().addValueObserver(hasPads -> hasDrumPads = hasPads); - - + + final RgbState scrollColor = RgbState.of(23); upButton.bindRepeatHold(this, () -> handleNavigateDevice(-1)); upButton.bindLight(this, () -> isNested ? scrollColor : RgbState.OFF); - + downButton.bindRepeatHold(this, () -> handleNavigateDevice(1)); downButton.bindLight(this, () -> canNavigateDown() ? scrollColor : RgbState.OFF); - + leftButton.bindRepeatHold(this, () -> handleNavigateChain(-1)); leftButton.bindLight(this, () -> device.hasPrevious().get() ? scrollColor : RgbState.OFF); - + rightButton.bindRepeatHold(this, () -> handleNavigateChain(1)); rightButton.bindLight(this, () -> device.hasNext().get() ? scrollColor : RgbState.OFF); } - + private boolean canNavigateDown() { return hasDrumPads || hasLayers || hasSlots; } - + private void handleNavigateChain(final int amount) { if (amount > 0) { device.selectNext(); @@ -89,7 +94,7 @@ private void handleNavigateChain(final int amount) { device.selectPrevious(); } } - + private void handleNavigateDevice(final int amount) { if (amount > 0) { if (device.hasDrumPads().get()) { @@ -103,7 +108,7 @@ private void handleNavigateDevice(final int amount) { device.selectParent(); } } - + private void handleNavigateParameters(final int amount, final CursorRemoteControlsPage parameters) { if (amount > 0) { parameters.selectNextPage(false); @@ -111,9 +116,9 @@ private void handleNavigateParameters(final int amount, final CursorRemoteContro parameters.selectPreviousPage(false); } } - - - private void initSceneButtons(final HwElements hwElements) { + + + private void initSceneButtons(final LpMiniHwElements hwElements) { for (int i = 0; i < 8; i++) { final int index = i; final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(index); @@ -121,11 +126,11 @@ private void initSceneButtons(final HwElements hwElements) { sceneButton.bindLight(this, () -> getColor(index)); } } - + private void handleSendSelect(final int index) { parameterBank.selectedPageIndex().set(index); } - + private RgbState getColor(final int index) { if (index < parameterPages) { if (pageIndex == index) { @@ -135,19 +140,19 @@ private RgbState getColor(final int index) { } return RgbState.OFF; } - + @Override protected void refreshTrackColors() { System.arraycopy(PARAM_COLORS, 0, tracksExistsColors, 0, PARAM_COLORS.length); midiProcessor.setFaderBank(0, tracksExistsColors, true, baseCcNr); } - + @Override protected void onActivate() { super.onActivate(); refreshTrackColors(); } - + @Override protected void onDeactivate() { super.onDeactivate(); diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/DrumLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/DrumLayer.java index aaa5a57c..3a66947f 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/DrumLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/DrumLayer.java @@ -1,21 +1,34 @@ package com.bitwig.extensions.controllers.novation.launchpadmini3.layers; -import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.novation.commonsmk3.*; -import com.bitwig.extensions.controllers.novation.launchpadmini3.HwElements; +import java.util.Arrays; +import java.util.List; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.DeviceBank; +import com.bitwig.extension.controller.api.DeviceMatcher; +import com.bitwig.extension.controller.api.DrumPad; +import com.bitwig.extension.controller.api.DrumPadBank; +import com.bitwig.extension.controller.api.NoteInput; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extension.controller.api.PlayingNote; +import com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup; +import com.bitwig.extensions.controllers.novation.commonsmk3.DrumButton; +import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; +import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; +import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; +import com.bitwig.extensions.controllers.novation.commonsmk3.SpecialDevices; +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.TrackState; -import com.bitwig.extensions.controllers.novation.launchpadmini3.ViewCursorControl; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.Inject; import com.bitwig.extensions.framework.di.PostConstruct; -import java.util.Arrays; -import java.util.List; - public class DrumLayer extends Layer { - + private DrumPadBank drumPadBank; private NoteInput noteInput; private final int[] padColors = new int[64]; @@ -25,20 +38,20 @@ public class DrumLayer extends Layer { private ViewCursorControl viewCursorControl; @Inject protected TrackState trackState; - + private int padsNoteOffset; - + public DrumLayer(final Layers layers) { super(layers, "DRUM_PAD_LAYER"); } - + @PostConstruct - public void init(final ControllerHost host, final MidiProcessor midiProcessor, final HwElements hwElements) { + public void init(final ControllerHost host, final MidiProcessor midiProcessor, final LpMiniHwElements hwElements) { noteInput = midiProcessor.getMidiIn().createNoteInput("MIDI", "88????", "98????"); noteInput.setShouldConsumeEvents(false); final CursorTrack cursorTrack = viewCursorControl.getCursorTrack(); cursorTrack.playingNotes().addValueObserver(this::handleNotes); - + final PinnableCursorDevice primaryDevice = viewCursorControl.getPrimaryDevice(); final DeviceBank drumBank = cursorTrack.createDeviceBank(1); final DeviceMatcher drumMatcher = host.createBitwigDeviceMatcher(SpecialDevices.DRUM.getUuid()); @@ -50,7 +63,7 @@ public void init(final ControllerHost host, final MidiProcessor midiProcessor, f applyNotes(padsNoteOffset); } }); - + final List drumButtons = hwElements.getDrumGridButtons(); for (int i = 0; i < drumButtons.size(); i++) { final int index = i; @@ -62,33 +75,33 @@ public void init(final ControllerHost host, final MidiProcessor midiProcessor, f } initNavigation(hwElements); } - - private void initNavigation(final HwElements hwElements) { + + private void initNavigation(final LpMiniHwElements hwElements) { final LabeledButton upButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.UP); final LabeledButton downButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.DOWN); final LabeledButton leftButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.LEFT); final LabeledButton rightButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.RIGHT); final RgbState pressedColor = RgbState.of(21); final RgbState baseColor = RgbState.of(1); - + leftButton.bindRepeatHold(this, () -> handleNavigateVertical(-4)); leftButton.bindHighlightButton(this, () -> (padsNoteOffset - 4) >= 4, baseColor, pressedColor); rightButton.bindRepeatHold(this, () -> handleNavigateVertical(4)); rightButton.bindHighlightButton(this, () -> (padsNoteOffset + 4 + 64) < 128, baseColor, pressedColor); - + downButton.bindRepeatHold(this, () -> handleNavigateVertical(-16)); downButton.bindHighlightButton(this, () -> (padsNoteOffset - 16) >= 4, baseColor, pressedColor); upButton.bindRepeatHold(this, () -> handleNavigateVertical(16)); upButton.bindHighlightButton(this, () -> (padsNoteOffset + 16 + 64) < 128, baseColor, pressedColor); } - + private void handleNavigateVertical(final int direction) { final int newPosition = padsNoteOffset + direction; if (newPosition >= 4 && newPosition + 64 < 128) { drumPadBank.scrollBy(direction); } } - + private RgbState getPadState(final int index, final DrumPad pad) { final boolean playing = isPlaying(index); if (pad.exists().get()) { @@ -102,7 +115,7 @@ private RgbState getPadState(final int index, final DrumPad pad) { } return playing ? RgbState.DIM_WHITE : RgbState.OFF; } - + public boolean isPlaying(final int index) { final int offset = padsNoteOffset + index; if (offset < 128) { @@ -110,7 +123,7 @@ public boolean isPlaying(final int index) { } return false; } - + public void applyNotes(final int noteOffset) { Arrays.fill(noteTable, -1); for (int note = 0; note < 64; note++) { @@ -119,7 +132,7 @@ public void applyNotes(final int noteOffset) { } noteInput.setKeyTranslationTable(noteTable); } - + private void handleNotes(final PlayingNote[] playingNotes) { if (!isActive()) { return; @@ -129,5 +142,5 @@ private void handleNotes(final PlayingNote[] playingNotes) { isPlaying[playingNote.pitch()] = true; } } - + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/NotePlayingLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/NotePlayingLayer.java index daee8d98..467993b6 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/NotePlayingLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/NotePlayingLayer.java @@ -1,35 +1,35 @@ package com.bitwig.extensions.controllers.novation.launchpadmini3.layers; +import java.util.ArrayList; +import java.util.List; + import com.bitwig.extension.controller.api.CursorTrack; import com.bitwig.extension.controller.api.PlayingNote; import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; -import com.bitwig.extensions.controllers.novation.launchpadmini3.ViewCursorControl; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.Inject; import com.bitwig.extensions.framework.di.PostConstruct; -import java.util.ArrayList; -import java.util.List; - public class NotePlayingLayer extends Layer { @Inject private ViewCursorControl viewCursorControl; @Inject private MidiProcessor midiProcessor; - + private final List lastNotes = new ArrayList<>(); - + public NotePlayingLayer(final Layers layers) { super(layers, "NOTE_PLAYING_LAYER"); } - + @PostConstruct public void init() { final CursorTrack cursorTrack = viewCursorControl.getCursorTrack(); cursorTrack.playingNotes().addValueObserver(this::handleNotes); } - + private void handleNotes(final PlayingNote[] playingNotes) { for (final Integer playing : lastNotes) { midiProcessor.sendMidi(0x8f, playing, 21); diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/PanSliderLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/PanSliderLayer.java index 9067a0cf..45a1ac7c 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/PanSliderLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/PanSliderLayer.java @@ -5,18 +5,18 @@ import com.bitwig.extension.controller.api.TrackBank; import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; import com.bitwig.extensions.controllers.novation.commonsmk3.SliderBinding; -import com.bitwig.extensions.controllers.novation.launchpadmini3.HwElements; -import com.bitwig.extensions.controllers.novation.launchpadmini3.ViewCursorControl; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; +import com.bitwig.extensions.controllers.novation.launchpadmini3.LpMiniHwElements; import com.bitwig.extensions.framework.Layers; public class PanSliderLayer extends TrackSliderLayer { - + public PanSliderLayer(final ViewCursorControl viewCursorControl, final HardwareSurface controlSurface, - final Layers layers, final MidiProcessor midiProcessor, final HwElements hwElements) { + final Layers layers, final MidiProcessor midiProcessor, final LpMiniHwElements hwElements) { super("PAN", controlSurface, layers, midiProcessor, 20, -1); bind(viewCursorControl.getTrackBank()); } - + @Override protected void bind(final TrackBank trackBank) { for (int i = 0; i < 8; i++) { @@ -26,12 +26,12 @@ protected void bind(final TrackBank trackBank) { valueBindings.add(binding); } } - + @Override protected void refreshTrackColors() { final boolean[] exists = trackState.getExists(); final int[] colorIndex = trackState.getColorIndex(); - + for (int i = 0; i < 8; i++) { if (exists[i]) { tracksExistsColors[i] = colorIndex[i]; @@ -40,7 +40,7 @@ protected void refreshTrackColors() { } } } - + @Override protected void updateFaderState() { if (isActive()) { @@ -49,18 +49,18 @@ protected void updateFaderState() { valueBindings.forEach(SliderBinding::update); } } - + @Override protected void onActivate() { super.onActivate(); refreshTrackColors(); updateFaderState(); } - + @Override protected void onDeactivate() { super.onDeactivate(); updateFaderState(); } - + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/SendsSliderLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/SendsSliderLayer.java index a090b88c..beddf164 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/SendsSliderLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/SendsSliderLayer.java @@ -1,5 +1,9 @@ package com.bitwig.extensions.controllers.novation.launchpadmini3.layers; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + import com.bitwig.extension.controller.api.HardwareSurface; import com.bitwig.extension.controller.api.Send; import com.bitwig.extension.controller.api.Track; @@ -7,105 +11,99 @@ import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; import com.bitwig.extensions.controllers.novation.commonsmk3.PanelLayout; import com.bitwig.extensions.controllers.novation.commonsmk3.SliderBinding; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; import com.bitwig.extensions.controllers.novation.launchpadmini3.LaunchPadPreferences; -import com.bitwig.extensions.controllers.novation.launchpadmini3.ViewCursorControl; import com.bitwig.extensions.framework.Layers; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - public class SendsSliderLayer extends TrackSliderLayer { - - private TrackBank trackBank; - private int sendItems = 0; - private final List> controlModeRemovedListeners = new ArrayList<>(); - - public SendsSliderLayer(final ViewCursorControl viewCursorControl, final HardwareSurface controlSurface, - final Layers layers, final MidiProcessor midiProcessor, - final LaunchPadPreferences preferences) { - super("SENDS", controlSurface, layers, midiProcessor, 30, 13); - bind(viewCursorControl.getTrackBank()); - preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> { - layout = newValue; - updateFaderState(); - })); - } - - @Override - public boolean canBeEntered(final ControlMode mode) { - if (mode == ControlMode.SENDS_A && sendItems > 0) { - return true; - } - return mode == ControlMode.SENDS_B && sendItems > 1; - } - - public void addControlModeRemoveListener(final Consumer listeners) { - controlModeRemovedListeners.add(listeners); - } - - @Override - protected void bind(final TrackBank trackBank) { - this.trackBank = trackBank; - for (int i = 0; i < 8; i++) { - final Track track = trackBank.getItemAt(i); - if (i == 0) { - track.sendBank().itemCount().addValueObserver(this::updateSendItemsAvailable); - } - final Send sends = track.sendBank().getItemAt(0); - final SliderBinding binding = new SliderBinding(baseCcNr, sends, sliders[i], i, midiProcessor); - addBinding(binding); - valueBindings.add(binding); - } - } - - private void updateSendItemsAvailable(final int items) { - if (items == 1 && sendItems > 1) { - controlModeRemovedListeners.forEach(l -> l.accept(ControlMode.SENDS_B)); - } else if (items == 0 && sendItems > 0) { - controlModeRemovedListeners.forEach(l -> l.accept(ControlMode.SENDS_A)); - } - sendItems = items; - } - - public void setControl(final ControlMode mode) { - for (int i = 0; i < 8; i++) { - final Track track = trackBank.getItemAt(i); - track.sendBank().scrollPosition().set(mode == ControlMode.SENDS_A ? 0 : 1); - } - } - - - @Override - protected void updateFaderState() { - if (isActive()) { - refreshTrackColors(); - midiProcessor.setFaderBank(layout == PanelLayout.VERTICAL ? 0 : 1, tracksExistsColors, true, baseCcNr); - valueBindings.forEach(SliderBinding::update); - } - } - - - @Override - protected void refreshTrackColors() { - final boolean[] exists = trackState.getExists(); - for (int i = 0; i < 8; i++) { - tracksExistsColors[i] = exists[i] ? baseColor : 0; - } - } - - @Override - protected void onDeactivate() { - super.onDeactivate(); - updateFaderState(); - } - - @Override - protected void onActivate() { - super.onActivate(); - refreshTrackColors(); - updateFaderState(); - } + + private TrackBank trackBank; + private int sendItems = 0; + private final List> controlModeRemovedListeners = new ArrayList<>(); + + public SendsSliderLayer(final ViewCursorControl viewCursorControl, final HardwareSurface controlSurface, + final Layers layers, final MidiProcessor midiProcessor, final LaunchPadPreferences preferences) { + super("SENDS", controlSurface, layers, midiProcessor, 30, 13); + bind(viewCursorControl.getTrackBank()); + preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> { + layout = newValue; + updateFaderState(); + })); + } + + @Override + public boolean canBeEntered(final ControlMode mode) { + if (mode == ControlMode.SENDS_A && sendItems > 0) { + return true; + } + return mode == ControlMode.SENDS_B && sendItems > 1; + } + + public void addControlModeRemoveListener(final Consumer listeners) { + controlModeRemovedListeners.add(listeners); + } + + @Override + protected void bind(final TrackBank trackBank) { + this.trackBank = trackBank; + for (int i = 0; i < 8; i++) { + final Track track = trackBank.getItemAt(i); + if (i == 0) { + track.sendBank().itemCount().addValueObserver(this::updateSendItemsAvailable); + } + final Send sends = track.sendBank().getItemAt(0); + final SliderBinding binding = new SliderBinding(baseCcNr, sends, sliders[i], i, midiProcessor); + addBinding(binding); + valueBindings.add(binding); + } + } + + private void updateSendItemsAvailable(final int items) { + if (items == 1 && sendItems > 1) { + controlModeRemovedListeners.forEach(l -> l.accept(ControlMode.SENDS_B)); + } else if (items == 0 && sendItems > 0) { + controlModeRemovedListeners.forEach(l -> l.accept(ControlMode.SENDS_A)); + } + sendItems = items; + } + + public void setControl(final ControlMode mode) { + for (int i = 0; i < 8; i++) { + final Track track = trackBank.getItemAt(i); + track.sendBank().scrollPosition().set(mode == ControlMode.SENDS_A ? 0 : 1); + } + updateFaderState(); + } + + @Override + protected void updateFaderState() { + if (isActive()) { + refreshTrackColors(); + midiProcessor.setFaderBank(layout == PanelLayout.VERTICAL ? 0 : 1, tracksExistsColors, true, baseCcNr); + valueBindings.forEach(SliderBinding::update); + } + } + + @Override + protected void refreshTrackColors() { + final boolean[] exists = trackState.getExists(); + for (int i = 0; i < 8; i++) { + tracksExistsColors[i] = exists[i] ? baseColor : 0; + } + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + updateFaderState(); + } + + @Override + protected void onActivate() { + super.onActivate(); + refreshTrackColors(); + updateFaderState(); + } } 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 b17c4c07..b13ccfc1 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 @@ -1,8 +1,32 @@ package com.bitwig.extensions.controllers.novation.launchpadmini3.layers; -import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.novation.commonsmk3.*; -import com.bitwig.extensions.controllers.novation.launchpadmini3.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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.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.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.GridButton; +import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; +import com.bitwig.extensions.controllers.novation.commonsmk3.LaunchPadButton; +import com.bitwig.extensions.controllers.novation.commonsmk3.LaunchpadDeviceConfig; +import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; +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.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; @@ -10,642 +34,637 @@ import com.bitwig.extensions.framework.di.Inject; import com.bitwig.extensions.framework.di.PostConstruct; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - public class SessionLayer extends AbstractLpSessionLayer { - - public static final int MOMENTARY_TIME = 500; - private static final int MODE_INACTIVE_COLOR = 1; - - private final int[] sceneColorIndex = new int[8]; - private final int[] sceneColorHorizontal = new int[8]; - private final int[] sceneColorHorizontalInactive = new int[8]; - private final boolean[] selectionField = new boolean[8]; - - private final TrackControlLayer verticalTrackControlLayer; - private final TrackControlLayer horizontalTrackControlLayer; - private final SceneControl sceneControlVertical; - private final SceneControl sceneControlHorizontal; - private TrackControlLayer currentTrackControlLayer; - private SceneControl currentSceneControl; - - private final Map controlSliderLayers = new HashMap<>(); - private int sceneOffset; - - private LpMode lpMode = LpMode.SESSION; - private ControlMode controlMode = ControlMode.NONE; - private TrackMode trackMode = TrackMode.NONE; - private PanelLayout panelLayout = PanelLayout.VERTICAL; - - private ControlMode stashedControlMode = ControlMode.NONE; - private TrackMode stashedTrackMode = TrackMode.NONE; - - private final Layer sceneTrackControlLayer; - private final Layer sceneControlHorizontalLayer; - - private final Layer verticalLayer; - private final Layer horizontalLayer; - - private SendsSliderLayer sendsSliderLayer; - - @Inject - private ViewCursorControl viewCursorControl; - @Inject - private MidiProcessor midiProcessor; - @Inject - private LaunchpadDeviceConfig config; - @Inject - private Transport transport; - - private LabeledButton modeButton = null; - - private boolean shiftHeld = false; - - private final List miniModeSequenceXtra = List.of(TrackMode.NONE, TrackMode.STOP, TrackMode.SOLO, - TrackMode.MUTE, TrackMode.CONTROL); - - public SessionLayer(final Layers layers, final Transport transport, final ControllerHost host) { - super(layers); - verticalLayer = new Layer(layers, "VERTICAL_LAUNCHING"); - horizontalLayer = new Layer(layers, "HORIZONTAL_LAUNCHING"); - - sceneControlHorizontalLayer = new Layer(layers, "SCENE_CONTROL_DEFAULT"); - sceneTrackControlLayer = new Layer(layers, "SCENE_TRACK_CONTROL"); - - sceneControlVertical = new SceneControl(this, layers); - sceneControlHorizontal = new SceneControl(this, layers); - - verticalTrackControlLayer = new TrackControlLayer(layers, this, transport, host, PanelLayout.VERTICAL); - horizontalTrackControlLayer = new TrackControlLayer(layers, this, transport, host, PanelLayout.HORIZONTAL); - currentTrackControlLayer = verticalTrackControlLayer; - currentSceneControl = sceneControlVertical; - } - - @PostConstruct - protected void init(final ControllerHost host, final Transport transport, final HwElements hwElements) { - clipLauncherOverdub = transport.isClipLauncherOverdubEnabled(); - clipLauncherOverdub.markInterested(); - - final Clip cursorClip = viewCursorControl.getCursorClip(); - cursorClip.getLoopLength().markInterested(); - - final TrackBank trackBank = viewCursorControl.getTrackBank(); - trackBank.setShouldShowClipLauncherFeedback(true); - - final SceneBank sceneBank = trackBank.sceneBank(); - final Scene targetScene = trackBank.sceneBank().getScene(0); - targetScene.clipCount().markInterested(); - - initClipControl(hwElements, trackBank); - initNavigation(hwElements, trackBank, sceneBank); - - trackBank.setShouldShowClipLauncherFeedback(true); - - int n = config.isMiniVersion() ? 7 : 8; - for (int i = 0; i < n; i++) { - final int index = i; - LabeledButton button = hwElements.getSceneLaunchButtons().get(i); - Track track = trackBank.getItemAt(i); - track.addIsSelectedInMixerObserver(selectedInMixer -> selectionField[index] = selectedInMixer); - button.bindPressed(sceneControlHorizontalLayer, pressed -> handleTrackSelect(pressed, track)); - button.bindLight(sceneControlHorizontalLayer, () -> getTrackColorSelect(index, track)); - } - - if (config.isMiniVersion()) { - initTrackControlSceneButtons(hwElements, sceneTrackControlLayer); - } else { - initTrackControlXSceneButtons(hwElements, sceneTrackControlLayer); - } - - initSceneControl(hwElements, sceneBank); - } - - private void handleTrackSelect(boolean pressed, Track track) { - if (!pressed) { - return; - } - track.selectInMixer(); - } - - private RgbState getTrackColorSelect(final int index, final Track track) { - if (track.exists().get()) { - if (selectionField[index]) { - return RgbState.WHITE; - } - return RgbState.DIM_WHITE; - } - return RgbState.OFF; - } - - @Activate - public void activation() { - setIsActive(true); - if (modeButton != null) { - modeButton.refresh(); - } - } - - @Override - public void setLayout(final PanelLayout layout) { - if (layout == panelLayout) { - return; - } - panelLayout = layout; - - horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); - verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); - - currentTrackControlLayer.reset(); - currentTrackControlLayer.deactivateLayer(); - currentTrackControlLayer = panelLayout == PanelLayout.VERTICAL ? verticalTrackControlLayer : horizontalTrackControlLayer; - currentTrackControlLayer.applyMode(trackMode); - currentTrackControlLayer.activateControlLayer(true); - - currentSceneControl.setActive(false); - applyPanelModeToSceneControl(); - if (lpMode == LpMode.MIXER) { - currentTrackControlLayer.applyMode(trackMode); - currentTrackControlLayer.activateControlLayer(lpMode == LpMode.MIXER); - } - } - - private void applyPanelModeToSceneControl() { - if (panelLayout == PanelLayout.VERTICAL) { - currentSceneControl = sceneControlVertical; - if (lpMode == LpMode.MIXER) { - sceneTrackControlLayer.setIsActive(true); - sceneControlHorizontalLayer.setIsActive(false); - currentSceneControl.setActive(false); - } else { - sceneTrackControlLayer.setIsActive(false); - sceneControlHorizontalLayer.setIsActive(false); - currentSceneControl.setActive(true); - } - } else { - currentSceneControl = sceneControlHorizontal; - if (lpMode == LpMode.MIXER) { - sceneTrackControlLayer.setIsActive(true); - sceneControlHorizontalLayer.setIsActive(false); - currentSceneControl.setActive(true); - } else { - sceneTrackControlLayer.setIsActive(false); - sceneControlHorizontalLayer.setIsActive(true); - currentSceneControl.setActive(true); - } - } - } - - public void setShiftHeld(boolean value) { - this.shiftHeld = value; - } - - public boolean isShiftHeld() { - return shiftHeld; - } - - public void registerControlLayer(final ControlMode controlMode, final AbstractSliderLayer sliderLayer) { - controlSliderLayers.put(controlMode, sliderLayer); - if (controlMode == ControlMode.SENDS && sliderLayer instanceof SendsSliderLayer) { - sendsSliderLayer = (SendsSliderLayer) sliderLayer; - sendsSliderLayer.addControlModeRemoveListener(this::sendRemoved); - } - } - - private void initNavigation(final HwElements hwElements, final TrackBank trackBank, final SceneBank sceneBank) { - final LabeledButton upButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.UP); - final LabeledButton downButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.DOWN); - final LabeledButton leftButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.LEFT); - final LabeledButton rightButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.RIGHT); - sceneBank.canScrollForwards().markInterested(); - sceneBank.canScrollBackwards().markInterested(); - trackBank.canScrollForwards().markInterested(); - trackBank.canScrollBackwards().markInterested(); - final RgbState baseColor = RgbState.of(1); - final RgbState pressedColor = RgbState.of(3); - - downButton.bindRepeatHold(verticalLayer, () -> sceneBank.scrollBy(1)); - downButton.bindHighlightButton(verticalLayer, sceneBank.canScrollForwards(), baseColor, pressedColor); - - upButton.bindRepeatHold(verticalLayer, () -> sceneBank.scrollBy(-1)); - upButton.bindHighlightButton(verticalLayer, sceneBank.canScrollBackwards(), baseColor, pressedColor); - - leftButton.bindRepeatHold(verticalLayer, () -> trackBank.scrollBy(-1)); - leftButton.bindHighlightButton(verticalLayer, trackBank.canScrollBackwards(), baseColor, pressedColor); - - rightButton.bindRepeatHold(verticalLayer, () -> trackBank.scrollBy(1)); - rightButton.bindHighlightButton(verticalLayer, trackBank.canScrollForwards(), baseColor, pressedColor); - - - downButton.bindRepeatHold(horizontalLayer, () -> trackBank.scrollBy(1)); - downButton.bindHighlightButton(horizontalLayer, trackBank.canScrollForwards(), baseColor, pressedColor); - - upButton.bindRepeatHold(horizontalLayer, () -> trackBank.scrollBy(-1)); - upButton.bindHighlightButton(horizontalLayer, trackBank.canScrollBackwards(), baseColor, pressedColor); - - leftButton.bindRepeatHold(horizontalLayer, () -> sceneBank.scrollBy(-1)); - leftButton.bindHighlightButton(horizontalLayer, sceneBank.canScrollBackwards(), baseColor, pressedColor); - - rightButton.bindRepeatHold(horizontalLayer, () -> sceneBank.scrollBy(1)); - rightButton.bindHighlightButton(horizontalLayer, sceneBank.canScrollForwards(), baseColor, pressedColor); - - } - - private void initClipControl(final HwElements hwElements, final TrackBank trackBank) { - for (int i = 0; i < 8; i++) { - final int trackIndex = i; - final Track track = trackBank.getItemAt(trackIndex); - markTrack(track); - for (int j = 0; j < 8; j++) { - final int sceneIndex = j; - final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); - prepareSlot(slot, sceneIndex, trackIndex); - final GridButton button = hwElements.getGridButton(sceneIndex, trackIndex); - button.bindPressed(verticalLayer, pressed -> handleSlot(pressed, slot)); - button.bindLight(verticalLayer, () -> getState(track, slot, trackIndex, sceneIndex)); - final GridButton buttonHorizontal = hwElements.getGridButton(trackIndex, sceneIndex); - buttonHorizontal.bindPressed(horizontalLayer, pressed -> handleSlot(pressed, slot)); - buttonHorizontal.bindLight(horizontalLayer, () -> getState(track, slot, trackIndex, sceneIndex)); - } - } - - verticalTrackControlLayer.initClipControl(hwElements, trackBank); - verticalTrackControlLayer.initControlLayer(hwElements, viewCursorControl); - horizontalTrackControlLayer.initClipControl(hwElements, trackBank); - horizontalTrackControlLayer.initControlLayer(hwElements, viewCursorControl); - } - - private void initSceneControl(final HwElements hwElements, final SceneBank sceneBank) { - sceneBank.setIndication(true); - sceneBank.scrollPosition().addValueObserver(value -> sceneOffset = value); - for (int i = 0; i < 8; i++) { - final int index = i; - final Scene scene = sceneBank.getScene(index); - scene.color().addValueObserver((r, g, b) -> { - sceneColorIndex[index] = ColorLookup.toColor(r, g, b); - sceneColorHorizontal[index] = adjustHorizontal(sceneColorIndex[index]); - sceneColorHorizontalInactive[index] = darkenHorizontal(sceneColorIndex[index]); - }); - } - - List sceneButtonsVertical = new ArrayList<>(); - final int n = config.isMiniVersion() ? 7 : 8; - for (int i = 0; i < n; i++) { - sceneButtonsVertical.add(hwElements.getSceneLaunchButtons().get(i)); - } - sceneControlVertical.initSceneControl(sceneBank, sceneButtonsVertical); - List sceneButtonsHorizontal = new ArrayList<>(); - for (int i = 0; i < 8; i++) { - sceneButtonsHorizontal.add(hwElements.getGridButton(7, i)); - } - sceneControlHorizontal.initSceneControl(sceneBank, sceneButtonsHorizontal); - - if (config.isMiniVersion()) { - modeButton = hwElements.getSceneLaunchButtons().get(7); - modeButton.bindPressed(sceneControlVertical.getLayer(), this::changeModeMini); - modeButton.bindLight(sceneControlVertical.getLayer(), () -> RgbState.of(trackMode.getColorIndex())); - - modeButton.bindPressed(sceneControlHorizontalLayer, this::changeModeMini); - modeButton.bindLight(sceneControlHorizontalLayer, () -> RgbState.of(trackMode.getColorIndex())); - } - } - - private static int darkenHorizontal(int colorIndex) { - if (colorIndex < 4) { - return 1; - } - return colorIndex + 2; - } - - public static int adjustHorizontal(int colorIndex) { - DebugMini.println(" COLOR %d", colorIndex); - if (colorIndex == 0) { - return 0; - } - if (colorIndex == 1) { - return 3; - } - return colorIndex; - } - - - private void initTrackControlXSceneButtons(final HwElements hwElements, final Layer layer) { - initVolumeControl(hwElements, layer, 0); - initPanControl(hwElements, layer, 1); - initSendsAControl(hwElements, layer, 2); - initSendsBControl(hwElements, layer, 3); - initStopControl(hwElements, layer, 4); - initMuteControl(hwElements, layer, 5); - initSoloControl(hwElements, layer, 6); - initArmControl(hwElements, layer, 7); - } - - private void initTrackControlSceneButtons(final HwElements hwElements, final Layer layer) { - initVolumeControl(hwElements, layer, 0); - initPanControl(hwElements, layer, 1); - initSendsAControl(hwElements, layer, 2); - initSendsBControl(hwElements, layer, 3); - initDeviceControl(hwElements, layer, 4); - initStopControl(hwElements, layer, 5); - initMuteControl(hwElements, layer, 6); - initSoloControl(hwElements, layer, 7); - } - - private void initVolumeControl(final HwElements hwElements, final Layer layer, final int index) { - final LabeledButton volumeButton = hwElements.getSceneLaunchButtons().get(index); - volumeButton.bindPressReleaseAfter(this, () -> intoControlMode(ControlMode.VOLUME), this::returnToPreviousMode, - MOMENTARY_TIME); - volumeButton.bindLight(layer, - () -> controlMode == ControlMode.VOLUME ? RgbState.of(9) : RgbState.of(MODE_INACTIVE_COLOR)); - } - - private void initPanControl(final HwElements hwElements, final Layer layer, final int index) { - final LabeledButton panButton = hwElements.getSceneLaunchButtons().get(index); - panButton.bindPressReleaseAfter(this, () -> intoControlMode(ControlMode.PAN), this::returnToPreviousMode, - MOMENTARY_TIME); - panButton.bindLight(layer, - () -> controlMode == ControlMode.PAN ? RgbState.of(9) : RgbState.of(MODE_INACTIVE_COLOR)); - } - - private void initSendsAControl(final HwElements hwElements, final Layer layer, final int index) { - final LabeledButton sendsAButton = hwElements.getSceneLaunchButtons().get(index); - sendsAButton.bindPressReleaseAfter(this, () -> intoControlMode(ControlMode.SENDS_A), this::returnToPreviousMode, - MOMENTARY_TIME); - sendsAButton.bindLight(layer, () -> getSendsState(ControlMode.SENDS_A)); - } - - private void initSendsBControl(final HwElements hwElements, final Layer layer, final int index) { - final LabeledButton sendsBButton = hwElements.getSceneLaunchButtons().get(index); - sendsBButton.bindPressReleaseAfter(this, () -> intoControlMode(ControlMode.SENDS_B), this::returnToPreviousMode, - MOMENTARY_TIME); - sendsBButton.bindLight(layer, () -> getSendsState(ControlMode.SENDS_B)); - } - - private void initDeviceControl(final HwElements hwElements, final Layer layer, final int index) { - final LabeledButton deviceButton = hwElements.getSceneLaunchButtons().get(index); - deviceButton.bindPressReleaseAfter(this, () -> intoControlMode(ControlMode.DEVICE), this::returnToPreviousMode, - MOMENTARY_TIME); - deviceButton.bindLight(layer, - () -> controlMode == ControlMode.DEVICE ? RgbState.of(33) : RgbState.of(MODE_INACTIVE_COLOR)); - } - - private void initStopControl(final HwElements hwElements, final Layer layer, final int index) { - final LabeledButton stopButton = hwElements.getSceneLaunchButtons().get(index); - stopButton.bindPressReleaseAfter(this, () -> intoTrackMode(TrackMode.STOP), this::returnToPreviousMode, - MOMENTARY_TIME); - stopButton.bindLight(layer, - () -> trackMode == TrackMode.STOP ? RgbState.of(5) : RgbState.of(MODE_INACTIVE_COLOR)); - } - - private void initMuteControl(final HwElements hwElements, final Layer layer, final int index) { - final LabeledButton muteButton = hwElements.getSceneLaunchButtons().get(index); - muteButton.bindPressReleaseAfter(this, () -> intoTrackMode(TrackMode.MUTE), this::returnToPreviousMode, - MOMENTARY_TIME); - muteButton.bindLight(layer, - () -> trackMode == TrackMode.MUTE ? RgbState.of(9) : RgbState.of(MODE_INACTIVE_COLOR)); - } - - private void initSoloControl(final HwElements hwElements, final Layer layer, final int index) { - final LabeledButton soloButton = hwElements.getSceneLaunchButtons().get(index); - soloButton.bindPressReleaseAfter(this, () -> intoTrackMode(TrackMode.SOLO), this::returnToPreviousMode, - MOMENTARY_TIME); - soloButton.bindLight(layer, - () -> trackMode == TrackMode.SOLO ? RgbState.of(13) : RgbState.of(MODE_INACTIVE_COLOR)); - } - - private void initArmControl(final HwElements hwElements, final Layer layer, final int index) { - final LabeledButton soloButton = hwElements.getSceneLaunchButtons().get(index); - soloButton.bindPressReleaseAfter(this, () -> intoTrackMode(TrackMode.ARM), this::returnToPreviousMode, - MOMENTARY_TIME); - soloButton.bindLight(layer, () -> trackMode == TrackMode.ARM ? RgbState.of(5) : RgbState.of(MODE_INACTIVE_COLOR)); - } - - private void sendRemoved(final ControlMode modeRemoved) { - if (controlMode == modeRemoved) { - final AbstractSliderLayer currentMode = controlSliderLayers.get(controlMode.getRefMode()); - if (currentMode != null) { + + public static final int MOMENTARY_TIME = 500; + private static final int MODE_INACTIVE_COLOR = 1; + + private final int[] sceneColorIndex = new int[8]; + private final int[] sceneColorHorizontal = new int[8]; + private final int[] sceneColorHorizontalInactive = new int[8]; + private final boolean[] selectionField = new boolean[8]; + + private final TrackControlLayer verticalTrackControlLayer; + private final TrackControlLayer horizontalTrackControlLayer; + private final SceneControl sceneControlVertical; + private final SceneControl sceneControlHorizontal; + private TrackControlLayer currentTrackControlLayer; + private SceneControl currentSceneControl; + + private final Map controlSliderLayers = new HashMap<>(); + private int sceneOffset; + + private LpMode lpMode = LpMode.SESSION; + private ControlMode controlMode = ControlMode.NONE; + private TrackMode trackMode = TrackMode.NONE; + private PanelLayout panelLayout = PanelLayout.VERTICAL; + + private ControlMode stashedControlMode = ControlMode.NONE; + private TrackMode stashedTrackMode = TrackMode.NONE; + + private final Layer sceneTrackControlLayer; + private final Layer sceneControlHorizontalLayer; + + private final Layer verticalLayer; + private final Layer horizontalLayer; + + private SendsSliderLayer sendsSliderLayer; + + @Inject + private ViewCursorControl viewCursorControl; + @Inject + private MidiProcessor midiProcessor; + @Inject + private LaunchpadDeviceConfig config; + @Inject + private Transport transport; + + private LabeledButton modeButton = null; + + private boolean shiftHeld = false; + + private final List miniModeSequenceXtra = + List.of(TrackMode.NONE, TrackMode.STOP, TrackMode.SOLO, TrackMode.MUTE, TrackMode.CONTROL); + + public SessionLayer(final Layers layers, final Transport transport, final ControllerHost host) { + super(layers); + verticalLayer = new Layer(layers, "VERTICAL_LAUNCHING"); + horizontalLayer = new Layer(layers, "HORIZONTAL_LAUNCHING"); + + sceneControlHorizontalLayer = new Layer(layers, "SCENE_CONTROL_DEFAULT"); + sceneTrackControlLayer = new Layer(layers, "SCENE_TRACK_CONTROL"); + + sceneControlVertical = new SceneControl(this, layers); + sceneControlHorizontal = new SceneControl(this, layers); + + verticalTrackControlLayer = new TrackControlLayer(layers, this, transport, host, PanelLayout.VERTICAL); + horizontalTrackControlLayer = new TrackControlLayer(layers, this, transport, host, PanelLayout.HORIZONTAL); + currentTrackControlLayer = verticalTrackControlLayer; + currentSceneControl = sceneControlVertical; + } + + @PostConstruct + protected void init(final ControllerHost host, final Transport transport, final LpMiniHwElements hwElements) { + clipLauncherOverdub = transport.isClipLauncherOverdubEnabled(); + clipLauncherOverdub.markInterested(); + + final Clip cursorClip = viewCursorControl.getCursorClip(); + cursorClip.getLoopLength().markInterested(); + + final TrackBank trackBank = viewCursorControl.getTrackBank(); + trackBank.setShouldShowClipLauncherFeedback(true); + + final SceneBank sceneBank = trackBank.sceneBank(); + final Scene targetScene = trackBank.sceneBank().getScene(0); + targetScene.clipCount().markInterested(); + + initClipControl(hwElements, trackBank); + initNavigation(hwElements, trackBank, sceneBank); + + final int n = config.isMiniVersion() ? 7 : 8; + for (int i = 0; i < n; i++) { + final int index = i; + final LabeledButton button = hwElements.getSceneLaunchButtons().get(i); + final Track track = trackBank.getItemAt(i); + track.addIsSelectedInMixerObserver(selectedInMixer -> selectionField[index] = selectedInMixer); + button.bindPressed(sceneControlHorizontalLayer, pressed -> handleTrackSelect(pressed, track)); + button.bindLight(sceneControlHorizontalLayer, () -> getTrackColorSelect(index, track)); + } + + if (config.isMiniVersion()) { + initTrackControlSceneButtons(hwElements, sceneTrackControlLayer); + } else { + initTrackControlXSceneButtons(hwElements, sceneTrackControlLayer); + } + + initSceneControl(hwElements, sceneBank); + } + + private void handleTrackSelect(final boolean pressed, final Track track) { + if (!pressed) { + return; + } + track.selectInMixer(); + } + + private RgbState getTrackColorSelect(final int index, final Track track) { + if (track.exists().get()) { + if (selectionField[index]) { + return RgbState.WHITE; + } + return RgbState.DIM_WHITE; + } + return RgbState.OFF; + } + + @Activate + public void activation() { + setIsActive(true); + if (modeButton != null) { + modeButton.refresh(); + } + } + + @Override + public void setLayout(final PanelLayout layout) { + if (layout == panelLayout) { + return; + } + panelLayout = layout; + + horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); + verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); + + currentTrackControlLayer.reset(); + currentTrackControlLayer.deactivateLayer(); + currentTrackControlLayer = + panelLayout == PanelLayout.VERTICAL ? verticalTrackControlLayer : horizontalTrackControlLayer; + currentTrackControlLayer.applyMode(trackMode); + currentTrackControlLayer.activateControlLayer(true); + + currentSceneControl.setActive(false); + applyPanelModeToSceneControl(); + if (lpMode == LpMode.MIXER) { + currentTrackControlLayer.applyMode(trackMode); + currentTrackControlLayer.activateControlLayer(lpMode == LpMode.MIXER); + } + } + + private void applyPanelModeToSceneControl() { + if (panelLayout == PanelLayout.VERTICAL) { + currentSceneControl = sceneControlVertical; + if (lpMode == LpMode.MIXER) { + sceneTrackControlLayer.setIsActive(true); + sceneControlHorizontalLayer.setIsActive(false); + currentSceneControl.setActive(false); + } else { + sceneTrackControlLayer.setIsActive(false); + sceneControlHorizontalLayer.setIsActive(false); + currentSceneControl.setActive(true); + } + } else { + currentSceneControl = sceneControlHorizontal; + if (lpMode == LpMode.MIXER) { + sceneTrackControlLayer.setIsActive(true); + sceneControlHorizontalLayer.setIsActive(false); + currentSceneControl.setActive(true); + } else { + sceneTrackControlLayer.setIsActive(false); + sceneControlHorizontalLayer.setIsActive(true); + currentSceneControl.setActive(true); + } + } + } + + public void setShiftHeld(final boolean value) { + this.shiftHeld = value; + } + + public boolean isShiftHeld() { + return shiftHeld; + } + + public void registerControlLayer(final ControlMode controlMode, final AbstractSliderLayer sliderLayer) { + controlSliderLayers.put(controlMode, sliderLayer); + if (controlMode == ControlMode.SENDS && sliderLayer instanceof SendsSliderLayer) { + sendsSliderLayer = (SendsSliderLayer) sliderLayer; + sendsSliderLayer.addControlModeRemoveListener(this::sendRemoved); + } + } + + private void initNavigation(final LpMiniHwElements hwElements, final TrackBank trackBank, + final SceneBank sceneBank) { + final LabeledButton upButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.UP); + final LabeledButton downButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.DOWN); + final LabeledButton leftButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.LEFT); + final LabeledButton rightButton = hwElements.getLabeledButton(LabelCcAssignmentsMini.RIGHT); + sceneBank.canScrollForwards().markInterested(); + sceneBank.canScrollBackwards().markInterested(); + trackBank.canScrollForwards().markInterested(); + trackBank.canScrollBackwards().markInterested(); + final RgbState baseColor = RgbState.of(1); + final RgbState pressedColor = RgbState.of(3); + + downButton.bindRepeatHold(verticalLayer, () -> sceneBank.scrollBy(1)); + downButton.bindHighlightButton(verticalLayer, sceneBank.canScrollForwards(), baseColor, pressedColor); + + upButton.bindRepeatHold(verticalLayer, () -> sceneBank.scrollBy(-1)); + upButton.bindHighlightButton(verticalLayer, sceneBank.canScrollBackwards(), baseColor, pressedColor); + + leftButton.bindRepeatHold(verticalLayer, () -> trackBank.scrollBy(-1)); + leftButton.bindHighlightButton(verticalLayer, trackBank.canScrollBackwards(), baseColor, pressedColor); + + rightButton.bindRepeatHold(verticalLayer, () -> trackBank.scrollBy(1)); + rightButton.bindHighlightButton(verticalLayer, trackBank.canScrollForwards(), baseColor, pressedColor); + + + downButton.bindRepeatHold(horizontalLayer, () -> trackBank.scrollBy(1)); + downButton.bindHighlightButton(horizontalLayer, trackBank.canScrollForwards(), baseColor, pressedColor); + + upButton.bindRepeatHold(horizontalLayer, () -> trackBank.scrollBy(-1)); + upButton.bindHighlightButton(horizontalLayer, trackBank.canScrollBackwards(), baseColor, pressedColor); + + leftButton.bindRepeatHold(horizontalLayer, () -> sceneBank.scrollBy(-1)); + leftButton.bindHighlightButton(horizontalLayer, sceneBank.canScrollBackwards(), baseColor, pressedColor); + + rightButton.bindRepeatHold(horizontalLayer, () -> sceneBank.scrollBy(1)); + rightButton.bindHighlightButton(horizontalLayer, sceneBank.canScrollForwards(), baseColor, pressedColor); + + } + + private void initClipControl(final LpMiniHwElements hwElements, final TrackBank trackBank) { + for (int i = 0; i < 8; i++) { + final int trackIndex = i; + final Track track = trackBank.getItemAt(trackIndex); + markTrack(track); + for (int j = 0; j < 8; j++) { + final int sceneIndex = j; + final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); + prepareSlot(slot, sceneIndex, trackIndex); + final GridButton button = hwElements.getGridButton(sceneIndex, trackIndex); + button.bindPressed(verticalLayer, pressed -> handleSlot(pressed, slot)); + button.bindLight(verticalLayer, () -> getState(track, slot, trackIndex, sceneIndex)); + final GridButton buttonHorizontal = hwElements.getGridButton(trackIndex, sceneIndex); + buttonHorizontal.bindPressed(horizontalLayer, pressed -> handleSlot(pressed, slot)); + buttonHorizontal.bindLight(horizontalLayer, () -> getState(track, slot, trackIndex, sceneIndex)); + } + } + + verticalTrackControlLayer.initClipControl(hwElements, trackBank); + verticalTrackControlLayer.initControlLayer(hwElements, viewCursorControl); + horizontalTrackControlLayer.initClipControl(hwElements, trackBank); + horizontalTrackControlLayer.initControlLayer(hwElements, viewCursorControl); + } + + private void initSceneControl(final LpMiniHwElements hwElements, final SceneBank sceneBank) { + sceneBank.setIndication(true); + sceneBank.scrollPosition().addValueObserver(value -> sceneOffset = value); + for (int i = 0; i < 8; i++) { + final int index = i; + final Scene scene = sceneBank.getScene(index); + scene.color().addValueObserver((r, g, b) -> { + sceneColorIndex[index] = ColorLookup.toColor(r, g, b); + sceneColorHorizontal[index] = adjustHorizontal(sceneColorIndex[index]); + sceneColorHorizontalInactive[index] = darkenHorizontal(sceneColorIndex[index]); + }); + } + + final List sceneButtonsVertical = new ArrayList<>(); + final int n = config.isMiniVersion() ? 7 : 8; + for (int i = 0; i < n; i++) { + sceneButtonsVertical.add(hwElements.getSceneLaunchButtons().get(i)); + } + sceneControlVertical.initSceneControl(sceneBank, sceneButtonsVertical); + final List sceneButtonsHorizontal = new ArrayList<>(); + for (int i = 0; i < 8; i++) { + sceneButtonsHorizontal.add(hwElements.getGridButton(7, i)); + } + sceneControlHorizontal.initSceneControl(sceneBank, sceneButtonsHorizontal); + + if (config.isMiniVersion()) { + modeButton = hwElements.getSceneLaunchButtons().get(7); + modeButton.bindPressed(sceneControlVertical.getLayer(), this::changeModeMini); + modeButton.bindLight(sceneControlVertical.getLayer(), () -> RgbState.of(trackMode.getColorIndex())); + + modeButton.bindPressed(sceneControlHorizontalLayer, this::changeModeMini); + modeButton.bindLight(sceneControlHorizontalLayer, () -> RgbState.of(trackMode.getColorIndex())); + } + } + + private static int darkenHorizontal(final int colorIndex) { + if (colorIndex < 4) { + return 1; + } + return colorIndex + 2; + } + + public static int adjustHorizontal(final int colorIndex) { + if (colorIndex == 0) { + return 0; + } + if (colorIndex == 1) { + return 3; + } + return colorIndex; + } + + private void initTrackControlXSceneButtons(final LpMiniHwElements hwElements, final Layer layer) { + initVolumeControl(hwElements, layer, 0); + initPanControl(hwElements, layer, 1); + initSendsAControl(hwElements, layer, 2); + initSendsBControl(hwElements, layer, 3); + initStopControl(hwElements, layer, 4); + initMuteControl(hwElements, layer, 5); + initSoloControl(hwElements, layer, 6); + initArmControl(hwElements, layer, 7); + } + + private void initTrackControlSceneButtons(final LpMiniHwElements hwElements, final Layer layer) { + initVolumeControl(hwElements, layer, 0); + initPanControl(hwElements, layer, 1); + initSendsAControl(hwElements, layer, 2); + initSendsBControl(hwElements, layer, 3); + initDeviceControl(hwElements, layer, 4); + initStopControl(hwElements, layer, 5); + initMuteControl(hwElements, layer, 6); + initSoloControl(hwElements, layer, 7); + } + + 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, + MOMENTARY_TIME); + 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, + MOMENTARY_TIME); + 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, + 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, + 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, + MOMENTARY_TIME); + 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, + MOMENTARY_TIME); + 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, + MOMENTARY_TIME); + 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, + MOMENTARY_TIME); + 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, + MOMENTARY_TIME); + soloButton.bindLight( + layer, () -> trackMode == TrackMode.ARM ? RgbState.of(5) : RgbState.of(MODE_INACTIVE_COLOR)); + } + + private void sendRemoved(final ControlMode modeRemoved) { + if (controlMode == modeRemoved) { + final AbstractSliderLayer currentMode = controlSliderLayers.get(controlMode.getRefMode()); + if (currentMode != null) { + currentMode.setIsActive(false); + } + controlMode = ControlMode.NONE; + } + } + + public RgbState getSendsState(final ControlMode mode) { + if (sendsSliderLayer.canBeEntered(mode)) { + return controlMode == mode ? RgbState.of(13) : RgbState.of(MODE_INACTIVE_COLOR); + } + return RgbState.OFF; + } + + private void intoTrackMode(final TrackMode mode) { + if (controlMode != ControlMode.NONE) { + switchToMode(ControlMode.NONE); + } + if (trackMode == mode) { + trackMode = TrackMode.NONE; + } else { + trackMode = mode; + verticalTrackControlLayer.resetCounts(); + } + currentTrackControlLayer.applyMode(trackMode); + } + + public void intoControlMode(final ControlMode mode) { + final AbstractSliderLayer currentMode = controlSliderLayers.get(controlMode.getRefMode()); + final AbstractSliderLayer modeLayer = controlSliderLayers.get(mode.getRefMode()); + if (modeLayer != null && !modeLayer.canBeEntered(mode)) { + return; + } + if (currentMode != null) { currentMode.setIsActive(false); - } - controlMode = ControlMode.NONE; - } - } - - public RgbState getSendsState(final ControlMode mode) { - if (sendsSliderLayer.canBeEntered(mode)) { - return controlMode == mode ? RgbState.of(13) : RgbState.of(MODE_INACTIVE_COLOR); - } - return RgbState.OFF; - } - - private void intoTrackMode(final TrackMode mode) { - if (controlMode != ControlMode.NONE) { - switchToMode(ControlMode.NONE); - } - if (trackMode == mode) { - trackMode = TrackMode.NONE; - } else { - trackMode = mode; - verticalTrackControlLayer.resetCounts(); - } - currentTrackControlLayer.applyMode(trackMode); - } - - public void intoControlMode(final ControlMode mode) { - final AbstractSliderLayer currentMode = controlSliderLayers.get(controlMode.getRefMode()); - final AbstractSliderLayer modeLayer = controlSliderLayers.get(mode.getRefMode()); - if (modeLayer != null && !modeLayer.canBeEntered(mode)) { - return; - } - if (currentMode != null) { - currentMode.setIsActive(false); - } - if (controlMode == mode) { - controlMode = ControlMode.NONE; - midiProcessor.toLayout(0x00); - } else { - controlMode = mode; - if (modeLayer != null) { + } + if (controlMode == mode) { + controlMode = ControlMode.NONE; + midiProcessor.toLayout(0x00); + } else { + controlMode = mode; + if (modeLayer != null) { + modeLayer.setIsActive(true); + if (modeLayer instanceof SendsSliderLayer sendsSliderLayer) { + sendsSliderLayer.setControl(mode); + } + } + } + trackMode = TrackMode.NONE; + currentTrackControlLayer.applyMode(trackMode); + } + + public void returnToPreviousMode(final boolean longPress) { + if (longPress) { + if (stashedControlMode != controlMode) { + switchToMode(stashedControlMode); + } + if (stashedTrackMode != trackMode) { + trackMode = stashedTrackMode; + currentTrackControlLayer.applyMode(trackMode); + } + } else { + stashedControlMode = controlMode; + stashedTrackMode = trackMode; + } + } + + private void switchToMode(final ControlMode newMode) { + final AbstractSliderLayer currentMode = controlSliderLayers.get(controlMode.getRefMode()); + final AbstractSliderLayer modeLayer = controlSliderLayers.get(newMode.getRefMode()); + if (modeLayer != null && !modeLayer.canBeEntered(newMode)) { + return; + } + if (currentMode != null) { + currentMode.setIsActive(false); + } + if (modeLayer instanceof final SendsSliderLayer sendingLayer) { + sendingLayer.setControl(newMode); + } + if (modeLayer != null) { modeLayer.setIsActive(true); - } - } - trackMode = TrackMode.NONE; - currentTrackControlLayer.applyMode(trackMode); - } - - public void returnToPreviousMode(final boolean longPress) { - if (longPress) { - if (stashedControlMode != controlMode) { - switchToMode(stashedControlMode); - } - if (stashedTrackMode != trackMode) { - trackMode = stashedTrackMode; + } + if (newMode == ControlMode.NONE && controlMode != ControlMode.NONE) { + midiProcessor.toLayout(0x00); + } + controlMode = newMode; + } + + public void setMode(final LpMode lpMode) { + this.lpMode = lpMode; + applyPanelModeToSceneControl(); + final AbstractSliderLayer currentSliderMode = controlSliderLayers.get(controlMode.getRefMode()); + if (currentSliderMode != null) { + currentSliderMode.setIsActive(lpMode != LpMode.SESSION); + } + currentTrackControlLayer.activateControlLayer(lpMode == LpMode.MIXER); + if (lpMode == LpMode.MIXER) { currentTrackControlLayer.applyMode(trackMode); - } - } else { - stashedControlMode = controlMode; - stashedTrackMode = trackMode; - } - } - - private void switchToMode(final ControlMode newMode) { - final AbstractSliderLayer currentMode = controlSliderLayers.get(controlMode.getRefMode()); - final AbstractSliderLayer modeLayer = controlSliderLayers.get(newMode.getRefMode()); - if (modeLayer != null && !modeLayer.canBeEntered(newMode)) { - return; - } - if (currentMode != null) { - currentMode.setIsActive(false); - } - if (modeLayer instanceof SendsSliderLayer) { - final SendsSliderLayer sendingLayer = (SendsSliderLayer) modeLayer; - sendingLayer.setControl(newMode); - } - if (modeLayer != null) { - modeLayer.setIsActive(true); - } - if (newMode == ControlMode.NONE && controlMode != ControlMode.NONE) { - midiProcessor.toLayout(0x00); - } - controlMode = newMode; - } - - public void setMode(final LpMode lpMode) { - this.lpMode = lpMode; - applyPanelModeToSceneControl(); - final AbstractSliderLayer currentSliderMode = controlSliderLayers.get(controlMode.getRefMode()); - if (currentSliderMode != null) { - currentSliderMode.setIsActive(lpMode != LpMode.SESSION); - } - currentTrackControlLayer.activateControlLayer(lpMode == LpMode.MIXER); - if (lpMode == LpMode.MIXER) { - currentTrackControlLayer.applyMode(trackMode); - } - } - - private void changeModeMini() { // change to sequence - trackMode = getNextMode(miniModeSequenceXtra); - currentTrackControlLayer.applyMode(trackMode); - } - - private TrackMode getNextMode(final List sequence) { - final int index = sequence.indexOf(trackMode); - if (index == -1) { - return sequence.get(0); - } - return sequence.get((index + 1) % sequence.size()); - } - - private void markTrack(final Track track) { - track.exists().markInterested(); - track.isStopped().markInterested(); - track.mute().markInterested(); - track.solo().markInterested(); - track.isQueuedForStop().markInterested(); - track.arm().markInterested(); - } - - private void prepareSlot(final ClipLauncherSlot slot, final int sceneIndex, final int trackIndex) { - slot.hasContent().markInterested(); - slot.isPlaying().markInterested(); - slot.isStopQueued().markInterested(); - slot.isRecordingQueued().markInterested(); - slot.isRecording().markInterested(); - slot.isPlaybackQueued().markInterested(); - slot.color().addValueObserver((r, g, b) -> colorIndex[sceneIndex][trackIndex] = ColorLookup.toColor(r, g, b)); - } - - private void handleSlot(final boolean pressed, final ClipLauncherSlot slot) { - if (pressed) { - if (shiftHeld) { - slot.launchAlt(); - } else { - slot.launch(); - } - } else { - if (shiftHeld) { - slot.launchReleaseAlt(); - } else { - slot.launchRelease(); - } - } - } - - void handleScene(final boolean pressed, final Scene scene, final int sceneIndex) { - if (pressed) { - viewCursorControl.focusScene(sceneIndex + sceneOffset); - if (shiftHeld) { - scene.launchAlt(); - } else { - scene.launch(); - } - } else { - if (shiftHeld) { - scene.launchReleaseAlt(); - } else { - scene.launchRelease(); - } - } - } - - RgbState getSceneColorVertical(final int sceneIndex, final Scene scene) { - if (scene.clipCount().get() > 0) { - if (sceneOffset + sceneIndex == viewCursorControl.getFocusSceneIndex() && viewCursorControl.hasQueuedForPlaying()) { - return RgbState.GREEN_FLASH; - } - return RgbState.of(sceneColorIndex[sceneIndex]); - } - return RgbState.OFF; - } - - RgbState getSceneColorHorizontal(final int sceneIndex, final Scene scene) { - if (scene.clipCount().get() > 0) { - if (sceneOffset + sceneIndex == viewCursorControl.getFocusSceneIndex() && viewCursorControl.hasQueuedForPlaying()) { - return RgbState.GREEN_FLASH; - } - return RgbState.of(sceneColorHorizontal[sceneIndex]); - } - return RgbState.of(sceneColorHorizontalInactive[sceneIndex]); - } - - public RgbState getRecordButtonColorRegular() { - final FocusSlot focusSlot = viewCursorControl.getFocusSlot(); - if (focusSlot != null) { - final ClipLauncherSlot slot = focusSlot.getSlot(); - if (slot.isRecordingQueued().get()) { - return RgbState.flash(5, 0); - } - if (slot.isRecording().get() || slot.isRecordingQueued().get()) { - return RgbState.pulse(5); - } - } - if (transport.isClipLauncherOverdubEnabled().get()) { - return RgbState.RED; - } else { - return RgbState.of(7); - } - } - - - @Override - protected void onActivate() { - super.onActivate(); - applyPanelModeToSceneControl(); - horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); - verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); - currentTrackControlLayer.activateControlLayer(lpMode == LpMode.MIXER); - } - - @Override - protected void onDeactivate() { - super.onDeactivate(); - currentSceneControl.setActive(false); - sceneTrackControlLayer.setIsActive(false); - horizontalLayer.setIsActive(false); - verticalLayer.setIsActive(false); - currentTrackControlLayer.deactivateLayer(); - } - - + } + } + + private void changeModeMini() { // change to sequence + trackMode = getNextMode(miniModeSequenceXtra); + currentTrackControlLayer.applyMode(trackMode); + } + + private TrackMode getNextMode(final List sequence) { + final int index = sequence.indexOf(trackMode); + if (index == -1) { + return sequence.get(0); + } + return sequence.get((index + 1) % sequence.size()); + } + + private void markTrack(final Track track) { + track.exists().markInterested(); + track.isStopped().markInterested(); + track.mute().markInterested(); + track.solo().markInterested(); + track.isQueuedForStop().markInterested(); + track.arm().markInterested(); + } + + private void prepareSlot(final ClipLauncherSlot slot, final int sceneIndex, final int trackIndex) { + slot.hasContent().markInterested(); + slot.isPlaying().markInterested(); + slot.isStopQueued().markInterested(); + slot.isRecordingQueued().markInterested(); + slot.isRecording().markInterested(); + slot.isPlaybackQueued().markInterested(); + slot.color().addValueObserver((r, g, b) -> colorIndex[sceneIndex][trackIndex] = ColorLookup.toColor(r, g, b)); + } + + private void handleSlot(final boolean pressed, final ClipLauncherSlot slot) { + if (pressed) { + if (shiftHeld) { + slot.launchAlt(); + } else { + slot.launch(); + } + } else { + if (shiftHeld) { + slot.launchReleaseAlt(); + } else { + slot.launchRelease(); + } + } + } + + void handleScene(final boolean pressed, final Scene scene, final int sceneIndex) { + if (pressed) { + if (shiftHeld) { + scene.launchAlt(); + } else { + scene.launch(); + } + } else { + if (shiftHeld) { + scene.launchReleaseAlt(); + } else { + scene.launchRelease(); + } + } + } + + RgbState getSceneColorVertical(final int sceneIndex, final Scene scene) { + if (scene.clipCount().get() > 0) { + if (viewCursorControl.hasQueuedForPlaying(sceneOffset + sceneIndex)) { + return RgbState.GREEN_FLASH; + } + return RgbState.of(sceneColorIndex[sceneIndex]); + } + return RgbState.OFF; + } + + RgbState getSceneColorHorizontal(final int sceneIndex, final Scene scene) { + if (scene.clipCount().get() > 0) { + if (viewCursorControl.hasQueuedForPlaying(sceneOffset + sceneIndex)) { + return RgbState.GREEN_FLASH; + } + return RgbState.of(sceneColorHorizontal[sceneIndex]); + } + return RgbState.of(sceneColorHorizontalInactive[sceneIndex]); + } + + public RgbState getRecordButtonColorRegular() { + final FocusSlot focusSlot = viewCursorControl.getFocusSlot(); + if (focusSlot != null) { + final ClipLauncherSlot slot = focusSlot.getSlot(); + if (slot.isRecordingQueued().get()) { + return RgbState.flash(5, 0); + } + if (slot.isRecording().get() || slot.isRecordingQueued().get()) { + return RgbState.pulse(5); + } + } + if (transport.isClipLauncherOverdubEnabled().get()) { + return RgbState.RED; + } else { + return RgbState.of(7); + } + } + + + @Override + protected void onActivate() { + super.onActivate(); + applyPanelModeToSceneControl(); + horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); + verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); + currentTrackControlLayer.activateControlLayer(lpMode == LpMode.MIXER); + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + currentTrackControlLayer.deactivateLayer(); + currentSceneControl.setActive(false); + sceneTrackControlLayer.setIsActive(false); + horizontalLayer.setIsActive(false); + verticalLayer.setIsActive(false); + } + + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/TrackControlLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/TrackControlLayer.java index 4061768e..172f7e42 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/TrackControlLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/TrackControlLayer.java @@ -1,223 +1,227 @@ package com.bitwig.extensions.controllers.novation.launchpadmini3.layers; -import com.bitwig.extension.controller.api.*; +import java.util.HashMap; +import java.util.Map; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.Project; +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.commonsmk3.GridButton; import com.bitwig.extensions.controllers.novation.commonsmk3.PanelLayout; import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; -import com.bitwig.extensions.controllers.novation.launchpadmini3.HwElements; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; +import com.bitwig.extensions.controllers.novation.launchpadmini3.LpMiniHwElements; import com.bitwig.extensions.controllers.novation.launchpadmini3.TrackMode; -import com.bitwig.extensions.controllers.novation.launchpadmini3.ViewCursorControl; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; -import java.util.HashMap; -import java.util.Map; - public class TrackControlLayer { - private final Layer muteLayer; - private final Layer soloLayer; - private final Layer armLayer; - private final Layer stopLayer; - private final Layer controlLayer; - private final Map trackModeLayerMap = new HashMap<>(); - private final PanelLayout layoutType; - private final Project project; - private final Transport transport; - private final SessionLayer sessionLayer; - private Layer currentControlGridLayer; - private int soloHeldCount = 0; - private int armHeldCount = 0; - - TrackControlLayer(final Layers layers, SessionLayer sessionLayer, Transport transport, ControllerHost host, - PanelLayout layoutType) { - this.transport = transport; - this.sessionLayer = sessionLayer; - transport.isPlaying().markInterested(); - transport.isClipLauncherOverdubEnabled().markInterested(); - transport.isMetronomeEnabled().markInterested(); - muteLayer = new Layer(layers, "MUTE_LAYER"); - soloLayer = new Layer(layers, "SOLO_LAYER"); - stopLayer = new Layer(layers, "STOP_LAYER"); - armLayer = new Layer(layers, "ARM_LAYER"); - controlLayer = new Layer(layers, "CONTROL_LAYER"); - trackModeLayerMap.put(TrackMode.NONE, null); - trackModeLayerMap.put(TrackMode.SOLO, soloLayer); - trackModeLayerMap.put(TrackMode.MUTE, muteLayer); - trackModeLayerMap.put(TrackMode.ARM, armLayer); - trackModeLayerMap.put(TrackMode.STOP, stopLayer); - trackModeLayerMap.put(TrackMode.CONTROL, controlLayer); - currentControlGridLayer = null; - project = host.getProject(); - this.layoutType = layoutType; - } - - void initClipControl(final HwElements hwElements, final TrackBank trackBank) { - for (int i = 0; i < 8; i++) { - final Track track = trackBank.getItemAt(i); - final GridButton button = getButton(hwElements, i); - button.bindPressed(stopLayer, track::stop); - button.bindLight(stopLayer, () -> getStopState(track)); - button.bindPressed(muteLayer, () -> track.mute().toggle()); - button.bindLight(muteLayer, () -> getMuteState(track)); - button.bindPressed(soloLayer, () -> handleSolo(true, track)); - button.bindRelease(soloLayer, () -> handleSolo(false, track)); - button.bindLight(soloLayer, () -> getSoloState(track)); - button.bindPressed(armLayer, () -> handleArm(true, track)); - button.bindRelease(armLayer, () -> handleArm(false, track)); - button.bindLight(armLayer, () -> getArmState(track)); - } - } - - void initControlLayer(final HwElements hwElements, ViewCursorControl viewCursorControl) { - int index = 0; - final GridButton playButton = getButton(hwElements, index++); - playButton.bindPressed(controlLayer, this::togglePlay); - playButton.bindLight(controlLayer, () -> transport.isPlaying().get() ? RgbState.of(21) : RgbState.of(23)); - final GridButton overButton = getButton(hwElements, index++); - - overButton.bindPressed(controlLayer, () -> viewCursorControl.globalRecordAction(transport)); - overButton.bindLight(controlLayer, sessionLayer::getRecordButtonColorRegular); - - final GridButton metroButton = getButton(hwElements, index++); - metroButton.bindPressed(controlLayer, () -> transport.isMetronomeEnabled().toggle()); - metroButton.bindLight(controlLayer, - () -> transport.isMetronomeEnabled().get() ? RgbState.of(37) : RgbState.of(39)); - for (int i = 0; i < 4; i++) { - final GridButton emptyButton = getButton(hwElements, index++); - emptyButton.bindPressed(controlLayer, () -> { - }); - emptyButton.bindLight(controlLayer, () -> RgbState.OFF); - } - final GridButton shiftButton = getButton(hwElements, index); - shiftButton.bindPressed(controlLayer, sessionLayer::setShiftHeld); - shiftButton.bindLightPressed(controlLayer, RgbState.pulse(2), RgbState.of(3)); - } - - private GridButton getButton(final HwElements hwElements, int index) { - if (layoutType == PanelLayout.VERTICAL) { - return hwElements.getGridButton(7, index); - } - return hwElements.getGridButton(index, 7); - } - - public void applyMode(TrackMode trackMode) { - activateLayer(trackModeLayerMap.get(trackMode)); - } - - void activateLayer(final Layer nextLayer) { - if (currentControlGridLayer != null) { - currentControlGridLayer.setIsActive(false); - } - currentControlGridLayer = nextLayer; - if (currentControlGridLayer != null) { - currentControlGridLayer.setIsActive(true); - } - } - - void activateControlLayer(boolean active) { - if (currentControlGridLayer != null) { - currentControlGridLayer.setIsActive(active); - } - } - - public void deactivateLayer() { - if (currentControlGridLayer != null) { - currentControlGridLayer.setIsActive(false); - } - } - - public void reset() { - soloHeldCount = 0; - armHeldCount = 0; - deactivateLayer(); - currentControlGridLayer = null; - } - - private RgbState getMuteState(final Track track) { - if (track.exists().get()) { - return track.mute().get() ? RgbState.of(9) : RgbState.of(11); - } - return RgbState.OFF; - } - - private RgbState getSoloState(final Track track) { - if (track.exists().get()) { - return track.solo().get() ? RgbState.of(13) : RgbState.of(15); - } - return RgbState.OFF; - } - - private RgbState getArmState(final Track track) { - if (track.exists().get()) { - return track.arm().get() ? RgbState.of(5) : RgbState.of(7); - } - return RgbState.OFF; - } - - - private RgbState getStopState(final Track track) { - if (track.exists().get()) { - if (track.isQueuedForStop().get()) { - return RgbState.flash(5, 0); - } else if (track.isStopped().get()) { - return RgbState.of(7); - } else { - return RgbState.RED; - } - } - return RgbState.OFF; - } - - private void togglePlay() { - if (sessionLayer.isShiftHeld()) { - transport.continuePlayback(); - } else { - if (transport.isPlaying().get()) { - transport.stop(); - } else { - transport.togglePlay(); - } - - } - } - - private void handleSolo(final boolean pressed, final Track track) { - if (pressed) { - track.solo().toggle(soloHeldCount == 0); - soloHeldCount++; - } else { - if (soloHeldCount > 0) { - soloHeldCount--; - } - } - } - - private void handleArm(final boolean pressed, final Track track) { - if (pressed) { - if (armHeldCount == 0) { - final boolean isArmed = track.arm().get(); - project.unarmAll(); - if (isArmed) { - track.arm().set(false); + private final Layer muteLayer; + private final Layer soloLayer; + private final Layer armLayer; + private final Layer stopLayer; + private final Layer controlLayer; + private final Map trackModeLayerMap = new HashMap<>(); + private final PanelLayout layoutType; + private final Project project; + private final Transport transport; + private final SessionLayer sessionLayer; + private Layer currentControlGridLayer; + private int soloHeldCount = 0; + private int armHeldCount = 0; + + TrackControlLayer(final Layers layers, final SessionLayer sessionLayer, final Transport transport, + final ControllerHost host, final PanelLayout layoutType) { + this.transport = transport; + this.sessionLayer = sessionLayer; + transport.isPlaying().markInterested(); + transport.isClipLauncherOverdubEnabled().markInterested(); + transport.isMetronomeEnabled().markInterested(); + muteLayer = new Layer(layers, "MUTE_LAYER"); + soloLayer = new Layer(layers, "SOLO_LAYER"); + stopLayer = new Layer(layers, "STOP_LAYER"); + armLayer = new Layer(layers, "ARM_LAYER"); + controlLayer = new Layer(layers, "CONTROL_LAYER"); + trackModeLayerMap.put(TrackMode.NONE, null); + trackModeLayerMap.put(TrackMode.SOLO, soloLayer); + trackModeLayerMap.put(TrackMode.MUTE, muteLayer); + trackModeLayerMap.put(TrackMode.ARM, armLayer); + trackModeLayerMap.put(TrackMode.STOP, stopLayer); + trackModeLayerMap.put(TrackMode.CONTROL, controlLayer); + currentControlGridLayer = null; + project = host.getProject(); + this.layoutType = layoutType; + } + + void initClipControl(final LpMiniHwElements hwElements, final TrackBank trackBank) { + for (int i = 0; i < 8; i++) { + final Track track = trackBank.getItemAt(i); + final GridButton button = getButton(hwElements, i); + button.bindPressed(stopLayer, track::stop); + button.bindLight(stopLayer, () -> getStopState(track)); + button.bindPressed(muteLayer, () -> track.mute().toggle()); + button.bindLight(muteLayer, () -> getMuteState(track)); + button.bindPressed(soloLayer, () -> handleSolo(true, track)); + button.bindRelease(soloLayer, () -> handleSolo(false, track)); + button.bindLight(soloLayer, () -> getSoloState(track)); + button.bindPressed(armLayer, () -> handleArm(true, track)); + button.bindRelease(armLayer, () -> handleArm(false, track)); + button.bindLight(armLayer, () -> getArmState(track)); + } + } + + void initControlLayer(final LpMiniHwElements hwElements, final ViewCursorControl viewCursorControl) { + int index = 0; + final GridButton playButton = getButton(hwElements, index++); + playButton.bindPressed(controlLayer, this::togglePlay); + playButton.bindLight(controlLayer, () -> transport.isPlaying().get() ? RgbState.of(21) : RgbState.of(23)); + final GridButton overButton = getButton(hwElements, index++); + + overButton.bindPressed(controlLayer, () -> viewCursorControl.globalRecordAction(transport)); + overButton.bindLight(controlLayer, sessionLayer::getRecordButtonColorRegular); + + final GridButton metroButton = getButton(hwElements, index++); + metroButton.bindPressed(controlLayer, () -> transport.isMetronomeEnabled().toggle()); + metroButton.bindLight(controlLayer, + () -> transport.isMetronomeEnabled().get() ? RgbState.of(37) : RgbState.of(39)); + for (int i = 0; i < 4; i++) { + final GridButton emptyButton = getButton(hwElements, index++); + emptyButton.bindPressed(controlLayer, () -> { + }); + emptyButton.bindLight(controlLayer, () -> RgbState.OFF); + } + final GridButton shiftButton = getButton(hwElements, index); + shiftButton.bindPressed(controlLayer, sessionLayer::setShiftHeld); + shiftButton.bindLightPressed(controlLayer, RgbState.pulse(2), RgbState.of(3)); + } + + private GridButton getButton(final LpMiniHwElements hwElements, final int index) { + if (layoutType == PanelLayout.VERTICAL) { + return hwElements.getGridButton(7, index); + } + return hwElements.getGridButton(index, 7); + } + + public void applyMode(final TrackMode trackMode) { + activateLayer(trackModeLayerMap.get(trackMode)); + } + + void activateLayer(final Layer nextLayer) { + if (currentControlGridLayer != null) { + currentControlGridLayer.setIsActive(false); + } + currentControlGridLayer = nextLayer; + if (currentControlGridLayer != null) { + currentControlGridLayer.setIsActive(true); + } + } + + void activateControlLayer(final boolean active) { + if (currentControlGridLayer != null) { + currentControlGridLayer.setIsActive(active); + } + } + + public void deactivateLayer() { + if (currentControlGridLayer != null) { + currentControlGridLayer.setIsActive(false); + } + } + + public void reset() { + soloHeldCount = 0; + armHeldCount = 0; + deactivateLayer(); + currentControlGridLayer = null; + } + + private RgbState getMuteState(final Track track) { + if (track.exists().get()) { + return track.mute().get() ? RgbState.of(9) : RgbState.of(11); + } + return RgbState.OFF; + } + + private RgbState getSoloState(final Track track) { + if (track.exists().get()) { + return track.solo().get() ? RgbState.of(13) : RgbState.of(15); + } + return RgbState.OFF; + } + + private RgbState getArmState(final Track track) { + if (track.exists().get()) { + return track.arm().get() ? RgbState.of(5) : RgbState.of(7); + } + return RgbState.OFF; + } + + + private RgbState getStopState(final Track track) { + if (track.exists().get()) { + if (track.isQueuedForStop().get()) { + return RgbState.flash(5, 0); + } else if (track.isStopped().get()) { + return RgbState.of(7); } else { - track.arm().set(true); - track.selectInEditor(); + return RgbState.RED; } - } else { - track.arm().toggle(); - } - armHeldCount++; - } else { - if (armHeldCount > 0) { - armHeldCount--; - } - } - } - - public void resetCounts() { - soloHeldCount = 0; - armHeldCount = 0; - } - - + } + return RgbState.OFF; + } + + private void togglePlay() { + if (sessionLayer.isShiftHeld()) { + transport.continuePlayback(); + } else { + if (transport.isPlaying().get()) { + transport.stop(); + } else { + transport.togglePlay(); + } + + } + } + + private void handleSolo(final boolean pressed, final Track track) { + if (pressed) { + track.solo().toggle(soloHeldCount == 0); + soloHeldCount++; + } else { + if (soloHeldCount > 0) { + soloHeldCount--; + } + } + } + + private void handleArm(final boolean pressed, final Track track) { + if (pressed) { + if (armHeldCount == 0) { + final boolean isArmed = track.arm().get(); + project.unarmAll(); + if (isArmed) { + track.arm().set(false); + } else { + track.arm().set(true); + track.selectInEditor(); + } + } else { + track.arm().toggle(); + } + armHeldCount++; + } else { + if (armHeldCount > 0) { + armHeldCount--; + } + } + } + + public void resetCounts() { + soloHeldCount = 0; + armHeldCount = 0; + } + + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/VolumeSliderLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/VolumeSliderLayer.java index 6515ad2b..0df58f2e 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/VolumeSliderLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadmini3/layers/VolumeSliderLayer.java @@ -6,61 +6,61 @@ import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; import com.bitwig.extensions.controllers.novation.commonsmk3.PanelLayout; import com.bitwig.extensions.controllers.novation.commonsmk3.SliderBinding; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; import com.bitwig.extensions.controllers.novation.launchpadmini3.LaunchPadPreferences; -import com.bitwig.extensions.controllers.novation.launchpadmini3.ViewCursorControl; import com.bitwig.extensions.framework.Layers; public class VolumeSliderLayer extends TrackSliderLayer { - - public VolumeSliderLayer(final ViewCursorControl viewCursorControl, final HardwareSurface controlSurface, - final Layers layers, final MidiProcessor midiProcessor, LaunchPadPreferences preferences) { - super("VOL", controlSurface, layers, midiProcessor, 20, 9); - bind(viewCursorControl.getTrackBank()); - preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> { - layout = newValue; - updateFaderState(); - })); - } - - @Override - protected void bind(final TrackBank trackBank) { - for (int i = 0; i < 8; i++) { - final Track track = trackBank.getItemAt(i); - final SliderBinding binding = new SliderBinding(baseCcNr, track.volume(), sliders[i], i, midiProcessor); - addBinding(binding); - valueBindings.add(binding); - } - } - - @Override - protected void updateFaderState() { - if (isActive()) { - refreshTrackColors(); - midiProcessor.setFaderBank(layout == PanelLayout.VERTICAL ? 0 : 1, tracksExistsColors, true, baseCcNr); - valueBindings.forEach(SliderBinding::update); - } - } - - - @Override - protected void refreshTrackColors() { - final boolean[] exists = trackState.getExists(); - for (int i = 0; i < 8; i++) { - tracksExistsColors[i] = exists[i] ? baseColor : 0; - } - } - - @Override - protected void onDeactivate() { - super.onDeactivate(); - updateFaderState(); - } - - @Override - protected void onActivate() { - super.onActivate(); - refreshTrackColors(); - updateFaderState(); - } - + + public VolumeSliderLayer(final ViewCursorControl viewCursorControl, final HardwareSurface controlSurface, + final Layers layers, final MidiProcessor midiProcessor, final LaunchPadPreferences preferences) { + super("VOL", controlSurface, layers, midiProcessor, 20, 9); + bind(viewCursorControl.getTrackBank()); + preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> { + layout = newValue; + updateFaderState(); + })); + } + + @Override + protected void bind(final TrackBank trackBank) { + for (int i = 0; i < 8; i++) { + final Track track = trackBank.getItemAt(i); + final SliderBinding binding = new SliderBinding(baseCcNr, track.volume(), sliders[i], i, midiProcessor); + addBinding(binding); + valueBindings.add(binding); + } + } + + @Override + protected void updateFaderState() { + if (isActive()) { + refreshTrackColors(); + midiProcessor.setFaderBank(layout == PanelLayout.VERTICAL ? 0 : 1, tracksExistsColors, true, baseCcNr); + valueBindings.forEach(SliderBinding::update); + } + } + + + @Override + protected void refreshTrackColors() { + final boolean[] exists = trackState.getExists(); + for (int i = 0; i < 8; i++) { + tracksExistsColors[i] = exists[i] ? baseColor : 0; + } + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + updateFaderState(); + } + + @Override + protected void onActivate() { + super.onActivate(); + refreshTrackColors(); + updateFaderState(); + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/DebugOutLp.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/DebugOutLp.java deleted file mode 100644 index 1d31583f..00000000 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/DebugOutLp.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.bitwig.extensions.controllers.novation.launchpadpromk3; - -import com.bitwig.extension.controller.api.ControllerHost; - -public class DebugOutLp { - - public static ControllerHost host; - - public static void println(final String format, final Object... args) { - if (host != null) { - host.println(String.format(format, args)); - } - } - - public static void main(final String[] args) { - // 1/4 1.0 - // 1/4 T 0.666 - // 1/8 0.5 - // 1/8T 0.33400 - // 1/16 0.25 - // 1/16T 0.166000 - // 1/32 0.125 - // 1/32T 0.084000 - final double sz = 0.25; - for (int i = 1; i <= 32; i++) { - System.out.println(i + " " + (i * sz)); - } - } -} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/LaunchPadProMk3ExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/LaunchPadProMk3ExtensionDefinition.java index 308bbeef..0249ac45 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/LaunchPadProMk3ExtensionDefinition.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/LaunchPadProMk3ExtensionDefinition.java @@ -65,13 +65,13 @@ public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList final String[] outputNames = new String[1]; switch (platformType) { - case LINUX: case WINDOWS: inputNames[0] = "MIDIIN3 (LPProMK3 MIDI)"; inputNames[1] = "LPProMK3 MIDI"; outputNames[0] = "MIDIOUT3 (LPProMK3 MIDI)"; break; case MAC: + case LINUX: inputNames[0] = "Launchpad Pro MK3 LPProMK3 DAW"; inputNames[1] = "Launchpad Pro MK3 LPProMK3 MIDI"; outputNames[0] = "Launchpad Pro MK3 LPProMK3 DAW"; 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 0ad1f2bf..20bd23dc 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 @@ -1,358 +1,389 @@ package com.bitwig.extensions.controllers.novation.launchpadpromk3; +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; + import com.bitwig.extension.api.util.midi.ShortMidiMessage; import com.bitwig.extension.callback.ShortMidiMessageReceivedCallback; import com.bitwig.extension.controller.ControllerExtension; import com.bitwig.extension.controller.ControllerExtensionDefinition; -import com.bitwig.extension.controller.api.*; +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.CursorTrack; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extension.controller.api.SettableEnumValue; +import com.bitwig.extension.controller.api.Transport; import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; import com.bitwig.extensions.controllers.novation.commonsmk3.LaunchpadDeviceConfig; +import com.bitwig.extensions.controllers.novation.commonsmk3.LpHwElements; import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; +import com.bitwig.extensions.controllers.novation.commonsmk3.OverviewLayer; import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.layers.*; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.sliderlayers.*; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.layers.ControlMode; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.layers.DrumLayer; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.layers.NotePlayingLayer; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.layers.SceneLaunchLayer; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.layers.SessionLayer; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.layers.TrackControlLayer; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.layers.TrackModeLayer; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.sliderlayers.DeviceSliderLayer; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.sliderlayers.PanSliderLayer; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.sliderlayers.SendsSliderLayer; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.sliderlayers.SliderLayer; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.sliderlayers.VolumeSliderLayer; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.di.Context; -import java.util.HashMap; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; - public class LaunchpadProMk3ControllerExtension extends ControllerExtension implements ModeHandler { - - private Transport transport; - - // Main Grid Buttons counting from top to bottom - - private ModifierStates modifierStates; - private Layer mainLayer; - private Layer shiftLayer; - private final HashMap controlMaps = new HashMap<>(); - - private TrackModeLayer trackModeLayer; - private OverviewLayer overviewLayer; - private ViewCursorControl viewControl; - private SysExHandler sysExHandler; - private HardwareSurface surface; - private LpBaseMode mainMode = LpBaseMode.SESSION; - private int mainModePage = 0; - private HwElements hwElements; - private DrumLayer drumPadLayer; - private boolean drumModeActive = false; - - protected LaunchpadProMk3ControllerExtension(final ControllerExtensionDefinition definition, - final ControllerHost host) { - super(definition, host); - } - - @Override - public void init() { - DebugOutLp.host = getHost(); - Context.registerCounter(getHost()); - - final Context diContext = new Context(this); - diContext.registerService(ModeHandler.class, this); - - final ControllerHost host = diContext.getService(ControllerHost.class); - surface = diContext.getService(HardwareSurface.class); - transport = diContext.getService(Transport.class); - - final MidiIn midiIn = host.getMidiInPort(0); - final MidiIn midiIn2 = host.getMidiInPort(1); - midiIn.setMidiCallback((ShortMidiMessageReceivedCallback) this::onMidi0); - midiIn2.setMidiCallback((ShortMidiMessageReceivedCallback) this::onMidi1); - - final MidiOut midiOut = host.getMidiOutPort(0); - final MidiProcessor midiProcessor = new MidiProcessor(host, midiIn, midiOut, - new LaunchpadDeviceConfig("LaunchPadProMk3", 0x0E, 0xB4, 0xB5, false)); - diContext.registerService(MidiProcessor.class, midiProcessor); - - midiIn2.createNoteInput("MIDI", "8?????", "9?????", "A?????", "D?????"); - - viewControl = diContext.getService(ViewCursorControl.class); - sysExHandler = diContext.create(SysExHandler.class); - - modifierStates = diContext.getService(ModifierStates.class); - hwElements = diContext.create(HwElements.class); - - midiIn.setSysexCallback(sysExHandler::handleSysEx); - mainLayer = diContext.createLayer("MainLayer"); - shiftLayer = diContext.createLayer("GlobalShiftLayer"); - - initTransportSection(); - assignModifiers(diContext.getService(Application.class)); - assignModeButtons(); - initViewControlListeners(); - sysExHandler.addPrintToClipDataListener(this::handlePrintToClipInvoked); - - final SessionLayer sessionLayer = diContext.create(SessionLayer.class); - final SceneLaunchLayer sceneLaunchLayer = diContext.create(SceneLaunchLayer.class); - drumPadLayer = diContext.create(DrumLayer.class); - final NotePlayingLayer notePlayingLayer = diContext.create(NotePlayingLayer.class); - overviewLayer = diContext.create(OverviewLayer.class); - - trackModeLayer = diContext.create(TrackModeLayer.class); - final TrackControlLayer trackControlLayer = diContext.create(TrackControlLayer.class); - initSliderLayers(diContext); - - mainLayer.activate(); - sessionLayer.activate(); - sceneLaunchLayer.activate(); - trackControlLayer.activate(); - trackModeLayer.activate(); - notePlayingLayer.activate(); - - sysExHandler.addModeChangeListener(this::handleModeChanged); - sysExHandler.deviceInquiry(); - diContext.activate(); - midiProcessor.start(); - } - - private void initSliderLayers(final Context diContext) { - controlMaps.put(ControlMode.VOLUME, diContext.create(VolumeSliderLayer.class)); - controlMaps.put(ControlMode.PAN, diContext.create(PanSliderLayer.class)); - controlMaps.put(ControlMode.SENDS, diContext.create(SendsSliderLayer.class)); - controlMaps.put(ControlMode.DEVICE, diContext.create(DeviceSliderLayer.class)); - } - - private void initViewControlListeners() { - final CursorTrack cursorTrack = viewControl.getCursorTrack(); - cursorTrack.canHoldNoteData().addValueObserver(canHoldNoteData -> sysExHandler.enableClipPrint(canHoldNoteData)); - final PinnableCursorDevice primaryDevice = viewControl.getPrimaryDevice(); - primaryDevice.hasDrumPads().addValueObserver(hasDrumPads -> { - - if (sysExHandler.getMode() == LpBaseMode.NOTE || sysExHandler.getMode() == LpBaseMode.CHORD) { - changeDrumMode(hasDrumPads); - } - }); - } - - private void changeDrumMode(final boolean drumModeActive) { - if (this.drumModeActive == drumModeActive) { - return; - } - sysExHandler.changeNoteMode(drumModeActive); - drumPadLayer.setIsActive(drumModeActive); - this.drumModeActive = 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); - - final LabeledButton clearButton = hwElements.getLabeledButton(LabelCcAssignments.CLEAR); - clearButton.bindPressed(mainLayer, this::handleClear); - clearButton.bindLightPressed(mainLayer, RgbState.BUTTON_INACTIVE, RgbState.BUTTON_ACTIVE); - clearButton.bindLightPressed(shiftLayer, RgbState.SHIFT_INACTIVE, RgbState.SHIFT_ACTIVE); - - final LabeledButton duplicateButton = hwElements.getLabeledButton(LabelCcAssignments.DUPLICATE); - duplicateButton.bindPressed(mainLayer, this::handleDuplicate); - duplicateButton.bindLightPressed(mainLayer, RgbState.BUTTON_INACTIVE, RgbState.BUTTON_ACTIVE); - duplicateButton.bindLightPressed(shiftLayer, RgbState.SHIFT_INACTIVE, RgbState.SHIFT_ACTIVE); - - final LabeledButton quantizeButton = hwElements.getLabeledButton(LabelCcAssignments.QUANTIZE); - quantizeButton.bindPressed(mainLayer, this::handleQuantize, RgbState.BUTTON_ACTIVE, RgbState.BUTTON_INACTIVE); - - final SettableEnumValue recordQuantizeValue = application.recordQuantizationGrid(); - recordQuantizeValue.markInterested(); - quantizeButton.bindPressed(shiftLayer, pressed -> toggleRecordQuantize(recordQuantizeValue, pressed)); - quantizeButton.bindLight(shiftLayer, - () -> recordQuantizeValue.get().equals("OFF") ? RgbState.RED_LO : RgbState.TURQUOISE); - } - - private void handleDuplicate(final boolean pressed) { - if (mainMode.isNoteHandler() && pressed) { // TODO consider long pressing - viewControl.handleDuplication(modifierStates.isShift()); - } - modifierStates.setDuplicate(pressed); - } - - private void handleClear(final boolean pressed) { - if (mainMode.isNoteHandler() && pressed) { - viewControl.handleClear(modifierStates.isShift()); - } - modifierStates.setClear(pressed); - } - - private void handleQuantize(final boolean pressed) { - if (mainMode.isNoteHandler() && pressed) { - viewControl.handleQuantize(modifierStates.isShift()); - } - modifierStates.setQuantize(pressed); - } - - private void toggleRecordQuantize(final SettableEnumValue recordQuant, final Boolean pressed) { - if (!pressed) { - return; - } - final String current = recordQuant.get(); - if ("OFF".equals(current)) { - recordQuant.set("1/16"); - } else { - recordQuant.set("OFF"); - } - } - - 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, - () -> sysExHandler.getMode() == LpBaseMode.SESSION ? RgbState.BLUE : RgbState.DIM_WHITE); - - final LabeledButton noteButton = hwElements.getLabeledButton(LabelCcAssignments.NOTE); - noteButton.bind(mainLayer, () -> { - DebugOutLp.println(" MODE NOTE"); - sysExHandler.changeMode(LpBaseMode.NOTE, 0); - }, () -> sysExHandler.getMode() == LpBaseMode.NOTE ? RgbState.ORANGE : RgbState.DIM_WHITE); - final LabeledButton chordButton = hwElements.getLabeledButton(LabelCcAssignments.CHORD); - chordButton.bind(mainLayer, () -> { - DebugOutLp.println(" MODE CHORD"); - sysExHandler.changeMode(LpBaseMode.CHORD, 0); - }, () -> sysExHandler.getMode() == LpBaseMode.CHORD ? RgbState.ORANGE : RgbState.DIM_WHITE); - } - - private void initTransportSection() { - transport.isPlaying().markInterested(); - transport.tempo().markInterested(); - transport.playPosition().markInterested(); - transport.isClipLauncherOverdubEnabled().markInterested(); - - final LabeledButton playButton = hwElements.getLabeledButton(LabelCcAssignments.PLAY); - playButton.bind(mainLayer, this::togglePlay, this::getPlayColor); - 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); - } - - private RgbState getPlayColor() { - return transport.isPlaying().get() ? RgbState.GREEN : RgbState.DIM_WHITE; - } - - private void togglePlay() { - transport.isPlaying().toggle(); - } - - private RgbState getRecordButtonColorRegular() { - final FocusSlot focusSlot = viewControl.getFocusSlot(); - if (focusSlot != null) { - final ClipLauncherSlot slot = focusSlot.getSlot(); - if (slot.isRecordingQueued().get()) { - return RgbState.flash(5, 0); - } - if (slot.isRecording().get() || slot.isRecordingQueued().get()) { - return RgbState.pulse(5); - } - } - if (transport.isClipLauncherOverdubEnabled().get()) { - return RgbState.RED; - } else { - return RgbState.of(1); - } - } - - private void toggleRecord() { - viewControl.globalRecordAction(transport); - } - - private void handlePrintToClipInvoked(final PrintToClipData printToClipData) { - viewControl.createNewClip(); - printToClipData.applyToClip(viewControl.getCursorClip()); - } - - private void onMidi0(final ShortMidiMessage msg) { - // DebugOut.println("MIDI %02X %02X %02X", msg.getStatusByte(), msg.getData1(), msg.getData2()); - } - - private void onMidi1(final ShortMidiMessage msg) { - //if (msg.getChannel() == 0 && msg.getStatusByte() == 144) { - // drumseqenceMode.notifyMidiEvent(msg.getData1(), msg.getData2()); - //} - // DebugOut.println("MIDI 2 -> %02X %02X %02X", msg.getStatusByte(), msg.getData1(), msg.getData2()); - } - - private void shutDownController(final CompletableFuture shutdown) { - sysExHandler.setDawMode(false); - try { - Thread.sleep(300); - } catch (final InterruptedException e) { - e.printStackTrace(); - } - shutdown.complete(true); - } - - @Override - public void exit() { - final CompletableFuture shutdown = new CompletableFuture<>(); - Executors.newSingleThreadExecutor().execute(() -> shutDownController(shutdown)); - try { - shutdown.get(); - } catch (final InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - } - - @Override - public void flush() { - surface.updateHardware(); - } - - @Override - public void toFaderMode(final ControlMode controlMode, final ControlMode previousMode) { - final SliderLayer nextControlLayer = controlMaps.get(controlMode); - final SliderLayer previousLayer = controlMaps.get(previousMode); - if (previousLayer != null) { - previousLayer.setIsActive(false); - sysExHandler.enableFaderMode(previousMode, false); - } - if (nextControlLayer != null) { - nextControlLayer.setIsActive(true); - sysExHandler.enableFaderMode(controlMode, true); - } else { - sysExHandler.setLayout(LpBaseMode.SESSION, 0); - } - } - - private void handleModeChanged(final LpBaseMode mode, final int page) { - final LpBaseMode prevMode = mainMode; - final int prevPage = mainModePage; - if (prevMode == LpBaseMode.FADER && mode != LpBaseMode.FADER) { - final ControlMode previousCtrlMode = ControlMode.fromPageId(prevPage); - final SliderLayer previousLayer = controlMaps.get(previousCtrlMode); - previousLayer.setIsActive(false); - } - mainMode = mode; - mainModePage = page; - if (mode == LpBaseMode.FADER) { - trackModeLayer.setIsActive(true); - trackModeLayer.setControlMode(ControlMode.fromPageId(page)); - } else { - trackModeLayer.setControlMode(ControlMode.NONE); - } - if (sysExHandler.getMode() == LpBaseMode.NOTE || sysExHandler.getMode() == LpBaseMode.CHORD) { - changeDrumMode(viewControl.getPrimaryDevice().hasDrumPads().get()); - } else if (prevMode == LpBaseMode.NOTE && drumModeActive) { - changeDrumMode(false); - } - } - + + private Transport transport; + + // Main Grid Buttons counting from top to bottom + + private ModifierStates modifierStates; + private Layer mainLayer; + private Layer shiftLayer; + private final HashMap controlMaps = new HashMap<>(); + + private TrackModeLayer trackModeLayer; + private OverviewLayer overviewLayer; + private ViewCursorControl viewControl; + private SysExHandler sysExHandler; + private HardwareSurface surface; + private LpBaseMode mainMode = LpBaseMode.SESSION; + private int mainModePage = 0; + private LpProHwElements hwElements; + private DrumLayer drumPadLayer; + private boolean drumModeActive = false; + + private static ControllerHost debugHost; + + public static void println(final String format, final Object... args) { + if (debugHost != null) { + debugHost.println(String.format(format, args)); + } + } + + protected LaunchpadProMk3ControllerExtension(final ControllerExtensionDefinition definition, + final ControllerHost host) { + super(definition, host); + } + + @Override + public void init() { + debugHost = getHost(); + Context.registerCounter(getHost()); + final Context diContext = new Context(this, ViewCursorControl.class.getPackage()); + diContext.registerService(ModeHandler.class, this); + + final ControllerHost host = diContext.getService(ControllerHost.class); + surface = diContext.getService(HardwareSurface.class); + transport = diContext.getService(Transport.class); + + final MidiIn midiIn = host.getMidiInPort(0); + final MidiIn midiIn2 = host.getMidiInPort(1); + midiIn.setMidiCallback((ShortMidiMessageReceivedCallback) this::onMidi0); + midiIn2.setMidiCallback((ShortMidiMessageReceivedCallback) this::onMidi1); + + final MidiOut midiOut = host.getMidiOutPort(0); + final MidiProcessor midiProcessor = new MidiProcessor(host, midiIn, midiOut, + new LaunchpadDeviceConfig("LaunchPadProMk3", 0x0E, 0xB4, 0xB5, false)); + diContext.registerService(MidiProcessor.class, midiProcessor); + + midiIn2.createNoteInput("MIDI", "8?????", "9?????", "A?????", "D?????"); + + viewControl = diContext.getService(ViewCursorControl.class); + sysExHandler = diContext.create(SysExHandler.class); + + modifierStates = diContext.getService(ModifierStates.class); + hwElements = diContext.create(LpProHwElements.class); + diContext.registerService(LpHwElements.class, hwElements); + + midiIn.setSysexCallback(sysExHandler::handleSysEx); + mainLayer = diContext.createLayer("MainLayer"); + shiftLayer = diContext.createLayer("GlobalShiftLayer"); + + initTransportSection(); + assignModifiers(diContext.getService(Application.class)); + assignModeButtons(); + initViewControlListeners(); + sysExHandler.addPrintToClipDataListener(this::handlePrintToClipInvoked); + + final SessionLayer sessionLayer = diContext.create(SessionLayer.class); + final SceneLaunchLayer sceneLaunchLayer = diContext.create(SceneLaunchLayer.class); + drumPadLayer = diContext.create(DrumLayer.class); + final NotePlayingLayer notePlayingLayer = diContext.create(NotePlayingLayer.class); + overviewLayer = diContext.create(OverviewLayer.class); + + trackModeLayer = diContext.create(TrackModeLayer.class); + final TrackControlLayer trackControlLayer = diContext.create(TrackControlLayer.class); + initSliderLayers(diContext); + + mainLayer.activate(); + sessionLayer.activate(); + sceneLaunchLayer.activate(); + trackControlLayer.activate(); + trackModeLayer.activate(); + notePlayingLayer.activate(); + + sysExHandler.addModeChangeListener(this::handleModeChanged); + sysExHandler.deviceInquiry(); + diContext.activate(); + midiProcessor.start(); + } + + private void initSliderLayers(final Context diContext) { + controlMaps.put(ControlMode.VOLUME, diContext.create(VolumeSliderLayer.class)); + controlMaps.put(ControlMode.PAN, diContext.create(PanSliderLayer.class)); + controlMaps.put(ControlMode.SENDS, diContext.create(SendsSliderLayer.class)); + controlMaps.put(ControlMode.DEVICE, diContext.create(DeviceSliderLayer.class)); + } + + private void initViewControlListeners() { + final CursorTrack cursorTrack = viewControl.getCursorTrack(); + cursorTrack.canHoldNoteData() + .addValueObserver(canHoldNoteData -> sysExHandler.enableClipPrint(canHoldNoteData)); + final PinnableCursorDevice primaryDevice = viewControl.getPrimaryDevice(); + primaryDevice.hasDrumPads().addValueObserver(hasDrumPads -> { + + if (sysExHandler.getMode() == LpBaseMode.NOTE || sysExHandler.getMode() == LpBaseMode.CHORD) { + changeDrumMode(hasDrumPads); + } + }); + } + + private void changeDrumMode(final boolean drumModeActive) { + if (this.drumModeActive == drumModeActive) { + return; + } + sysExHandler.changeNoteMode(drumModeActive); + drumPadLayer.setIsActive(drumModeActive); + this.drumModeActive = 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); + + final LabeledButton clearButton = hwElements.getLabeledButton(LabelCcAssignments.CLEAR); + clearButton.bindPressed(mainLayer, this::handleClear); + clearButton.bindLightPressed(mainLayer, RgbState.BUTTON_INACTIVE, RgbState.BUTTON_ACTIVE); + clearButton.bindLightPressed(shiftLayer, RgbState.SHIFT_INACTIVE, RgbState.SHIFT_ACTIVE); + + final LabeledButton duplicateButton = hwElements.getLabeledButton(LabelCcAssignments.DUPLICATE); + duplicateButton.bindPressed(mainLayer, this::handleDuplicate); + duplicateButton.bindLightPressed(mainLayer, RgbState.BUTTON_INACTIVE, RgbState.BUTTON_ACTIVE); + duplicateButton.bindLightPressed(shiftLayer, RgbState.SHIFT_INACTIVE, RgbState.SHIFT_ACTIVE); + + final LabeledButton quantizeButton = hwElements.getLabeledButton(LabelCcAssignments.QUANTIZE); + quantizeButton.bindPressed(mainLayer, this::handleQuantize, RgbState.BUTTON_ACTIVE, RgbState.BUTTON_INACTIVE); + + final SettableEnumValue recordQuantizeValue = application.recordQuantizationGrid(); + recordQuantizeValue.markInterested(); + quantizeButton.bindPressed(shiftLayer, pressed -> toggleRecordQuantize(recordQuantizeValue, pressed)); + quantizeButton.bindLight(shiftLayer, + () -> recordQuantizeValue.get().equals("OFF") ? RgbState.RED_LO : RgbState.TURQUOISE); + } + + private void handleDuplicate(final boolean pressed) { + if (mainMode.isNoteHandler() && pressed) { // TODO consider long pressing + viewControl.handleDuplication(modifierStates.isShift()); + } + modifierStates.setDuplicate(pressed); + } + + private void handleClear(final boolean pressed) { + if (mainMode.isNoteHandler() && pressed) { + viewControl.handleClear(modifierStates.isShift()); + } + modifierStates.setClear(pressed); + } + + private void handleQuantize(final boolean pressed) { + if (mainMode.isNoteHandler() && pressed) { + viewControl.handleQuantize(modifierStates.isShift()); + } + modifierStates.setQuantize(pressed); + } + + private void toggleRecordQuantize(final SettableEnumValue recordQuant, final Boolean pressed) { + if (!pressed) { + return; + } + final String current = recordQuant.get(); + if ("OFF".equals(current)) { + recordQuant.set("1/16"); + } else { + recordQuant.set("OFF"); + } + } + + 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, + () -> 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); + final LabeledButton chordButton = hwElements.getLabeledButton(LabelCcAssignments.CHORD); + chordButton.bind(mainLayer, () -> { + sysExHandler.changeMode(LpBaseMode.CHORD, 0); + }, () -> sysExHandler.getMode() == LpBaseMode.CHORD ? RgbState.ORANGE : RgbState.DIM_WHITE); + } + + private void initTransportSection() { + transport.isPlaying().markInterested(); + transport.tempo().markInterested(); + transport.playPosition().markInterested(); + transport.isClipLauncherOverdubEnabled().markInterested(); + + final LabeledButton playButton = hwElements.getLabeledButton(LabelCcAssignments.PLAY); + playButton.bind(mainLayer, this::togglePlay, this::getPlayColor); + 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); + } + + private RgbState getPlayColor() { + return transport.isPlaying().get() ? RgbState.GREEN : RgbState.DIM_WHITE; + } + + private void togglePlay() { + transport.isPlaying().toggle(); + } + + private RgbState getRecordButtonColorRegular() { + final FocusSlot focusSlot = viewControl.getFocusSlot(); + if (focusSlot != null) { + final ClipLauncherSlot slot = focusSlot.getSlot(); + if (slot.isRecordingQueued().get()) { + return RgbState.flash(5, 0); + } + if (slot.isRecording().get() || slot.isRecordingQueued().get()) { + return RgbState.pulse(5); + } + } + if (transport.isClipLauncherOverdubEnabled().get()) { + return RgbState.RED; + } else { + return RgbState.of(1); + } + } + + private void toggleRecord() { + viewControl.globalRecordAction(transport); + } + + private void handlePrintToClipInvoked(final PrintToClipData printToClipData) { + viewControl.createNewClip(); + printToClipData.applyToClip(viewControl.getCursorClip()); + } + + private void onMidi0(final ShortMidiMessage msg) { + // DebugOut.println("MIDI %02X %02X %02X", msg.getStatusByte(), msg.getData1(), msg.getData2()); + } + + private void onMidi1(final ShortMidiMessage msg) { + //if (msg.getChannel() == 0 && msg.getStatusByte() == 144) { + // drumseqenceMode.notifyMidiEvent(msg.getData1(), msg.getData2()); + //} + // DebugOut.println("MIDI 2 -> %02X %02X %02X", msg.getStatusByte(), msg.getData1(), msg.getData2()); + } + + private void shutDownController(final CompletableFuture shutdown) { + sysExHandler.setDawMode(false); + try { + Thread.sleep(300); + } + catch (final InterruptedException e) { + e.printStackTrace(); + } + shutdown.complete(true); + } + + @Override + public void exit() { + final CompletableFuture shutdown = new CompletableFuture<>(); + Executors.newSingleThreadExecutor().execute(() -> shutDownController(shutdown)); + try { + shutdown.get(); + } + catch (final InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + + @Override + public void flush() { + surface.updateHardware(); + } + + @Override + public void toFaderMode(final ControlMode controlMode, final ControlMode previousMode) { + final SliderLayer nextControlLayer = controlMaps.get(controlMode); + final SliderLayer previousLayer = controlMaps.get(previousMode); + if (previousLayer != null) { + previousLayer.setIsActive(false); + sysExHandler.enableFaderMode(previousMode, false); + } + if (nextControlLayer != null) { + nextControlLayer.setIsActive(true); + sysExHandler.enableFaderMode(controlMode, true); + } else { + sysExHandler.setLayout(LpBaseMode.SESSION, 0); + } + } + + private void handleModeChanged(final LpBaseMode mode, final int page) { + final LpBaseMode prevMode = mainMode; + final int prevPage = mainModePage; + if (prevMode == LpBaseMode.FADER && mode != LpBaseMode.FADER) { + final ControlMode previousCtrlMode = ControlMode.fromPageId(prevPage); + final SliderLayer previousLayer = controlMaps.get(previousCtrlMode); + previousLayer.setIsActive(false); + } + mainMode = mode; + mainModePage = page; + if (mode == LpBaseMode.FADER) { + trackModeLayer.setIsActive(true); + trackModeLayer.setControlMode(ControlMode.fromPageId(page)); + } else { + trackModeLayer.setControlMode(ControlMode.NONE); + } + if (sysExHandler.getMode() == LpBaseMode.NOTE || sysExHandler.getMode() == LpBaseMode.CHORD) { + changeDrumMode(viewControl.getPrimaryDevice().hasDrumPads().get()); + } else if (prevMode == LpBaseMode.NOTE && drumModeActive) { + changeDrumMode(false); + } + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/HwElements.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/LpProHwElements.java similarity index 88% rename from src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/HwElements.java rename to src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/LpProHwElements.java index c63d5571..43109c80 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/HwElements.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/LpProHwElements.java @@ -4,6 +4,7 @@ import com.bitwig.extensions.controllers.novation.commonsmk3.DrumButton; import com.bitwig.extensions.controllers.novation.commonsmk3.GridButton; import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; +import com.bitwig.extensions.controllers.novation.commonsmk3.LpHwElements; import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; import com.bitwig.extensions.framework.di.PostConstruct; @@ -12,13 +13,13 @@ import java.util.List; import java.util.Map; -public class HwElements { +public class LpProHwElements implements LpHwElements { private final GridButton[][] gridButtons = new GridButton[8][8]; private final Map labeledButtons = new HashMap<>(); private final List sceneLaunchButtons = new ArrayList<>(); private final List trackSelectButtons = new ArrayList<>(); private final List drumGridButtons = new ArrayList<>(); - + @PostConstruct public void init(final HardwareSurface surface, final MidiProcessor midiProcessor) { initGridButtons(surface, midiProcessor); @@ -30,16 +31,16 @@ public void init(final HardwareSurface surface, final MidiProcessor midiProcesso } for (int i = 0; i < 8; i++) { final LabeledButton sceneButton = new LabeledButton("SCENE_LAUNCH_" + (i + 1), surface, midiProcessor, - LabelCcAssignments.R8_PRINT_TO_CLIP.getCcValue() + (7 - i) * 10); + LabelCcAssignments.R8_PRINT_TO_CLIP.getCcValue() + (7 - i) * 10); sceneLaunchButtons.add(sceneButton); - + final LabeledButton trackButton = new LabeledButton("TRACK_" + (i + 1), surface, midiProcessor, - LabelCcAssignments.TRACK_SEL_1.getCcValue() + i); + LabelCcAssignments.TRACK_SEL_1.getCcValue() + i); trackSelectButtons.add(trackButton); } - + } - + private void initGridButtons(final HardwareSurface surface, final MidiProcessor midiProcessor) { for (int row = 0; row < 8; row++) { for (int col = 0; col < 8; col++) { @@ -51,23 +52,26 @@ private void initGridButtons(final HardwareSurface surface, final MidiProcessor drumGridButtons.add(new DrumButton(surface, midiProcessor, 8, noteValue)); } } - + + @Override public GridButton getGridButton(final int row, final int col) { return gridButtons[row][col]; } - + public LabeledButton getLabeledButton(final LabelCcAssignments assignment) { return labeledButtons.get(assignment); } - + + @Override public List getDrumGridButtons() { return drumGridButtons; } - + + @Override public List getSceneLaunchButtons() { return sceneLaunchButtons; } - + public List getTrackSelectButtons() { return trackSelectButtons; } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/PrintToClipData.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/PrintToClipData.java index 70f9ff7d..04e7a786 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/PrintToClipData.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/PrintToClipData.java @@ -1,10 +1,10 @@ package com.bitwig.extensions.controllers.novation.launchpadpromk3; -import com.bitwig.extension.controller.api.Clip; - import java.util.ArrayList; import java.util.List; +import com.bitwig.extension.controller.api.Clip; + public class PrintToClipData { private static final int HEX_CHAR_NUM = 58; private static final int HEX_CHAR_0 = 48; @@ -13,23 +13,24 @@ public class PrintToClipData { private static final double TIME_FACTOR = 500.0; private static final int NOTE_BYTES = 6; private static int patternCount = 1; - + private static final double[] RESOLUTIONS = {1.0, 0.666666, 0.5, 0.33333, 0.25, 0.1666666, 0.125, 0.0833333}; - private static final double[] STEP_RES = {500.0, 333.3333, 250.0, 166.666666, 125.0, 83.3333333, 62.5, 41.6666666666}; - + private static final double[] STEP_RES = + {500.0, 333.3333, 250.0, 166.666666, 125.0, 83.3333333, 62.5, 41.6666666666}; + public static class Note { private final int pitch; private final int velocity; private final int startTime; private final double length; - + private Note(final int pitch, final int velocity, final int startTime, final double length) { this.pitch = pitch; this.velocity = velocity; this.length = length; this.startTime = startTime; } - + public static Note fromData(final int offset, final byte[] data, final int packetOffset) { final int startTime = beatTimeNote(data, offset) + packetOffset; final double length = Math.abs(beatTimeNote(data, offset + 2) / TIME_FACTOR); @@ -37,10 +38,10 @@ public static Note fromData(final int offset, final byte[] data, final int packe final int velocity = data[offset + 5]; return new Note(pitch, velocity, startTime, length); } - + int calcResolutionMatches() { final int lengthResIndex = lengthMatch(); - + for (int resolutionIndex = 0; resolutionIndex < STEP_RES.length; resolutionIndex++) { final double xPositionDouble = startTime / STEP_RES[resolutionIndex]; if (isValidXPosition(xPositionDouble)) { @@ -49,7 +50,7 @@ int calcResolutionMatches() { } return lengthResIndex; } - + private int lengthMatch() { for (int resolutionIndex = 0; resolutionIndex < STEP_RES.length; resolutionIndex++) { if (equalsEpsilon(length, RESOLUTIONS[resolutionIndex])) { @@ -58,46 +59,46 @@ private int lengthMatch() { } return -1; } - + private static int beatTimeNote(final byte[] data, final int offset) { return (data[offset] << 7) + data[offset + 1]; } - + private static boolean isValidXPosition(final double value) { final int x = (int) Math.round(value); return x < 32 && Math.abs((double) x - value) < 0.01; } - + private static boolean equalsEpsilon(final double val1, final double val2) { return Math.abs(val1 - val2) < 0.001; } - + public int getPitch() { return pitch; } - + public int getVelocity() { return velocity; } - + public double getStart() { return Math.abs(startTime / TIME_FACTOR); } - + public double getLength() { return length; } - + @Override public String toString() { return String.format("s=%f l=%f %02d %02d s=%d", getStart(), length, pitch, velocity, startTime); } } - + private int track; private final List notes = new ArrayList<>(); private double patternLength; - + void setData(final int type, final String sysExBlock) { final int n = sysExBlock.length(); switch (type) { @@ -113,20 +114,19 @@ void setData(final int type, final String sysExBlock) { default: } } - + private double calculateStepSize() { int selectRes = -1; for (final Note note : notes) { selectRes = Math.max(selectRes, note.calcResolutionMatches()); } - DebugOutLp.println(" OVERALL Resolution = %f", RESOLUTIONS[selectRes]); return RESOLUTIONS[selectRes]; } - + public void applyToClip(final Clip clip) { clip.getPlayStop().set(patternLength); clip.getLoopLength().set(patternLength); - DebugOutLp.println(" PATTERN length = %f", patternLength); + clip.setName(String.format("Lpp#%d - %d", patternCount++, track)); if (notes.isEmpty()) { return; } @@ -140,9 +140,8 @@ public void applyToClip(final Clip clip) { clip.scrollToStep(page * 192); clip.setStep(xp, y, note.getVelocity(), note.getLength()); } - clip.setName(String.format("Lpp#%d - %d", patternCount++, track)); } - + private void readNoteData(final byte[] data) { final int len = data.length - DATA_OFFSET; final int packOffset = beatTimePattern(data); @@ -151,7 +150,7 @@ private void readNoteData(final byte[] data) { notes.add(Note.fromData(DATA_OFFSET + i, data, packOffset)); } } - + private byte[] toDataBlock(final String sysEx) { final byte[] data = new byte[sysEx.length() / 2]; for (int i = 0; i < data.length; i++) { @@ -159,13 +158,15 @@ private byte[] toDataBlock(final String sysEx) { } return data; } - + private static byte toHex(final int c1, final int c2) { - return (byte) (((c1 < HEX_CHAR_NUM ? c1 - HEX_CHAR_0 : c1 - HEX_CHAR_A) << 4) | (c2 < HEX_CHAR_NUM ? c2 - HEX_CHAR_0 : c2 - 87)); + return (byte) (((c1 < HEX_CHAR_NUM ? c1 - HEX_CHAR_0 : c1 - HEX_CHAR_A) << 4) | (c2 < HEX_CHAR_NUM ? c2 + - HEX_CHAR_0 : c2 - 87)); } - + private static int beatTimePattern(final byte[] data) { - return (data[PrintToClipData.DATA_OFFSET - 3] << 14) + (data[PrintToClipData.DATA_OFFSET - 2] << 7) + data[PrintToClipData.DATA_OFFSET - 1]; + return (data[PrintToClipData.DATA_OFFSET - 3] << 14) + (data[PrintToClipData.DATA_OFFSET - 2] << 7) + data[ + PrintToClipData.DATA_OFFSET - 1]; } - + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/SysExHandler.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/SysExHandler.java index f5eb2c49..eb3ca964 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/SysExHandler.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/SysExHandler.java @@ -1,13 +1,13 @@ package com.bitwig.extensions.controllers.novation.launchpadpromk3; -import com.bitwig.extension.controller.api.MidiOut; -import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.layers.ControlMode; - import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.layers.ControlMode; + public class SysExHandler { private static final String SYSEX_HEADER = "F0 00 20 29 02 0E "; private static final String MODE_CHANGE = SYSEX_HEADER + "00 %02X %02X 00 F7"; @@ -17,31 +17,31 @@ public class SysExHandler { private static final String PRINT_TO_CLIP_ENABLE = "F0 00 20 29 02 0E 18 %02X F7"; private static final String FADER_SET = "%02X %02X %02X %02X "; private static final String DAW_FADER = "F0 00 20 29 02 0E 00 %02X %02X 00 F7"; - + private static final String DAW_MODE = SYSEX_HEADER + "0E %02X F7"; private static final String NOTE_MODE = SYSEX_HEADER + "00 %02X F7"; - + private static final String DEVICE_REPLY = "f07e00060200202923010000000"; - + private static final String MODE_CHANGE_REPLY = "f0002029020e00"; private static final String PTC_HEAD = "f0002029020e03"; - + private final MidiOut midiOut; - + private LpBaseMode mode = LpBaseMode.SESSION; private int page = 0; private PrintToClipData printToClip = null; public List> printDataListeners = new ArrayList<>(); public List modeChangeListeners = new ArrayList<>(); - + public interface ModeChangeListener { void handleModeChange(LpBaseMode mode, int page); } - + public SysExHandler(final MidiProcessor midiProcessor) { midiOut = midiProcessor.getMidiOut(); } - + public void changeMode(final LpBaseMode newMode, final int page) { if (mode == newMode) { return; @@ -49,7 +49,7 @@ public void changeMode(final LpBaseMode newMode, final int page) { mode = newMode; setLayout(mode, page); } - + public void setFaderBank(final int orient, final ControlMode mode, final int[] colorIndex) { if (mode == ControlMode.NONE) { return; @@ -63,44 +63,44 @@ public void setFaderBank(final int orient, final ControlMode mode, final int[] c midiOut.sendSysex(sysEx.toString()); midiOut.sendSysex("F0 00 20 29 02 0E 00 0D F7"); } - + public void changeNoteMode(final boolean drumMode) { midiOut.sendSysex(String.format(NOTE_MODE, drumMode ? 2 : 1)); } - + public void enableFaderMode(final ControlMode mode, final boolean active) { if (mode.hasFaders()) { midiOut.sendSysex(String.format(DAW_FADER, active ? 1 : 0, mode.getBankId())); } } - + public LpBaseMode getMode() { return mode; } - + public void setLayout(final LpBaseMode mode, final int page) { midiOut.sendSysex(String.format(MODE_CHANGE, mode.getSysExId(), page)); } - + public void enableClipPrint(final boolean enabled) { midiOut.sendSysex(String.format(PRINT_TO_CLIP_ENABLE, enabled ? 1 : 0)); } - + public void setDawMode(final boolean on) { midiOut.sendSysex(String.format(DAW_MODE_CMD, on ? 1 : 0)); } - + public void requestLayout() { midiOut.sendSysex(LAYOUT_REQUEST); } - + public void deviceInquiry() { midiOut.sendSysex(DEVICE_INQUIRY); } - + public void handleSysEx(final String sysEx) { if (sysEx.startsWith(DEVICE_REPLY)) { - DebugOutLp.println(" >> Device Reply "); + LaunchpadProMk3ControllerExtension.println(" >> Device Reply "); setDawMode(true); setLayout(LpBaseMode.SESSION, 0); // int v1 = getValue(sysEx, 12); @@ -123,18 +123,18 @@ public void handleSysEx(final String sysEx) { printDataListeners.forEach(listener -> listener.accept(printToClip)); } } else { - DebugOutLp.println("Unknown = " + sysEx); + LaunchpadProMk3ControllerExtension.println("Unknown = " + sysEx); } } - + public void addPrintToClipDataListener(final Consumer listener) { printDataListeners.add(listener); } - + public void addModeChangeListener(final ModeChangeListener listener) { modeChangeListeners.add(listener); } - + private int getValue(final String sysExString, final int byteLocation) { final int index = byteLocation * 2; if (index >= sysExString.length()) { @@ -143,5 +143,5 @@ private int getValue(final String sysExString, final int byteLocation) { final String valueStr = sysExString.substring(index, index + 2); return Integer.parseInt(valueStr, 16); } - + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/ViewCursorControl.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/ViewCursorControl.java deleted file mode 100644 index f780a4a2..00000000 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/ViewCursorControl.java +++ /dev/null @@ -1,357 +0,0 @@ -package com.bitwig.extensions.controllers.novation.launchpadpromk3; - -import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.novation.commonsmk3.OverviewGrid; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.layers.SessionLayer; -import com.bitwig.extensions.framework.di.Component; -import com.bitwig.extensions.framework.di.Inject; - -import java.util.Optional; - -@Component -public class ViewCursorControl { - @Inject - Application application; - - private final CursorTrack cursorTrack; - private final DeviceBank deviceBank; - private final PinnableCursorDevice primaryDevice; - private final TrackBank trackBank; - private final PinnableCursorDevice cursorDevice; - private final Clip cursorClip; - - private final Track rootTrack; - private final ClipLauncherSlotBank mainTrackSlotBank; - private final Track largeFocusTrack; - private FocusSlot focusSlot; - private final TrackBank maxTrackBank; - private int queuedForPlaying = 0; - private int focusSceneIndex; - - private final OverviewGrid overviewGrid = new OverviewGrid(); - - public ViewCursorControl(final ControllerHost host) { - rootTrack = host.getProject().getRootTrackGroup(); - maxTrackBank = host.createTrackBank(64, 1, 1); - - setUpFocusScene(); - - trackBank = host.createTrackBank(8, 1, 8); - - trackBank.sceneBank().itemCount().addValueObserver(overviewGrid::setNumberOfScenes); - trackBank.channelCount().addValueObserver(overviewGrid::setNumberOfTracks); - trackBank.scrollPosition().addValueObserver(overviewGrid::setTrackPosition); - trackBank.sceneBank().scrollPosition().addValueObserver(overviewGrid::setScenePosition); - - cursorTrack = host.createCursorTrack(8, 8); - for (int i = 0; i < 8; i++) { - prepareTrack(trackBank.getItemAt(i)); - } - - cursorTrack.name().markInterested(); - cursorDevice = cursorTrack.createCursorDevice(); - cursorClip = host.createLauncherCursorClip(32 * 6, 127); - - cursorTrack.clipLauncherSlotBank().cursorIndex().addValueObserver(index -> { - // RemoteConsole.out.println(" => {}", index); - }); - prepareTrack(cursorTrack); - - deviceBank = cursorTrack.createDeviceBank(8); - primaryDevice = cursorTrack.createCursorDevice("drumdetection", "Pad Device", 8, - CursorDeviceFollowMode.FIRST_INSTRUMENT); - primaryDevice.hasDrumPads().markInterested(); - primaryDevice.exists().markInterested(); - - - final TrackBank singleTrackBank = host.createTrackBank(1, 0, 16); - singleTrackBank.scrollPosition().markInterested(); - singleTrackBank.followCursorTrack(cursorTrack); - largeFocusTrack = singleTrackBank.getItemAt(0); - prepareTrack(largeFocusTrack); - final BooleanValue equalsToCursorTrack = largeFocusTrack.createEqualsValue(cursorTrack); - equalsToCursorTrack.markInterested(); - - mainTrackSlotBank = largeFocusTrack.clipLauncherSlotBank(); - - for (int i = 0; i < 16; i++) { - final int index = i; - final ClipLauncherSlot slot = mainTrackSlotBank.getItemAt(i); - prepareSlot(slot); - - slot.isSelected().addValueObserver(selected -> { - if (selected) { - focusSlot = new FocusSlot(largeFocusTrack, slot, index, equalsToCursorTrack); - } - }); - } - } - - private void setUpFocusScene() { - maxTrackBank.sceneBank().scrollPosition().addValueObserver(scrollPos -> this.focusSceneIndex = scrollPos); - for (int i = 0; i < 64; i++) { - Track track = maxTrackBank.getItemAt(i); - final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(0); - slot.isPlaybackQueued().addValueObserver(queued -> { - if (queued) { - queuedForPlaying++; - } else if (queuedForPlaying > 0) { - queuedForPlaying--; - } - }); - } - } - - public boolean hasQueuedForPlaying() { - return queuedForPlaying > 0; - } - - public void focusScene(final int sceneIndex) { - maxTrackBank.sceneBank().scrollPosition().set(sceneIndex); - } - - private void prepareTrack(final Track track) { - track.arm().markInterested(); - track.monitorMode().markInterested(); - track.sourceSelector().hasAudioInputSelected().markInterested(); - track.sourceSelector().hasNoteInputSelected().markInterested(); - } - - private void prepareSlot(final ClipLauncherSlot slot) { - slot.isRecording().markInterested(); - slot.isRecordingQueued().markInterested(); - slot.hasContent().markInterested(); - slot.name().markInterested(); - slot.isPlaying().markInterested(); - slot.isSelected().markInterested(); - } - - public void scrollToOverview(final int trackIndex, final int sceneIndex) { - final int posX = trackIndex * 8; - final int posY = sceneIndex * 8; - if (posX < overviewGrid.getNumberOfTracks() && posY < overviewGrid.getNumberOfScenes()) { - trackBank.scrollPosition().set(posX); - trackBank.sceneBank().scrollPosition().set(posY); - } - } - - public boolean inOverviewGrid(final int trackIndex, final int sceneIndex) { - final int posX = trackIndex * 8; - final int posY = sceneIndex * 8; - return posX < overviewGrid.getNumberOfTracks() && posY < overviewGrid.getNumberOfScenes(); - } - - public boolean inOverviewGridFocus(final int trackIndex, final int sceneIndex) { - final int locX = overviewGrid.getTrackPosition() / 8; - final int locY = overviewGrid.getScenePosition() / 8; - return locX == trackIndex && locY == sceneIndex; - } - - public TrackBank getTrackBank() { - return trackBank; - } - - public CursorTrack getCursorTrack() { - return cursorTrack; - } - - public DeviceBank getDeviceBank() { - return deviceBank; - } - - public PinnableCursorDevice getPrimaryDevice() { - return primaryDevice; - } - - public PinnableCursorDevice getCursorDevice() { - return cursorDevice; - } - - public Clip getCursorClip() { - return cursorClip; - } - - public Track getRootTrack() { - return rootTrack; - } - - public void focusSlot(final FocusSlot slot) { - focusSlot = slot; - } - - public FocusSlot getFocusSlot() { - return focusSlot; - } - - public void createNewClip() { - if (focusSlot != null) { - final ClipLauncherSlot slot = focusSlot.getSlot(); - if (!slot.hasContent().get()) { - slot.createEmptyClip(4); - slot.select(); - } else { - cursorTrack.createNewLauncherClip(0); - // TODO try to move to next slot if possible - } - } else { - cursorTrack.createNewLauncherClip(0); - } - } - - private boolean trackOfFocusSlotArmed() { - return focusSlot != null && focusSlot.getTrack().arm().get(); - } - - public void globalRecordAction(final Transport transport) { - if (largeFocusTrack.arm().get() || trackOfFocusSlotArmed()) { - if (focusSlot != null) { - handleFocusedSlotOnArmedTrack(transport); - } else { - handleNoFocusSlotOnArmedTrack(transport); - } - } else if (focusSlot != null) { - handleRecordFocusSlotNotArmed(transport); - } else { - transport.isClipLauncherOverdubEnabled().toggle(); - } - } - - private void handleNoFocusSlotOnArmedTrack(final Transport transport) { - final Optional playingSlot = findPlayingSlot(); - if (playingSlot.isPresent()) { - toggleRecording(playingSlot.get(), transport); - } else { - findEmptySlotAndLaunch(transport, -1); - } - } - - private void handleFocusedSlotOnArmedTrack(final Transport transport) { - if (focusSlot.getTrack().arm().get()) { - if (focusSlot.isEmpty()) { - recordToEmptySlot(focusSlot.getSlot(), transport); - } else { - toggleRecording(focusSlot.getSlot(), transport); - } - } else { - findEmptySlotAndLaunch(transport, focusSlot.getSlotIndex()); - } - } - - private void handleRecordFocusSlotNotArmed(final Transport transport) { - final Track track = focusSlot.getTrack(); - if (canRecord(focusSlot.getTrack())) { - track.arm().set(true); - track.selectInEditor(); - if (!focusSlot.isEmpty()) { - toggleRecording(focusSlot.getSlot(), transport); - } else { - recordToEmptySlot(focusSlot.getSlot(), transport); - } - } else { - transport.isClipLauncherOverdubEnabled().toggle(); - } - } - - private void findEmptySlotAndLaunch(final Transport transport, final int slotIndex) { - final Optional slot = findCursorFirstEmptySlot(slotIndex); - if (slot.isPresent()) { - recordToEmptySlot(slot.get(), transport); - } else { - transport.isClipLauncherOverdubEnabled().toggle(); - } - } - - private boolean canRecord(final Track track) { - return track.sourceSelector().hasNoteInputSelected().get() || track.sourceSelector() - .hasAudioInputSelected() - .get(); - } - - public int getFocusSceneIndex() { - return focusSceneIndex; - } - - public Optional findCursorFirstEmptySlot(final int firstIndex) { - if (firstIndex >= 0 && firstIndex < 16) { - final ClipLauncherSlot slot = mainTrackSlotBank.getItemAt(firstIndex); - if (!slot.hasContent().get()) { - return Optional.of(slot); - } - } - for (int i = 0; i < 16; i++) { - final ClipLauncherSlot slot = mainTrackSlotBank.getItemAt(i); - if (!slot.hasContent().get()) { - return Optional.of(slot); - } - } - return Optional.empty(); - } - - private void toggleRecording(final ClipLauncherSlot slot, final Transport transport) { - if (slot.isRecordingQueued().get() || slot.isRecording().get()) { - slot.launch(); - transport.isClipLauncherOverdubEnabled().set(false); - } else if (!slot.isPlaying().get()) { - slot.launch(); - transport.isClipLauncherOverdubEnabled().set(true); - } else if (transport.isPlaying().get()) { - transport.isClipLauncherOverdubEnabled().toggle(); - } else { - transport.restart(); - slot.launch(); - transport.isClipLauncherOverdubEnabled().set(true); - } - } - - private void recordToEmptySlot(final ClipLauncherSlot slot, final Transport transport) { - if (!transport.isPlaying().get()) { - transport.restart(); - } - slot.select(); - slot.launch(); - transport.isClipLauncherOverdubEnabled().set(true); - } - - public Optional findPlayingSlot() { - for (int i = 0; i < 16; i++) { - final ClipLauncherSlot slot = mainTrackSlotBank.getItemAt(i); - if (slot.hasContent().get() && slot.isPlaying().get()) { - return Optional.of(slot); - } - } - return Optional.empty(); - } - - public void handleQuantize(final boolean shift) { - cursorClip.quantize(1.0); - final ClipLauncherSlot slot = cursorClip.clipLauncherSlot(); - slot.showInEditor(); - } - - public void handleDuplication(final boolean shift) { - if (focusSlot == null || focusSlot.isEmpty() || !focusSlot.isCursorTrack()) { - return; - } - if (!shift) { - cursorClip.duplicate(); - } else { - if (cursorClip.getLoopLength().get() < SessionLayer.MAX_LENGTH_FOR_DUPLICATE) { - cursorClip.duplicateContent(); - cursorClip.clipLauncherSlot().showInEditor(); - } - } - } - - public void handleClear(final boolean shift) { - if (focusSlot == null || focusSlot.isEmpty() || !focusSlot.isCursorTrack()) { - return; - } - if (!shift) { - cursorClip.clearSteps(); - cursorClip.clipLauncherSlot().showInEditor(); - } else { - focusSlot.getSlot().deleteObject(); - } - } - -} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/DrumLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/DrumLayer.java index d3996552..149133fc 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/DrumLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/DrumLayer.java @@ -1,138 +1,148 @@ package com.bitwig.extensions.controllers.novation.launchpadpromk3.layers; -import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.novation.commonsmk3.*; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.DebugOutLp; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.HwElements; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.DeviceBank; +import com.bitwig.extension.controller.api.DeviceMatcher; +import com.bitwig.extension.controller.api.DrumPad; +import com.bitwig.extension.controller.api.DrumPadBank; +import com.bitwig.extension.controller.api.NoteInput; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extension.controller.api.PlayingNote; +import com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup; +import com.bitwig.extensions.controllers.novation.commonsmk3.DrumButton; +import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; +import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; +import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; +import com.bitwig.extensions.controllers.novation.commonsmk3.SpecialDevices; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.LpProHwElements; import com.bitwig.extensions.controllers.novation.launchpadpromk3.LabelCcAssignments; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.ViewCursorControl; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.Inject; import com.bitwig.extensions.framework.di.PostConstruct; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - public class DrumLayer extends Layer { - - private DrumPadBank drumPadBank; - private NoteInput noteInput; - private final int[] padColors = new int[64]; - private final Integer[] noteTable = new Integer[128]; - private final boolean[] isPlaying = new boolean[128]; - private final Set padNotes = new HashSet<>(); - @Inject - private ViewCursorControl viewCursorControl; - @Inject - private TrackState trackState; - private int padsNoteOffset; - - public DrumLayer(final Layers layers) { - super(layers, "DRUM_PAD_LAYER"); - } - - @PostConstruct - public void init(final ControllerHost host, final MidiProcessor midiProcessor, final HwElements hwElements) { - DebugOutLp.println("Drum Pad Bank"); - - noteInput = midiProcessor.getMidiIn().createNoteInput("MIDI", "88????", "98????"); - noteInput.setShouldConsumeEvents(false); - final CursorTrack cursorTrack = viewCursorControl.getCursorTrack(); - cursorTrack.playingNotes().addValueObserver(this::handleNotes); - - final PinnableCursorDevice primaryDevice = viewCursorControl.getPrimaryDevice(); - final DeviceBank drumBank = cursorTrack.createDeviceBank(1); - final DeviceMatcher drumMatcher = host.createBitwigDeviceMatcher(SpecialDevices.DRUM.getUuid()); - drumBank.setDeviceMatcher(drumMatcher); - drumPadBank = primaryDevice.createDrumPadBank(64); - drumPadBank.scrollPosition().addValueObserver(index -> { - padsNoteOffset = index; - if (isActive()) { - applyNotes(padsNoteOffset); - } - }); - - final List drumButtons = hwElements.getDrumGridButtons(); - for (int i = 0; i < drumButtons.size(); i++) { - final int index = i; - final DrumPad pad = drumPadBank.getItemAt(i); - pad.exists().markInterested(); - pad.color().addValueObserver((r, g, b) -> padColors[index] = ColorLookup.toColor(r, g, b)); - final DrumButton button = drumButtons.get(i); - button.bindLight(this, () -> getPadState(index, pad)); - } - initNavigation(hwElements); - } - - private void initNavigation(final HwElements hwElements) { - final LabeledButton upButton = hwElements.getLabeledButton(LabelCcAssignments.UP); - final LabeledButton downButton = hwElements.getLabeledButton(LabelCcAssignments.DOWN); - final LabeledButton leftButton = hwElements.getLabeledButton(LabelCcAssignments.LEFT); - final LabeledButton rightButton = hwElements.getLabeledButton(LabelCcAssignments.RIGHT); - final RgbState pressedColor = RgbState.of(21); - final RgbState baseColor = RgbState.of(1); - - leftButton.bindRepeatHold(this, () -> handleNavigateVertical(-4)); - leftButton.bindHighlightButton(this, () -> (padsNoteOffset - 4) >= 4, baseColor, pressedColor); - rightButton.bindRepeatHold(this, () -> handleNavigateVertical(4)); - rightButton.bindHighlightButton(this, () -> (padsNoteOffset + 4 + 64) < 128, baseColor, pressedColor); - - downButton.bindRepeatHold(this, () -> handleNavigateVertical(-16)); - downButton.bindHighlightButton(this, () -> (padsNoteOffset - 16) >= 4, baseColor, pressedColor); - upButton.bindRepeatHold(this, () -> handleNavigateVertical(16)); - upButton.bindHighlightButton(this, () -> (padsNoteOffset + 16 + 64) < 128, baseColor, pressedColor); - } - - private void handleNavigateVertical(final int direction) { - final int newPosition = padsNoteOffset + direction; - if (newPosition >= 4 && newPosition + 64 < 128) { - drumPadBank.scrollBy(direction); - } - } - - - private RgbState getPadState(final int index, final DrumPad pad) { - final boolean playing = isPlaying(index); - if (pad.exists().get()) { - if (playing) { - return RgbState.WHITE; - } - if (padColors[index] == 0) { - return RgbState.of(trackState.getCursorColor()); - } - return RgbState.of(padColors[index]); - } - return playing ? RgbState.DIM_WHITE : RgbState.OFF; - } - - public boolean isPlaying(final int index) { - final int offset = padsNoteOffset + index; - if (offset < 128) { - return isPlaying[offset]; - } - return false; - } - - public void applyNotes(final int noteOffset) { - Arrays.fill(noteTable, -1); - for (int note = 0; note < 64; note++) { - final int value = noteOffset + note; - noteTable[36 + note] = value < 128 ? value : -1; - } - noteInput.setKeyTranslationTable(noteTable); - } - - private void handleNotes(final PlayingNote[] playingNotes) { - if (!isActive()) { - return; - } - Arrays.fill(isPlaying, false); - for (final PlayingNote playingNote : playingNotes) { - isPlaying[playingNote.pitch()] = true; - } - } - + + private DrumPadBank drumPadBank; + private NoteInput noteInput; + private final int[] padColors = new int[64]; + private final Integer[] noteTable = new Integer[128]; + private final boolean[] isPlaying = new boolean[128]; + private final Set padNotes = new HashSet<>(); + @Inject + private ViewCursorControl viewCursorControl; + @Inject + private TrackState trackState; + private int padsNoteOffset; + + public DrumLayer(final Layers layers) { + super(layers, "DRUM_PAD_LAYER"); + } + + @PostConstruct + public void init(final ControllerHost host, final MidiProcessor midiProcessor, final LpProHwElements hwElements) { + noteInput = midiProcessor.getMidiIn().createNoteInput("MIDI", "88????", "98????"); + noteInput.setShouldConsumeEvents(false); + final CursorTrack cursorTrack = viewCursorControl.getCursorTrack(); + cursorTrack.playingNotes().addValueObserver(this::handleNotes); + + final PinnableCursorDevice primaryDevice = viewCursorControl.getPrimaryDevice(); + final DeviceBank drumBank = cursorTrack.createDeviceBank(1); + final DeviceMatcher drumMatcher = host.createBitwigDeviceMatcher(SpecialDevices.DRUM.getUuid()); + drumBank.setDeviceMatcher(drumMatcher); + drumPadBank = primaryDevice.createDrumPadBank(64); + drumPadBank.scrollPosition().addValueObserver(index -> { + padsNoteOffset = index; + if (isActive()) { + applyNotes(padsNoteOffset); + } + }); + + final List drumButtons = hwElements.getDrumGridButtons(); + for (int i = 0; i < drumButtons.size(); i++) { + final int index = i; + final DrumPad pad = drumPadBank.getItemAt(i); + pad.exists().markInterested(); + pad.color().addValueObserver((r, g, b) -> padColors[index] = ColorLookup.toColor(r, g, b)); + final DrumButton button = drumButtons.get(i); + button.bindLight(this, () -> getPadState(index, pad)); + } + initNavigation(hwElements); + } + + private void initNavigation(final LpProHwElements hwElements) { + final LabeledButton upButton = hwElements.getLabeledButton(LabelCcAssignments.UP); + final LabeledButton downButton = hwElements.getLabeledButton(LabelCcAssignments.DOWN); + final LabeledButton leftButton = hwElements.getLabeledButton(LabelCcAssignments.LEFT); + final LabeledButton rightButton = hwElements.getLabeledButton(LabelCcAssignments.RIGHT); + final RgbState pressedColor = RgbState.of(21); + final RgbState baseColor = RgbState.of(1); + + leftButton.bindRepeatHold(this, () -> handleNavigateVertical(-4)); + leftButton.bindHighlightButton(this, () -> (padsNoteOffset - 4) >= 4, baseColor, pressedColor); + rightButton.bindRepeatHold(this, () -> handleNavigateVertical(4)); + rightButton.bindHighlightButton(this, () -> (padsNoteOffset + 4 + 64) < 128, baseColor, pressedColor); + + downButton.bindRepeatHold(this, () -> handleNavigateVertical(-16)); + downButton.bindHighlightButton(this, () -> (padsNoteOffset - 16) >= 4, baseColor, pressedColor); + upButton.bindRepeatHold(this, () -> handleNavigateVertical(16)); + upButton.bindHighlightButton(this, () -> (padsNoteOffset + 16 + 64) < 128, baseColor, pressedColor); + } + + private void handleNavigateVertical(final int direction) { + final int newPosition = padsNoteOffset + direction; + if (newPosition >= 4 && newPosition + 64 < 128) { + drumPadBank.scrollBy(direction); + } + } + + + private RgbState getPadState(final int index, final DrumPad pad) { + final boolean playing = isPlaying(index); + if (pad.exists().get()) { + if (playing) { + return RgbState.WHITE; + } + if (padColors[index] == 0) { + return RgbState.of(trackState.getCursorColor()); + } + return RgbState.of(padColors[index]); + } + return playing ? RgbState.DIM_WHITE : RgbState.OFF; + } + + public boolean isPlaying(final int index) { + final int offset = padsNoteOffset + index; + if (offset < 128) { + return isPlaying[offset]; + } + return false; + } + + public void applyNotes(final int noteOffset) { + Arrays.fill(noteTable, -1); + for (int note = 0; note < 64; note++) { + final int value = noteOffset + note; + noteTable[36 + note] = value < 128 ? value : -1; + } + noteInput.setKeyTranslationTable(noteTable); + } + + private void handleNotes(final PlayingNote[] playingNotes) { + if (!isActive()) { + return; + } + Arrays.fill(isPlaying, false); + for (final PlayingNote playingNote : playingNotes) { + isPlaying[playingNote.pitch()] = true; + } + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/NotePlayingLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/NotePlayingLayer.java index ce8e5b76..bdd8efef 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/NotePlayingLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/NotePlayingLayer.java @@ -1,35 +1,35 @@ package com.bitwig.extensions.controllers.novation.launchpadpromk3.layers; +import java.util.ArrayList; +import java.util.List; + import com.bitwig.extension.controller.api.CursorTrack; import com.bitwig.extension.controller.api.PlayingNote; import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.ViewCursorControl; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.Inject; import com.bitwig.extensions.framework.di.PostConstruct; -import java.util.ArrayList; -import java.util.List; - public class NotePlayingLayer extends Layer { @Inject private ViewCursorControl viewCursorControl; @Inject private MidiProcessor midiProcessor; - + private final List lastNotes = new ArrayList<>(); - + public NotePlayingLayer(final Layers layers) { super(layers, "NOTE_PLAYING_LAYER"); } - + @PostConstruct public void init() { final CursorTrack cursorTrack = viewCursorControl.getCursorTrack(); cursorTrack.playingNotes().addValueObserver(this::handleNotes); } - + private void handleNotes(final PlayingNote[] playingNotes) { for (final Integer playing : lastNotes) { midiProcessor.sendMidi(0x8f, playing, 21); diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/OverviewLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/OverviewLayer.java deleted file mode 100644 index b85c1163..00000000 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/OverviewLayer.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.bitwig.extensions.controllers.novation.launchpadpromk3.layers; - -import com.bitwig.extensions.controllers.novation.commonsmk3.GridButton; -import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.HwElements; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.ViewCursorControl; -import com.bitwig.extensions.framework.Layer; -import com.bitwig.extensions.framework.Layers; -import com.bitwig.extensions.framework.di.Inject; -import com.bitwig.extensions.framework.di.PostConstruct; - -public class OverviewLayer extends Layer { - - @Inject - private ViewCursorControl viewCursorControl; - - public OverviewLayer(final Layers layers) { - super(layers, "SESSION_OVERVIEW_LAYER"); - } - - @PostConstruct - public void initView(final HwElements hwElements, final ViewCursorControl viewCursorControl) { - initClipControl(hwElements); - } - - private void initClipControl(final HwElements hwElements) { - for (int i = 0; i < 8; i++) { - final int trackIndex = i; - for (int j = 0; j < 8; j++) { - final int sceneIndex = j; - final GridButton button = hwElements.getGridButton(sceneIndex, trackIndex); - button.bindPressed(this, () -> handleSelection(trackIndex, sceneIndex)); - button.bindLight(this, () -> getState(trackIndex, sceneIndex)); - } - } - } - - private void handleSelection(final int trackIndex, final int sceneIndex) { - viewCursorControl.scrollToOverview(trackIndex, sceneIndex); - } - - private RgbState getState(final int trackIndex, final int sceneIndex) { - if (viewCursorControl.inOverviewGridFocus(trackIndex, sceneIndex)) { - return RgbState.of(21); - } - if (viewCursorControl.inOverviewGrid(trackIndex, sceneIndex)) { - return RgbState.of(8); - } - return RgbState.OFF; - } - -} diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/SceneLaunchLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/SceneLaunchLayer.java index f3d7bdd0..37d18d8a 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/SceneLaunchLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/SceneLaunchLayer.java @@ -1,128 +1,129 @@ package com.bitwig.extensions.controllers.novation.launchpadpromk3.layers; -import com.bitwig.extension.controller.api.*; +import com.bitwig.extension.controller.api.Action; +import com.bitwig.extension.controller.api.Application; +import com.bitwig.extension.controller.api.Scene; +import com.bitwig.extension.controller.api.SceneBank; +import com.bitwig.extension.controller.api.TrackBank; import com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup; 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.launchpadpromk3.HwElements; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.LpProHwElements; import com.bitwig.extensions.controllers.novation.launchpadpromk3.LppPreferences; import com.bitwig.extensions.controllers.novation.launchpadpromk3.ModifierStates; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.ViewCursorControl; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.Inject; public class SceneLaunchLayer extends Layer { - @Inject - private ModifierStates modifiers; - @Inject - private ViewCursorControl viewCursorControl; - @Inject - private LppPreferences preferences; - - private Action sceneCreateAction; - private final int[] colorIndex = new int[8]; - private int sceneOffset; - private final Layer verticalLayer; - private final Layer horizontalLayer; - private PanelLayout panelLayout = PanelLayout.VERTICAL; - - public SceneLaunchLayer(final Layers layers, final ViewCursorControl viewCursorControl, final HwElements hwElements, - LppPreferences preferences) { - super(layers, "SCENE_LAYER"); - verticalLayer = new Layer(layers, "SCENE_VERTICAL"); - horizontalLayer = new Layer(layers, "SCENE_HORIZONTAL"); - - final TrackBank trackBank = viewCursorControl.getTrackBank(); - trackBank.setShouldShowClipLauncherFeedback(true); - final SceneBank sceneBank = trackBank.sceneBank(); - sceneBank.scrollPosition().addValueObserver(value -> sceneOffset = value); - initSceneControl(hwElements, sceneBank); - preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> setLayout(newValue))); - panelLayout = preferences.getPanelLayout().get(); - } - - private void initSceneControl(final HwElements hwElements, final SceneBank sceneBank) { - sceneBank.setIndication(true); - sceneBank.cursorIndex().markInterested(); - for (int i = 0; i < 8; i++) { - final int index = i; - final Scene scene = sceneBank.getScene(index); - final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(index); - scene.clipCount().markInterested(); - scene.color().addValueObserver((r, g, b) -> colorIndex[index] = ColorLookup.toColor(r, g, b)); - sceneButton.bindPressed(verticalLayer, pressed -> handleScene(pressed, scene, index)); - sceneButton.bindLight(verticalLayer, () -> getSceneColor(index, scene)); - final LabeledButton trackButton = hwElements.getTrackSelectButtons().get(index); - trackButton.bindPressed(horizontalLayer, pressed -> handleScene(pressed, scene, index)); - trackButton.bindLight(horizontalLayer, () -> getSceneColor(index, scene)); - } - } - - @Inject - public void setApplication(final Application application) { - sceneCreateAction = application.getAction("Create Scene From Playing Launcher Clips"); - } - - public void setLayout(final PanelLayout layout) { - panelLayout = layout; - horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); - verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); - } - - private void handleScene(final boolean pressed, final Scene scene, final int sceneIndex) { - if (pressed) { - if (modifiers.isClear()) { - scene.deleteObject(); - } else if (modifiers.isDuplicate()) { - if (modifiers.isShift()) { - sceneCreateAction.invoke(); - } else { - scene.nextSceneInsertionPoint().copySlotsOrScenes(scene); + @Inject + private ModifierStates modifiers; + @Inject + private ViewCursorControl viewCursorControl; + @Inject + private LppPreferences preferences; + + private Action sceneCreateAction; + private final int[] colorIndex = new int[8]; + private int sceneOffset; + private final Layer verticalLayer; + private final Layer horizontalLayer; + private PanelLayout panelLayout = PanelLayout.VERTICAL; + + public SceneLaunchLayer(final Layers layers, final ViewCursorControl viewCursorControl, + final LpProHwElements hwElements, final LppPreferences preferences) { + super(layers, "SCENE_LAYER"); + verticalLayer = new Layer(layers, "SCENE_VERTICAL"); + horizontalLayer = new Layer(layers, "SCENE_HORIZONTAL"); + + final TrackBank trackBank = viewCursorControl.getTrackBank(); + trackBank.setShouldShowClipLauncherFeedback(true); + final SceneBank sceneBank = trackBank.sceneBank(); + sceneBank.scrollPosition().addValueObserver(value -> sceneOffset = value); + initSceneControl(hwElements, sceneBank); + preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> setLayout(newValue))); + panelLayout = preferences.getPanelLayout().get(); + } + + private void initSceneControl(final LpProHwElements hwElements, final SceneBank sceneBank) { + sceneBank.setIndication(true); + sceneBank.cursorIndex().markInterested(); + for (int i = 0; i < 8; i++) { + final int index = i; + final Scene scene = sceneBank.getScene(index); + final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(index); + scene.clipCount().markInterested(); + scene.color().addValueObserver((r, g, b) -> colorIndex[index] = ColorLookup.toColor(r, g, b)); + sceneButton.bindPressed(verticalLayer, pressed -> handleScene(pressed, scene, index)); + sceneButton.bindLight(verticalLayer, () -> getSceneColor(index, scene)); + final LabeledButton trackButton = hwElements.getTrackSelectButtons().get(index); + trackButton.bindPressed(horizontalLayer, pressed -> handleScene(pressed, scene, index)); + trackButton.bindLight(horizontalLayer, () -> getSceneColor(index, scene)); + } + } + + @Inject + public void setApplication(final Application application) { + sceneCreateAction = application.getAction("Create Scene From Playing Launcher Clips"); + } + + public void setLayout(final PanelLayout layout) { + panelLayout = layout; + horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); + verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); + } + + private void handleScene(final boolean pressed, final Scene scene, final int sceneIndex) { + if (pressed) { + if (modifiers.isClear()) { + scene.deleteObject(); + } else if (modifiers.isDuplicate()) { + if (modifiers.isShift()) { + sceneCreateAction.invoke(); + } else { + scene.nextSceneInsertionPoint().copySlotsOrScenes(scene); + } + } else if (modifiers.onlyShift()) { + if (preferences.getAltModeWithShift().get()) { + scene.selectInEditor(); + scene.launchAlt(); + } else { + scene.selectInEditor(); + } + } else if (modifiers.noModifier()) { + scene.launch(); } - } else if (modifiers.onlyShift()) { - if (preferences.getAltModeWithShift().get()) { - viewCursorControl.focusScene(sceneIndex + sceneOffset); - scene.selectInEditor(); - scene.launchAlt(); - } else { - scene.selectInEditor(); - viewCursorControl.focusScene(sceneIndex + sceneOffset); + } else { + if (modifiers.onlyShift()) { + scene.launchReleaseAlt(); + } else if (modifiers.noModifier() && preferences.getAltModeWithShift().get()) { + scene.launchRelease(); } - } else if (modifiers.noModifier()) { - viewCursorControl.focusScene(sceneIndex + sceneOffset); - scene.launch(); - } - } else { - if (modifiers.onlyShift()) { - scene.launchReleaseAlt(); - } else if (modifiers.noModifier() && preferences.getAltModeWithShift().get()) { - scene.launchRelease(); - } - } - } - - private RgbState getSceneColor(final int sceneIndex, final Scene scene) { - if (scene.clipCount().get() > 0) { - if (sceneOffset + sceneIndex == viewCursorControl.getFocusSceneIndex() && viewCursorControl.hasQueuedForPlaying()) { - return RgbState.GREEN_FLASH; - } - return RgbState.of(colorIndex[sceneIndex]); - } - return RgbState.OFF; - } - - @Override - protected void onActivate() { - super.onActivate(); - horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); - verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); - } - - @Override - protected void onDeactivate() { - horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); - verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); - } + } + } + + private RgbState getSceneColor(final int sceneIndex, final Scene scene) { + if (scene.clipCount().get() > 0) { + if (viewCursorControl.hasQueuedForPlaying(sceneOffset + sceneIndex)) { + return RgbState.GREEN_FLASH; + } + return RgbState.of(colorIndex[sceneIndex]); + } + return RgbState.OFF; + } + + @Override + protected void onActivate() { + super.onActivate(); + horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); + verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); + } + + @Override + protected void onDeactivate() { + horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); + verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); + } } 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 4419f937..d6701a5b 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 @@ -1,227 +1,242 @@ package com.bitwig.extensions.controllers.novation.launchpadpromk3.layers; -import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.novation.commonsmk3.*; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.*; +import com.bitwig.extension.controller.api.BooleanValue; +import com.bitwig.extension.controller.api.Clip; +import com.bitwig.extension.controller.api.ClipLauncherSlot; +import com.bitwig.extension.controller.api.Scene; +import com.bitwig.extension.controller.api.SceneBank; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.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.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.LpProHwElements; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.LabelCcAssignments; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.LppPreferences; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.ModifierStates; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.Inject; import com.bitwig.extensions.framework.di.PostConstruct; public class SessionLayer extends AbstractLpSessionLayer { - public static final double MAX_LENGTH_FOR_DUPLICATE = 512 * 4.0; - - private Clip cursorClip; - @Inject - private ModifierStates modifiers; - @Inject - private ViewCursorControl viewCursorControl; - @Inject - private LppPreferences preferences; - private final Layer verticalLayer; - private final Layer horizontalLayer; - private PanelLayout panelLayout = PanelLayout.VERTICAL; - - public SessionLayer(final Layers layers) { - super(layers); - verticalLayer = new Layer(layers, "VERTICAL_LAUNCHING"); - horizontalLayer = new Layer(layers, "HORIZONTAL_LAUNCHING"); - } - - @PostConstruct - protected void init(final Transport transport, final HwElements hwElements, LppPreferences preferences) { - clipLauncherOverdub = transport.isClipLauncherOverdubEnabled(); - clipLauncherOverdub.markInterested(); - cursorClip = viewCursorControl.getCursorClip(); - cursorClip.getLoopLength().markInterested(); - - final TrackBank trackBank = viewCursorControl.getTrackBank(); - trackBank.setShouldShowClipLauncherFeedback(true); - - final SceneBank sceneBank = trackBank.sceneBank(); - final Scene targetScene = trackBank.sceneBank().getScene(0); - targetScene.clipCount().markInterested(); - initClipControl(hwElements, trackBank); - initNavigation(hwElements, trackBank, sceneBank); - preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> setLayout(newValue))); - panelLayout = preferences.getPanelLayout().get(); - } - - @Override - public void setLayout(final PanelLayout layout) { - panelLayout = layout; - horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); - verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); - } - - private void initNavigation(final HwElements hwElements, final TrackBank trackBank, final SceneBank sceneBank) { - final LabeledButton upButton = hwElements.getLabeledButton(LabelCcAssignments.UP); - final LabeledButton downButton = hwElements.getLabeledButton(LabelCcAssignments.DOWN); - final LabeledButton leftButton = hwElements.getLabeledButton(LabelCcAssignments.LEFT); - final LabeledButton rightButton = hwElements.getLabeledButton(LabelCcAssignments.RIGHT); - sceneBank.canScrollForwards().markInterested(); - sceneBank.canScrollBackwards().markInterested(); - trackBank.canScrollForwards().markInterested(); - trackBank.canScrollBackwards().markInterested(); - final RgbState baseColor = RgbState.of(1); - final RgbState pressedColor = RgbState.of(3); - - // TODO Shift function needed - downButton.bindRepeatHold(verticalLayer, () -> sceneBank.scrollBy(1)); - downButton.bindHighlightButton(verticalLayer, sceneBank.canScrollForwards(), baseColor, pressedColor); - - upButton.bindRepeatHold(verticalLayer, () -> sceneBank.scrollBy(-1)); - upButton.bindHighlightButton(verticalLayer, sceneBank.canScrollBackwards(), baseColor, pressedColor); - - leftButton.bindRepeatHold(verticalLayer, () -> trackBank.scrollBy(-1)); - leftButton.bindHighlightButton(verticalLayer, trackBank.canScrollBackwards(), baseColor, pressedColor); - - rightButton.bindRepeatHold(verticalLayer, () -> trackBank.scrollBy(1)); - rightButton.bindHighlightButton(verticalLayer, trackBank.canScrollForwards(), baseColor, pressedColor); - - // horizonal - rightButton.bindRepeatHold(horizontalLayer, () -> sceneBank.scrollBy(1)); - rightButton.bindHighlightButton(horizontalLayer, sceneBank.canScrollForwards(), baseColor, pressedColor); - - leftButton.bindRepeatHold(horizontalLayer, () -> sceneBank.scrollBy(-1)); - leftButton.bindHighlightButton(horizontalLayer, sceneBank.canScrollBackwards(), baseColor, pressedColor); - - upButton.bindRepeatHold(horizontalLayer, () -> trackBank.scrollBy(-1)); - upButton.bindHighlightButton(horizontalLayer, trackBank.canScrollBackwards(), baseColor, pressedColor); - - downButton.bindRepeatHold(horizontalLayer, () -> trackBank.scrollBy(1)); - downButton.bindHighlightButton(horizontalLayer, trackBank.canScrollForwards(), baseColor, pressedColor); - - } - - private void initClipControl(final HwElements hwElements, final TrackBank trackBank) { - for (int i = 0; i < 8; i++) { - final int trackIndex = i; - final Track track = trackBank.getItemAt(trackIndex); - final BooleanValue equalsToCursorTrack = track.createEqualsValue(viewCursorControl.getCursorTrack()); - equalsToCursorTrack.markInterested(); - markTrack(track); - for (int j = 0; j < 8; j++) { - final int sceneIndex = j; - final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); - slot.isSelected() - .addValueObserver( - selected -> handleSlotSelection(selected, track, sceneIndex, slot, equalsToCursorTrack)); - prepareSlot(slot, sceneIndex, trackIndex); - - final GridButton button = hwElements.getGridButton(sceneIndex, trackIndex); - button.bindPressed(verticalLayer, pressed -> handleSlot(pressed, track, slot, trackIndex)); - button.bindLight(verticalLayer, () -> getState(track, slot, trackIndex, sceneIndex)); - - final GridButton buttonHorizontal = hwElements.getGridButton(trackIndex, sceneIndex); - buttonHorizontal.bindPressed(horizontalLayer, pressed -> handleSlot(pressed, track, slot, trackIndex)); - buttonHorizontal.bindLight(horizontalLayer, () -> getState(track, slot, trackIndex, sceneIndex)); - } - } - } - - private void handleSlotSelection(final boolean wasSelected, final Track track, final int sceneIndex, - final ClipLauncherSlot slot, BooleanValue equalsToCursorTrack) { - if (wasSelected) { - viewCursorControl.focusSlot(new FocusSlot(track, slot, sceneIndex, equalsToCursorTrack)); - } - } - - private void markTrack(final Track track) { - track.isStopped().markInterested(); - track.mute().markInterested(); - track.solo().markInterested(); - track.isQueuedForStop().markInterested(); - track.arm().markInterested(); - } - - private void prepareSlot(final ClipLauncherSlot slot, final int sceneIndex, final int trackIndex) { - slot.hasContent().markInterested(); - slot.isPlaying().markInterested(); - slot.isStopQueued().markInterested(); - slot.isRecordingQueued().markInterested(); - slot.isRecording().markInterested(); - slot.isPlaybackQueued().markInterested(); - slot.color().addValueObserver((r, g, b) -> colorIndex[sceneIndex][trackIndex] = ColorLookup.toColor(r, g, b)); - } - - private void handleSlot(final boolean pressed, final Track track, final ClipLauncherSlot slot, - final int trackIndex) { - if (pressed) { - if (modifiers.isShift()) { - if (modifiers.isQuantize()) { - doQuantize(slot); - } else if (modifiers.isClear()) { - if (slot.hasContent().get()) { - selectClip(slot); - cursorClip.clearSteps(); - } - } else if (modifiers.isDuplicate()) { - if (slot.hasContent().get()) { - selectClip(slot); - if (cursorClip.getLoopLength().get() < 128.0) { - cursorClip.duplicateContent(); - slot.showInEditor(); - } - } - } else { - if (preferences.getAltModeWithShift().get() && slot.hasContent().get()) { - slot.launchAlt(); - } else { - slot.select(); - } + private Clip cursorClip; + @Inject + private ModifierStates modifiers; + @Inject + private ViewCursorControl viewCursorControl; + @Inject + private LppPreferences preferences; + private final Layer verticalLayer; + private final Layer horizontalLayer; + private PanelLayout panelLayout = PanelLayout.VERTICAL; + + public SessionLayer(final Layers layers) { + super(layers); + verticalLayer = new Layer(layers, "VERTICAL_LAUNCHING"); + horizontalLayer = new Layer(layers, "HORIZONTAL_LAUNCHING"); + } + + @PostConstruct + protected void init(final Transport transport, final LpProHwElements hwElements, final LppPreferences preferences) { + clipLauncherOverdub = transport.isClipLauncherOverdubEnabled(); + clipLauncherOverdub.markInterested(); + cursorClip = viewCursorControl.getCursorClip(); + cursorClip.getLoopLength().markInterested(); + + final TrackBank trackBank = viewCursorControl.getTrackBank(); + trackBank.setShouldShowClipLauncherFeedback(true); + + final SceneBank sceneBank = trackBank.sceneBank(); + final Scene targetScene = trackBank.sceneBank().getScene(0); + targetScene.clipCount().markInterested(); + initClipControl(hwElements, trackBank); + initNavigation(hwElements, trackBank, sceneBank); + preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> setLayout(newValue))); + panelLayout = preferences.getPanelLayout().get(); + } + + @Override + public void setLayout(final PanelLayout layout) { + panelLayout = layout; + horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); + verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); + } + + private void initNavigation(final LpProHwElements hwElements, final TrackBank trackBank, + final SceneBank sceneBank) { + final LabeledButton upButton = hwElements.getLabeledButton(LabelCcAssignments.UP); + final LabeledButton downButton = hwElements.getLabeledButton(LabelCcAssignments.DOWN); + final LabeledButton leftButton = hwElements.getLabeledButton(LabelCcAssignments.LEFT); + final LabeledButton rightButton = hwElements.getLabeledButton(LabelCcAssignments.RIGHT); + sceneBank.canScrollForwards().markInterested(); + sceneBank.canScrollBackwards().markInterested(); + trackBank.canScrollForwards().markInterested(); + trackBank.canScrollBackwards().markInterested(); + final RgbState baseColor = RgbState.of(1); + final RgbState pressedColor = RgbState.of(3); + + // TODO Shift function needed + downButton.bindRepeatHold(verticalLayer, () -> sceneBank.scrollBy(1)); + downButton.bindHighlightButton(verticalLayer, sceneBank.canScrollForwards(), baseColor, pressedColor); + + upButton.bindRepeatHold(verticalLayer, () -> sceneBank.scrollBy(-1)); + upButton.bindHighlightButton(verticalLayer, sceneBank.canScrollBackwards(), baseColor, pressedColor); + + leftButton.bindRepeatHold(verticalLayer, () -> trackBank.scrollBy(-1)); + leftButton.bindHighlightButton(verticalLayer, trackBank.canScrollBackwards(), baseColor, pressedColor); + + rightButton.bindRepeatHold(verticalLayer, () -> trackBank.scrollBy(1)); + rightButton.bindHighlightButton(verticalLayer, trackBank.canScrollForwards(), baseColor, pressedColor); + + // horizonal + rightButton.bindRepeatHold(horizontalLayer, () -> sceneBank.scrollBy(1)); + rightButton.bindHighlightButton(horizontalLayer, sceneBank.canScrollForwards(), baseColor, pressedColor); + + leftButton.bindRepeatHold(horizontalLayer, () -> sceneBank.scrollBy(-1)); + leftButton.bindHighlightButton(horizontalLayer, sceneBank.canScrollBackwards(), baseColor, pressedColor); + + upButton.bindRepeatHold(horizontalLayer, () -> trackBank.scrollBy(-1)); + upButton.bindHighlightButton(horizontalLayer, trackBank.canScrollBackwards(), baseColor, pressedColor); + + downButton.bindRepeatHold(horizontalLayer, () -> trackBank.scrollBy(1)); + downButton.bindHighlightButton(horizontalLayer, trackBank.canScrollForwards(), baseColor, pressedColor); + + } + + private void initClipControl(final LpProHwElements hwElements, final TrackBank trackBank) { + for (int i = 0; i < 8; i++) { + final int trackIndex = i; + final Track track = trackBank.getItemAt(trackIndex); + final BooleanValue equalsToCursorTrack = track.createEqualsValue(viewCursorControl.getCursorTrack()); + equalsToCursorTrack.markInterested(); + markTrack(track); + for (int j = 0; j < 8; j++) { + final int sceneIndex = j; + final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); + slot.isSelected().addValueObserver( + selected -> handleSlotSelection(selected, track, sceneIndex, slot, equalsToCursorTrack)); + prepareSlot(slot, sceneIndex, trackIndex); + + final GridButton button = hwElements.getGridButton(sceneIndex, trackIndex); + button.bindPressed(verticalLayer, pressed -> handleSlot(pressed, track, slot, trackIndex)); + button.bindLight(verticalLayer, () -> getState(track, slot, trackIndex, sceneIndex)); + + final GridButton buttonHorizontal = hwElements.getGridButton(trackIndex, sceneIndex); + buttonHorizontal.bindPressed(horizontalLayer, pressed -> handleSlot(pressed, track, slot, trackIndex)); + buttonHorizontal.bindLight(horizontalLayer, () -> getState(track, slot, trackIndex, sceneIndex)); } - } else { - handleSlotPressNoShift(slot); - } - } else if (preferences.getAltModeWithShift().get()) { - if (slot.hasContent().get()) { + } + } + + private void handleSlotSelection(final boolean wasSelected, final Track track, final int sceneIndex, + final ClipLauncherSlot slot, final BooleanValue equalsToCursorTrack) { + if (wasSelected) { + viewCursorControl.focusSlot(new FocusSlot(track, slot, sceneIndex, equalsToCursorTrack)); + } + } + + private void markTrack(final Track track) { + track.isStopped().markInterested(); + track.mute().markInterested(); + track.solo().markInterested(); + track.isQueuedForStop().markInterested(); + track.arm().markInterested(); + } + + private void prepareSlot(final ClipLauncherSlot slot, final int sceneIndex, final int trackIndex) { + slot.hasContent().markInterested(); + slot.isPlaying().markInterested(); + slot.isStopQueued().markInterested(); + slot.isRecordingQueued().markInterested(); + slot.isRecording().markInterested(); + slot.isPlaybackQueued().markInterested(); + slot.color().addValueObserver((r, g, b) -> colorIndex[sceneIndex][trackIndex] = ColorLookup.toColor(r, g, b)); + } + + private void handleSlot(final boolean pressed, final Track track, final ClipLauncherSlot slot, + final int trackIndex) { + if (pressed) { if (modifiers.isShift()) { - slot.launchReleaseAlt(); - } else if (!modifiers.anyModifierHeld()) { - slot.launchRelease(); + if (modifiers.isQuantize()) { + doQuantize(slot); + } else if (modifiers.isClear()) { + if (slot.hasContent().get()) { + selectClip(slot); + cursorClip.clearSteps(); + } + } else if (modifiers.isDuplicate()) { + if (slot.hasContent().get()) { + selectClip(slot); + if (cursorClip.getLoopLength().get() < 128.0) { + cursorClip.duplicateContent(); + slot.showInEditor(); + } + } + } else { + if (preferences.getAltModeWithShift().get() && slot.hasContent().get()) { + slot.launchAlt(); + } else { + slot.select(); + } + } + } else { + handleSlotPressNoShift(slot); } - } - } - } - - private void handleSlotPressNoShift(final ClipLauncherSlot slot) { - if (modifiers.isQuantize()) { - doQuantize(slot); - } else if (modifiers.isClear()) { - slot.deleteObject(); - } else if (modifiers.isDuplicate()) { - slot.duplicateClip(); - } else { - slot.launch(); - slot.select(); - } - } - - private void doQuantize(final ClipLauncherSlot slot) { - if (slot.hasContent().get()) { - selectClip(slot); - cursorClip.quantize(1.0); - slot.showInEditor(); - } - } - - private void selectClip(final ClipLauncherSlot slot) { - slot.select(); - } - - @Override - protected void onActivate() { - super.onActivate(); - horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); - verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); - } - - @Override - protected void onDeactivate() { - super.onDeactivate(); - horizontalLayer.setIsActive(false); - verticalLayer.setIsActive(false); - } - + } else if (preferences.getAltModeWithShift().get()) { + if (slot.hasContent().get()) { + if (modifiers.isShift()) { + slot.launchReleaseAlt(); + } else if (!modifiers.anyModifierHeld()) { + slot.launchRelease(); + } + } + } + } + + private void handleSlotPressNoShift(final ClipLauncherSlot slot) { + if (modifiers.isQuantize()) { + doQuantize(slot); + } else if (modifiers.isClear()) { + slot.deleteObject(); + } else if (modifiers.isDuplicate()) { + slot.duplicateClip(); + } else { + slot.launch(); + slot.select(); + } + } + + private void doQuantize(final ClipLauncherSlot slot) { + if (slot.hasContent().get()) { + selectClip(slot); + cursorClip.quantize(1.0); + slot.showInEditor(); + } + } + + private void selectClip(final ClipLauncherSlot slot) { + slot.select(); + } + + @Override + protected void onActivate() { + super.onActivate(); + horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); + verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + horizontalLayer.setIsActive(false); + verticalLayer.setIsActive(false); + } + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/TrackControlLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/TrackControlLayer.java index 5156c3a1..d1c548ed 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/TrackControlLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/TrackControlLayer.java @@ -1,268 +1,272 @@ package com.bitwig.extensions.controllers.novation.launchpadpromk3.layers; -import com.bitwig.extension.controller.api.*; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +import com.bitwig.extension.controller.api.SettableBeatTimeValue; +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.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.launchpadpromk3.HwElements; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.LpProHwElements; import com.bitwig.extensions.controllers.novation.launchpadpromk3.LppPreferences; import com.bitwig.extensions.controllers.novation.launchpadpromk3.ModifierStates; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.ViewCursorControl; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.Inject; import com.bitwig.extensions.framework.di.PostConstruct; import com.bitwig.extensions.framework.values.ValueObject; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; - public class TrackControlLayer extends Layer { - - private final boolean[] selectionField = new boolean[8]; - private final HashSet heldTracksButtons = new HashSet<>(); - private final HashMap modeLayers = new HashMap<>(); - - @Inject - private ModifierStates modifiers; - @Inject - private TrackState trackState; - @Inject - private LppPreferences preferences; - - private SettableBeatTimeValue postRecordingTimeOffset; - - private final Layer verticalLayer; - private final Layer horizontalLayer; - private final Layer fixedLengthLayer; - private PanelLayout panelLayout = PanelLayout.VERTICAL; - private TrackModeButtonMode trackMode; - - public TrackControlLayer(final Layers layers) { - super(layers, "TRACK_CONTROL_LAYER"); - verticalLayer = new Layer(layers, "TRACK_VERTICAL"); - horizontalLayer = new Layer(layers, "TRACK_HORIZONTAL"); - fixedLengthLayer = new Layer(layers, "FIXED_LENGTH"); - } - - @PostConstruct - public void init(final HwElements hwElements, final ViewCursorControl viewCursorControl, - LppPreferences preferences) { - - final TrackBank trackBank = viewCursorControl.getTrackBank(); - final List trackButtons = hwElements.getTrackSelectButtons(); - for (int i = 0; i < 8; i++) { - final int trackIndex = i; - final Track track = trackBank.getItemAt(trackIndex); - final LabeledButton button = trackButtons.get(i); - final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(i); - prepareTrack(track); - track.addIsSelectedInMixerObserver(selectedInMixer -> selectionField[trackIndex] = selectedInMixer); - - button.bindLight(verticalLayer, () -> getTrackColor(trackIndex, track)); - button.bindPressed(verticalLayer, pressed -> handleTrack(pressed, trackIndex, track)); - - button.bindLight(fixedLengthLayer, () -> getFixedLengthColor(trackIndex)); - button.bindPressed(fixedLengthLayer, pressed -> handleFixedLength(pressed, trackIndex + 1)); - - sceneButton.bindLight(horizontalLayer, () -> getTrackColor(trackIndex, track)); - sceneButton.bindPressed(horizontalLayer, pressed -> handleTrack(pressed, trackIndex, track)); - } - preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> setLayout(newValue))); - panelLayout = preferences.getPanelLayout().get(); - } - - public void setLayout(final PanelLayout layout) { - panelLayout = layout; - selectLayers(); - } - - @Inject - public void setTrackModes(final TrackModeLayer trackModes) { - final ValueObject trackButtonMode = trackModes.getButtonsMode(); - trackMode = trackButtonMode.get(); - trackButtonMode.addValueObserver(((oldValue, newValue) -> { - trackMode = newValue; - selectLayers(); - })); - } - - private void selectLayers() { - if (trackMode == TrackModeButtonMode.FIXED_LENGTH) { - fixedLengthLayer.setIsActive(true); - horizontalLayer.setIsActive(false); - verticalLayer.setIsActive(false); - } else { - fixedLengthLayer.setIsActive(false); - horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); - verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); - } - } - - @Inject - public void setTransport(final Transport transport) { - postRecordingTimeOffset = transport.getClipLauncherPostRecordingTimeOffset(); - final SettableEnumValue postRecordingAction = transport.clipLauncherPostRecordingAction(); - postRecordingAction.markInterested(); - postRecordingTimeOffset.markInterested(); - } - - private void prepareTrack(final Track track) { - track.exists().markInterested(); - track.arm().markInterested(); - track.solo().markInterested(); - track.mute().markInterested(); - track.isQueuedForStop().markInterested(); - track.isStopped().markInterested(); - } - - private void handleTrack(final boolean pressed, final int index, final Track track) { - if (pressed) { - heldTracksButtons.add(index); - } else { - heldTracksButtons.remove(index); - } - switch (trackMode) { - case SELECT: - handleTrackSelected(pressed, index, track); - break; - case ARM: - handleTrackArm(pressed, index, track); - break; - case MUTE: - handleTrackMute(pressed, index, track); - break; - case SOLO: - handleTrackSolo(pressed, index, track); - break; - case STOP: - handleTrackStop(pressed, index, track); - break; - case FIXED_LENGTH: - handleFixedLength(pressed, index + 1); - break; - } - } - - - private void handleTrackSelected(final boolean pressed, final int index, final Track track) { - if (!pressed) { - return; - } - if (track.exists().get()) { - if (modifiers.isClear()) { - track.deleteObject(); - } else if (modifiers.isDuplicate()) { - track.duplicate(); - } else if (modifiers.isShift()) { - track.selectInEditor(); - } else { - track.selectInMixer(); - } - } - } - - private void handleFixedLength(final boolean pressed, final int index) { - postRecordingTimeOffset.set(index * 4.0); - } - - private void handleTrackArm(final boolean pressed, final int index, final Track track) { - if (!pressed) { - return; - } - if (track.exists().get()) { - track.arm().toggle(); - } - } - - private void handleTrackMute(final boolean pressed, final int index, final Track track) { - if (!pressed) { - return; - } - if (track.exists().get()) { - track.mute().toggle(); - } - } - - private void handleTrackSolo(final boolean pressed, final int index, final Track track) { - if (!pressed) { - return; - } - if (track.exists().get()) { - track.solo().toggle(heldTracksButtons.size() < 2); - } - } - - private void handleTrackStop(final boolean pressed, final int index, final Track track) { - if (!pressed) { - return; - } - if (track.exists().get()) { - if (preferences.getAltModeWithShift().get() && modifiers.onlyShift()) { - // Ready for stop alt - } - track.stop(); - } - } - - private RgbState getFixedLengthColor(final int index) { - final double len = postRecordingTimeOffset.get() / 4; - if (index < len) { - return RgbState.ORANGE_PULSE; - } - return RgbState.OFF; - } - - private RgbState getTrackColor(final int index, final Track track) { - if (trackMode == TrackModeButtonMode.FIXED_LENGTH) { - return getFixedLengthColor(index); - } - if (!track.exists().get()) { - return RgbState.OFF; - } - switch (trackMode) { - case SELECT: - return getTrackColorSelect(index, track); - case ARM: - return track.arm().get() ? RgbState.RED : RgbState.RED_LO; - case MUTE: - return track.mute().get() ? RgbState.ORANGE : RgbState.ORANGE_LO; - case SOLO: - return track.solo().get() ? RgbState.YELLOW : RgbState.YELLOW_LO; - case STOP: { - if (track.isStopped().get()) { - return RgbState.RED_LO; + + private final boolean[] selectionField = new boolean[8]; + private final HashSet heldTracksButtons = new HashSet<>(); + private final HashMap modeLayers = new HashMap<>(); + + @Inject + private ModifierStates modifiers; + @Inject + private TrackState trackState; + @Inject + private LppPreferences preferences; + + private SettableBeatTimeValue postRecordingTimeOffset; + + private final Layer verticalLayer; + private final Layer horizontalLayer; + private final Layer fixedLengthLayer; + private PanelLayout panelLayout = PanelLayout.VERTICAL; + private TrackModeButtonMode trackMode; + + public TrackControlLayer(final Layers layers) { + super(layers, "TRACK_CONTROL_LAYER"); + verticalLayer = new Layer(layers, "TRACK_VERTICAL"); + horizontalLayer = new Layer(layers, "TRACK_HORIZONTAL"); + fixedLengthLayer = new Layer(layers, "FIXED_LENGTH"); + } + + @PostConstruct + public void init(final LpProHwElements hwElements, final ViewCursorControl viewCursorControl, + final LppPreferences preferences) { + + final TrackBank trackBank = viewCursorControl.getTrackBank(); + final List trackButtons = hwElements.getTrackSelectButtons(); + for (int i = 0; i < 8; i++) { + final int trackIndex = i; + final Track track = trackBank.getItemAt(trackIndex); + final LabeledButton button = trackButtons.get(i); + final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(i); + prepareTrack(track); + track.addIsSelectedInMixerObserver(selectedInMixer -> selectionField[trackIndex] = selectedInMixer); + + button.bindLight(verticalLayer, () -> getTrackColor(trackIndex, track)); + button.bindPressed(verticalLayer, pressed -> handleTrack(pressed, trackIndex, track)); + + button.bindLight(fixedLengthLayer, () -> getFixedLengthColor(trackIndex)); + button.bindPressed(fixedLengthLayer, pressed -> handleFixedLength(pressed, trackIndex + 1)); + + sceneButton.bindLight(horizontalLayer, () -> getTrackColor(trackIndex, track)); + sceneButton.bindPressed(horizontalLayer, pressed -> handleTrack(pressed, trackIndex, track)); + } + preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> setLayout(newValue))); + panelLayout = preferences.getPanelLayout().get(); + } + + public void setLayout(final PanelLayout layout) { + panelLayout = layout; + selectLayers(); + } + + @Inject + public void setTrackModes(final TrackModeLayer trackModes) { + final ValueObject trackButtonMode = trackModes.getButtonsMode(); + trackMode = trackButtonMode.get(); + trackButtonMode.addValueObserver(((oldValue, newValue) -> { + trackMode = newValue; + selectLayers(); + })); + } + + private void selectLayers() { + if (trackMode == TrackModeButtonMode.FIXED_LENGTH) { + fixedLengthLayer.setIsActive(true); + horizontalLayer.setIsActive(false); + verticalLayer.setIsActive(false); + } else { + fixedLengthLayer.setIsActive(false); + horizontalLayer.setIsActive(panelLayout == PanelLayout.HORIZONTAL); + verticalLayer.setIsActive(panelLayout == PanelLayout.VERTICAL); + } + } + + @Inject + public void setTransport(final Transport transport) { + postRecordingTimeOffset = transport.getClipLauncherPostRecordingTimeOffset(); + final SettableEnumValue postRecordingAction = transport.clipLauncherPostRecordingAction(); + postRecordingAction.markInterested(); + postRecordingTimeOffset.markInterested(); + } + + private void prepareTrack(final Track track) { + track.exists().markInterested(); + track.arm().markInterested(); + track.solo().markInterested(); + track.mute().markInterested(); + track.isQueuedForStop().markInterested(); + track.isStopped().markInterested(); + } + + private void handleTrack(final boolean pressed, final int index, final Track track) { + if (pressed) { + heldTracksButtons.add(index); + } else { + heldTracksButtons.remove(index); + } + switch (trackMode) { + case SELECT: + handleTrackSelected(pressed, index, track); + break; + case ARM: + handleTrackArm(pressed, index, track); + break; + case MUTE: + handleTrackMute(pressed, index, track); + break; + case SOLO: + handleTrackSolo(pressed, index, track); + break; + case STOP: + handleTrackStop(pressed, index, track); + break; + case FIXED_LENGTH: + handleFixedLength(pressed, index + 1); + break; + } + } + + + private void handleTrackSelected(final boolean pressed, final int index, final Track track) { + if (!pressed) { + return; + } + if (track.exists().get()) { + if (modifiers.isClear()) { + track.deleteObject(); + } else if (modifiers.isDuplicate()) { + track.duplicate(); + } else if (modifiers.isShift()) { + track.selectInEditor(); + } else { + track.selectInMixer(); } - if (track.isQueuedForStop().get()) { - return RgbState.flash(5, 0); + } + } + + private void handleFixedLength(final boolean pressed, final int index) { + postRecordingTimeOffset.set(index * 4.0); + } + + private void handleTrackArm(final boolean pressed, final int index, final Track track) { + if (!pressed) { + return; + } + if (track.exists().get()) { + track.arm().toggle(); + } + } + + private void handleTrackMute(final boolean pressed, final int index, final Track track) { + if (!pressed) { + return; + } + if (track.exists().get()) { + track.mute().toggle(); + } + } + + private void handleTrackSolo(final boolean pressed, final int index, final Track track) { + if (!pressed) { + return; + } + if (track.exists().get()) { + track.solo().toggle(heldTracksButtons.size() < 2); + } + } + + private void handleTrackStop(final boolean pressed, final int index, final Track track) { + if (!pressed) { + return; + } + if (track.exists().get()) { + if (preferences.getAltModeWithShift().get() && modifiers.onlyShift()) { + // Ready for stop alt } - return RgbState.RED; - } - } - return RgbState.OFF; - } - - private RgbState getTrackColorSelect(final int index, final Track track) { - if (track.exists().get()) { - if (selectionField[index]) { - return RgbState.of(trackState.getColorOfTrack(index)); - } - return RgbState.DIM_WHITE; - } - return RgbState.OFF; - } - - @Override - protected void onActivate() { - super.onActivate(); - selectLayers(); - } - - @Override - protected void onDeactivate() { - fixedLengthLayer.setIsActive(false); - horizontalLayer.setIsActive(false); - verticalLayer.setIsActive(false); - } + track.stop(); + } + } + + private RgbState getFixedLengthColor(final int index) { + final double len = postRecordingTimeOffset.get() / 4; + if (index < len) { + return RgbState.ORANGE_PULSE; + } + return RgbState.OFF; + } + + private RgbState getTrackColor(final int index, final Track track) { + if (trackMode == TrackModeButtonMode.FIXED_LENGTH) { + return getFixedLengthColor(index); + } + if (!track.exists().get()) { + return RgbState.OFF; + } + switch (trackMode) { + case SELECT: + return getTrackColorSelect(index, track); + case ARM: + return track.arm().get() ? RgbState.RED : RgbState.RED_LO; + case MUTE: + return track.mute().get() ? RgbState.ORANGE : RgbState.ORANGE_LO; + case SOLO: + return track.solo().get() ? RgbState.YELLOW : RgbState.YELLOW_LO; + case STOP: { + if (track.isStopped().get()) { + return RgbState.RED_LO; + } + if (track.isQueuedForStop().get()) { + return RgbState.flash(5, 0); + } + return RgbState.RED; + } + } + return RgbState.OFF; + } + + private RgbState getTrackColorSelect(final int index, final Track track) { + if (track.exists().get()) { + if (selectionField[index]) { + return RgbState.of(trackState.getColorOfTrack(index)); + } + return RgbState.DIM_WHITE; + } + return RgbState.OFF; + } + + @Override + protected void onActivate() { + super.onActivate(); + selectLayers(); + } + + @Override + protected void onDeactivate() { + fixedLengthLayer.setIsActive(false); + horizontalLayer.setIsActive(false); + verticalLayer.setIsActive(false); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/TrackModeLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/TrackModeLayer.java index 83d617c8..5fa5db5f 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/TrackModeLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/TrackModeLayer.java @@ -7,7 +7,11 @@ import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; import com.bitwig.extensions.controllers.novation.commonsmk3.NovationColor; import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.*; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.LpProHwElements; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.LabelCcAssignments; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.ModeHandler; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.ModifierStates; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; import com.bitwig.extensions.framework.di.Inject; @@ -15,21 +19,21 @@ public class TrackModeLayer extends Layer { public static final int MOMENTARY_TIME = 500; - - // TODO this need to be Event Driven + + // TODO this need to be Event Driven private TrackModeButtonMode returnToMode = null; - + private final SettableEnumValue postRecordingAction; private ControlMode controlMode = ControlMode.NONE; private TrackModeButtonMode stashedButtonMode = TrackModeButtonMode.SELECT; private ControlMode stashedControlMode = ControlMode.NONE; - private ValueObject buttonsMode = new ValueObject<>(TrackModeButtonMode.SELECT); - + private final ValueObject buttonsMode = new ValueObject<>(TrackModeButtonMode.SELECT); + @Inject private ModeHandler modeHandler; - + public TrackModeLayer(final Layers layers, final ModifierStates modifiers, final Application application, - final Transport transport, final HwElements hwElements, final ViewCursorControl viewControl) { + final Transport transport, final LpProHwElements hwElements, final ViewCursorControl viewControl) { super(layers, "TRACK_MODE_LAYER"); final Layer shiftLayer = new Layer(layers, "SHIFT_LAYER_TRACKMODE"); application.canRedo().markInterested(); @@ -40,75 +44,83 @@ public TrackModeLayer(final Layers layers, final ModifierStates modifiers, final postRecordingAction = transport.clipLauncherPostRecordingAction(); postRecordingAction.markInterested(); postRecordingTimeOffset.markInterested(); - + final LabeledButton armButton = hwElements.getLabeledButton(LabelCcAssignments.RECORD_ARM_UNDO); armButton.bindPressReleaseAfter(this, () -> setButtonMode(TrackModeButtonMode.ARM), this::returnToPreviousMode, - MOMENTARY_TIME); - armButton.bindLight(this, () -> buttonsMode.get() == TrackModeButtonMode.ARM ? RgbState.RED : RgbState.DIM_WHITE); + MOMENTARY_TIME); + armButton.bindLight( + this, () -> buttonsMode.get() == TrackModeButtonMode.ARM ? RgbState.RED : RgbState.DIM_WHITE); armButton.bind(shiftLayer, application::undo, - () -> application.canUndo().get() ? RgbState.WHITE : RgbState.OFF); - + () -> application.canUndo().get() ? RgbState.WHITE : RgbState.OFF); + final LabeledButton muteButton = hwElements.getLabeledButton(LabelCcAssignments.MUTE_REDO); muteButton.bindPressReleaseAfter(this, () -> setButtonMode(TrackModeButtonMode.MUTE), - this::returnToPreviousMode, MOMENTARY_TIME); - muteButton.bindLight(this, - () -> buttonsMode.get() == TrackModeButtonMode.MUTE ? NovationColor.AMBER.getMainColor() : RgbState.DIM_WHITE); - + this::returnToPreviousMode, MOMENTARY_TIME); + muteButton.bindLight( + this, () -> buttonsMode.get() == TrackModeButtonMode.MUTE + ? NovationColor.AMBER.getMainColor() + : RgbState.DIM_WHITE); + muteButton.bind(shiftLayer, application::redo, - () -> application.canRedo().get() ? RgbState.WHITE : RgbState.OFF); - + () -> application.canRedo().get() ? RgbState.WHITE : RgbState.OFF); + final LabeledButton soloButton = hwElements.getLabeledButton(LabelCcAssignments.SOLO_CLICK); soloButton.bindPressReleaseAfter(this, () -> setButtonMode(TrackModeButtonMode.SOLO), - this::returnToPreviousMode, MOMENTARY_TIME); - soloButton.bindLight(this, - () -> buttonsMode.get() == TrackModeButtonMode.SOLO ? NovationColor.YELLOW.getMainColor() : RgbState.DIM_WHITE); + this::returnToPreviousMode, MOMENTARY_TIME); + soloButton.bindLight( + this, () -> buttonsMode.get() == TrackModeButtonMode.SOLO + ? NovationColor.YELLOW.getMainColor() + : RgbState.DIM_WHITE); soloButton.bind(shiftLayer, () -> transport.isMetronomeEnabled().toggle(), - () -> transport.isMetronomeEnabled().get() ? RgbState.TURQUOISE : RgbState.RED_LO); - + () -> transport.isMetronomeEnabled().get() ? RgbState.TURQUOISE : RgbState.RED_LO); + final LabeledButton sendButton = hwElements.getLabeledButton(LabelCcAssignments.SENDS_TAP); sendButton.bindPressed(shiftLayer, transport::tapTempo); sendButton.bindLightPressed(shiftLayer, RgbState.SHIFT_INACTIVE, RgbState.WHITE); - - + + final LabeledButton stopButton = hwElements.getLabeledButton(LabelCcAssignments.STOP_CLIP_SWING); stopButton.bindPressReleaseAfter(this, () -> setButtonMode(TrackModeButtonMode.STOP), - this::returnToPreviousMode, MOMENTARY_TIME); - stopButton.bindLight(this, () -> buttonsMode.get() == TrackModeButtonMode.STOP ? RgbState.of(5) : RgbState.DIM_WHITE); + this::returnToPreviousMode, MOMENTARY_TIME); + stopButton.bindLight( + this, () -> buttonsMode.get() == TrackModeButtonMode.STOP ? RgbState.of(5) : RgbState.DIM_WHITE); stopButton.bindPressed(shiftLayer, () -> viewControl.getRootTrack().stop()); stopButton.bindLightPressed(shiftLayer, RgbState.SHIFT_INACTIVE, RgbState.SHIFT_ACTIVE); - + final LabeledButton fixedLengthButton = hwElements.getLabeledButton(LabelCcAssignments.FIXED_LENGTH); - + fixedLengthButton.bindDelayedAction(this, this::enterFixedSetMode, this::releasedFixedLength, 300); - fixedLengthButton.bindLight(this, pressed -> postRecordingAction.get() - .equals("play_recorded") ? (pressed ? RgbState.ORANGE_PULSE : RgbState.ORANGE) : RgbState.DIM_WHITE); + fixedLengthButton.bindLight( + this, pressed -> postRecordingAction.get().equals("play_recorded") ? (pressed + ? RgbState.ORANGE_PULSE + : RgbState.ORANGE) : RgbState.DIM_WHITE); fixedLengthButton.disable(shiftLayer); - + initSliderModeButtons(hwElements, shiftLayer); } - - private void initSliderModeButtons(final HwElements hwElements, final Layer shiftLayer) { + + private void initSliderModeButtons(final LpProHwElements hwElements, final Layer shiftLayer) { final LabeledButton volumeButton = hwElements.getLabeledButton(LabelCcAssignments.VOLUME); bindSliderButton(volumeButton, ControlMode.VOLUME, RgbState.of(ControlMode.VOLUME.getColor())); volumeButton.disable(shiftLayer); - + final LabeledButton panButton = hwElements.getLabeledButton(LabelCcAssignments.PAN); bindSliderButton(panButton, ControlMode.PAN, RgbState.of(61)); panButton.disable(shiftLayer); - + final LabeledButton sendButton = hwElements.getLabeledButton(LabelCcAssignments.SENDS_TAP); bindSliderButton(sendButton, ControlMode.SENDS, RgbState.of(ControlMode.SENDS.getColor())); - + final LabeledButton deviceButton = hwElements.getLabeledButton(LabelCcAssignments.DEVICE_TEMPO); bindSliderButton(deviceButton, ControlMode.DEVICE, RgbState.of(ControlMode.DEVICE.getColor())); deviceButton.disable(shiftLayer); } - + private void bindSliderButton(final LabeledButton button, final ControlMode mode, final RgbState color) { button.bindPressReleaseAfter(this, () -> toggleControlMode(mode), this::returnToPreviousMode, MOMENTARY_TIME); button.bindLight(this, () -> controlMode == mode ? color : RgbState.DIM_WHITE); } - + private void returnToPreviousMode(final boolean longPress) { if (longPress) { final TrackModeButtonMode previousMode = buttonsMode.get(); @@ -120,7 +132,7 @@ private void returnToPreviousMode(final boolean longPress) { stashedControlMode = controlMode; } } - + private void toggleControlMode(final ControlMode controlMode) { stashedControlMode = this.controlMode; stashedButtonMode = buttonsMode.get(); @@ -136,13 +148,13 @@ private void toggleControlMode(final ControlMode controlMode) { } modeHandler.toFaderMode(this.controlMode, stashedControlMode); } - + private void enterFixedSetMode() { returnToMode = buttonsMode.get(); setButtonMode(TrackModeButtonMode.FIXED_LENGTH); postRecordingAction.set("play_recorded"); } - + private void releasedFixedLength() { if (returnToMode != null) { setButtonMode(returnToMode); @@ -155,12 +167,12 @@ private void releasedFixedLength() { } } } - - + + public void setControlMode(final ControlMode mode) { controlMode = mode; } - + void setButtonMode(final TrackModeButtonMode mode) { stashedButtonMode = buttonsMode.get(); toggleButtonMode(mode, stashedButtonMode); @@ -168,7 +180,7 @@ void setButtonMode(final TrackModeButtonMode mode) { toStandardMode(); } } - + private void toggleButtonMode(final TrackModeButtonMode mode, final TrackModeButtonMode previousMode) { if (buttonsMode.get() == mode) { if (buttonsMode.get() != TrackModeButtonMode.SELECT) { @@ -178,7 +190,7 @@ private void toggleButtonMode(final TrackModeButtonMode mode, final TrackModeBut buttonsMode.set(mode); } } - + private void toStandardMode() { if (controlMode == ControlMode.NONE) { return; @@ -187,7 +199,7 @@ private void toStandardMode() { controlMode = ControlMode.NONE; modeHandler.toFaderMode(controlMode, stashedControlMode); } - + public ValueObject getButtonsMode() { return buttonsMode; } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/TrackState.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/TrackState.java index 866f4cfe..2b8a7a94 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/TrackState.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/layers/TrackState.java @@ -1,36 +1,36 @@ package com.bitwig.extensions.controllers.novation.launchpadpromk3.layers; +import java.util.ArrayList; +import java.util.List; + import com.bitwig.extension.controller.api.CursorTrack; import com.bitwig.extension.controller.api.Track; import com.bitwig.extension.controller.api.TrackBank; import com.bitwig.extensions.controllers.novation.commonsmk3.ColorLookup; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.ViewCursorControl; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; import com.bitwig.extensions.framework.di.Component; -import java.util.ArrayList; -import java.util.List; - @Component public class TrackState { private final int[] colorIndex; private final boolean[] exists; private int cursorColor = ColorLookup.toColor(0, 0, 0); - + private final List existsListeners = new ArrayList<>(); private final List colorListeners = new ArrayList<>(); - + public interface BoolStateListener { void changed(int trackIndex, boolean state); } - + public interface ColorStateListener { void changed(int trackIndex, int color); } - + public TrackState(final ViewCursorControl viewCursorControl) { final TrackBank trackBank = viewCursorControl.getTrackBank(); final int sizeOfBank = trackBank.getSizeOfBank(); - + final CursorTrack cursorTrack = viewCursorControl.getCursorTrack(); cursorTrack.color().addValueObserver((r, g, b) -> cursorColor = ColorLookup.toColor(r, g, b)); colorIndex = new int[sizeOfBank]; @@ -42,39 +42,39 @@ public TrackState(final ViewCursorControl viewCursorControl) { track.exists().addValueObserver(exists -> handleExistsChanged(trackIndex, exists)); } } - + public void addColorStateListener(final ColorStateListener colorStateListener) { colorListeners.add(colorStateListener); } - + public void addExistsListener(final BoolStateListener listener) { existsListeners.add(listener); } - + private void handleColorChanged(final int trackIndex, final int color) { colorIndex[trackIndex] = color; colorListeners.forEach(l -> l.changed(trackIndex, color)); } - + private void handleExistsChanged(final int trackIndex, final boolean exists) { this.exists[trackIndex] = exists; existsListeners.forEach(l -> l.changed(trackIndex, exists)); } - + public int[] getColorIndex() { return colorIndex; } - + public boolean[] getExists() { return exists; } - + public int getCursorColor() { return cursorColor; } - + public int getColorOfTrack(final int index) { return colorIndex[index]; } - + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/DeviceSliderLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/DeviceSliderLayer.java index 68e89849..b3a94e5e 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/DeviceSliderLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/DeviceSliderLayer.java @@ -1,18 +1,27 @@ package com.bitwig.extensions.controllers.novation.launchpadpromk3.sliderlayers; -import com.bitwig.extension.controller.api.*; +import com.bitwig.extension.controller.api.CursorDeviceLayer; +import com.bitwig.extension.controller.api.CursorRemoteControlsPage; +import com.bitwig.extension.controller.api.DeviceBank; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.PinnableCursorDevice; +import com.bitwig.extension.controller.api.RemoteControl; import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; import com.bitwig.extensions.controllers.novation.commonsmk3.SliderBinding; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.*; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.LpProHwElements; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.LabelCcAssignments; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.LpBaseMode; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.SysExHandler; import com.bitwig.extensions.controllers.novation.launchpadpromk3.layers.ControlMode; import com.bitwig.extensions.framework.Layers; public class DeviceSliderLayer extends SliderLayer { - + private static final int[] PARAM_COLORS = {5, 9, 13, 25, 29, 41, 49, 57}; - + private final CursorRemoteControlsPage parameterBank; private final DeviceBank drumDeviceBank; final PinnableCursorDevice device; @@ -23,10 +32,10 @@ public class DeviceSliderLayer extends SliderLayer { private boolean hasLayers; private boolean hasSlots; private String[] deviceSlotNames = new String[0]; - + public DeviceSliderLayer(final ViewCursorControl viewCursorControl, final HardwareSurface controlSurface, - final Layers layers, final MidiProcessor midiProcessor, final SysExHandler sysExHandler, - final HwElements hwElements) { + final Layers layers, final MidiProcessor midiProcessor, final SysExHandler sysExHandler, + final LpProHwElements hwElements) { super("DEVICE", ControlMode.DEVICE, controlSurface, layers, midiProcessor, sysExHandler); device = viewCursorControl.getCursorDevice(); parameterBank = device.createCursorRemoteControlsPage(8); @@ -35,10 +44,11 @@ public DeviceSliderLayer(final ViewCursorControl viewCursorControl, final Hardwa final PinnableCursorDevice primary = viewCursorControl.getPrimaryDevice(); final CursorDeviceLayer drumCursor = primary.createCursorLayer(); drumDeviceBank = drumCursor.createDeviceBank(8); - + initSceneButtons(hwElements); for (int i = 0; i < 8; i++) { final RemoteControl parameter = parameterBank.getParameter(i); + parameter.setIndication(true); final SliderBinding binding = new SliderBinding(mode.getCcNr(), parameter, sliders[i], i, midiProcessor); addBinding(binding); @@ -46,42 +56,42 @@ public DeviceSliderLayer(final ViewCursorControl viewCursorControl, final Hardwa } initNavigation(hwElements); } - - private void initNavigation(final HwElements hwElements) { + + private void initNavigation(final LpProHwElements hwElements) { final LabeledButton upButton = hwElements.getLabeledButton(LabelCcAssignments.UP); final LabeledButton downButton = hwElements.getLabeledButton(LabelCcAssignments.DOWN); final LabeledButton leftButton = hwElements.getLabeledButton(LabelCcAssignments.LEFT); final LabeledButton rightButton = hwElements.getLabeledButton(LabelCcAssignments.RIGHT); parameterBank.hasNext().markInterested(); parameterBank.hasPrevious().markInterested(); - + device.hasNext().markInterested(); device.hasPrevious().markInterested(); - + device.isNested().addValueObserver(nested -> isNested = nested); device.slotNames().addValueObserver(slotNames -> deviceSlotNames = slotNames); device.hasSlots().addValueObserver(hasSlots -> this.hasSlots = hasSlots); device.hasLayers().addValueObserver(hasLayers -> this.hasLayers = hasLayers); device.hasDrumPads().addValueObserver(hasPads -> hasDrumPads = hasPads); - + final RgbState scrollColor = RgbState.of(23); upButton.bindRepeatHold(this, () -> handleNavigateDevice(-1)); upButton.bindLight(this, () -> isNested ? scrollColor : RgbState.OFF); - + downButton.bindRepeatHold(this, () -> handleNavigateDevice(1)); downButton.bindLight(this, () -> canNavigateDown() ? scrollColor : RgbState.OFF); - + leftButton.bindRepeatHold(this, () -> handleNavigateChain(-1)); leftButton.bindLight(this, () -> device.hasPrevious().get() ? scrollColor : RgbState.OFF); - + rightButton.bindRepeatHold(this, () -> handleNavigateChain(1)); rightButton.bindLight(this, () -> device.hasNext().get() ? scrollColor : RgbState.OFF); } - + private boolean canNavigateDown() { return hasDrumPads || hasLayers || hasSlots; } - + private void handleNavigateChain(final int amount) { if (amount > 0) { device.selectNext(); @@ -89,7 +99,7 @@ private void handleNavigateChain(final int amount) { device.selectPrevious(); } } - + private void handleNavigateDevice(final int amount) { if (amount > 0) { if (device.hasDrumPads().get()) { @@ -103,7 +113,7 @@ private void handleNavigateDevice(final int amount) { device.selectParent(); } } - + private void handleNavigateParameters(final int amount, final CursorRemoteControlsPage parameters) { if (amount > 0) { parameters.selectNextPage(false); @@ -111,9 +121,9 @@ private void handleNavigateParameters(final int amount, final CursorRemoteContro parameters.selectPreviousPage(false); } } - - - private void initSceneButtons(final HwElements hwElements) { + + + private void initSceneButtons(final LpProHwElements hwElements) { for (int i = 0; i < 8; i++) { final int index = i; final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(index); @@ -121,11 +131,11 @@ private void initSceneButtons(final HwElements hwElements) { sceneButton.bindLight(this, () -> getColor(index)); } } - + private void handleSendSelect(final int index) { parameterBank.selectedPageIndex().set(index); } - + private RgbState getColor(final int index) { if (index < parameterPages) { if (pageIndex == index) { @@ -135,12 +145,12 @@ private RgbState getColor(final int index) { } return RgbState.OFF; } - + @Override protected void refreshTrackColors() { System.arraycopy(PARAM_COLORS, 0, tracksExistsColors, 0, PARAM_COLORS.length); } - + @Override protected void onActivate() { super.onActivate(); @@ -148,5 +158,5 @@ protected void onActivate() { modeHandler.setFaderBank(0, mode, tracksExistsColors); modeHandler.changeMode(LpBaseMode.FADER, mode.getBankId()); } - + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/PanSliderLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/PanSliderLayer.java index 59d3a69c..8819fa2c 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/PanSliderLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/PanSliderLayer.java @@ -6,26 +6,26 @@ import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; import com.bitwig.extensions.controllers.novation.commonsmk3.SliderBinding; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.HwElements; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.LpProHwElements; import com.bitwig.extensions.controllers.novation.launchpadpromk3.LpBaseMode; import com.bitwig.extensions.controllers.novation.launchpadpromk3.SysExHandler; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.ViewCursorControl; import com.bitwig.extensions.controllers.novation.launchpadpromk3.layers.ControlMode; import com.bitwig.extensions.framework.Layer; import com.bitwig.extensions.framework.Layers; public class PanSliderLayer extends TrackSliderLayer { private final Layer sceneButtonLayer; - + public PanSliderLayer(final ViewCursorControl viewCursorControl, final HardwareSurface controlSurface, - final Layers layers, final MidiProcessor midiProcessor, final SysExHandler sysExHandler, - final HwElements hwElements) { + final Layers layers, final MidiProcessor midiProcessor, final SysExHandler sysExHandler, + final LpProHwElements hwElements) { super("PAN", ControlMode.PAN, controlSurface, layers, midiProcessor, sysExHandler); bind(viewCursorControl.getTrackBank()); sceneButtonLayer = new Layer(layers, "VOL_SCENE_BUTTONS"); initSceneButtons(hwElements); } - + @Override protected void bind(final TrackBank trackBank) { for (int i = 0; i < 8; i++) { @@ -35,12 +35,12 @@ protected void bind(final TrackBank trackBank) { valueBindings.add(binding); } } - + @Override protected void refreshTrackColors() { final boolean[] exists = trackState.getExists(); final int[] colorIndex = trackState.getColorIndex(); - + for (int i = 0; i < 8; i++) { if (exists[i]) { tracksExistsColors[i] = colorIndex[i]; @@ -49,15 +49,15 @@ protected void refreshTrackColors() { } } } - - private void initSceneButtons(final HwElements hwElements) { + + private void initSceneButtons(final LpProHwElements hwElements) { for (int index = 0; index < 8; index++) { final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(index); sceneButton.disable(sceneButtonLayer); } } - - + + @Override protected void onActivate() { super.onActivate(); @@ -66,13 +66,13 @@ protected void onActivate() { modeHandler.setFaderBank(1, mode, tracksExistsColors); modeHandler.changeMode(LpBaseMode.FADER, mode.getBankId()); } - - + + @Override protected void onDeactivate() { super.onDeactivate(); sceneButtonLayer.setIsActive(false); updateFaderState(); } - + } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/SendsSliderLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/SendsSliderLayer.java index acdb8206..8f42bf99 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/SendsSliderLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/SendsSliderLayer.java @@ -9,76 +9,77 @@ import com.bitwig.extensions.framework.Layers; public class SendsSliderLayer extends TrackSliderLayer { - - private TrackBank trackBank; - private int sendItems = 0; - private int selectedSendItem = 0; - - public SendsSliderLayer(final ViewCursorControl viewCursorControl, final HardwareSurface controlSurface, - final Layers layers, final MidiProcessor midiProcessor, final SysExHandler sysExHandler, - final HwElements hwElements, LppPreferences preferences) { - super("SENDS", ControlMode.SENDS, controlSurface, layers, midiProcessor, sysExHandler); - bind(viewCursorControl.getTrackBank()); - initSceneButtons(hwElements); - preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> { - panelLayout = newValue; - updateFaderState(); - })); - } - - private void initSceneButtons(final HwElements hwElements) { - for (int i = 0; i < 8; i++) { - final int index = i; - final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(index); - sceneButton.bindPressed(this, pressed -> handleSendSelect(pressed, index)); - sceneButton.bindLight(this, () -> sendsExistState(index)); - } - } - - private void handleSendSelect(final boolean pressed, final int index) { - if (pressed) { - if (index < sendItems) { - for (int i = 0; i < 8; i++) { - final Track track = trackBank.getItemAt(i); - track.sendBank().scrollPosition().set(index); + + private TrackBank trackBank; + private int sendItems = 0; + private int selectedSendItem = 0; + + public SendsSliderLayer(final ViewCursorControl viewCursorControl, final HardwareSurface controlSurface, + final Layers layers, final MidiProcessor midiProcessor, final SysExHandler sysExHandler, + final LpProHwElements hwElements, LppPreferences preferences) { + super("SENDS", ControlMode.SENDS, controlSurface, layers, midiProcessor, sysExHandler); + bind(viewCursorControl.getTrackBank()); + initSceneButtons(hwElements); + preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> { + panelLayout = newValue; + updateFaderState(); + })); + } + + private void initSceneButtons(final LpProHwElements hwElements) { + for (int i = 0; i < 8; i++) { + final int index = i; + final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(index); + sceneButton.bindPressed(this, pressed -> handleSendSelect(pressed, index)); + sceneButton.bindLight(this, () -> sendsExistState(index)); + } + } + + private void handleSendSelect(final boolean pressed, final int index) { + if (pressed) { + if (index < sendItems) { + for (int i = 0; i < 8; i++) { + final Track track = trackBank.getItemAt(i); + track.sendBank().scrollPosition().set(index); + } } - } - } - } - - private RgbState sendsExistState(final int index) { - if (index < sendItems) { - if (index == selectedSendItem) { - return RgbState.WHITE; - } - return RgbState.DIM_WHITE; - } - return RgbState.OFF; - } - - @Override - protected void bind(final TrackBank trackBank) { - this.trackBank = trackBank; - - for (int i = 0; i < 8; i++) { - final Track track = trackBank.getItemAt(i); - if (i == 0) { - track.sendBank().itemCount().addValueObserver(items -> sendItems = items); - track.sendBank().scrollPosition().addValueObserver(scrollPos -> selectedSendItem = scrollPos); - } - - final SliderBinding binding = new SliderBinding(ControlMode.SENDS.getCcNr(), track.sendBank().getItemAt(0), - sliders[i], i, midiProcessor); - addBinding(binding); - valueBindings.add(binding); - } - } - - @Override - protected void onActivate() { - super.onActivate(); - refreshTrackColors(); - modeHandler.setFaderBank(panelLayout == PanelLayout.VERTICAL ? 0 : 1, mode, tracksExistsColors); - modeHandler.changeMode(LpBaseMode.FADER, mode.getBankId()); - } + } + } + + private RgbState sendsExistState(final int index) { + if (index < sendItems) { + if (index == selectedSendItem) { + return RgbState.WHITE; + } + return RgbState.DIM_WHITE; + } + return RgbState.OFF; + } + + @Override + protected void bind(final TrackBank trackBank) { + this.trackBank = trackBank; + + for (int i = 0; i < 8; i++) { + final Track track = trackBank.getItemAt(i); + if (i == 0) { + track.sendBank().itemCount().addValueObserver(items -> sendItems = items); + track.sendBank().scrollPosition().addValueObserver(scrollPos -> selectedSendItem = scrollPos); + } + + final SliderBinding binding = + new SliderBinding(ControlMode.SENDS.getCcNr(), track.sendBank().getItemAt(0), sliders[i], i, + midiProcessor); + addBinding(binding); + valueBindings.add(binding); + } + } + + @Override + protected void onActivate() { + super.onActivate(); + refreshTrackColors(); + modeHandler.setFaderBank(panelLayout == PanelLayout.VERTICAL ? 0 : 1, mode, tracksExistsColors); + modeHandler.changeMode(LpBaseMode.FADER, mode.getBankId()); + } } diff --git a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/VolumeSliderLayer.java b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/VolumeSliderLayer.java index 29f57f9f..5ea6411b 100644 --- a/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/VolumeSliderLayer.java +++ b/src/main/java/com/bitwig/extensions/controllers/novation/launchpadpromk3/sliderlayers/VolumeSliderLayer.java @@ -1,216 +1,231 @@ package com.bitwig.extensions.controllers.novation.launchpadpromk3.sliderlayers; -import com.bitwig.extension.controller.api.*; -import com.bitwig.extensions.controllers.novation.commonsmk3.*; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.*; -import com.bitwig.extensions.controllers.novation.launchpadpromk3.layers.ControlMode; -import com.bitwig.extensions.framework.Layer; -import com.bitwig.extensions.framework.Layers; -import com.bitwig.extensions.framework.values.BooleanValueObject; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -public class VolumeSliderLayer extends TrackSliderLayer { - - private int volumeMode = 0; - private final int[] vuLevels = new int[8]; - private final boolean[] masterExists = new boolean[8]; - private final Layer volumeLayer; - private final Layer masterLayer; - private final Layer sceneButtonLayer; - private Layer currentLayer; - private final List masterBindings = new ArrayList<>(); - private final Map modeLayers = new HashMap<>(); - private final BooleanValueObject touchActive = new BooleanValueObject(); - - public VolumeSliderLayer(final ViewCursorControl viewCursorControl, final ModifierStates modifierStates, - final HardwareSurface controlSurface, final Layers layers, - final MidiProcessor midiProcessor, final SysExHandler sysExHandler, - final ControllerHost host, final HwElements hwElements, LppPreferences preferences) { - super("VOL", ControlMode.VOLUME, controlSurface, layers, midiProcessor, sysExHandler); - volumeLayer = new Layer(layers, "VOL_VOL"); - masterLayer = new Layer(layers, "VOL_MASTER"); - final Layer levelLayer = new Layer(layers, "VOL_LEVEL"); - sceneButtonLayer = new Layer(layers, "VOL_SCENE_BUTTONS"); - modeLayers.put(0, volumeLayer); - modeLayers.put(1, masterLayer); - modeLayers.put(2, levelLayer); - - modifierStates.getShiftActive().addValueObserver(shift -> switchSceneLayer(!shift)); - currentLayer = volumeLayer; - final MasterTrack masterTrack = host.createMasterTrack(1); - final TrackBank effectTrackBank = host.createEffectTrackBank(7, 1); - bind(viewCursorControl.getTrackBank()); - bindMaster(masterTrack, effectTrackBank); - initSceneButtons(hwElements); - preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> { - panelLayout = newValue; - updateFaderState(); - })); - } - - private void switchSceneLayer(final boolean deactivate) { - if (isActive()) { - sceneButtonLayer.setIsActive(deactivate); - } - } - - private void bindMaster(final MasterTrack masterTrack, final TrackBank effectBank) { - for (int i = 0; i < 7; i++) { - final Track track = effectBank.getItemAt(i); - final int index = i; - track.exists().addValueObserver(effExists -> masterExists[index] = effExists); - final SliderBinding binding = new SliderBinding(mode.getCcNr(), track.volume(), sliders[i], i, midiProcessor); - masterLayer.addBinding(binding); - masterBindings.add(binding); - } - final SliderBinding binding = new SliderBinding(mode.getCcNr(), masterTrack.volume(), sliders[7], 7, - midiProcessor); - masterLayer.addBinding(binding); - masterBindings.add(binding); - } +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.MasterTrack; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.controllers.novation.commonsmk3.LabeledButton; +import com.bitwig.extensions.controllers.novation.commonsmk3.MidiProcessor; +import com.bitwig.extensions.controllers.novation.commonsmk3.PanelLayout; +import com.bitwig.extensions.controllers.novation.commonsmk3.RgbState; +import com.bitwig.extensions.controllers.novation.commonsmk3.SliderBinding; +import com.bitwig.extensions.controllers.novation.commonsmk3.ViewCursorControl; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.LpProHwElements; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.LpBaseMode; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.LppPreferences; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.ModifierStates; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.SysExHandler; +import com.bitwig.extensions.controllers.novation.launchpadpromk3.layers.ControlMode; +import com.bitwig.extensions.framework.Layer; +import com.bitwig.extensions.framework.Layers; +import com.bitwig.extensions.framework.values.BooleanValueObject; - @Override - protected void bind(final TrackBank trackBank) { - final MidiOut midiOut = midiProcessor.getMidiOut(); - for (int i = 0; i < 8; i++) { - final Track track = trackBank.getItemAt(i); - final int index = i; - //volumeLayer.bind(sliders[i], track.volume().value()); - final SliderBinding binding = new SliderBinding(mode.getCcNr(), track.volume(), sliders[i], i, midiProcessor); - volumeLayer.addBinding(binding); - valueBindings.add(binding); - } - touchActive.addValueObserver(touched -> { - for (int i = 0; i < 8; i++) { +public class VolumeSliderLayer extends TrackSliderLayer { + + private int volumeMode = 0; + private final int[] vuLevels = new int[8]; + private final boolean[] masterExists = new boolean[8]; + private final Layer volumeLayer; + private final Layer masterLayer; + private final Layer sceneButtonLayer; + private Layer currentLayer; + private final List masterBindings = new ArrayList<>(); + private final Map modeLayers = new HashMap<>(); + private final BooleanValueObject touchActive = new BooleanValueObject(); + + public VolumeSliderLayer(final ViewCursorControl viewCursorControl, final ModifierStates modifierStates, + final HardwareSurface controlSurface, final Layers layers, final MidiProcessor midiProcessor, + final SysExHandler sysExHandler, final ControllerHost host, final LpProHwElements hwElements, + final LppPreferences preferences) { + super("VOL", ControlMode.VOLUME, controlSurface, layers, midiProcessor, sysExHandler); + volumeLayer = new Layer(layers, "VOL_VOL"); + masterLayer = new Layer(layers, "VOL_MASTER"); + final Layer levelLayer = new Layer(layers, "VOL_LEVEL"); + sceneButtonLayer = new Layer(layers, "VOL_SCENE_BUTTONS"); + modeLayers.put(0, volumeLayer); + modeLayers.put(1, masterLayer); + modeLayers.put(2, levelLayer); + + modifierStates.getShiftActive().addValueObserver(shift -> switchSceneLayer(!shift)); + currentLayer = volumeLayer; + final MasterTrack masterTrack = host.createMasterTrack(1); + final TrackBank effectTrackBank = host.createEffectTrackBank(7, 1); + bind(viewCursorControl.getTrackBank()); + bindMaster(masterTrack, effectTrackBank); + initSceneButtons(hwElements); + preferences.getPanelLayout().addValueObserver(((oldValue, newValue) -> { + panelLayout = newValue; + updateFaderState(); + })); + } + + private void switchSceneLayer(final boolean deactivate) { + if (isActive()) { + sceneButtonLayer.setIsActive(deactivate); + } + } + + private void bindMaster(final MasterTrack masterTrack, final TrackBank effectBank) { + for (int i = 0; i < 7; i++) { + final Track track = effectBank.getItemAt(i); + final int index = i; + track.exists().addValueObserver(effExists -> masterExists[index] = effExists); + final SliderBinding binding = + new SliderBinding(mode.getCcNr(), track.volume(), sliders[i], i, midiProcessor); + masterLayer.addBinding(binding); + masterBindings.add(binding); + } + final SliderBinding binding = + new SliderBinding(mode.getCcNr(), masterTrack.volume(), sliders[7], 7, midiProcessor); + masterLayer.addBinding(binding); + masterBindings.add(binding); + } + + @Override + protected void bind(final TrackBank trackBank) { + final MidiOut midiOut = midiProcessor.getMidiOut(); + for (int i = 0; i < 8; i++) { final Track track = trackBank.getItemAt(i); - track.volume().touch(touched); - } - }); - } - - private void handleVu(final int index, final int value) { - if (isActive() && volumeMode == 2) { - vuLevels[index] = value; - midiProcessor.sendMidi(0xB4, mode.getCcNr() + index, value); - } - } - - private void initSceneButtons(final HwElements hwElements) { - for (int index = 0; index < 8; index++) { - final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(index); - sceneButton.disable(sceneButtonLayer); - } - } - - private void initSceneButtons_advanced(final HwElements hwElements) { - for (int i = 0; i < 7; i++) { - final int index = i; - final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(index); - sceneButton.bindPressed(sceneButtonLayer, pressed -> handleSelect(pressed, index)); - sceneButton.bindLight(sceneButtonLayer, () -> getModeState_advanced(index)); - } - final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(7); - sceneButton.bindPressed(sceneButtonLayer, touchActive::set); - sceneButton.bindLight(sceneButtonLayer, () -> touchActive.get() ? RgbState.of(9) : RgbState.of(11)); - } - - private RgbState getModeState_advanced(final int index) { - if (index < 3) { - switch (index) { - case 0: - return volumeMode == 0 ? RgbState.of(25) : RgbState.of(27); - case 1: - return volumeMode == 1 ? RgbState.of(33) : RgbState.of(35); - case 2: - return volumeMode == 2 ? RgbState.of(41) : RgbState.of(43); - } - } - return RgbState.OFF; - } - - private void handleSelect(final boolean pressed, final int index) { - if (!pressed || volumeMode == index) { - return; - } - final Layer nextLayer = modeLayers.get(index); - if (nextLayer == null) { - return; - } - currentLayer.setIsActive(false); - currentLayer = nextLayer; - currentLayer.setIsActive(true); - volumeMode = index; - updateFaderState(); - } - - @Override - protected void updateFaderState() { - if (isActive()) { - refreshTrackColors(); - modeHandler.setFaderBank(panelLayout == PanelLayout.VERTICAL ? 0 : 1, mode, tracksExistsColors); - switch (volumeMode) { - case 0: - valueBindings.forEach(SliderBinding::update); - break; - case 1: - masterBindings.forEach(SliderBinding::update); - break; - case 2: - for (int i = 0; i < 8; i++) { - midiProcessor.sendMidi(0xB4, mode.getCcNr() + i, vuLevels[i]); - } - } - - } - } - - - @Override - protected void refreshTrackColors() { - final boolean[] exists = trackState.getExists(); - final int[] colorIndex = trackState.getColorIndex(); - - for (int i = 0; i < 8; i++) { - switch (volumeMode) { - case 0: - tracksExistsColors[i] = exists[i] ? mode.getColor() : 0; - break; - case 1: - if (i == 7) { - tracksExistsColors[i] = 1; - } else { - tracksExistsColors[i] = masterExists[i] ? 4 : 0; - } - break; - case 2: - tracksExistsColors[i] = exists[i] ? colorIndex[i] : 0; - break; - } - } - } - - @Override - protected void onDeactivate() { - super.onDeactivate(); - currentLayer.setIsActive(false); - sceneButtonLayer.setIsActive(false); - updateFaderState(); - } - - @Override - protected void onActivate() { - super.onActivate(); - DebugOutLp.println("Activate Volume Layer"); - refreshTrackColors(); - currentLayer.setIsActive(true); - sceneButtonLayer.setIsActive(true); - modeHandler.changeMode(LpBaseMode.FADER, mode.getBankId()); - modeHandler.setFaderBank(panelLayout == PanelLayout.VERTICAL ? 0 : 1, mode, tracksExistsColors); - updateFaderState(); - } - + final int index = i; + //volumeLayer.bind(sliders[i], track.volume().value()); + final SliderBinding binding = + new SliderBinding(mode.getCcNr(), track.volume(), sliders[i], i, midiProcessor); + volumeLayer.addBinding(binding); + valueBindings.add(binding); + } + touchActive.addValueObserver(touched -> { + for (int i = 0; i < 8; i++) { + final Track track = trackBank.getItemAt(i); + track.volume().touch(touched); + } + }); + } + + private void handleVu(final int index, final int value) { + if (isActive() && volumeMode == 2) { + vuLevels[index] = value; + midiProcessor.sendMidi(0xB4, mode.getCcNr() + index, value); + } + } + + private void initSceneButtons(final LpProHwElements hwElements) { + for (int index = 0; index < 8; index++) { + final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(index); + sceneButton.disable(sceneButtonLayer); + } + } + + private void initSceneButtons_advanced(final LpProHwElements hwElements) { + for (int i = 0; i < 7; i++) { + final int index = i; + final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(index); + sceneButton.bindPressed(sceneButtonLayer, pressed -> handleSelect(pressed, index)); + sceneButton.bindLight(sceneButtonLayer, () -> getModeState_advanced(index)); + } + final LabeledButton sceneButton = hwElements.getSceneLaunchButtons().get(7); + sceneButton.bindPressed(sceneButtonLayer, touchActive::set); + sceneButton.bindLight(sceneButtonLayer, () -> touchActive.get() ? RgbState.of(9) : RgbState.of(11)); + } + + private RgbState getModeState_advanced(final int index) { + if (index < 3) { + switch (index) { + case 0: + return volumeMode == 0 ? RgbState.of(25) : RgbState.of(27); + case 1: + return volumeMode == 1 ? RgbState.of(33) : RgbState.of(35); + case 2: + return volumeMode == 2 ? RgbState.of(41) : RgbState.of(43); + } + } + return RgbState.OFF; + } + + private void handleSelect(final boolean pressed, final int index) { + if (!pressed || volumeMode == index) { + return; + } + final Layer nextLayer = modeLayers.get(index); + if (nextLayer == null) { + return; + } + currentLayer.setIsActive(false); + currentLayer = nextLayer; + currentLayer.setIsActive(true); + volumeMode = index; + updateFaderState(); + } + + @Override + protected void updateFaderState() { + if (isActive()) { + refreshTrackColors(); + modeHandler.setFaderBank(panelLayout == PanelLayout.VERTICAL ? 0 : 1, mode, tracksExistsColors); + switch (volumeMode) { + case 0: + valueBindings.forEach(SliderBinding::update); + break; + case 1: + masterBindings.forEach(SliderBinding::update); + break; + case 2: + for (int i = 0; i < 8; i++) { + midiProcessor.sendMidi(0xB4, mode.getCcNr() + i, vuLevels[i]); + } + } + + } + } + + + @Override + protected void refreshTrackColors() { + final boolean[] exists = trackState.getExists(); + final int[] colorIndex = trackState.getColorIndex(); + + for (int i = 0; i < 8; i++) { + switch (volumeMode) { + case 0: + tracksExistsColors[i] = exists[i] ? mode.getColor() : 0; + break; + case 1: + if (i == 7) { + tracksExistsColors[i] = 1; + } else { + tracksExistsColors[i] = masterExists[i] ? 4 : 0; + } + break; + case 2: + tracksExistsColors[i] = exists[i] ? colorIndex[i] : 0; + break; + } + } + } + + @Override + protected void onDeactivate() { + super.onDeactivate(); + currentLayer.setIsActive(false); + sceneButtonLayer.setIsActive(false); + updateFaderState(); + } + + @Override + protected void onActivate() { + super.onActivate(); + refreshTrackColors(); + currentLayer.setIsActive(true); + sceneButtonLayer.setIsActive(true); + modeHandler.changeMode(LpBaseMode.FADER, mode.getBankId()); + modeHandler.setFaderBank(panelLayout == PanelLayout.VERTICAL ? 0 : 1, mode, tracksExistsColors); + updateFaderState(); + } + } 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 aba4a916..213e7b1f 100644 --- a/src/main/java/com/bitwig/extensions/framework/di/Context.java +++ b/src/main/java/com/bitwig/extensions/framework/di/Context.java @@ -1,497 +1,521 @@ package com.bitwig.extensions.framework.di; +import java.io.IOException; +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.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; -import java.io.IOException; -import java.lang.reflect.*; -import java.util.*; - /** * 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) { - initializeExtension(extension); - scanPackage(extension.getClass()); - } - - public void activate() { - final HashSet serviced = new HashSet<>(); - final Collection components = services.values(); - for (final Object component : components) { - if (!serviced.contains(component) && invokeActivation(component)) { - serviced.add(component); - } - } - } - - private boolean invokeActivation(final Object comp) { - final Class clazz = comp.getClass(); - final Method[] methods = clazz.getMethods(); - for (final Method method : methods) { - final Activate activateAnnotation = method.getAnnotation(Activate.class); - 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 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() { + final HashSet serviced = new HashSet<>(); + final Collection components = services.values(); + for (final Object component : components) { + if (!serviced.contains(component) && invokeActivation(component)) { + serviced.add(component); } - } - } - return false; - } - - private record ComponentClassBind(Class clazz, Component component) implements Comparable { - // - static Optional create(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(ComponentClassBind o) { - return o.component.priority() - component.priority(); - } - } - - private void scanPackage(final Class baseClass) { - try { - final List> classes = PackageHelper.getClasses(baseClass); - List components = classes.stream()// - .map(ComponentClassBind::create) // - .flatMap(o -> o.stream()).sorted() // - .toList(); - for (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()); - } - - 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 clazz = comp.getClass(); + final Method[] methods = clazz.getMethods(); + for (final Method method : methods) { + final Activate activateAnnotation = method.getAnnotation(Activate.class); + 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) { + } + 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; + } + } + 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()); + } + } + } + + 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 boolean success = inject(object, field); - if (!success) { - closure.missingComponents.add(field.getType()); - } + final Object serviceObject = getServiceImpl(field.getType()); + if (serviceObject == null) { + missingComponents.add(field.getType()); + } } - } - - 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 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(); } - } - 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()); + } + 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); + } } - } - } - 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 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) { + } + } + + 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/di/PackageHelper.java b/src/main/java/com/bitwig/extensions/framework/di/PackageHelper.java index 2921d22c..10313660 100644 --- a/src/main/java/com/bitwig/extensions/framework/di/PackageHelper.java +++ b/src/main/java/com/bitwig/extensions/framework/di/PackageHelper.java @@ -4,7 +4,11 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; -import java.util.*; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Optional; +import java.util.TreeSet; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -12,17 +16,27 @@ * Utility class to get classes from packages. */ public class PackageHelper { - + private PackageHelper() { // just a utility class } - - public static List> getClasses(final Class baseClass) throws IOException, ClassNotFoundException { - return getClasses(baseClass.getPackageName(), baseClass.getClassLoader()); + + public static List> getClasses(final Class baseClass, final Package... packages) throws + IOException, + ClassNotFoundException { + final ClassLoader classLoader = baseClass.getClassLoader(); + final List> classes = new ArrayList<>(); + final List> baseList = getClasses(baseClass.getPackageName(), classLoader); + for (final Package pack : packages) { + classes.addAll(getClasses(pack.getName(), classLoader)); + } + classes.addAll(baseList); + return classes; } - - public static List> getClasses(final String packageName, - final ClassLoader classLoader) throws IOException, ClassNotFoundException { + + private static List> getClasses(final String packageName, final ClassLoader classLoader) throws + IOException, + ClassNotFoundException { assert classLoader != null; final String path = packageName.replace('.', '/'); final Enumeration resources = classLoader.getResources(path); @@ -31,7 +45,7 @@ public static List> getClasses(final String packageName, final URL resource = resources.nextElement(); dirs.add(resource.getFile()); } - + final TreeSet classes = new TreeSet<>(); for (final String directory : dirs) { classes.addAll(findClasses(directory, packageName)); @@ -40,11 +54,11 @@ public static List> getClasses(final String packageName, for (final String clazz : classes) { classList.add(Class.forName(clazz)); } - + return classList; } - - + + private static Optional toDirectoryFilePath(final String directory) throws MalformedURLException { if (directory.startsWith("file:") && directory.contains("!")) { final String[] split = directory.split("!"); @@ -52,41 +66,39 @@ private static Optional toDirectoryFilePath(final String directory) throws } return Optional.empty(); } - + private static String toSystemPath(final String directory) { if (File.separatorChar == '\\') { return directory.replace("%20", " "); } return directory; } - + private static Optional classNameFromZipEntry(final ZipEntry entry, final String packageName) { if (entry.getName().endsWith(".class")) { - final String className = entry.getName() - .replaceAll("[$].*", "") - .replaceAll("[.]class", "") - .replace('/', '.'); + final String className = + entry.getName().replaceAll("[$].*", "").replaceAll("[.]class", "").replace('/', '.'); if (className.startsWith(packageName)) { return Optional.of(className); } } return Optional.empty(); } - + private static TreeSet findClasses(final String directory, final String packageName) throws IOException { final TreeSet classes = new TreeSet<>(); - + final Optional dirUrl = toDirectoryFilePath(directory); if (dirUrl.isPresent()) { try (final ZipInputStream zip = new ZipInputStream(dirUrl.get().openStream())) { ZipEntry entry; while ((entry = zip.getNextEntry()) != null) { classNameFromZipEntry(entry, packageName) // - .ifPresent(classes::add); + .ifPresent(classes::add); } } } - + final File dir = new File(toSystemPath(directory)); if (!dir.exists()) { return classes; diff --git a/src/main/java/com/bitwig/extensions/framework/values/BasicDoubleValue.java b/src/main/java/com/bitwig/extensions/framework/values/BasicDoubleValue.java new file mode 100644 index 00000000..994a6147 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/framework/values/BasicDoubleValue.java @@ -0,0 +1,59 @@ +package com.bitwig.extensions.framework.values; + +import java.util.ArrayList; +import java.util.List; + +import com.bitwig.extension.callback.DoubleValueChangedCallback; +import com.bitwig.extension.controller.api.SettableDoubleValue; + +public class BasicDoubleValue implements SettableDoubleValue { + + private double value; + private final List changeListeners = new ArrayList<>(); + + @Override + public double get() { + return value; + } + + @Override + public void markInterested() { + + } + + @Override + public void addValueObserver(final DoubleValueChangedCallback doubleValueChangedCallback) { + changeListeners.add(doubleValueChangedCallback); + } + + @Override + public boolean isSubscribed() { + return true; + } + + @Override + public void setIsSubscribed(final boolean b) { + } + + @Override + public void subscribe() { + } + + @Override + public void unsubscribe() { + } + + @Override + public void set(final double v) { + if (v != this.value) { + this.value = v; + changeListeners.forEach(callback -> callback.valueChanged(v)); + } + } + + @Override + public void inc(final double v) { + final double newValue = this.value + v; + set(newValue); + } +} diff --git a/src/main/java/com/bitwig/extensions/framework/values/BooleanValueObject.java b/src/main/java/com/bitwig/extensions/framework/values/BooleanValueObject.java index 32ec47b0..27a60a1d 100644 --- a/src/main/java/com/bitwig/extensions/framework/values/BooleanValueObject.java +++ b/src/main/java/com/bitwig/extensions/framework/values/BooleanValueObject.java @@ -10,91 +10,94 @@ import com.bitwig.extension.controller.api.SettableBooleanValue; public class BooleanValueObject implements SettableBooleanValue { - - private boolean value = false; - private final List callbacks = new ArrayList<>(); - - public BooleanValueObject() { - } - - public BooleanValueObject(final boolean initValue) { - this.value = initValue; - } - - @Override - public void markInterested() { - } - - @Override - public void toggle() { - this.value = !this.value; - for (final BooleanValueChangedCallback booleanValueChangedCallback : callbacks) { - booleanValueChangedCallback.valueChanged(value); - } - } - - @Override - public void addValueObserver(final BooleanValueChangedCallback callback) { - if (!callbacks.contains(callback)) { - callbacks.add(callback); - } - } - - @Override - public boolean isSubscribed() { - return !callbacks.isEmpty(); - } - - @Override - public void setIsSubscribed(final boolean value) { - } - - @Override - public void subscribe() { - } - - @Override - public void unsubscribe() { - } - - @Override - public void set(final boolean value) { - if (this.value == value) { - return; - } - this.value = value; - for (final BooleanValueChangedCallback booleanValueChangedCallback : callbacks) { - booleanValueChangedCallback.valueChanged(value); - } - } - - @Override - public boolean get() { - return value; - } - - @Override - public HardwareActionBinding addBinding(final HardwareAction action) { - return null; - } - - @Override - public void invoke() { - } - - @Override - public HardwareActionBindable toggleAction() { - return null; - } - - @Override - public HardwareActionBindable setToTrueAction() { - return null; - } - - @Override - public HardwareActionBindable setToFalseAction() { - return null; - } - + + private boolean value = false; + private final List callbacks = new ArrayList<>(); + + public BooleanValueObject() { + } + + public BooleanValueObject(final boolean initValue) { + this.value = initValue; + } + + @Override + public void markInterested() { + } + + @Override + public void toggle() { + this.value = !this.value; + for (final BooleanValueChangedCallback booleanValueChangedCallback : callbacks) { + booleanValueChangedCallback.valueChanged(value); + } + } + + @Override + public void addValueObserver(final BooleanValueChangedCallback callback) { + if (!callbacks.contains(callback)) { + callbacks.add(callback); + } + } + + @Override + public boolean isSubscribed() { + return !callbacks.isEmpty(); + } + + @Override + public void setIsSubscribed(final boolean value) { + } + + @Override + public void subscribe() { + } + + @Override + public void unsubscribe() { + } + + @Override + public void set(final boolean value) { + if (this.value == value) { + return; + } + this.value = value; + for (final BooleanValueChangedCallback booleanValueChangedCallback : callbacks) { + booleanValueChangedCallback.valueChanged(value); + } + } + + @Override + public boolean get() { + return value; + } + + @Override + public HardwareActionBinding addBinding(final HardwareAction action) { + return null; + } + + @Override + public void invoke() { + } + + @Override + public HardwareActionBindable toggleAction() { + return null; + } + + @Override + public HardwareActionBindable setToTrueAction() { + return null; + } + + @Override + public HardwareActionBindable setToFalseAction() { + return null; + } + + public void setDirect(final boolean b) { + this.value = b; + } } diff --git a/src/main/java/com/bitwig/extensions/framework/values/IntValueObject.java b/src/main/java/com/bitwig/extensions/framework/values/IntValueObject.java new file mode 100644 index 00000000..b399e528 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/framework/values/IntValueObject.java @@ -0,0 +1,76 @@ +package com.bitwig.extensions.framework.values; + +import java.util.ArrayList; +import java.util.List; + +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); + } + +} diff --git a/src/main/java/com/bitwig/extensions/framework/values/LayoutType.java b/src/main/java/com/bitwig/extensions/framework/values/LayoutType.java new file mode 100644 index 00000000..d7bef59e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/framework/values/LayoutType.java @@ -0,0 +1,34 @@ +package com.bitwig.extensions.framework.values; + +public enum LayoutType { + LAUNCHER("MIX"), // + ARRANGER("ARRANGE"), + EDIT("EDIT"); + + private final String name; + + LayoutType(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static LayoutType toType(final String layoutName) { + for (final LayoutType layoutType : LayoutType.values()) { + if (layoutType.name.equals(layoutName)) { + return layoutType; + } + } + return LAUNCHER; + } + + public LayoutType other() { + if (this == LAUNCHER) { + return ARRANGER; + } + return LAUNCHER; + } + +} diff --git a/src/main/resources/Documentation/Controllers/Akai/AKAI APC64.pdf b/src/main/resources/Documentation/Controllers/Akai/AKAI APC64.pdf new file mode 100644 index 00000000..da1335e2 Binary files /dev/null and b/src/main/resources/Documentation/Controllers/Akai/AKAI APC64.pdf differ diff --git a/src/main/resources/Documentation/Controllers/MIDIPLUS/XPro Keyboards.html b/src/main/resources/Documentation/Controllers/MIDIPLUS/XPro Keyboards.html index 51e7c2c8..e0fe5ecb 100644 --- a/src/main/resources/Documentation/Controllers/MIDIPLUS/XPro Keyboards.html +++ b/src/main/resources/Documentation/Controllers/MIDIPLUS/XPro Keyboards.html @@ -144,6 +144,7 @@

MIDIPlus XPro Keyboards

  • Keys, Pitch-Bend and Mod-Wheel are working
  • Transport buttons are working
  • +

    Make sure your controller has the latest firmware update.

    diff --git a/src/main/resources/Documentation/Controllers/MIDIPLUS/Xmini Keyboards.html b/src/main/resources/Documentation/Controllers/MIDIPLUS/Xmini Keyboards.html index cefa9b4b..2d20090b 100644 --- a/src/main/resources/Documentation/Controllers/MIDIPLUS/Xmini Keyboards.html +++ b/src/main/resources/Documentation/Controllers/MIDIPLUS/Xmini Keyboards.html @@ -142,6 +142,7 @@

    MIDIPlus Xmini Keyboards

  • Keys, Pitch-Bend and Mod-Wheel are working
  • Transport buttons are working
  • +

    Make sure your controller has the latest firmware update.

    diff --git a/src/main/resources/Documentation/Controllers/Novation/Launchpad Mini MK3.pdf b/src/main/resources/Documentation/Controllers/Novation/Launchpad Mini MK3.pdf index 35dcc1bf..a3e0481b 100644 Binary files a/src/main/resources/Documentation/Controllers/Novation/Launchpad Mini MK3.pdf and b/src/main/resources/Documentation/Controllers/Novation/Launchpad Mini MK3.pdf differ diff --git a/src/main/resources/Documentation/Controllers/Novation/Launchpad Pro Mk3.pdf b/src/main/resources/Documentation/Controllers/Novation/Launchpad Pro Mk3.pdf index 32ebbc13..ed1f2d16 100644 Binary files a/src/main/resources/Documentation/Controllers/Novation/Launchpad Pro Mk3.pdf and b/src/main/resources/Documentation/Controllers/Novation/Launchpad Pro Mk3.pdf differ diff --git a/src/main/resources/Documentation/Controllers/Novation/Launchpad X.pdf b/src/main/resources/Documentation/Controllers/Novation/Launchpad X.pdf index e2a485fe..8b35ebe2 100644 Binary files a/src/main/resources/Documentation/Controllers/Novation/Launchpad X.pdf and b/src/main/resources/Documentation/Controllers/Novation/Launchpad X.pdf differ diff --git a/src/main/resources/Documentation/Controllers/SSL/SSL UF8-UF1.pdf b/src/main/resources/Documentation/Controllers/SSL/SSL UF8-UF1.pdf new file mode 100644 index 00000000..949776e7 Binary files /dev/null and b/src/main/resources/Documentation/Controllers/SSL/SSL UF8-UF1.pdf differ diff --git a/src/main/resources/Documentation/Controllers/iCon/P1-M.pdf b/src/main/resources/Documentation/Controllers/iCon/P1-M.pdf new file mode 100644 index 00000000..257ffbc1 Binary files /dev/null and b/src/main/resources/Documentation/Controllers/iCon/P1-M.pdf differ diff --git a/src/main/resources/Documentation/Controllers/iCon/P1-Nano.pdf b/src/main/resources/Documentation/Controllers/iCon/P1-Nano.pdf new file mode 100644 index 00000000..735caac9 Binary files /dev/null and b/src/main/resources/Documentation/Controllers/iCon/P1-Nano.pdf differ diff --git a/src/main/resources/Documentation/Controllers/iCon/V1-M.pdf b/src/main/resources/Documentation/Controllers/iCon/V1-M.pdf new file mode 100644 index 00000000..b2e27316 Binary files /dev/null and b/src/main/resources/Documentation/Controllers/iCon/V1-M.pdf differ diff --git a/src/main/resources/Documentation/Controllers/iCon/mappings/Bitwig P1-M.zip b/src/main/resources/Documentation/Controllers/iCon/mappings/Bitwig P1-M.zip new file mode 100644 index 00000000..59d5b1f6 Binary files /dev/null and b/src/main/resources/Documentation/Controllers/iCon/mappings/Bitwig P1-M.zip differ diff --git a/src/main/resources/Documentation/Controllers/iCon/mappings/Bitwig P1-Nano.zip b/src/main/resources/Documentation/Controllers/iCon/mappings/Bitwig P1-Nano.zip new file mode 100644 index 00000000..25a313a5 Binary files /dev/null and b/src/main/resources/Documentation/Controllers/iCon/mappings/Bitwig P1-Nano.zip differ diff --git a/src/main/resources/Documentation/Controllers/iCon/mappings/Bitwig V1-M.zip b/src/main/resources/Documentation/Controllers/iCon/mappings/Bitwig V1-M.zip new file mode 100644 index 00000000..3e7a0f2c Binary files /dev/null and b/src/main/resources/Documentation/Controllers/iCon/mappings/Bitwig V1-M.zip 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 b15b554d..71db03ad 100644 --- a/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition +++ b/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition @@ -4,6 +4,7 @@ com.bitwig.extensions.controllers.akai.mpk_mini_mk3.MpkMiniMk3ControllerExtensio com.bitwig.extensions.controllers.akai.mpkminiplus.MpkMiniPlusControllerExtensionDefinition 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.arturia.keylab.mk1.ArturiaKeylab25ControllerExtensionDefinition com.bitwig.extensions.controllers.arturia.keylab.mk2.ArturiaKeylab49MkIIControllerExtensionDefinition @@ -43,6 +44,8 @@ com.bitwig.extensions.controllers.icon.VCastDefinition com.bitwig.extensions.controllers.icon.VCastProDefinition com.bitwig.extensions.controllers.icon.VCastRxDefinition +com.bitwig.extensions.controllers.intuitive_instruments.exquis.ExquisControllerExtensionDefinition + com.bitwig.extensions.controllers.keith_mcmillen.QuNexusDefinition com.bitwig.extensions.controllers.kenton.KillaMixMiniExtensionDefinition @@ -76,6 +79,24 @@ com.bitwig.extensions.controllers.mackie.definition.IconQconProXXt2ExtensionDefi com.bitwig.extensions.controllers.mackie.definition.IconQconProXXt3ExtensionDefinition com.bitwig.extensions.controllers.mackie.definition.IconPlatformMExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.ssl.SslUf8ExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.ssl.SslUf8Plus1ExtenderExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.ssl.SslUf8Plus2ExtenderExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.ssl.SslUf8Plus3ExtenderExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.icon.IconP1mExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.icon.IconP1mPlus1ExtenderExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.icon.IconP1mPlus2ExtenderExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.icon.IconP1mPlus3ExtenderExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.icon.IconP1NanoExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.icon.IconP1NanoPlus1ExtenderExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.icon.IconP1NanoPlus2ExtenderExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.icon.IconP1NanoPlus3ExtenderExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.icon.IconV1mExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.icon.IconV1mPlus1ExtenderExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.icon.IconV1mPlus2ExtenderExtensionDefinition +com.bitwig.extensions.controllers.mcu.definitions.icon.IconV1mPlus3ExtenderExtensionDefinition +#com.bitwig.extensions.controllers.mcu.definitions.mackie.MackieMcuProExtensionDefinition + com.bitwig.extensions.controllers.maudio.oxygenpro.definition.OxygenProMiniExtensionDefinition com.bitwig.extensions.controllers.maudio.oxygenpro.definition.OxygenPro25ExtensionDefinition com.bitwig.extensions.controllers.maudio.oxygenpro.definition.OxygenPro49ExtensionDefinition