Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Akai APC64 and some improvements for Launchpads Mk3 #78

Merged
merged 6 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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));
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,4 +19,5 @@ public interface MidiProcessor {
void setModeChangeListener(final IntConsumer modeChangeListener);

MidiIn getMidiIn();

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.bitwig.extensions.controllers.akai.apc.common;

public enum PanelLayout {
VERTICAL,
HORIZONTAL
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
package com.bitwig.extensions.controllers.akai.apcmk2.control;
package com.bitwig.extensions.controllers.akai.apc.common.control;

import java.util.function.Function;
import java.util.function.Supplier;

import com.bitwig.extension.controller.api.HardwareActionBindable;
import com.bitwig.extension.controller.api.HardwareButton;
import com.bitwig.extension.controller.api.HardwareSurface;
import com.bitwig.extension.controller.api.InternalHardwareLightState;
import com.bitwig.extension.controller.api.MidiIn;
import com.bitwig.extension.controller.api.MultiStateHardwareLight;
import com.bitwig.extensions.controllers.akai.apcmk2.led.RgbLightState;
import com.bitwig.extensions.controllers.akai.apcmk2.midi.MidiProcessor;
import com.bitwig.extension.controller.api.*;
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;

public abstract class ApcButton {
public static final int STD_REPEAT_DELAY = 400;
public static final int STD_REPEAT_FREQUENCY = 50;
Expand All @@ -35,16 +32,21 @@ protected ApcButton(final int channel, final int midiId, final String name, fina
hwButton.pressedAction().setPressureActionMatcher(midiIn.createNoteOnVelocityValueMatcher(channel, midiId));
hwButton.releasedAction().setActionMatcher(midiIn.createNoteOffActionMatcher(channel, midiId));
light = surface.createMultiStateHardwareLight(name + "_LIGHT_" + midiId);
hwButton.setBackgroundLight(light);
light.state().setValue(RgbLightState.OFF);
light.setColorToStateFunction(RgbLightState::forColor);
hwButton.setBackgroundLight(light);
hwButton.isPressed().markInterested();
}


public void refresh() {
light.state().setValue(null);
}

public void bindIsPressed(final Layer layer, final Consumer<Boolean> 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);
}
Expand All @@ -61,6 +63,10 @@ public void bindLight(final Layer layer, final Supplier<InternalHardwareLightSta
layer.bindLightState(supplier, light);
}

public void bindLightPressed(final Layer layer, final Function<Boolean, InternalHardwareLightState> supplier) {
layer.bindLightState(() -> supplier.apply(hwButton.isPressed().get()), light);
}

public void bindLight(final Layer layer, final Function<Boolean, InternalHardwareLightState> pressedCombine) {
layer.bindLightState(() -> pressedCombine.apply(hwButton.isPressed().get()), light);
}
Expand All @@ -70,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<Boolean> 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<Boolean> 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<Boolean> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
Expand All @@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
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, final 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 {
light.state().onUpdateHardware(this::updateState);
}
}

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 final RgbLightState state) {
if (internalHardwareLightState instanceof RgbLightState state) {
midiProcessor.sendMidi(Midi.NOTE_ON | 0x9, midiId, state.getColorIndex());
} else {
midiProcessor.sendMidi(Midi.NOTE_ON, midiId, 0);
Expand All @@ -29,8 +36,7 @@ private void updateDrumState(final InternalHardwareLightState internalHardwareLi


private void updateState(final InternalHardwareLightState internalHardwareLightState) {
if (internalHardwareLightState instanceof RgbLightState) {
final RgbLightState state = (RgbLightState) internalHardwareLightState;
if (internalHardwareLightState instanceof RgbLightState state) {
midiProcessor.sendMidi(state.getMidiCode(), midiId, state.getColorIndex());
} else {
midiProcessor.sendMidi(Midi.NOTE_ON, midiId, 0);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Loading
Loading