diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/MiniLab3ExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/MiniLab3ExtensionDefinition.java index b802f2c5..900598e8 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/MiniLab3ExtensionDefinition.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/MiniLab3ExtensionDefinition.java @@ -36,9 +36,10 @@ public String getHardwareModel() { public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, final PlatformType platformType) { if (platformType == PlatformType.WINDOWS) { - list.add(new String[] {PORT_NAME_MIDI}, new String[] {PORT_NAME_MIDI}); list.add(new String[] {PORT_NAME}, new String[] {PORT_NAME}); - appendRenamedPorts(4, list); + for (int i = 1; i < 5; i++) { + appendWinPrefix(list, i); + } } else if (platformType == PlatformType.MAC) { list.add(new String[] {PORT_NAME_MIDI}, new String[] {PORT_NAME_MIDI}); } else if (platformType == PlatformType.LINUX) { @@ -46,14 +47,11 @@ public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList } } - private void appendRenamedPorts(final int count, final AutoDetectionMidiPortNamesList list) { - for (int i = 2; i < count + 2; i++) { - list.add(getRenamedPorts(i), getRenamedPorts(i)); - } - } - - private String[] getRenamedPorts(final int index) { - return new String[] {"%d- %s".formatted(index, PORT_NAME_MIDI)}; + private void appendWinPrefix(final AutoDetectionMidiPortNamesList list, final int index) { + final String prefix = index > 1 ? "%d- ".formatted(index) : ""; + list.add( + new String[] {"%s%s MIDI".formatted(prefix, PORT_NAME)}, + new String[] {"%s%s MIDI".formatted(prefix, PORT_NAME)}); } @Override diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/ConnectionListener.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/ConnectionListener.java new file mode 100644 index 00000000..a8821d9b --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/ConnectionListener.java @@ -0,0 +1,6 @@ +package com.bitwig.extensions.controllers.softube.console1; + +@FunctionalInterface +public interface ConnectionListener { + void isConnected(boolean connected); +} diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/Console1Extension.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/Console1Extension.java new file mode 100644 index 00000000..7115aa98 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/Console1Extension.java @@ -0,0 +1,82 @@ +package com.bitwig.extensions.controllers.softube.console1; + +import java.time.format.DateTimeFormatter; +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.api.Application; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.HardwareSurface; +import com.bitwig.extension.controller.api.Transport; +import com.bitwig.extensions.controllers.softube.console1.definition.Console1ExtensionDefinition; +import com.bitwig.extensions.framework.di.Context; + +public class Console1Extension extends ControllerExtension { + private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("hh:mm:ss SSS"); + + private HardwareSurface surface; + private final Console1ExtensionDefinition definition; + private ViewControl viewControl; + private TrackControl trackSlotControl; + private ConsoleMidiProcessor midiProcessor; + + // 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 Console1Extension(final Console1ExtensionDefinition definition, final ControllerHost host) { + super(definition, host); + this.definition = definition; + } + + @Override + public void init() { + final Context diContext = new Context(this); + midiProcessor = new ConsoleMidiProcessor(getHost(), definition.getNumMidiInPorts(), definition.getName()); + + surface = diContext.getService(HardwareSurface.class); + final Application application = diContext.getService(Application.class); + + viewControl = diContext.getService(ViewControl.class); + + trackSlotControl = new TrackControl(viewControl.getTrackBank(), getHost(), midiProcessor, + diContext.getService(Transport.class)); + midiProcessor.setTrackSlotControl(trackSlotControl); + application.hasActiveEngine().addValueObserver(active -> midiProcessor.handleEngineSwitch(active)); + + midiProcessor.startHandshake(); + } + + @Override + public void exit() { + final CompletableFuture shutdown = new CompletableFuture<>(); + Executors.newSingleThreadExecutor().execute(() -> { + trackSlotControl.resetAll(); + midiProcessor.invokeReset(); + try { + Thread.sleep(100); + } + catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + shutdown.complete(true); + }); + try { + shutdown.get(); + } + catch (final InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + } + } + + @Override + public void flush() { + surface.updateHardware(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/ConsoleMidiProcessor.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/ConsoleMidiProcessor.java new file mode 100644 index 00000000..6f11f829 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/ConsoleMidiProcessor.java @@ -0,0 +1,165 @@ +package com.bitwig.extensions.controllers.softube.console1; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MidiOut; + +public class ConsoleMidiProcessor { + private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("hh:mm:ss SSS"); + + private MidiOut midiOut = null; + private final ControllerHost host; + private boolean connectionInit = false; + + private TrackControl trackSlotControl; + private final int portCount; + private final List connectionListeners = new ArrayList<>(); + + private boolean engineActiveState = false; + private final String name; + + public ConsoleMidiProcessor(final ControllerHost host, final int ports, final String name) { + this.host = host; + this.name = name; + this.portCount = ports; + for (int i = 0; i < ports; i++) { + final int portIndex = i; + final MidiIn midiIn = host.getMidiInPort(i); + midiIn.setSysexCallback(data -> handleSysEx(portIndex, data)); + } + } + + private void println(final String format, final Object... args) { + host.println(" " + LocalDateTime.now().format(DF) + " " + format.formatted(args)); + } + + public void startHandshake() { + for (int port = 0; port < portCount; port++) { + println(" ######### Init Handshake %d ###### ", port); + host.getMidiOutPort(port).sendSysex(SysexUtil.toJsonSysEx(SysexUtil.INIT_HANDSHAKE)); + } + } + + public void addConnectionListener(final ConnectionListener listener) { + this.connectionListeners.add(listener); + } + + public void invokeReset() { + // println(" Daw-> Console : Reset"); + for (int i = 0; i < portCount; i++) { + host.getMidiOutPort(i).sendSysex(SysexUtil.toJsonSysEx(SysexUtil.RESET_CMD)); + } + } + + public void setTrackSlotControl(final TrackControl trackSlotControl) { + this.trackSlotControl = trackSlotControl; + } + + public void sendJsonSysEx(final String jsonString) { + if (midiOut == null) { + return; + } + midiOut.sendSysex(SysexUtil.toJsonSysEx(jsonString)); + } + + private void handleSysEx(final int port, final String data) { + final String json = SysexUtil.toJson(data); + if (json != null) { + handleJson(port, json); + } else { + println("NON JSON : \n" + data); + } + } + + private void handleJson(final int port, final String json) { + final JsonParser parser = new JsonParser(json); + final JsonObject obj = parser.parse(); + //host.println(" JSON " + json); + if (obj.contains("trackId")) { + trackSlotControl.update(obj); + } else if (obj.contains("handshake")) { + final boolean acknowledge = obj.getJsonObject("handshake").getBool("ack"); + handleHandshake(port, acknowledge); + } else if (obj.contains("cmd")) { + final String cmdValue = obj.getString("cmd"); + if ("RESET".equals(cmdValue)) { + handleResetReceived(); + } else if ("ENABLE".equals(cmdValue)) { + println(" ENABLE <%s>", name); + } else if ("DISABLE".equals(cmdValue)) { + println(" DISABLE <%s>", name); + connectionListeners.forEach(listener -> listener.isConnected(false)); + } else { + println(" Unknown COMMAND %s", cmdValue); + } + } else if (obj.contains("activeMeters")) { + trackSlotControl.meterActivate(obj.getStringList("activeMeters")); + } else { + host.println("OTHER JSON : \n" + json); + } + } + + private void handleResetReceived() { + println(" RESET received INIT=%s", connectionInit); + if (!connectionInit) { + trackSlotControl.setBlockMidi(false); + startHandshake(); + } else { + trackSlotControl.updateAllTracks(); + } + } + + private void handleHandshake(final int port, final boolean acknowledged) { + println( + " C1 => Handshake Acknowledge=%s con_init=%s> send reset => PORT=%d ", acknowledged, connectionInit, port); + if (acknowledged) { + connectToDevice(port); + } else { + if (connectionInit) { + println(" Let us ignore this "); + } else { + connectionInit = false; + host.getMidiOutPort(port).sendSysex(SysexUtil.toJsonSysEx(SysexUtil.RESET_CMD)); + connectionListeners.forEach(listener -> listener.isConnected(false)); + } + } + } + + private void connectToDevice(final int port) { + // println(" ####### Connect to Device %d ########### ", port); + midiOut = host.getMidiOutPort(port); + + host.scheduleTask(this::launchTrackUpdate, 500); + if (!connectionInit) { + connectionInit = true; + connectionListeners.forEach(listener -> listener.isConnected(true)); + } + + } + + private void launchTrackUpdate() { + trackSlotControl.setBlockMidi(false); + trackSlotControl.updateAllTracks(); + } + + public void handleEngineSwitch(final boolean engineActive) { + if (engineActiveState == engineActive) { + return; + } + this.engineActiveState = engineActive; + if (!connectionInit) { + return; + } + // println(" Engine Active %s init = %s", engineActive, connectionInit); + if (connectionInit && engineActive) { + trackSlotControl.resetAll(); + invokeReset(); + trackSlotControl.updateAllTracks(); + } + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/JsonObject.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/JsonObject.java new file mode 100644 index 00000000..0e1aaab6 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/JsonObject.java @@ -0,0 +1,92 @@ +package com.bitwig.extensions.controllers.softube.console1; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +public class JsonObject { + public static final Pattern DIGIT = Pattern.compile("^-?\\d+$"); + public static final Pattern FLOAT = Pattern.compile("^[-+]?[0-9]*\\.?[0-9]*$"); + private final Map data = new HashMap<>(); + + public Object getValue(final String key) { + return data.get(key); + } + + public Optional getStringValue(final String key) { + if (data.get(key) instanceof final String stringValue) { + return Optional.of(stringValue); + } + return Optional.empty(); + } + + public String getString(final String key) { + if (data.get(key) instanceof final String stringValue) { + return stringValue; + } + return null; + } + + public boolean getBool(final String key) { + if (data.get(key) instanceof final Boolean boolValue) { + return boolValue; + } + return false; + } + + public List getStringList(final String key) { + if (data.get(key) instanceof final List list) { + return list; + } + return List.of(); + } + + public void set(final String key, final Object value) { + if (value instanceof final String strValue) { + if (strValue.startsWith("\"") && strValue.endsWith("\"")) { + data.put(key, strValue.substring(1, strValue.length() - 1)); + } else if ("true".equals(strValue)) { + data.put(key, Boolean.TRUE); + } else if ("false".equals(strValue)) { + data.put(key, Boolean.FALSE); + } else if (DIGIT.matcher(strValue).matches()) { // strValue.matches("-?\\d+") + data.put(key, Integer.parseInt(strValue)); + } else if (FLOAT.matcher(strValue).matches()) { + data.put(key, Double.parseDouble(strValue)); + } else { + data.put(key, strValue); + } + } else { + data.put(key, value); + } + } + + public JsonObject getJsonObject(final String key) { + final Object value = data.get(key); + if (value instanceof final JsonObject jsonObject) { + return jsonObject; + } + return null; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + for (final var entry : data.entrySet()) { + sb.append(entry.getKey() + " = <" + entry.getValue() + ">").append("\n"); + } + return sb.toString(); + } + + public boolean contains(final String key) { + return data.containsKey(key); + } + + public Stream> stream() { + return data.entrySet().stream(); + } +} + diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/JsonParser.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/JsonParser.java new file mode 100644 index 00000000..d1dcc583 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/JsonParser.java @@ -0,0 +1,227 @@ +package com.bitwig.extensions.controllers.softube.console1; + +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +public class JsonParser { + + private final String input; + private int pos = 0; + + private Token token = Token.NONE; + private JsonObject current; + private List currentArray; + private StringBuilder key; + private StringBuilder value; + private final Stack stack = new Stack<>(); + private final Stack keyStack = new Stack<>(); + + private record Extract(List elements, int nextPos) { + + } + + public static void main(final String[] args) { + final String test = """ + {"abc":"deff", "nested":{ "other":"x", "nxcc": true } "it": true} + """; + + final String test2 = """ + {"deckId":"A","metadata":{"title":"Contradictions","artist":"Native Instruments","album":"Decoded Forms (Expansion)","cover":"124/2PK3GTD2OXK3KC0M1GHFAUJH22ZA","duration":"03:40","bpm":162,"key":"10A","bitrate":320000},"player":{"isPlaying":false,"isCueing":false,"isLooping":false,"loopSize":"4","loopActive":false,"slip":false,"isSlipping":false,"sync":false,"syncInPhase":false,"syncInRange":true,"tempoRange":"8%","tempo":0,"bpm":162,"keyAdjust":0,"resultingKey":"10A","keySync":false}} + """; + + final String arrayTest = """ + { + "activeMeters": [ + "fea0147c-9173-4593-ba5c-890e3cef778d", + "25dbf170-2656-4b7b-9813-d7e5f5cab189", + "c791ac5b-22c8-46e5-a96b-b3dc1bb5175c", + "8d2d6a88-0b9a-4b03-b39f-a2b16c986958", + "30150793-cbc5-4d86-871c-ac0cfdf04ed0", + "e0abde81-1345-479c-ab5e-381e29a4dcc9" + ], + "test": "funnx" + } + """; + + final JsonParser parser = new JsonParser(arrayTest); + final JsonObject json = parser.parse(); + + //System.out.println("DECKID = " + json.getValue("deckId")); + final JsonObject meta = json.getJsonObject("activeMeters"); + System.out.printf("c=%s v=%s\n", json.contains("activeMeters"), json.getStringList("activeMeters")); + System.out.printf("c=%s v=%s\n", json.contains("test"), json.getString("test")); + if (meta != null) { + System.out.println(" KEY = " + meta.getValue("key")); + } + } + + private enum Token { + NONE, + OBJECT, + START_KEY, + EXPECT_VALUE, + VALUE + } + + public JsonParser(final String input) { + this.input = input; + } + + public JsonObject parse() { + final JsonObject root = new JsonObject(); + stack.push(root); + current = root; + while (pos < input.length()) { + final char c = input.charAt(pos); + switch (token) { + case NONE -> handleNone(c); + case OBJECT -> handleObject(c); + case START_KEY -> handleInKey(c); + case EXPECT_VALUE -> handleForValue(c); + case VALUE -> readValue(c); + } + //System.out.println(" <%c> %s".formatted(c,token)); + pos++; + } + return root; + } + + private void readValue(final char c) { + if (c == ',') { + current.set(key.toString().trim(), value.toString().trim()); + token = Token.OBJECT; + } else if (c == '}') { + current.set(key.toString().trim(), value.toString().trim()); + if (!keyStack.isEmpty()) { + current.set(key.toString().trim(), value.toString().trim()); + final String keyPrev = keyStack.pop(); + final JsonObject parent = stack.pop(); + parent.set(keyPrev, current); + current = parent; + } + token = Token.OBJECT; + } else if (c == '{') { + keyStack.push(key.toString().trim()); + stack.push(current); + current = new JsonObject(); + token = Token.OBJECT; + } else if (c == '[') { + final Extract extract = extractArrayContent(); + final List list = extract.elements().stream().map(this::readElement).toList(); + current.set(key.toString().trim(), list); + pos = extract.nextPos; + token = Token.OBJECT; + } else { + value.append(c); + } + } + + private Object readElement(final String strValue) { + if (strValue.startsWith("\"") && strValue.endsWith("\"")) { + return strValue.substring(1, strValue.length() - 1); + } else if ("true".equals(strValue)) { + return Boolean.TRUE; + } else if ("false".equals(strValue)) { + return Boolean.FALSE; + } else if (JsonObject.DIGIT.matcher(strValue).matches()) { // strValue.matches("-?\\d+") + return Integer.parseInt(strValue); + } else if (JsonObject.FLOAT.matcher(strValue).matches()) { + return Double.parseDouble(strValue); + } else if (strValue.startsWith("{")) { + final JsonParser parser = new JsonParser(strValue); + return parser.parse(); + } + return strValue; + } + + private Extract extractArrayContent() { + int posNow = pos; + int bracketCount = 0; + final boolean endFound = false; + final StringBuilder content = new StringBuilder(); + while (posNow < input.length() && !endFound) { + final char c = input.charAt(posNow); + if (c == '[') { + bracketCount++; + } else if (c == ']') { + bracketCount--; + if (bracketCount == 0) { + return new Extract(extractArrayElements(content.toString().trim()), posNow); + } + } else { + content.append(c); + } + posNow++; + } + return new Extract(extractArrayElements(content.toString().trim()), posNow); + } + + private List extractArrayElements(final String arrayString) { + final List elements = new ArrayList<>(); + int pos = 0; + int objBracketCount = 0; + int arrayBracketCount = 0; + StringBuilder current = new StringBuilder(); + while (pos < arrayString.length()) { + final char c = arrayString.charAt(pos); + if (c == '{') { + objBracketCount++; + current.append(c); + } else if (c == '[') { + arrayBracketCount++; + current.append(c); + } else if (c == ']') { + arrayBracketCount--; + current.append(c); + } else if (c == '}') { + objBracketCount--; + current.append(c); + } else if (c == ',') { + if (objBracketCount > 0 || arrayBracketCount > 0) { + current.append(c); + } else { + elements.add(current.toString().trim()); + current = new StringBuilder(); + } + } else { + current.append(c); + } + pos++; + } + final String lastElement = current.toString().trim(); + if (lastElement.length() > 0) { + elements.add(lastElement); + } + return elements; + } + + private void handleForValue(final char c) { + if (c == ':') { + token = Token.VALUE; + value = new StringBuilder(); + } + } + + private void handleInKey(final char c) { + if (c == '\"') { + token = Token.EXPECT_VALUE; + } else { + key.append(c); + } + } + + private void handleObject(final char c) { + if (c == '\"') { + token = Token.START_KEY; + key = new StringBuilder(); + } + } + + private void handleNone(final char c) { + if (c == '{') { + token = Token.OBJECT; + } + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/SysexUtil.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/SysexUtil.java new file mode 100644 index 00000000..390bd908 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/SysexUtil.java @@ -0,0 +1,128 @@ +package com.bitwig.extensions.controllers.softube.console1; + +import java.io.ByteArrayOutputStream; + +public class SysexUtil { + //private static final int[] PREFIX_MATCH = {0x7d, 0x73, 0x74, 0x63, 0x31}; + private static final String MATCH = "}stc1"; + + public static final String INIT_HANDSHAKE = """ + { + "handshake": { + "dawName": "Bitwig", + "protocolVersion": [1,2] + } + } + """; + + public static final String RESET_CMD = "{\"cmd\":\"RESET\"}"; + + private static void appendByte(final StringBuilder sb, final char c) { + if (c < 127) { + final int b1 = c >> 4; + final int b2 = c & 0xf; + final char c1 = (char) (b1 < 10 ? b1 + '0' : (b1 - 10) + 'A'); + final char c2 = (char) (b2 < 10 ? b2 + '0' : (b2 - 10) + 'A'); + sb.append(c1); + sb.append(c2); + sb.append(' '); + } + } + + private static void appendByte(final StringBuilder sb, final String s) { + for (int i = 0; i < s.length(); i++) { + appendByte(sb, s.charAt(i)); + } + } + + private static void appendByte(final ByteArrayOutputStream bos, final String s) { + for (int i = 0; i < s.length(); i++) { + bos.write(s.charAt(i)); + } + } + + public static String toSysexStringAcii(final String s) { + final StringBuilder sb = new StringBuilder("F0 7D "); + appendByte(sb, "stc1"); + boolean inQuote = false; + for (int i = 0; i < s.length(); i++) { + final char c = s.charAt(i); + if (c > 127) { + appendByte(sb, "\\u" + Integer.toHexString(c | 0x10000).substring(1)); + } else if (c == 10 || (c == 32 && !inQuote)) { + // + } else if (c == 34) { + appendByte(sb, c); + inQuote = !inQuote; + } else { + appendByte(sb, c); + } + } + sb.append("F7"); + return sb.toString(); + } + + public static byte[] toJsonSysEx(final String s) { + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write(0xF0); + bos.write(0x7d); + appendByte(bos, "stc1"); + boolean inQuote = false; + for (int i = 0; i < s.length(); i++) { + final char c = s.charAt(i); + + if (c > 127) { + appendByte(bos, "\\u" + Integer.toHexString(c | 0x10000).substring(1)); + } else if (c == 10 || (c == 32 && !inQuote)) { + // + } else if (c == 34) { + bos.write(c); + inQuote = !inQuote; + } else { + bos.write(c); + } + } + bos.write(0xF7); + return bos.toByteArray(); + } + + public static String toSysExJson(final String s) { + final StringBuilder sb = new StringBuilder(); + boolean inQuote = false; + for (int i = 0; i < s.length(); i++) { + final char c = s.charAt(i); + if (c > 127) { + sb.append("\\u" + Integer.toHexString(c | 0x10000).substring(1)); + } else if (c == 10 || (c == 32 && !inQuote)) { + // + } else if (c == 34) { + sb.append(c); + inQuote = !inQuote; + } else { + sb.append(c); + } + } + return sb.toString(); + } + + public static int toInt(char c) { + if (c <= '9') { + return c - '0'; + } + return c - 'a' + 10; + } + + + public static String toJson(String sysEx) { + StringBuilder sb = new StringBuilder(); + for (int pos = 0; pos < sysEx.length(); pos += 2) { + int value = (SysexUtil.toInt(sysEx.charAt(pos)) << 4) + SysexUtil.toInt(sysEx.charAt(pos + 1)); + sb.append((char) value); + } + final int index = sb.lastIndexOf(MATCH); + if(index != -1) { + return sb.substring(index); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/TrackControl.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/TrackControl.java new file mode 100644 index 00000000..59844f6a --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/TrackControl.java @@ -0,0 +1,229 @@ +package com.bitwig.extensions.controllers.softube.console1; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.bitwig.extension.controller.api.Channel; +import com.bitwig.extension.controller.api.ChannelBank; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.DeviceMatcher; +import com.bitwig.extension.controller.api.Transport; + +public class TrackControl { + private final ControllerHost host; + private final ConsoleMidiProcessor midiProcessor; + private final List slots = new ArrayList<>(); + private final DeviceMatcher console1Matcher; + private Object updateTask = null; + private final Map trackIdLookup = new HashMap<>(); + private final Map existingTrackIds = new HashMap<>(); + private boolean blockMidi = true; + private boolean trackTouchAutomationActive; + private final Transport transport; + private final Map meteringCalls = new HashMap<>(); + private boolean connected; + + record MeterCall(int left, int right) { + public String getJson(final String trackId) { + return "{\"trackId\":\"" + trackId + // + "\",\"meter\":[" + // + (left / 100.0) + "," + (right / 100.0) + // + "]}"; + } + } + + public TrackControl(final ChannelBank trackBank, final ControllerHost host, + final ConsoleMidiProcessor midiProcessor, final Transport transport) { + this.host = host; + this.midiProcessor = midiProcessor; + console1Matcher = createDeviceMatcherC1(host); + this.trackTouchAutomationActive = false; + this.transport = transport; + transport.isArrangerAutomationWriteEnabled().markInterested(); + transport.isClipLauncherAutomationWriteEnabled().markInterested(); + transport.automationWriteMode().addValueObserver(this::updateAutomationMode); + for (int index = 0; index < trackBank.getSizeOfBank(); index++) { + slots.add(new TrackSlot(index, trackBank.getItemAt(index), this)); + } + midiProcessor.addConnectionListener(this::handleConnection); + host.scheduleTask(this::handleMeterTask, 100); + } + + private void handleConnection(final boolean connected) { + //host.println(" Track Control Connect > " + connected); + this.connected = connected; + } + + public boolean isConnected() { + return connected; + } + + public void println(final String formated, final Object... args) { + this.host.println(formated.formatted(args)); + } + + private void handleMeterTask() { + if (!meteringCalls.isEmpty()) { + final StringBuilder sb = new StringBuilder(256); // + sb.append("{\"trackBatch\":["); // + boolean first = true; // + for (final Map.Entry entry : meteringCalls.entrySet()) { // + if (!first) { + sb.append(','); // + } + first = false; // + sb.append(entry.getValue().getJson(entry.getKey())); // + } // + sb.append("]}"); // + midiProcessor.sendJsonSysEx(sb.toString()); // + meteringCalls.clear(); // + } + host.scheduleTask(this::handleMeterTask, 50); + } + + private void updateAutomationMode(final String mode) { + trackTouchAutomationActive = "touch".equals(mode); + } + + public boolean touchAutomationInPlace() { + return trackTouchAutomationActive && (transport.isClipLauncherAutomationWriteEnabled().get() + || transport.isArrangerAutomationWriteEnabled().get()); + } + + private DeviceMatcher createDeviceMatcherC1(final ControllerHost host) { + final DeviceMatcher console1Matcher; + final DeviceMatcher vst3Matcher = host.createVST3DeviceMatcher("2FF966F3A2DA4112BBB38DC29B336457"); + final DeviceMatcher vst2Matcher = host.createVST2DeviceMatcher(1399017577); + console1Matcher = host.createOrDeviceMatcher(vst2Matcher, vst3Matcher); + return console1Matcher; + } + + public DeviceMatcher getConsole1Matcher() { + return console1Matcher; + } + + public void setBlockMidi(final boolean blockMidi) { + this.blockMidi = blockMidi; + } + + public void updateTrackJson(final String trackId, final String key, final String value) { + if (trackId == null || !connected) { + return; + } + sendJsonInfo("{\"trackId\":\"%s\",\"%s\":%s}".formatted(trackId, key, value)); + } + + private String trackIdValue(final String trackId, final String key, final boolean value) { + return "{\"trackId\":\"" + trackId + "\",\"" + key + "\":" + value + "}"; + } + + public void meterActivate(final List activeMeters) { + for (final TrackSlot slot : slots) { + slot.setMeteringActive(false); + } + //Console1Extension.println(" Active meters = " + activeMeters); + for (final String id : activeMeters) { + final TrackSlot slot = trackIdLookup.get(id); + if (slot != null) { + slot.setMeteringActive(true); + } + } + } + + public void updateTrackJson(final String trackId, final String key, final boolean value) { + if (trackId == null) { + return; + } + sendJson(trackIdValue(trackId, key, value)); + } + + public void sendMeterJson(final String trackId, final int left, final int right) { + if (blockMidi || !connected) { + return; + } + meteringCalls.put(trackId, new MeterCall(left, right)); + } + + + private void sendJson(final String json) { + if (blockMidi || !connected) { + return; + } + //Console1Extension.println(" SEND JSON <%s> %s", blockMidi, json); + midiProcessor.sendJsonSysEx(json); + } + + private void sendJsonInfo(final String json) { + if (blockMidi || !connected) { + return; + } + midiProcessor.sendJsonSysEx(json); + } + + public void update(final JsonObject obj) { + final String trackId = obj.getString("trackId"); + final TrackSlot slot = trackIdLookup.get(trackId); + if (slot != null) { + slot.applyUpdate(obj, host); + } + } + + public void updateAllTracks() { + for (final TrackSlot slot : slots) { + if (!slot.isActive()) { + deactivate(slot); + } + } + for (final TrackSlot slot : slots) { + if (slot.isActive()) { + activate(slot); + } + } + } + + public void resetAll() { + slots.stream().forEach(slot -> deactivate(slot)); + } + + public void activate(final TrackSlot slot) { + if (slot.getTrackId() == null || !connected) { + return; + } + sendJsonInfo(slot.getJsonData()); + } + + public void deactivate(final TrackSlot slot) { + if (slot.getTrackId() == null || !connected) { + return; + } + sendJsonInfo(slot.getDisableData()); + } + + public void launchUpdateTask() { + if (updateTask == null) { + updateTask = 0; + host.scheduleTask(this::collectTrackUpdates, 300); + } + } + + private void collectTrackUpdates() { + slots.stream().filter(TrackSlot::isDefined).map(TrackSlot::getTrackId) + .forEach(id -> existingTrackIds.remove(id)); + for (final TrackId id : existingTrackIds.values()) { + sendJsonInfo(id.getDisableData()); + } + + trackIdLookup.clear(); + slots.stream().filter(TrackSlot::isDefined).forEach(this::activate); + slots.stream().filter(TrackSlot::isDefined).forEach(slot -> trackIdLookup.put(slot.getTrackId(), slot)); + + existingTrackIds.clear(); + slots.stream().filter(TrackSlot::isDefined) + .forEach(slot -> existingTrackIds.put(slot.getTrackId(), slot.toTrackId())); + + updateTask = null; + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/TrackId.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/TrackId.java new file mode 100644 index 00000000..57154657 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/TrackId.java @@ -0,0 +1,11 @@ +package com.bitwig.extensions.controllers.softube.console1; + +record TrackId(int channelIndex, String trackId, String name) { + // + public String getDisableData() { + return "{\"trackId\":\"" + trackId + "\"," + // + "\"track\":" + channelIndex + "," + // + "\"isActive\":false" + // + "}"; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/TrackSlot.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/TrackSlot.java new file mode 100644 index 00000000..3bebe46e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/TrackSlot.java @@ -0,0 +1,438 @@ +package com.bitwig.extensions.controllers.softube.console1; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import com.bitwig.extension.controller.api.Channel; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.Device; +import com.bitwig.extension.controller.api.DeviceBank; +import com.bitwig.extension.controller.api.Parameter; +import com.bitwig.extension.controller.api.Send; +import com.bitwig.extension.controller.api.SendBank; + +public class TrackSlot { + private final int channelIndex; + private long lastReleased; + public static final String NEG_INFINITY = "\"-Infinity\""; + private static final String CONSOLE_1 = "2FF966F3A2DA4112BBB38DC29B336457"; + + private String trackId; + private final Channel track; + private final TrackControl control; + + private int meterLeft = 0; + private int meterRight = 0; + + private String name; + private boolean active; + private boolean selected; + private boolean solo; + private boolean mute; + private String colorValue = "0"; + private String volume = NEG_INFINITY; + private String pan = "0.5"; + private final String[] sends = new String[6]; + private final boolean[] sendsActive = new boolean[6]; + private final int channels = 2; + private final String maxVolumeValue = "6.02"; + private final String maxSendValue = "0"; + private final String meterType = "VU"; + private boolean containsConsole1 = false; + private boolean meteringActive = true; + private long lastConsoleLoadInvocation = 0; + + private static class ValueUpdate { + private Object lastValue = new Object(); + private final Parameter parameter; + + public ValueUpdate(final Parameter parameter) { + this.parameter = parameter; + } + + public void updateValue(final Object v) { + parameter.value().setImmediately(fromDouble(v)); + } + + public void updateDecibel(final Object v) { + final double vd = fromDecibel(v); + final double currentRaw = parameter.value().getRaw(); + if (currentRaw != vd) { + parameter.value().setRaw(vd); + lastValue = v; + } + } + + public void update0Decibel(final Object v) { + parameter.value().setImmediately(from0Decibel(v)); + } + + public void touch(final boolean touchState) { + parameter.touch(touchState); + } + } + + private final Map valueHandlers = new HashMap<>(); + + public TrackSlot(final int index, final Channel track, final TrackControl control) { + this.channelIndex = index + 1; + this.track = track; + this.trackId = null; + this.control = control; + + valueHandlers.put("volume", new ValueUpdate(track.volume())); + valueHandlers.put("pan", new ValueUpdate(track.pan())); + for (int i = 0; i < 6; i++) { + valueHandlers.put("send%d".formatted(i + 1), new ValueUpdate(track.sendBank().getItemAt(i))); + } + Arrays.fill(sendsActive, false); + Arrays.fill(sends, NEG_INFINITY); + track.name().addValueObserver(this::handNameChanged); + track.channelId().addValueObserver(this::handleIdChanged); + track.exists().addValueObserver(this::handleExistChanged); + track.color().addValueObserver(this::handleColorChanged); + track.volume().value().addRawValueObserver(this::volumeChange); + track.pan().value().addValueObserver(this::panChange); + track.solo().addValueObserver(this::handleSoloChanged); + track.mute().addValueObserver(this::handleMuteChanged); + + //track.channelIndex().addValueObserver(this::handleChannelIndex); + + track.addIsSelectedInMixerObserver(this::handleSelectedChanged); + track.addVuMeterObserver(100, 0, false, this::handleMeterLeft); + track.addVuMeterObserver(100, 1, false, this::handleMeterRight); + final DeviceBank deviceBank = track.createDeviceBank(1); + deviceBank.setDeviceMatcher(control.getConsole1Matcher()); + final Device device = deviceBank.getDevice(0); + device.exists().addValueObserver(this::handleConsole1Appeared); + final SendBank bank = track.sendBank(); + for (int j = 0; j < bank.getSizeOfBank(); j++) { + final int sendIndex = j; + final Send send = bank.getItemAt(sendIndex); + send.exists().addValueObserver(exists -> this.handleSendExists(sendIndex, exists)); + send.value().addValueObserver(value -> this.handleSendChanged(sendIndex, value)); + } + } + + private void handleConsole1Appeared(final boolean exists) { + //Console1Extension.println(" Console1 %d %s".formatted(this.slotIndex, exists)); + containsConsole1 = exists; + // control.launchUpdateTask(); + //control.updateTrackJson(this.trackId, "plugin", exists); + //control.activate(this); + } + + private void handleMeterLeft(final int value) { + if (meteringActive && value != meterLeft) { + meterLeft = value; + control.sendMeterJson(trackId, meterLeft, meterRight); + } + } + + private void handleMeterRight(final int value) { + if (meteringActive && value != meterRight) { + meterRight = value; + control.sendMeterJson(trackId, meterLeft, meterRight); + } + } + + private void handleIdChanged(final String channelId) { + //Console1Extension.println("IC: %d - %s : %s", channelIndex, trackId, channelId); + if (!channelId.isBlank()) { + trackId = channelId; + } else { + trackId = null; + } + control.launchUpdateTask(); + } + + private void handNameChanged(final String name) { + this.name = name; + control.activate(this); + } + + public int getIndex() { + return channelIndex; + } + + public String getName() { + return name; + } + + public boolean isActive() { + return active; + } + + public boolean isDefined() { + return trackId != null && active; + } + + public TrackId toTrackId() { + return new TrackId(channelIndex, trackId, name); + } + + public void setMeteringActive(final boolean meteringActive) { + this.meteringActive = meteringActive; + } + + private void handleExistChanged(final boolean exists) { + if (!exists && !this.active) { + return; + } + this.active = exists; + control.launchUpdateTask(); + } + + private void handleSoloChanged(final boolean value) { + this.solo = value; + control.updateTrackJson(trackId, "solo", this.solo); + } + + private void handleMuteChanged(final boolean value) { + this.mute = value; + control.updateTrackJson(trackId, "mute", this.mute); + } + + private void handleSelectedChanged(final boolean value) { + this.selected = value; + control.updateTrackJson(trackId, "selected", this.selected); + } + + private void handleSendExists(final int index, final boolean exists) { + setSendsActive(index, exists); + control.updateTrackJson(trackId, "send" + (index + 1) + "On", sendsActive[index]); + } + + private void handleSendChanged(final int index, final double value) { + setSend(index, to0dbLevel(value)); + control.updateTrackJson(trackId, "send" + (index + 1), sends[index]); + } + + private void volumeChange(final double newValue) { + this.volume = to6dbLevel(newValue); + control.updateTrackJson(trackId, "volume", this.volume); + } + + private void panChange(final double newValue) { + this.pan = Double.toString(newValue); + control.updateTrackJson(trackId, "pan", this.pan); + } + + private void handleColorChanged(final float r, final float g, final float b) { + final int color = toColor(r, g, b); + this.colorValue = Integer.toString(color); + control.updateTrackJson(trackId, "color", this.colorValue); + } + + private int toColor(final float r, final float g, final float b) { + final int rv = (int) Math.floor(r * 255) << 0; + final int gv = (int) Math.floor(g * 255) << 8; + final int bv = (int) Math.floor(b * 255) << 16; + return rv | gv | bv; + } + + public String getTrackId() { + return trackId; + } + + public void setSend(final int index, final String val) { + this.sends[index] = val; + } + + public void setSendsActive(final int index, final boolean sendsActive) { + this.sendsActive[index] = sendsActive; + } + + public String toString() { + final String sb = + " %02d - %8s %12s %s %s".formatted( + this.channelIndex, this.trackId, name, this.active ? "X" : "-", containsConsole1 ? "o" : " "); + return sb; + } + + public String getJsonData() { + final StringBuilder sb = new StringBuilder("{"); + sb.append("\"trackId\":\"").append(trackId).append("\","); // + sb.append("\"track\":").append(channelIndex).append(","); // + sb.append("\"name\":\"").append(name).append("\","); // + sb.append("\"color\":").append(colorValue).append(","); // + sb.append("\"isActive\":").append(active).append(","); // + sb.append("\"selected\":").append(selected).append(","); // + sb.append("\"mute\":").append(mute).append(","); // + sb.append("\"solo\":").append(solo).append(","); // + sb.append("\"volume\":").append(volume).append(","); // + sb.append("\"pan\":").append(pan).append(","); // + + for (int i = 0; i < sends.length; i++) { // + sb.append("\"send").append(i + 1).append("\":").append(sends[i]).append(","); // + sb.append("\"send").append(i + 1).append("On\":").append(sendsActive[i]).append(","); // + } // + + sb.append("\"channels\":").append(channels).append(","); // + sb.append("\"maxSendValue\":").append(maxSendValue).append(","); // + sb.append("\"maxVolumeValue\":").append(maxVolumeValue).append(","); // + sb.append("\"meterType\":\"").append(meterType).append("\""); // + sb.append("}"); // + return sb.toString(); + } + + public String getDisableData() { + final String sb = "{\"trackId\": \"%s\",".formatted(trackId) + "\"track\": %d,".formatted(channelIndex) + + "\"isActive\": false" + "}"; + return sb; + } + + public void applyUpdate(final JsonObject obj, final ControllerHost host) { + if (obj.contains("touchState")) { + final boolean touched = "TOUCHED".equals(obj.getString("touchState")); + if (touched) { + obj.stream() // + .filter(entry -> !"trackId".equals(entry.getKey())) // + .filter(entry -> !entry.getKey().equals("touchState")) + .forEach(entry -> applyTouch(entry.getKey(), true, host)); + } else { + obj.stream() // + .filter(entry -> !"trackId".equals(entry.getKey())) // + .filter(entry -> !entry.getKey().equals("touchState")) + .forEach(entry -> applyTouch(entry.getKey(), false, host)); + } + } else { + obj.stream() // + .filter(entry -> !"trackId".equals(entry.getKey())) + .forEach(entry -> applyChange(entry.getKey(), entry.getValue())); + } + } + + private void applyTouch(final String key, final boolean touchState, final ControllerHost host) { + if (!touchState) { + lastReleased = System.currentTimeMillis(); + } + final ValueUpdate valueHandler = valueHandlers.get(key); + if (valueHandler != null) { + valueHandler.touch(touchState); + } else { + control.println(" Touch UNKNOWN TRACK ID = %s", key); + } + } + + private void applyDecibel(final String type, final Object value) { + final ValueUpdate valueHandler = valueHandlers.get(type); + if (valueHandler != null) { + valueHandler.updateDecibel(value); + } else { + control.println(" Param UNKNOWN TRACK TYPE = %s", type); + } + } + + private void apply0Decibel(final String type, final Object value) { + final ValueUpdate valueHandler = valueHandlers.get(type); + if (valueHandler != null) { + valueHandler.update0Decibel(value); + } else { + control.println(" Param UNKNOWN TRACK TYPE = %s", type); + } + } + + private void applyValue(final String type, final Object value) { + final ValueUpdate valueHandler = valueHandlers.get(type); + if (valueHandler != null) { + valueHandler.updateValue(value); + } else { + control.println(" Param UNKNOWN TRACK TYPE = %s", type); + } + } + + private void applyChange(final String key, final Object value) { + if (control.touchAutomationInPlace()) { + final long diff = System.currentTimeMillis() - lastReleased; + if (diff < 500) { + //Console1Extension.println(" DEFLECT "); + return; + } + } + switch (key) { + case "volume" -> applyDecibel(key, value); + case "pan" -> applyValue(key, value); + case "send1", "send2", "send3", "send4", "send5", "send6" -> apply0Decibel(key, value); + case "mute" -> track.mute().set(getBoolean(value)); + case "solo" -> track.solo().set(getBoolean(value)); + case "selected" -> this.actionOnTrue(value, this::handleSelection); + case "plugin" -> this.loadPlugin(value.toString()); + } + } + + private void loadPlugin(final String plugin) { + final long diff = System.currentTimeMillis() - lastConsoleLoadInvocation; + if (diff > 10000) { + track.endOfDeviceChainInsertionPoint().insertVST3Device(CONSOLE_1); + lastConsoleLoadInvocation = System.currentTimeMillis(); + } + } + + private void handleSelection() { + track.selectInEditor(); + track.selectInMixer(); + } + + private void actionOnTrue(final Object o, final Runnable action) { + if (o instanceof final Boolean value && value) { + action.run(); + } + } + + private boolean getBoolean(final Object o) { + if (o instanceof final Boolean value) { + return value; + } + return false; + } + + private static double from0Decibel(final Object o) { + if (o instanceof final String sv && "-Infinity".equals(sv)) { + return 0; + } else if (o instanceof final Double value) { + return Math.min(1, Math.pow(10, value / 60.0)); + } else if (o instanceof final Integer value) { + return Math.pow(10, value / 60.0); + } + return 0; + } + + protected static double fromDecibel(final Object o) { + if (o instanceof final String sv && "-Infinity".equals(sv)) { + return 0; + } else if (o instanceof final Double value) { + return Math.pow(10, value / 60.0); + } else if (o instanceof final Integer value) { + return Math.pow(10, value / 60.0); + } + return 0; + } + + private static double fromDouble(final Object o) { + if (o instanceof final Double value) { + return Math.min(1, Math.max(0, value)); + } else if (o instanceof final Integer value) { + return value; + } + return 0; + } + + + private static String to6dbLevel(final double value) { + if (value == 0.0) { + return NEG_INFINITY; + } + return Double.toString(60 * Math.log10(value)); + } + + private static String to0dbLevel(final double value) { + if (value == 0) { + return NEG_INFINITY; + } + return Double.toString(60 * Math.log10(value)); + } + +} + diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/ViewControl.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/ViewControl.java new file mode 100644 index 00000000..21cb05b3 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/ViewControl.java @@ -0,0 +1,38 @@ +package com.bitwig.extensions.controllers.softube.console1; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.CursorTrack; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extension.controller.api.TrackBankFlatteningMode; +import com.bitwig.extensions.framework.di.Component; + +@Component +public class ViewControl { + + private final Track rootTrack; + private final CursorTrack cursorTrack; + private final TrackBank trackBank; + + public ViewControl(final ControllerHost host) { + rootTrack = host.getProject().getRootTrackGroup(); + trackBank = host.createTrackBank(1024, 6, 1, true); + trackBank.setFlatteningMode(TrackBankFlatteningMode.FLATTEN); + trackBank.setShouldIncludeAllMixerChannels(true); + cursorTrack = host.createCursorTrack(6, 16); + } + + public TrackBank getTrackBank() { + return trackBank; + } + + public CursorTrack getCursorTrack() { + return cursorTrack; + } + + public Track getRootTrack() { + return rootTrack; + } + + +} \ No newline at end of file diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1ChannelExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1ChannelExtensionDefinition.java new file mode 100644 index 00000000..2524b973 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1ChannelExtensionDefinition.java @@ -0,0 +1,49 @@ +package com.bitwig.extensions.controllers.softube.console1.definition; + +import java.util.UUID; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extensions.controllers.softube.console1.Console1Extension; + +public class Console1ChannelExtensionDefinition extends Console1ExtensionDefinition { + + private static final UUID DRIVER_ID = UUID.fromString("98630b94-ed23-4a11-9981-05d3995b4964"); + + public Console1ChannelExtensionDefinition() { + } + + @Override + public String getName() { + return "Console 1 Mk III"; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + + @Override + public String getHardwareModel() { + return "Console 1 Channel Mk III"; + } + + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) { + if (platformType == PlatformType.WINDOWS) { // + list.add(new String[] {"Console 1 Channel Mk III"}, new String[] {"Console 1 Channel Mk III"}); + } else { + list.add( + new String[] {"Console 1 Channel Mk III DAW FA0000000073"}, + new String[] {"Console 1 Channel Mk III DAW FA0000000073"}); + } + } + + @Override + public Console1Extension createInstance(final ControllerHost host) { + return new Console1Extension(this, host); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1ExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1ExtensionDefinition.java new file mode 100644 index 00000000..2e682ab9 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1ExtensionDefinition.java @@ -0,0 +1,54 @@ +package com.bitwig.extensions.controllers.softube.console1.definition; + +import com.bitwig.extension.controller.ControllerExtensionDefinition; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extensions.controllers.softube.console1.Console1Extension; + +public abstract class Console1ExtensionDefinition extends ControllerExtensionDefinition { + + + public Console1ExtensionDefinition() { + } + + @Override + public String getAuthor() { + return "Bitwig"; + } + + @Override + public String getVersion() { + return "0.9"; + } + + @Override + public String getHardwareVendor() { + return "Softube"; + } + + @Override + public int getRequiredAPIVersion() { + return 25; + } + + @Override + public int getNumMidiInPorts() { + return 1; + } + + @Override + public int getNumMidiOutPorts() { + return 1; + } + + @Override + public String getHelpFilePath() { + return "Controllers/Softube/Console 1 MkIII.pdf"; + } + + @Override + public Console1Extension createInstance(final ControllerHost host) { + return new Console1Extension(this, host); + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1FaderExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1FaderExtensionDefinition.java new file mode 100644 index 00000000..034fdfda --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1FaderExtensionDefinition.java @@ -0,0 +1,40 @@ +package com.bitwig.extensions.controllers.softube.console1.definition; + +import java.util.UUID; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; + +public class Console1FaderExtensionDefinition extends Console1ExtensionDefinition { + + private static final UUID DRIVER_ID = UUID.fromString("98630b94-ed23-4a11-9981-05d3995b4965"); + + @Override + public String getName() { + return "Console 1 Fader Mk III"; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + + @Override + public String getHardwareModel() { + return "Console 1 Fader Mk III"; + } + + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) { + if (platformType == PlatformType.WINDOWS) { // + list.add(new String[] {"Console 1 Fader Mk III"}, new String[] {"Console 1 Fader Mk III"}); + } else { + list.add(new String[] {"Console 1 Fader Mk III"}, new String[] {"Console 1 Fader Mk III"}); + list.add( + new String[] {"Console 1 Channel Mk III DAW FA0000000073"}, + new String[] {"Console 1 Channel Mk III DAW FA0000000073"}); + } + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1Multi2ExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1Multi2ExtensionDefinition.java new file mode 100644 index 00000000..d1210420 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1Multi2ExtensionDefinition.java @@ -0,0 +1,66 @@ +package com.bitwig.extensions.controllers.softube.console1.definition; + +import java.util.UUID; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extensions.controllers.softube.console1.Console1Extension; + +public class Console1Multi2ExtensionDefinition extends Console1ExtensionDefinition { + + private static final UUID DRIVER_ID = UUID.fromString("98630b94-ed23-4a11-9981-05d3995b49AA"); + + public Console1Multi2ExtensionDefinition() { + } + + @Override + public String getName() { + return "Console 1 Mk III x 2"; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + + @Override + public String getHardwareModel() { + return "Console 1 Mk III x 2"; + } + + @Override + public int getNumMidiInPorts() { + return 2; + } + + @Override + public int getNumMidiOutPorts() { + return 2; + } + + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) { + if (platformType == PlatformType.WINDOWS) { // + list.add( + new String[] {"Console 1 Fader Mk III", "Console 1 Channel Mk III"}, + new String[] {"Console 1 Fader Mk III", "Console 1 Channel Mk III"}); + list.add( + new String[] {"Console 1 Fader Mk III", "Console 1 Channel Mk III"}, + new String[] {"Console 1 Fader Mk III", "Console 1 Channel Mk III"}); + } else { + list.add( + new String[] {"Console 1 Channel Mk III DAW FA0000000073", "Console 1 Channel Mk III DAW FA0000000073"}, + new String[] { + "Console 1 Channel Mk III DAW FA0000000073", "Console 1 Channel Mk III DAW FA0000000073" + }); + } + } + + @Override + public Console1Extension createInstance(final ControllerHost host) { + return new Console1Extension(this, host); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1Multi3ExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1Multi3ExtensionDefinition.java new file mode 100644 index 00000000..26f48a00 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1Multi3ExtensionDefinition.java @@ -0,0 +1,69 @@ +package com.bitwig.extensions.controllers.softube.console1.definition; + +import java.util.UUID; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extensions.controllers.softube.console1.Console1Extension; + +public class Console1Multi3ExtensionDefinition extends Console1ExtensionDefinition { + + private static final UUID DRIVER_ID = UUID.fromString("98630b94-ed23-4a11-9981-05d3995b49AB"); + + public Console1Multi3ExtensionDefinition() { + } + + @Override + public String getName() { + return "Console 1 Mk III x 3"; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + + @Override + public String getHardwareModel() { + return "Console 1 Mk III x 3"; + } + + @Override + public int getNumMidiInPorts() { + return 3; + } + + @Override + public int getNumMidiOutPorts() { + return 3; + } + + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) { + if (platformType == PlatformType.WINDOWS) { // + list.add( + new String[] {"Console 1 Fader Mk III", "Console 1 Channel Mk III", "Console 1 Channel Mk III x"}, + new String[] {"Console 1 Fader Mk III", "Console 1 Channel Mk III", "Console 1 Channel Mk III x"}); + list.add( + new String[] {"Console 1 Fader Mk III", "Console 1 Channel Mk III", "Console 1 Channel Mk III x"}, + new String[] {"Console 1 Fader Mk III", "Console 1 Channel Mk III", "Console 1 Channel Mk III x"}); + } else { + list.add( + new String[] { + "Console 1 Channel Mk III DAW FA0000000073", "Console 1 Channel Mk III DAW FA0000000074", + "Console 1 Channel Mk III DAW FA0000000075" + }, new String[] { + "Console 1 Channel Mk III DAW FA0000000073", "Console 1 Channel Mk III DAW FA0000000074", + "Console 1 Channel Mk III DAW FA0000000075" + }); + } + } + + @Override + public Console1Extension createInstance(final ControllerHost host) { + return new Console1Extension(this, host); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1Multi4ExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1Multi4ExtensionDefinition.java new file mode 100644 index 00000000..3ac691f2 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/softube/console1/definition/Console1Multi4ExtensionDefinition.java @@ -0,0 +1,79 @@ +package com.bitwig.extensions.controllers.softube.console1.definition; + +import java.util.UUID; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extensions.controllers.softube.console1.Console1Extension; + +public class Console1Multi4ExtensionDefinition extends Console1ExtensionDefinition { + + private static final UUID DRIVER_ID = UUID.fromString("a86f0b94-ed23-4a11-9981-05d3995b49AB"); + + public Console1Multi4ExtensionDefinition() { + } + + @Override + public String getName() { + return "Console 1 Mk III x 4"; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + + @Override + public String getHardwareModel() { + return "Console 1 Mk III x 4"; + } + + @Override + public int getNumMidiInPorts() { + return 4; + } + + @Override + public int getNumMidiOutPorts() { + return 4; + } + + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) { + if (platformType == PlatformType.WINDOWS) { // + list.add( + new String[] { + "Console 1 Fader Mk III", "Console 1 Channel Mk 00", "Console 1 Channel Mk 01", + "Console 1 Channel Mk 02" + }, new String[] { + "Console 1 Fader Mk III", "Console 1 Channel Mk 00", "Console 1 Channel Mk 01", + "Console 1 Channel Mk 02" + }); + list.add( + new String[] { + "Console 1 Fader Mk III", "Console 1 Channel Mk 00", "Console 1 Channel Mk 01", + "Console 1 Channel Mk 02" + }, new String[] { + "Console 1 Fader Mk III", "Console 1 Channel Mk 00", "Console 1 Channel Mk 01", + "Console 1 Channel Mk 02" + }); + } else { + list.add( + new String[] { + "Console 1 Channel Mk III DAW FA0000000073", "Console 1 Channel Mk III DAW FA0000000074", + "Console 1 Channel Mk III DAW FA0000000075", "Console 1 Channel Mk III DAW FA0000000076" + }, new String[] { + "Console 1 Channel Mk III DAW FA0000000073", "Console 1 Channel Mk III DAW FA0000000074", + "Console 1 Channel Mk III DAW FA0000000075", "Console 1 Channel Mk III DAW FA0000000076" + }); + } + } + + @Override + public Console1Extension createInstance(final ControllerHost host) { + return new Console1Extension(this, host); + } + +} diff --git a/src/main/resources/Documentation/Controllers/Softube/Console 1 MkIII.pdf b/src/main/resources/Documentation/Controllers/Softube/Console 1 MkIII.pdf new file mode 100644 index 00000000..0a708e8d Binary files /dev/null and b/src/main/resources/Documentation/Controllers/Softube/Console 1 MkIII.pdf differ diff --git a/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition b/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition index 01e4e10c..58242aaa 100644 --- a/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition +++ b/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition @@ -134,7 +134,7 @@ com.bitwig.extensions.controllers.novation.launchpadmini3.LaunchPadXExtensionDef com.bitwig.extensions.controllers.novation.slmk3.SlMk3ExtensionDefinition com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.definition.LaunchControlXlExtensionDefinition -#com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.definition.LaunchControlExtensionDefinition +com.bitwig.extensions.controllers.novation.launchcontrolxlmk3.definition.LaunchControlExtensionDefinition com.bitwig.extensions.controllers.presonus.atom.PresonusAtomDefinition com.bitwig.extensions.controllers.presonus.faderport.PresonusFaderPort8Definition @@ -146,6 +146,11 @@ com.bitwig.extensions.controllers.roger_linn_design.LinnStrumentDefinition com.bitwig.extensions.controllers.roli.SeaboardRISEDefinition +com.bitwig.extensions.controllers.softube.console1.definition.Console1ChannelExtensionDefinition +com.bitwig.extensions.controllers.softube.console1.definition.Console1Multi2ExtensionDefinition +com.bitwig.extensions.controllers.softube.console1.definition.Console1Multi3ExtensionDefinition +com.bitwig.extensions.controllers.softube.console1.definition.Console1Multi4ExtensionDefinition + com.bitwig.extensions.controllers.vault.Apex25ControllerDefinition com.bitwig.extensions.controllers.vault.Apex49ControllerDefinition com.bitwig.extensions.controllers.vault.Apex61ControllerDefinition