Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b65bde9
DI added Tags for Components
ericahrens Oct 26, 2025
eb3d338
Launchkontrol Mk3 New Produkt
ericahrens Oct 26, 2025
6297aa3
Arturia Keylab Mk3 Stop all scene hold
ericahrens Oct 26, 2025
46b5930
Novation Definition changed
ericahrens Oct 26, 2025
d462730
Merge branch 'main' of https://github.com/ericahrens/bitwig-extensions
ericahrens Nov 2, 2025
5528606
Launchcontrol Mk3 - Vast Improvements and Layering and Binding
ericahrens Nov 9, 2025
4d355ac
Launchcontrol Mk4 latest fixes
ericahrens Nov 23, 2025
c2f73bb
Added Mpk IV
ericahrens Dec 13, 2025
14e7e90
Akai MPK mini IV Clip Control added and Controls Mapped
ericahrens Dec 17, 2025
a4527f8
Merge branch 'main' of https://github.com/ericahrens/bitwig-extensions
ericahrens Dec 17, 2025
97fb56f
Merge branch 'main' of https://github.com/ericahrens/bitwig-extensions
ericahrens Dec 17, 2025
52d9b50
First Display implementations
ericahrens Dec 19, 2025
775760c
Merge branch 'main' of https://github.com/ericahrens/bitwig-extensions
ericahrens Dec 19, 2025
26687ee
MPK4 Display Used
ericahrens Dec 25, 2025
5bfe746
MPK4 Drum Pad Mode added
ericahrens Dec 27, 2025
350bd6e
MPK4 Menus added
ericahrens Jan 9, 2026
a567756
Akai MPK mini IV Improve Remotes and other details
ericahrens Jan 12, 2026
2f93a13
Akai MPK Mini Iv Release Fixes
ericahrens Jan 17, 2026
18954af
Mpk Mk4 Version 1.0
ericahrens Jan 20, 2026
f50a178
Allen & Heath Xone K3 added
ericahrens Jan 21, 2026
39ed649
Remove Novation Launchcontrol Mk3 for now
ericahrens Jan 22, 2026
8ccf88a
Launchkontrol XL back in
ericahrens Jan 22, 2026
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
@@ -1,30 +1,35 @@
package com.bitwig.extensions.controllers.akai.apc.common.control;

import com.bitwig.extension.controller.api.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

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

protected MultiStateHardwareLight light;
protected HardwareButton hwButton;
protected MidiProcessor midiProcessor;
private TimedEvent currentTimer;
private long recordedDownTime;
protected final int midiId;

protected ApcButton(final int channel, final int midiId, final String name, final HardwareSurface surface,
final MidiProcessor midiProcessor) {
final MidiProcessor midiProcessor) {
this.midiProcessor = midiProcessor;
final MidiIn midiIn = midiProcessor.getMidiIn();
this.midiId = midiId;
Expand All @@ -36,69 +41,71 @@ protected ApcButton(final int channel, final int midiId, final String name, fina
hwButton.setBackgroundLight(light);
hwButton.isPressed().markInterested();
}



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

public void bindIsPressed(final Layer layer, final Consumer<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);
}

public void bindPressed(final Layer layer, final HardwareActionBindable action) {
layer.bind(hwButton, hwButton.pressedAction(), action);
}

public void bindRelease(final Layer layer, final Runnable action) {
layer.bind(hwButton, hwButton.releasedAction(), action);
}

public void bindLight(final Layer layer, final Supplier<InternalHardwareLightState> supplier) {
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);
}

public void bindLightPressed(final Layer layer, final InternalHardwareLightState state,
final InternalHardwareLightState holdState) {
final InternalHardwareLightState holdState) {
layer.bindLightState(() -> hwButton.isPressed().get() ? holdState : state, light);
}

/**
* Models following behavior. Pressing and Releasing the button within the given delay time executes the click event.
* Models following behavior. Pressing and Releasing the button within the given delay time executes the click
* event.
* Long Pressing the button invokes the holdAction with true and then the same action with false once released.
*
* @param layer the layer
* @param clickAction the action invoked if the button is pressed and release in less than the given delay time
* @param holdAction action called with true when the delay time expires and with false if released under this condition
* @param holdAction action called with true when the delay time expires and with false if released under this
* condition
* @param delayTime the delay time
*/
public void bindDelayedHold(final Layer layer, final Runnable clickAction, final Consumer<Boolean> holdAction,
final long delayTime) {
final long delayTime) {
layer.bind(hwButton, hwButton.pressedAction(), () -> initiateHold(holdAction, delayTime));
layer.bind(hwButton, hwButton.releasedAction(), () -> handleDelayedRelease(clickAction, holdAction));
}

private void initiateHold(final Consumer<Boolean> holdAction, final long delayTime) {
recordedDownTime = System.currentTimeMillis();
currentTimer = new TimedDelayEvent(() -> {
holdAction.accept(true);
}, delayTime);
currentTimer = new TimedDelayEvent(
() -> {
holdAction.accept(true);
}, delayTime);
midiProcessor.queueEvent(currentTimer);
}

private void handleDelayedRelease(final Runnable clickAction, final Consumer<Boolean> holdAction) {
if (currentTimer != null && !currentTimer.isCompleted()) {
currentTimer.cancel();
Expand All @@ -108,7 +115,7 @@ private void handleDelayedRelease(final Runnable clickAction, final Consumer<Boo
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 All @@ -118,22 +125,23 @@ private void handleDelayedRelease(final Runnable clickAction, final Consumer<Boo
* @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.pressedAction(),
() -> initiateRepeat(action, STD_REPEAT_DELAY, STD_REPEAT_FREQUENCY));
layer.bind(hwButton, hwButton.releasedAction(), this::cancelEvent);
}

public void initiateRepeat(final Runnable action, final int repeatDelay, final int repeatFrequency) {
action.run();
currentTimer = new TimeRepeatEvent(action, repeatDelay, repeatFrequency);
midiProcessor.queueEvent(currentTimer);
}

private void cancelEvent() {
if (currentTimer != null) {
currentTimer.cancel();
currentTimer = null;
}
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.bitwig.extensions.controllers.akai.apc64;

public class StringUtil {
private static final char[] SPECIALS = {'ä', 'ü', 'ö', 'Ä', 'Ü', 'Ö', 'ß', 'é', 'è', 'ê', 'â', 'á', 'à', //
'û', 'ú', 'ù', 'ô', 'ó', 'ò'};
private static final String[] REPLACE = {"a", "u", "o", "A", "U", "O", "ss", "e", "e", "e", "a", "a", "a", //
"u", "u", "u", "o", "o", "o"};

private static final char[] SPECIALS = {
'ä', 'ü', 'ö', 'Ä', 'Ü', 'Ö', 'ß', 'é', 'è', 'ê', 'â', 'á', 'à', //
'û', 'ú', 'ù', 'ô', 'ó', 'ò'
};
private static final String[] REPLACE = {
"a", "u", "o", "A", "U", "O", "s", "e", "e", "e", "a", "a", "a", //
"u", "u", "u", "o", "o", "o"
};

public static String nextValue(final String currentValue, final String[] list, final int inc, final boolean wrap) {
int index = -1;
for (int i = 0; i < list.length; i++) {
Expand All @@ -25,14 +29,14 @@ public static String nextValue(final String currentValue, final String[] list, f
}
return list[0];
}

public static String toAsciiDisplay(final String name, final int maxLen) {
final StringBuilder b = new StringBuilder();
for (int i = 0; i < name.length() && b.length() < maxLen; i++) {
final char c = name.charAt(i);
// if (c == 32) {
// continue;
// }
// if (c == 32) {
// continue;
// }
if (c < 128) {
b.append(c);
} else {
Expand All @@ -44,7 +48,7 @@ public static String toAsciiDisplay(final String name, final int maxLen) {
}
return b.toString();
}

private static int getReplace(final char c) {
for (int i = 0; i < SPECIALS.length; i++) {
if (c == SPECIALS[i]) {
Expand All @@ -53,6 +57,6 @@ private static int getReplace(final char c) {
}
return -1;
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.bitwig.extensions.controllers.akai.mpkmk4;

import com.bitwig.extensions.framework.values.BasicStringValue;
import com.bitwig.extensions.framework.values.BooleanValueObject;

public class GlobalStates {

private final BooleanValueObject shiftHeld = new BooleanValueObject();
private final MpkMk4ControllerExtension.Variant variant;
private final BasicStringValue focusDeviceName = new BasicStringValue();

public GlobalStates(final MpkMk4ControllerExtension.Variant variant) {
this.variant = variant;
}

public MpkMk4ControllerExtension.Variant getVariant() {
return variant;
}

public BooleanValueObject getShiftHeld() {
return shiftHeld;
}

public BasicStringValue getFocusDeviceName() {
return focusDeviceName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.bitwig.extensions.controllers.akai.mpkmk4;

import java.util.Optional;

import com.bitwig.extension.controller.api.Clip;
import com.bitwig.extension.controller.api.ClipLauncherSlot;
import com.bitwig.extension.controller.api.ClipLauncherSlotBank;
import com.bitwig.extension.controller.api.ControllerHost;
import com.bitwig.extension.controller.api.CursorTrack;
import com.bitwig.extension.controller.api.Transport;
import com.bitwig.extensions.framework.di.Component;

@Component
public class MpkFocusClip {
private final Clip mainCursoClip;
private final int slotRange;
private final CursorTrack cursorTrack;
private final Transport transport;
private final ClipLauncherSlotBank slotBank;
private int selectedSlotIndex;

public MpkFocusClip(final ControllerHost host, final Transport transport, final MpkViewControl viewControl) {
this.cursorTrack = viewControl.getCursorTrack();
this.transport = transport;
mainCursoClip = host.createLauncherCursorClip(16, 1);
slotBank = cursorTrack.clipLauncherSlotBank();
slotRange = slotBank.getSizeOfBank();
for (int i = 0; i < slotBank.getSizeOfBank(); i++) {
final int index = i;
final ClipLauncherSlot slot = slotBank.getItemAt(i);
slot.isRecording().markInterested();
slot.isPlaying().markInterested();
slot.hasContent().markInterested();
slot.exists().markInterested();
slot.isSelected().addValueObserver(selected -> this.handleSelection(selected, index));
}
}

private void handleSelection(final boolean selected, final int index) {
if (selected) {
this.selectedSlotIndex = index;
}
}

public void setSelectedSlotIndex(final int pos) {
this.selectedSlotIndex = pos;
if (selectedSlotIndex < slotBank.getSizeOfBank()) {
slotBank.getItemAt(selectedSlotIndex).select();
}
}

public void invokeRecord() {
if (selectedSlotIndex != -1) {
final ClipLauncherSlot slot = slotBank.getItemAt(selectedSlotIndex);
if (!transport.isPlaying().get()) {
transport.play();
}
if (slot.isRecording().get()) {
slot.launch();
transport.isClipLauncherOverdubEnabled().set(false);
} else if (slot.isPlaying().get()) {
transport.isClipLauncherOverdubEnabled().toggle();
} else {
slot.launch();
transport.isClipLauncherOverdubEnabled().set(true);
}
} else {
findNextEmptySlot(true).ifPresent(slot -> {
slot.launch();
transport.isClipLauncherOverdubEnabled().set(true);
});
}
}

private Optional<ClipLauncherSlot> findNextEmptySlot(final boolean select) {
ClipLauncherSlot lastEmpty = null;
int lastEmptyIndex = -1;
for (int i = slotRange - 1; i >= 0; i--) {
final ClipLauncherSlot slot = slotBank.getItemAt(i);
if (slot.hasContent().get()) {
if (lastEmpty != null) {
break;
}
} else {
lastEmpty = slot;
lastEmptyIndex = i;
}
}
if (lastEmpty != null) {
if (select) {
slotBank.select(lastEmptyIndex);
}
return Optional.of(lastEmpty);
}
return Optional.empty();
}

public void quantize(final double amount) {
mainCursoClip.quantize(amount);
}


}
Loading
Loading