Skip to content

Commit

Permalink
Merge pull request #78 from ericahrens/api-18
Browse files Browse the repository at this point in the history
Added Akai APC64 and some improvements for Launchpads Mk3
  • Loading branch information
bitwig-thomas committed Feb 1, 2024
2 parents a45ea22 + a9c342b commit 4b17d7f
Show file tree
Hide file tree
Showing 118 changed files with 10,170 additions and 5,145 deletions.
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

0 comments on commit 4b17d7f

Please sign in to comment.