diff --git a/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/MiniLab3Extension.java b/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/MiniLab3Extension.java index c7e5dde0..fa41eb0a 100644 --- a/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/MiniLab3Extension.java +++ b/src/main/java/com/bitwig/extensions/controllers/arturia/minilab3/MiniLab3Extension.java @@ -184,11 +184,11 @@ public void init() { private void handleSysExData(final String sysEx) { switch (sysEx) { case "f000206b7f420200406300f7": - MiniLab3Extension.println(" ==> BANK A"); + //MiniLab3Extension.println(" ==> BANK A"); toBankMode(PadBank.BANK_A); break; case "f000206b7f420200406301f7": - MiniLab3Extension.println(" ==> BANK B"); + //MiniLab3Extension.println(" ==> BANK B"); toBankMode(PadBank.BANK_B); break; case "f000206b7f420200406201f7": // Arturia Mode @@ -652,8 +652,10 @@ private void mainEncoderAction(final int dir) { encoderStateMaschine.notifyTurn(false); oled.disableValues(); switch (encoderStateMaschine.getState()) { - case INITIAL -> navigateScenesOrPads(dir); - case HOLD -> navigateParametersBanks(dir); + // Since the Encoder sends different CCs when SHIFT is held (see mainEncoderShiftAction) + // There is no conflict here. + case SHIFT, INITIAL -> navigateScenesOrPads(dir); // SHIFT State active when in Transport Lock + case SHIFT_HOLD, HOLD -> navigateParametersBanks(dir); // SHIFT State active when in Transport Lock } } diff --git a/src/main/java/com/bitwig/extensions/controllers/embodme/ClipLayer.java b/src/main/java/com/bitwig/extensions/controllers/embodme/ClipLayer.java new file mode 100644 index 00000000..bfdf9e30 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/embodme/ClipLayer.java @@ -0,0 +1,485 @@ +package com.bitwig.extensions.controllers.embodme; + +import java.util.Arrays; +import java.util.Optional; + +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.Scene; +import com.bitwig.extension.controller.api.SceneBank; +import com.bitwig.extension.controller.api.SendBank; +import com.bitwig.extension.controller.api.SettableRangedValue; +import com.bitwig.extension.controller.api.Track; +import com.bitwig.extension.controller.api.TrackBank; +import com.bitwig.extensions.framework.view.OverviewGrid; + +public class ClipLayer { + public static final int INDEX_OFFSET_ARM = 0x21; + public static final int INDEX_OFFSET_MUTE = 0x15; + public static final int INDEX_OFFSET_SOLO = 0x9; + + public enum Mode { + VOLUME, + SENDS, + GRID + } + + private final Track rootTrack; + private int sceneOffset; + private final OverviewGrid overviewGrid; + private boolean active; + private final boolean horizontalMode; + private final EraeMidiProcessor midiProcessor; + private final TrackBank trackBank; + private final TrackState[] trackStates; + private final SceneState[] sceneStates; + private final SlotState[][] slotStates; + private final int trackSize; + private final int sceneSize; + private final int[] sliderValues = new int[128]; + private Mode currentHeightMode = Mode.GRID; + private int gridHeight; + + private class SlotState { + private final ClipLauncherSlot slot; + private final TrackState trackState; + private boolean stopQueued; + private boolean recordingQueued; + private boolean playbackQueued; + private boolean recording; + private boolean playing; + private int color; + private int displayColor; + private int state; + private boolean hasContent; + private final int sceneIndex; + + public SlotState(final ClipLauncherSlot slot, final TrackState trackState, final int sceneIndex) { + this.slot = slot; + this.trackState = trackState; + this.sceneIndex = sceneIndex; + } + + public int getTrackIndex() { + return trackState.getIndex(); + } + + public int getSceneIndex() { + return sceneIndex; + } + + public ClipLauncherSlot getSlot() { + return slot; + } + + public void setHasContent(final boolean hasContent) { + this.hasContent = hasContent; + } + + public void setStopQueued(final boolean stopQueued) { + this.stopQueued = stopQueued; + } + + public void setRecordingQueued(final boolean recordingQueued) { + this.recordingQueued = recordingQueued; + } + + public void setPlaybackQueued(final boolean playbackQueued) { + this.playbackQueued = playbackQueued; + } + + public void setRecording(final boolean recording) { + this.recording = recording; + } + + public void setPlaying(final boolean playing) { + this.playing = playing; + } + + public void setColor(final int color) { + this.color = color; + } + + public int getState() { + return state; + } + + private boolean calcState() { + final int oldState = this.state; + final int oldColor = this.displayColor; + if (!hasContent) { + if (trackState.isArmed()) { + this.displayColor = recordingQueued ? EraeColor.RED : EraeColor.RED_DIM; + } else { + this.displayColor = EraeColor.OFF; + } + if (recordingQueued || stopQueued || playbackQueued) { + this.state = EraeColor.STATE_BLINK; + } else { + this.state = EraeColor.STATE_NORMAL; + } + } else if (stopQueued) { + this.state = EraeColor.STATE_BLINK; + this.displayColor = EraeColor.GREEN; + } else if (recordingQueued) { + this.state = EraeColor.STATE_BLINK; + this.displayColor = EraeColor.RED; + } else if (playbackQueued) { + this.state = EraeColor.STATE_BLINK; + this.displayColor = color; + } else if (recording) { + this.state = EraeColor.STATE_PULSE; + this.displayColor = EraeColor.RED; + } else if (playing) { + this.state = EraeColor.STATE_PULSE; + this.displayColor = EraeColor.GREEN; + } else { + this.state = EraeColor.STATE_NORMAL; + this.displayColor = color; + } + return oldState != state || oldColor != displayColor; + } + + public int getDisplayColor() { + return displayColor; + } + } + + public ClipLayer(final ControllerHost host, final int width, final int height, + final EraeMidiProcessor midiProcessor, final boolean horizontalMode, final OverviewGrid overviewGrid) { + this.horizontalMode = horizontalMode; + this.overviewGrid = overviewGrid; + this.trackSize = horizontalMode ? height : width; + this.sceneSize = horizontalMode ? width : height; + overviewGrid.addQueuedSceneListener(this::handleQueued); + rootTrack = host.getProject().getRootTrackGroup(); + trackBank = host.createTrackBank(trackSize, 2, sceneSize); + slotStates = new SlotState[trackSize][sceneSize]; + trackStates = new TrackState[trackSize]; + sceneStates = new SceneState[sceneSize]; + Arrays.fill(sliderValues, 0); + this.gridHeight = height; + this.midiProcessor = midiProcessor; + + + for (int i = 0; i < this.trackSize; i++) { + final int trackIndex = i; + final Track track = trackBank.getItemAt(i); + trackStates[trackIndex] = new TrackState(track, i); + prepareTrackState(trackStates[trackIndex]); + for (int j = 0; j < this.sceneSize; j++) { + final int sceneIndex = j; + final ClipLauncherSlotBank slotBank = track.clipLauncherSlotBank(); + final ClipLauncherSlot slot = slotBank.getItemAt(j); + slotStates[trackIndex][sceneIndex] = new SlotState(slot, trackStates[trackIndex], sceneIndex); + prepareSlot(slotStates[trackIndex][sceneIndex]); + } + final SettableRangedValue volume = track.volume().value(); + volume.addValueObserver(128, v -> handleVolumeChange(trackIndex, v)); + final SendBank sendBank = track.sendBank(); + sendBank.getItemAt(0).value().addValueObserver(128, v -> handleSendChange(trackIndex, 0, v)); + sendBank.getItemAt(1).value().addValueObserver(128, v -> handleSendChange(trackIndex, 1, v)); + } + final SceneBank sceneBank = trackBank.sceneBank(); + sceneBank.scrollPosition().addValueObserver(sceneOffset -> { + this.sceneOffset = sceneOffset; + }); + + + for (int j = 0; j < height; j++) { + final Scene scene = sceneBank.getScene(j); + final SceneState sceneState = new SceneState(j); + sceneStates[j] = sceneState; + scene.clipCount().addValueObserver(count -> handleSceneCount(sceneState, count)); + scene.exists().addValueObserver(exists -> handleSceneExists(sceneState, exists)); + scene.color().addValueObserver((r, g, b) -> { + final int colorIndex = EraeColor.rgbToIndex(r, g, b); + sceneState.setColor(colorIndex); + updateSceneLed(sceneState); + }); + } + } + + private void handleQueued(final int index, final boolean queued) { + final int overviewOffset = overviewGrid.getSceneOffset(); + final int diff = sceneOffset - overviewOffset; + final int target = index - diff; + if (target >= 0 && target < this.sceneSize) { // TODO Questionable + sceneStates[target].setQueued(queued); + updateSceneLed(sceneStates[target]); + } + } + + private void handleSceneExists(final SceneState sceneState, final boolean exists) { + sceneState.setExists(exists); + updateSceneLed(sceneState); + } + + private void handleSceneCount(final SceneState state, final int count) { + state.setCount(count); + updateSceneLed(state); + } + + private void updateSceneLed(final SceneState state) { + final boolean changed = state.calcState(); + if (isActive() && changed && state.getIndex() < gridHeight) { + midiProcessor.sendActionButtonUpdate(state.getIndex(), state.getState(), state.getDisplayColor()); + } + } + + private void prepareTrackState(final TrackState trackState) { + trackState.getTrack().arm().addValueObserver(armed -> { + trackState.setArmed(armed); + handleArmUpdate(trackState); + }); + trackState.getTrack().mute().addValueObserver(muted -> { + trackState.setMuted(muted); + updateMuteState(trackState); + }); + trackState.getTrack().solo().addValueObserver(soloed -> { + trackState.setSolo(soloed); + updateSoloState(trackState); + }); + } + + private void handleArmUpdate(final TrackState trackState) { + if (isActive()) { + for (int sceneIndex = 0; sceneIndex < this.sceneSize; sceneIndex++) { // TODO Watch this. + final SlotState slot = slotStates[trackState.getIndex()][sceneIndex]; + if (slot.calcState()) { + sendGridUpdate(slot); + } + } + } + updateArmState(trackState); + } + + private void sendGridUpdate(final SlotState slot) { + if (horizontalMode) { + midiProcessor.sendGridButtonUpdate( + slot.getTrackIndex(), slot.getSceneIndex(), slot.getState(), slot.getDisplayColor()); + } else { + midiProcessor.sendGridButtonUpdate( + slot.getSceneIndex(), slot.getTrackIndex(), slot.getState(), slot.getDisplayColor()); + } + } + + private void updateArmState(final TrackState trackState) { + if (isActive()) { + midiProcessor.sendActionButtonUpdate( + trackState.getIndex() + INDEX_OFFSET_ARM, 0, + trackState.isArmed() ? 1 : 0); + } + } + + private void updateMuteState(final TrackState trackState) { + if (isActive()) { + midiProcessor.sendActionButtonUpdate( + trackState.getIndex() + INDEX_OFFSET_MUTE, 0, + trackState.isMuted() ? 1 : 0); + } + } + + private void updateSoloState(final TrackState trackState) { + if (isActive()) { + midiProcessor.sendActionButtonUpdate( + trackState.getIndex() + INDEX_OFFSET_SOLO, 0, + trackState.isSolo() ? 1 : 0); + } + } + + private void handleSendChange(final int trackIndex, final int sendIndex, final int value) { + final int sliderIndex = (sendIndex + 1) * 12 + trackIndex; + this.sliderValues[sliderIndex] = value; + if (active && currentHeightMode == Mode.SENDS) { + midiProcessor.sendSliderUpdate(sliderIndex, value); + } + } + + private void handleVolumeChange(final int trackIndex, final int value) { + this.sliderValues[trackIndex] = value; + if (active && currentHeightMode == Mode.VOLUME || currentHeightMode == Mode.SENDS) { + midiProcessor.sendSliderUpdate(trackIndex, value); + } + } + + private Optional getTrack(final int index, final int offset) { + final int trackIndex = index - offset; + if (trackIndex >= 0 && trackIndex < this.trackSize) { + return Optional.of(trackBank.getItemAt(trackIndex)); + } + return Optional.empty(); + } + + public void handleGridButton(final int index, final boolean pressed) { + if (index < INDEX_OFFSET_SOLO) { + trackBank.sceneBank().getScene(index).launch(); + } else if (index < INDEX_OFFSET_MUTE) { + getTrack(index, INDEX_OFFSET_SOLO).ifPresent(track -> track.solo().toggle()); + } else if (index < INDEX_OFFSET_ARM) { + getTrack(index, INDEX_OFFSET_MUTE).ifPresent(track -> track.mute().toggle()); + } else if (index < 0x2D) { + getTrack(index, INDEX_OFFSET_ARM).ifPresent(track -> track.arm().toggle()); + } else if (index == 0x3F) { + rootTrack.stop(); + } else if (index == 0x5A) { + Erae2ControllerExtension.println(" STOP Transport"); + } else if (index == 0x5B) { + Erae2ControllerExtension.println(" PLAY Transport"); + } else if (index == 0x64) { + if (horizontalMode) { + trackBank.scrollBy(-1); + } else { + trackBank.sceneBank().scrollBy(-1); + } + } else if (index == 0x65) { + if (horizontalMode) { + trackBank.scrollBy(1); + } else { + trackBank.sceneBank().scrollBy(1); + } + } else if (index == 0x66) { + if (horizontalMode) { + trackBank.sceneBank().scrollBy(-1); + } else { + trackBank.scrollBy(-1); + } + } else if (index == 0x67) { + if (horizontalMode) { + trackBank.sceneBank().scrollBy(1); + } else { + trackBank.scrollBy(1); + } + } + } + + public void handleGridButton(final int rowIndex, final int columnIndex, final boolean on) { + if (horizontalMode) { + if (rowIndex < trackSize && columnIndex < sceneSize) { + final ClipLauncherSlot clip = + trackBank.getItemAt(rowIndex).clipLauncherSlotBank().getItemAt(columnIndex); + clip.launch(); + } + } else { + if (rowIndex < sceneSize && columnIndex < trackSize) { + final ClipLauncherSlot clip = + trackBank.getItemAt(columnIndex).clipLauncherSlotBank().getItemAt(rowIndex); + clip.launch(); + } + } + } + + public void setCurrentHeightMode(final int height) { + if (this.trackSize == 12) { + switch (height) { + case 1 -> this.currentHeightMode = Mode.SENDS; + case 5 -> this.currentHeightMode = Mode.VOLUME; + case INDEX_OFFSET_SOLO -> this.currentHeightMode = Mode.GRID; + } + } else { + switch (height) { + case 1 -> this.currentHeightMode = Mode.SENDS; + case 3 -> this.currentHeightMode = Mode.VOLUME; + case 12 -> this.currentHeightMode = Mode.GRID; + } + } + this.gridHeight = height; + // Erae2ControllerExtension.println(" MODE=%s %d", this.currentHeightMode, height); + } + + public boolean isActive() { + return active; + } + + public void setActive(final boolean active) { + this.active = active; + trackBank.setShouldShowClipLauncherFeedback(active); + this.overviewGrid.setScenePosition(sceneOffset); + } + + private void triggerPadSlotUpdate(final SlotState slot) { + final boolean changed = slot.calcState(); + if (horizontalMode) { + if (isActive() && changed && slot.getTrackIndex() < gridHeight) { + sendGridUpdate(slot); + } + } else { + if (isActive() && changed && slot.getSceneIndex() < gridHeight) { + sendGridUpdate(slot); + } + } + } + + private void prepareSlot(final SlotState slot) { + slot.getSlot().hasContent().addValueObserver(content -> { + slot.setHasContent(content); + triggerPadSlotUpdate(slot); + }); + slot.getSlot().isPlaying().addValueObserver(playing -> { + slot.setPlaying(playing); + triggerPadSlotUpdate(slot); + }); + slot.getSlot().isStopQueued().addValueObserver(stopQueued -> { + slot.setStopQueued(stopQueued); + triggerPadSlotUpdate(slot); + }); + slot.getSlot().isRecordingQueued().addValueObserver(recordingQueued -> { + slot.setRecordingQueued(recordingQueued); + triggerPadSlotUpdate(slot); + }); + slot.getSlot().isRecording().addValueObserver(recording -> { + slot.setRecording(recording); + triggerPadSlotUpdate(slot); + }); + slot.getSlot().isPlaybackQueued().addValueObserver(playback -> { + slot.setPlaybackQueued(playback); + triggerPadSlotUpdate(slot); + }); + slot.getSlot().color().addValueObserver((r, g, b) -> { + final int colorIndex = EraeColor.rgbToIndex(r, g, b); + slot.setColor(colorIndex); + triggerPadSlotUpdate(slot); + }); + } + + + public void updateState() { + for (int trackIndex = 0; trackIndex < trackSize; trackIndex++) { + for (int sceneIndex = 0; sceneIndex < gridHeight; sceneIndex++) { + final SlotState slot = slotStates[trackIndex][sceneIndex]; + sendGridUpdate(slotStates[trackIndex][sceneIndex]); + } + if (currentHeightMode == Mode.VOLUME || currentHeightMode == Mode.SENDS) { + midiProcessor.sendSliderUpdate(trackIndex, sliderValues[trackIndex]); + } + if (currentHeightMode == Mode.SENDS) { + midiProcessor.sendSliderUpdate(trackIndex + 0xC, sliderValues[trackIndex + 0xC]); + midiProcessor.sendSliderUpdate(trackIndex + 0x18, sliderValues[trackIndex + 0x18]); + } + updateSoloState(trackStates[trackIndex]); + updateMuteState(trackStates[trackIndex]); + updateArmState(trackStates[trackIndex]); + } + for (int sceneIndex = 0; sceneIndex < gridHeight; sceneIndex++) { + final SceneState state = sceneStates[sceneIndex]; + midiProcessor.sendActionButtonUpdate(sceneIndex, state.getState(), state.getDisplayColor()); + } + } + + public void handleSliderValue(final int sliderIndex, final int value) { + if (sliderValues[sliderIndex] != value) { + sliderValues[sliderIndex] = value; + final int groupIndex = sliderIndex / 12; + final int paramIndex = sliderIndex % 12; + final double val = (double) value / 127.0; + if (groupIndex == 0) { + trackBank.getItemAt(sliderIndex).volume().set(val); + } else if (groupIndex == 1) { + trackBank.getItemAt(paramIndex).sendBank().getItemAt(0).value().set(val); + } else if (groupIndex == 2) { + trackBank.getItemAt(paramIndex).sendBank().getItemAt(1).value().set(val); + } + } + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/embodme/Erae2ControllerExtension.java b/src/main/java/com/bitwig/extensions/controllers/embodme/Erae2ControllerExtension.java new file mode 100644 index 00000000..f1463250 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/embodme/Erae2ControllerExtension.java @@ -0,0 +1,107 @@ +package com.bitwig.extensions.controllers.embodme; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import com.bitwig.extension.controller.ControllerExtension; +import com.bitwig.extension.controller.ControllerExtensionDefinition; +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.TrackBank; +import com.bitwig.extensions.framework.values.LayoutType; +import com.bitwig.extensions.framework.view.OverviewGrid; + +public class Erae2ControllerExtension extends ControllerExtension { + + private static ControllerHost debugHost; + private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("hh:mm:ss SSS"); + private HardwareSurface surface; + private ControllerHost host; + private EraeMidiProcessor midiProcessor; + private ClipLayer activeLayer; + private ClipLayer smallClipLayer; + private ClipLayer largeClipLayer; + private Application application; + private LayoutType panelLayout; + private OverviewGrid overviewGrid; + + 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)); + } + } + + protected Erae2ControllerExtension(final ControllerExtensionDefinition definition, final ControllerHost host) { + super(definition, host); + } + + @Override + public void init() { + debugHost = getHost(); + this.host = getHost(); + this.surface = host.createHardwareSurface(); + this.application = host.createApplication(); + final TrackBank maxTrackBank = host.createTrackBank(64, 1, 64, false); + overviewGrid = new OverviewGrid(maxTrackBank); + overviewGrid.initMaxBank(); + + midiProcessor = new EraeMidiProcessor(host, this); + application.panelLayout().addValueObserver(this::handleLayoutChanged); + initLayers(); + midiProcessor.init(); + } + + private void handleLayoutChanged(final String layout) { + this.panelLayout = LayoutType.toType(layout); + Erae2ControllerExtension.println(" LAYOUT = " + this.panelLayout); + } + + private void initLayers() { + smallClipLayer = new ClipLayer(host, 9, 5, midiProcessor, false, overviewGrid); + largeClipLayer = new ClipLayer(host, 12, 9, midiProcessor, false, overviewGrid); + activeLayer = smallClipLayer; + activeLayer.setActive(true); + } + + public void handleMode(final int width, final int height) { + Erae2ControllerExtension.println(" Window width=%d height=%d %s", width, height, panelLayout); + activeLayer.setActive(false); + if (width == 9) { + activeLayer = smallClipLayer; + } else if (width == 12) { + activeLayer = largeClipLayer; + } + activeLayer.setCurrentHeightMode(height); + activeLayer.setActive(true); + activeLayer.updateState(); + } + + public void handleGridButton(final int row, final int col, final boolean pressed) { + if (pressed) { + activeLayer.handleGridButton(row, col, true); + } + } + + public void handleSlider(final int sliderIndex, final int value) { + activeLayer.handleSliderValue(sliderIndex, value); + } + + public void handleIndexButton(final int index, final boolean pressed) { + if (pressed) { + activeLayer.handleGridButton(index, pressed); + } + } + + @Override + public void exit() { + + } + + @Override + public void flush() { + surface.updateHardware(); + } + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/embodme/Erae2ExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/embodme/Erae2ExtensionDefinition.java new file mode 100644 index 00000000..d0aac386 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/embodme/Erae2ExtensionDefinition.java @@ -0,0 +1,76 @@ +package com.bitwig.extensions.controllers.embodme; + +import java.util.UUID; + +import com.bitwig.extension.api.PlatformType; +import com.bitwig.extension.controller.AutoDetectionMidiPortNamesList; +import com.bitwig.extension.controller.ControllerExtensionDefinition; +import com.bitwig.extension.controller.api.ControllerHost; + +public class Erae2ExtensionDefinition extends ControllerExtensionDefinition { + private static final UUID DRIVER_ID = UUID.fromString("a4f71be3-f377-411d-94b2-5a913c2080a3"); + + public Erae2ExtensionDefinition() { + } + + @Override + public String getName() { + return "Erae 2"; + } + + @Override + public String getAuthor() { + return "Bitwig"; + } + + @Override + public String getVersion() { + return "0.5"; + } + + @Override + public UUID getId() { + return DRIVER_ID; + } + + @Override + public String getHardwareVendor() { + return "Embodme"; + } + + @Override + public String getHardwareModel() { + return "Erae 2"; + } + + @Override + public int getRequiredAPIVersion() { + return 24; + } + + @Override + public int getNumMidiInPorts() { + return 2; + } + + @Override + public int getNumMidiOutPorts() { + return 1; + } + + @Override + public String getHelpFilePath() { + return "Controllers/Embodme/Embodme Erae 2.pdf"; + } + + @Override + public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, + final PlatformType platformType) { + list.add(new String[] {"Erae 2", "MIDIIN2 (Erae 2)"}, new String[] {"Erae 2"}); + } + + @Override + public Erae2ControllerExtension createInstance(final ControllerHost host) { + return new Erae2ControllerExtension(this, host); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/embodme/EraeColor.java b/src/main/java/com/bitwig/extensions/controllers/embodme/EraeColor.java new file mode 100644 index 00000000..fad5d14e --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/embodme/EraeColor.java @@ -0,0 +1,177 @@ +package com.bitwig.extensions.controllers.embodme; + +import java.util.List; + +import com.bitwig.extension.api.Color; + +public class EraeColor { + private static final List COLOR_SEQ = List.of( + new ColorMatch(0x00, 0x00, 0x00, 0x00), new ColorMatch(0x01, 0x1E, 0x1E, 0x1E), + new ColorMatch(0x02, 0x7F, 0x7F, 0x7F), new ColorMatch(0x03, 0xFF, 0xFF, 0xFF), + new ColorMatch(0x04, 0xFF, 0x4C, 0x4C), new ColorMatch(0x05, 0xFF, 0x00, 0x00), + new ColorMatch(0x06, 0x59, 0x00, 0x00), new ColorMatch(0x07, 0x19, 0x00, 0x00), + new ColorMatch(0x08, 0xFF, 0xBD, 0x6C), new ColorMatch(0x09, 0xFF, 0x54, 0x00), + new ColorMatch(0x0A, 0x59, 0x1D, 0x00), new ColorMatch(0x0B, 0x27, 0x18, 0x00), + new ColorMatch(0x0C, 0xFF, 0xFF, 0x4C), new ColorMatch(0x0D, 0xFF, 0xFF, 0x00), + new ColorMatch(0x0E, 0x59, 0x59, 0x00), new ColorMatch(0x0F, 0x19, 0x19, 0x00), + + new ColorMatch(0x10, 0x88, 0xFF, 0x4C), new ColorMatch(0x11, 0x54, 0xFF, 0x00), + new ColorMatch(0x12, 0x1D, 0x59, 0x00), new ColorMatch(0x13, 0x14, 0x28, 0x00), + new ColorMatch(0x14, 0x4C, 0xFF, 0x4C), new ColorMatch(0x15, 0x00, 0xFF, 0x00), + new ColorMatch(0x16, 0x00, 0x59, 0x00), new ColorMatch(0x17, 0x00, 0x19, 0x00), + new ColorMatch(0x18, 0x4C, 0xFF, 0x5E), new ColorMatch(0x19, 0x00, 0xFF, 0x19), + new ColorMatch(0x1A, 0x00, 0x59, 0x0D), new ColorMatch(0x1B, 0x00, 0x19, 0x02), + new ColorMatch(0x1C, 0x4C, 0xFF, 0x88), new ColorMatch(0x1D, 0x00, 0xFF, 0x55), + new ColorMatch(0x1E, 0x00, 0x59, 0x1D), new ColorMatch(0x1F, 0x00, 0x19, 0x12), + + new ColorMatch(0x20, 0x4D, 0x06, 0xC7), new ColorMatch(0x21, 0x55, 0x05, 0xFF), + new ColorMatch(0x22, 0x00, 0x1D, 0x71), new ColorMatch(0x23, 0x00, 0x00, 0x19), + new ColorMatch(0x24, 0x4C, 0xF8, 0xFF), new ColorMatch(0x25, 0x00, 0xA9, 0xFF), + new ColorMatch(0x26, 0x00, 0x41, 0x52), new ColorMatch(0x27, 0x00, 0x10, 0x19), + new ColorMatch(0x28, 0x4C, 0xE8, 0xFF), new ColorMatch(0x29, 0x00, 0x65, 0xFF), + new ColorMatch(0x2A, 0x00, 0x27, 0x52), new ColorMatch(0x2B, 0x00, 0x0C, 0x19), + new ColorMatch(0x2C, 0x4C, 0xD8, 0xFF), new ColorMatch(0x2D, 0x00, 0x00, 0xFF), + new ColorMatch(0x2E, 0x00, 0x11, 0x52), new ColorMatch(0x2F, 0x00, 0x04, 0x19), + + new ColorMatch(0x30, 0x87, 0xC7, 0xFF), new ColorMatch(0x31, 0x54, 0x00, 0xFF), + new ColorMatch(0x32, 0x19, 0x00, 0x52), new ColorMatch(0x33, 0x0F, 0x00, 0x19), + new ColorMatch(0x34, 0x4C, 0xA8, 0xFF), new ColorMatch(0x35, 0x00, 0x00, 0xFF), + new ColorMatch(0x36, 0x00, 0x00, 0x59), new ColorMatch(0x37, 0x00, 0x00, 0x19), + new ColorMatch(0x38, 0x4C, 0x88, 0xFF), new ColorMatch(0x39, 0x00, 0xA9, 0xFF), + new ColorMatch(0x3A, 0x00, 0x36, 0x4D), new ColorMatch(0x3B, 0x00, 0x0D, 0x19), + new ColorMatch(0x3C, 0x4C, 0x69, 0xFF), new ColorMatch(0x3D, 0x00, 0x54, 0xFF), + new ColorMatch(0x3E, 0x00, 0x1D, 0x59), new ColorMatch(0x3F, 0x00, 0x10, 0x19), + + new ColorMatch(0x40, 0x4C, 0x95, 0x00), new ColorMatch(0x41, 0x00, 0x2C, 0xB9), + new ColorMatch(0x42, 0x00, 0x08, 0x19), new ColorMatch(0x43, 0x00, 0x08, 0x19), + new ColorMatch(0x44, 0x4C, 0x69, 0xFF), new ColorMatch(0x45, 0x00, 0x00, 0xFF), + new ColorMatch(0x46, 0x00, 0x00, 0x59), new ColorMatch(0x47, 0x00, 0x00, 0x19), + new ColorMatch(0x48, 0x87, 0x67, 0xFF), new ColorMatch(0x49, 0x7A, 0x00, 0xFF), + new ColorMatch(0x4A, 0x19, 0x00, 0x52), new ColorMatch(0x4B, 0x0F, 0x00, 0x30), + new ColorMatch(0x4C, 0xFF, 0x4C, 0xFF), new ColorMatch(0x4D, 0xFF, 0x00, 0xFF), + new ColorMatch(0x4E, 0x59, 0x00, 0x59), new ColorMatch(0x4F, 0x19, 0x00, 0x19), + + new ColorMatch(0x50, 0xFF, 0x4C, 0xD8), new ColorMatch(0x51, 0xFF, 0x00, 0x54), + new ColorMatch(0x52, 0x59, 0x00, 0x1D), new ColorMatch(0x53, 0x22, 0x00, 0x13), + new ColorMatch(0x54, 0xFF, 0x4C, 0xAD), new ColorMatch(0x55, 0xFF, 0x00, 0x73), + new ColorMatch(0x56, 0x59, 0x00, 0x26), new ColorMatch(0x57, 0x19, 0x00, 0x0D), + new ColorMatch(0x58, 0xFF, 0x15, 0x00), new ColorMatch(0x59, 0x99, 0x35, 0x00), + new ColorMatch(0x5A, 0x79, 0x51, 0x00), new ColorMatch(0x5B, 0x43, 0x60, 0x00), + new ColorMatch(0x5C, 0x03, 0x38, 0x00), new ColorMatch(0x5D, 0x14, 0x4F, 0x10), + new ColorMatch(0x5E, 0x0D, 0x5C, 0x05), new ColorMatch(0x5F, 0x15, 0x62, 0x00), + + new ColorMatch(0x60, 0xFF, 0x90, 0x00), new ColorMatch(0x61, 0x88, 0xCC, 0x00), + new ColorMatch(0x62, 0x91, 0xCC, 0x00), new ColorMatch(0x63, 0x43, 0x74, 0x00), + new ColorMatch(0x64, 0x03, 0x39, 0x00), new ColorMatch(0x65, 0x00, 0x57, 0x45), + new ColorMatch(0x66, 0x00, 0x54, 0x7F), new ColorMatch(0x67, 0x00, 0x00, 0xFF), + new ColorMatch(0x68, 0x00, 0x45, 0x4F), new ColorMatch(0x69, 0x25, 0x00, 0xDC), + new ColorMatch(0x6A, 0x7F, 0x7F, 0x7F), new ColorMatch(0x6B, 0x20, 0x20, 0x20), + new ColorMatch(0x6C, 0xFF, 0x00, 0x00), new ColorMatch(0x6D, 0xBE, 0x2D, 0x2D), + new ColorMatch(0x6E, 0xB0, 0x18, 0x18), new ColorMatch(0x6F, 0x64, 0xF1, 0xD5), + + new ColorMatch(0x70, 0x10, 0x88, 0x00), new ColorMatch(0x71, 0x14, 0x4B, 0x00), + new ColorMatch(0x72, 0x07, 0x42, 0x00), new ColorMatch(0x73, 0x00, 0xA1, 0x00), + new ColorMatch(0x74, 0x39, 0x00, 0x00), new ColorMatch(0x75, 0x7A, 0x51, 0x00), + new ColorMatch(0x76, 0x73, 0x17, 0x00), new ColorMatch(0x77, 0x5B, 0xFF, 0x00), + new ColorMatch(0x78, 0x3B, 0xDD, 0x08), new ColorMatch(0x79, 0x59, 0xFF, 0x00), + new ColorMatch(0x7A, 0x38, 0xCE, 0x00), new ColorMatch(0x7B, 0x40, 0x40, 0x40), + new ColorMatch(0x7C, 0x75, 0x75, 0x75), new ColorMatch(0x7D, 0xE1, 0x20, 0x00), + new ColorMatch(0x7E, 0xB0, 0xB4, 0x00), new ColorMatch(0x7F, 0x4B, 0x15, 0x02)); + + private record ColorMatch(int index, int red, int green, int blue, Color color) { + public ColorMatch(final int index, final int red, final int green, final int blue) { + this(index, red, green, blue, Color.fromRGB255(red, green, blue)); + } + + public boolean isGrayTone() { + return red == green && green == blue; + } + + private double colorDistance(final double r1, final double g1, final double b1) { + final double dr = r1 - (this.red / 255.0); + final double dg = g1 - (this.green / 255.0); + final double db = b1 - (this.blue / 255.0); + + return Math.sqrt(dr * dr + dg * dg + db * db); // Euclidean distance + } + } + + private final int index; + private int state; + + public static final int OFF = 0; + public static final int WHITE_DIM = 0x1; + public static final int GREY = 0x2; + public static final int WHITE = 0x3; + public static final int RED = 0x5; + public static final int RED_DIM = 0x7; + public static final int YELLOW = 0x15; + public static final int YELLOW_DIM = 0x18; + public static final int ORANGE = 0x9; + public static final int ORANGE_DIM = 0xC; + public static final int GREEN = 0x15; + public static final int GREEN_DIM = 0x18; + public static final int BLUE = 0x2D; + public static final int BLUE_DIM = 0x2F; + + public static final int STATE_NORMAL = 0; + public static final int STATE_BLINK = 1; + public static final int STATE_PULSE = 2; + + public EraeColor(final int index) { + this.index = index; + this.state = state; + } + + public int getIndex() { + return index; + } + + public void setState(final int state) { + this.state = state; + } + + public int getState() { + return state; + } + + public static EraeColor toColor(final double r, final double g, final double b) { + final int colorIndex = rgbToIndex(r, g, b); + return new EraeColor(colorIndex); + } + + public static int rgbToIndex(final double red, final double green, final double blue) { + int closestIndex = -1; + double minDistance = Double.MAX_VALUE; + + if (red == green && green == blue) { + for (int i = 0; i < 128; i++) { + final ColorMatch c = COLOR_SEQ.get(i); + if (c.isGrayTone()) { + final double distance = c.colorDistance(red, green, blue); + if (distance < minDistance) { + minDistance = distance; + closestIndex = i; + } + } + } + } else { + for (int i = 0; i < 0x32; i++) { + final ColorMatch c = COLOR_SEQ.get(i); + final double distance = c.colorDistance(red, green, blue); + + if (distance < minDistance) { + minDistance = distance; + closestIndex = i; + } + } + } + + final ColorMatch colorMatch = COLOR_SEQ.get(closestIndex); + // Erae2ControllerExtension.println( + // "MATCH %02X = %02X %02X %02X %02X %02X %02X", closestIndex, colorMatch.red, colorMatch.green, + // colorMatch.blue, (int) Math.round(red * 255), (int) Math.round(green * 255), (int) Math.round + // (blue * 255)); + return colorMatch.index(); + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/embodme/EraeMidiProcessor.java b/src/main/java/com/bitwig/extensions/controllers/embodme/EraeMidiProcessor.java new file mode 100644 index 00000000..c4e7a48b --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/embodme/EraeMidiProcessor.java @@ -0,0 +1,116 @@ +package com.bitwig.extensions.controllers.embodme; + +import com.bitwig.extension.controller.api.ControllerHost; +import com.bitwig.extension.controller.api.MidiIn; +import com.bitwig.extension.controller.api.MidiOut; +import com.bitwig.extension.controller.api.NoteInput; + +public class EraeMidiProcessor { + private static final String IDENT_REQUEST = "f0 7e 7f 06 01 f7"; + private static final String IDENT_REPLY = "f07e0106020021500001000256010102f7"; + private static final String ERAE_SYSEX_HEADER = "F0 00 21 50 00 01 00 02 01 01 "; + private static final String REQUEST_STATE = ERAE_SYSEX_HEADER + "02 03 F7"; + private static final String IN_ERAE_SYSEX_HEADER = "f0002150000300012"; + + private static final byte[] BUTTON_UPDATE = { + (byte) 0xF0, 0x00, 0x21, 0x50, 0x00, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x02, // + 0x1B,// 12: index row*12 + columnn + 0x01, // 13: color style 0x0 const 0x1 blink 0x2 pulse + 0x05, // 14: color + (byte) 0xF7 + }; + + private static final byte[] ACTION_BUTTON_UPDATE = { + (byte) 0xF0, 0x00, 0x21, 0x50, 0x00, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, // + 0x1B,// 12: index row*12 + columnn + 0x01, // 13: color style 0x0 const 0x1 blink 0x2 pulse + 0x05, // 14: color + (byte) 0xF7 + }; + + // F0 00 21 50 00 01 00 02 01 01 02 0F [target_id] [value] F7 + private static final byte[] SLIDER_UPDATE = { + (byte) 0xF0, 0x00, 0x21, 0x50, 0x00, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x0F, // + 0x01, // 12: target Id + 0x05, // 13: value + (byte) 0xF7 + }; + + private final MidiIn midiInDaw; + private final MidiOut midiOutDaw; + private final Erae2ControllerExtension controller; + + public EraeMidiProcessor(final ControllerHost host, final Erae2ControllerExtension controller) { + this.controller = controller; + midiInDaw = host.getMidiInPort(0); + midiOutDaw = host.getMidiOutPort(0); + midiInDaw.createNoteInput("Erae MIDI Port", "??????"); + midiInDaw.setSysexCallback(this::handleSysEx); + + final MidiIn noteMidiIn = host.getMidiInPort(1); + final NoteInput noteInputMpe = noteMidiIn.createNoteInput("Erae MPE Port", "??????"); + noteInputMpe.setUseExpressiveMidi(true, 0, 48); + } + + public void init() { + midiOutDaw.sendSysex(IDENT_REQUEST); + } + + + private void handleSysEx(final String fullData) { + if (fullData.startsWith(IN_ERAE_SYSEX_HEADER)) { + final String data = fullData.substring(IN_ERAE_SYSEX_HEADER.length() + 1, fullData.length() - 2); + + if ("2c".equals(data)) { + midiOutDaw.sendSysex(REQUEST_STATE); + } else if (data.startsWith("26")) { + final int size = Integer.parseInt(data.substring(2, 4), 16); + final int mode = Integer.parseInt(data.substring(4, 6), 16); + controller.handleMode(size, mode); + } else if (data.startsWith("01")) { + final int buttonIndex = Integer.parseInt(data.substring(2, 4), 16); + final int value = Integer.parseInt(data.substring(4, 6), 16); + controller.handleIndexButton(buttonIndex, value > 0); + } else if (data.startsWith("02")) { + final int buttonIndex = Integer.parseInt(data.substring(2, 4), 16); + final int value = Integer.parseInt(data.substring(4, 6), 16); + final int row = buttonIndex / 12; + final int col = buttonIndex % 12; + controller.handleGridButton(row, col, value > 0); + } else if (data.startsWith("0f")) { + final int sliderIndex = Integer.parseInt(data.substring(2, 4), 16); + final int value = Integer.parseInt(data.substring(4, 6), 16); + controller.handleSlider(sliderIndex, value); + } else { + Erae2ControllerExtension.println(" DATA = %s", data); + } + } else if (fullData.equals(IDENT_REPLY)) { + Erae2ControllerExtension.println(" IDENT REPLY"); + midiOutDaw.sendSysex(REQUEST_STATE); + } else { + Erae2ControllerExtension.println(" SysEx = %s", fullData); + } + } + + public void sendGridButtonUpdate(final int row, final int column, final int style, final int color) { + BUTTON_UPDATE[12] = (byte) ((row * 12 + column) & 0x7F); + BUTTON_UPDATE[13] = (byte) (style & 0x7F); + BUTTON_UPDATE[14] = (byte) (color & 0x7F); + midiOutDaw.sendSysex(BUTTON_UPDATE); + } + + public void sendActionButtonUpdate(final int index, final int style, final int color) { + ACTION_BUTTON_UPDATE[12] = (byte) (index & 0x7F); + ACTION_BUTTON_UPDATE[13] = (byte) (style & 0x7F); + ACTION_BUTTON_UPDATE[14] = (byte) (color & 0x7F); + midiOutDaw.sendSysex(ACTION_BUTTON_UPDATE); + } + + public void sendSliderUpdate(final int targetId, final int value) { + SLIDER_UPDATE[12] = (byte) (targetId & 0x7F); + SLIDER_UPDATE[13] = (byte) (value & 0x7F); + midiOutDaw.sendSysex(SLIDER_UPDATE); + } + + +} diff --git a/src/main/java/com/bitwig/extensions/controllers/embodme/SceneState.java b/src/main/java/com/bitwig/extensions/controllers/embodme/SceneState.java new file mode 100644 index 00000000..f6fb5d08 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/embodme/SceneState.java @@ -0,0 +1,69 @@ +package com.bitwig.extensions.controllers.embodme; + +class SceneState { + private final int index; + private int count; + private int state = 0; + private int color = EraeColor.GREEN; + private int displayColor = 0; + private boolean exists; + private boolean queued; + + public SceneState(final int index) { + this.index = index; + } + + public void setCount(final int count) { + this.count = count; + } + + public void setColor(final int color) { + if (color == 0x7b) { + this.color = EraeColor.GREEN; + } else { + this.color = color; + } + } + + public int getIndex() { + return index; + } + + public int getCount() { + return count; + } + + public int getState() { + return state; + } + + public int getDisplayColor() { + return displayColor; + } + + public boolean calcState() { + final int oldState = this.state; + final int oldColor = this.displayColor; + + if (exists) { + if (queued) { + displayColor = color; + } else if (count == 0) { + displayColor = 0; + } else { + displayColor = color + 2; + } + state = queued ? 1 : 0; + } + + return oldState != state || oldColor != displayColor; + } + + public void setExists(final boolean exists) { + this.exists = exists; + } + + public void setQueued(final boolean queued) { + this.queued = queued; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/embodme/TrackState.java b/src/main/java/com/bitwig/extensions/controllers/embodme/TrackState.java new file mode 100644 index 00000000..3a4e77b9 --- /dev/null +++ b/src/main/java/com/bitwig/extensions/controllers/embodme/TrackState.java @@ -0,0 +1,57 @@ +package com.bitwig.extensions.controllers.embodme; + +import com.bitwig.extension.controller.api.Track; + +class TrackState { + private final Track track; + private final int index; + private boolean armed; + private boolean muted; + private boolean solo; + private int color; + + public TrackState(final Track track, final int trackIndex) { + this.track = track; + this.index = trackIndex; + } + + public Track getTrack() { + return track; + } + + public int getIndex() { + return index; + } + + public void setArmed(final boolean armed) { + this.armed = armed; + } + + public boolean isArmed() { + return armed; + } + + public boolean isMuted() { + return muted; + } + + public void setMuted(final boolean muted) { + this.muted = muted; + } + + public boolean isSolo() { + return solo; + } + + public void setSolo(final boolean solo) { + this.solo = solo; + } + + public int getColor() { + return color; + } + + public void setColor(final int color) { + this.color = color; + } +} diff --git a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/definition/NksConnectExtensionDefinition.java b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/definition/NksConnectExtensionDefinition.java index 89a60172..d95852a2 100644 --- a/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/definition/NksConnectExtensionDefinition.java +++ b/src/main/java/com/bitwig/extensions/controllers/nativeinstruments/komplete/definition/NksConnectExtensionDefinition.java @@ -8,50 +8,50 @@ import com.bitwig.extensions.controllers.nativeinstruments.komplete.KontrolSMk3Extension; public class NksConnectExtensionDefinition extends AbstractKompleteKontrolExtensionDefinition { - + private static final UUID DRIVER_ID = UUID.fromString("8b07a8e9-886c-f35b-e133-da9210306ce4"); private static final String MODEL = "NKS Connect"; - + public NksConnectExtensionDefinition() { super(MODEL); } - + @Override public String getName() { return MODEL; } - + @Override public String getHardwareModel() { return MODEL; } - + @Override public UUID getId() { return DRIVER_ID; } - + @Override public int getNumMidiInPorts() { return 1; } - + @Override public int getNumMidiOutPorts() { return 1; } - + @Override public void listAutoDetectionMidiPortNames(final AutoDetectionMidiPortNamesList list, final PlatformType platformType) { list.add(new String[] {"NKS Connect"}, new String[] {"NKS Connect"}); } - + @Override public String getHelpFilePath() { - return "Controllers/Native Instruments/NKS Connect/NKS Connect.pdf"; + return "Controllers/Native Instruments/Kontrol S-Series/Kontrol S-Series Mk3.pdf"; } - + @Override public KontrolSMk3Extension createInstance(final ControllerHost host) { return new KontrolSMk3Extension(this, host, true); diff --git a/src/main/java/com/bitwig/extensions/framework/view/OverviewGrid.java b/src/main/java/com/bitwig/extensions/framework/view/OverviewGrid.java index 87cb488e..443f74c1 100644 --- a/src/main/java/com/bitwig/extensions/framework/view/OverviewGrid.java +++ b/src/main/java/com/bitwig/extensions/framework/view/OverviewGrid.java @@ -1,5 +1,8 @@ package com.bitwig.extensions.framework.view; +import java.util.ArrayList; +import java.util.List; + import com.bitwig.extension.controller.api.ClipLauncherSlot; import com.bitwig.extension.controller.api.Scene; import com.bitwig.extension.controller.api.Track; @@ -21,6 +24,12 @@ public class OverviewGrid { private final int size; private final TrackBank maxTrackBank; + private final List queueChangeListeners = new ArrayList<>(); + + @FunctionalInterface + public interface QueueChangeListener { + void notifySceneHasQueued(int index, boolean queued); + } public OverviewGrid(final TrackBank maxTrackBank) { this.size = maxTrackBank.getSizeOfBank(); @@ -31,6 +40,19 @@ public OverviewGrid(final TrackBank maxTrackBank) { prepare(maxTrackBank); } + public void initMaxBank() { + for (int i = 0; i < size; i++) { + final int trackIndex = i; + final Track track = maxTrackBank.getItemAt(trackIndex); + for (int j = 0; j < track.clipLauncherSlotBank().getSizeOfBank(); j++) { + final int sceneIndex = j; + final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); + slot.isPlaybackQueued().addValueObserver(isQueued -> markSceneQueued(sceneIndex, isQueued)); + slot.isPlaying().addValueObserver(isQueued -> markScenePlaying(sceneIndex, isQueued)); + } + } + } + public void setUpFocusScene(final TrackBank mainBank) { prepare(mainBank); mainBank.sceneBank().itemCount().addValueObserver(this::setNumberOfScenes); @@ -47,19 +69,11 @@ public void setUpFocusScene(final TrackBank mainBank) { maxTrackBank.sceneBank().scrollPosition().set(getSceneOffset()); } }); - for (int i = 0; i < size; i++) { - final int trackIndex = i; - final Track track = maxTrackBank.getItemAt(trackIndex); - - final Scene scene = maxTrackBank.sceneBank().getScene(i); - scene.clipCount().addValueObserver(count -> this.clipCount[trackIndex] = count); - for (int j = 0; j < size; j++) { - final int sceneIndex = j; - final ClipLauncherSlot slot = track.clipLauncherSlotBank().getItemAt(sceneIndex); - slot.isPlaybackQueued().addValueObserver(isQueued -> markSceneQueued(sceneIndex, isQueued)); - slot.isPlaying().addValueObserver(isQueued -> markScenePlaying(sceneIndex, isQueued)); - } - } + initMaxBank(); + } + + public void addQueuedSceneListener(final QueueChangeListener listener) { + queueChangeListeners.add(listener); } public int getNumberOfScenes() { @@ -105,11 +119,17 @@ public int getSceneOffset() { } private void markSceneQueued(final int sceneIndex, final boolean isQueued) { + final int previous = sceneQueuedClips[sceneIndex]; if (isQueued) { sceneQueuedClips[sceneIndex]++; } else if (sceneQueuedClips[sceneIndex] > 0) { sceneQueuedClips[sceneIndex]--; } + if (sceneQueuedClips[sceneIndex] > 0 && previous == 0) { + queueChangeListeners.forEach(l -> l.notifySceneHasQueued(sceneIndex, true)); + } else if (sceneQueuedClips[sceneIndex] <= 0 && previous > 0) { + queueChangeListeners.forEach(l -> l.notifySceneHasQueued(sceneIndex, false)); + } } private void markScenePlaying(final int sceneIndex, final boolean isPlaying) { diff --git a/src/main/resources/Documentation/Controllers/Embodme/Embodme Erae 2.pdf b/src/main/resources/Documentation/Controllers/Embodme/Embodme Erae 2.pdf new file mode 100644 index 00000000..412acb59 Binary files /dev/null and b/src/main/resources/Documentation/Controllers/Embodme/Embodme Erae 2.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 0eb7ee9a..d277b9dc 100644 --- a/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition +++ b/src/main/resources/META-INF/services/com.bitwig.extension.ExtensionDefinition @@ -56,6 +56,8 @@ com.bitwig.extensions.controllers.keith_mcmillen.QuNexusDefinition com.bitwig.extensions.controllers.kenton.KillaMixMiniExtensionDefinition +com.bitwig.extensions.controllers.embodme.Erae2ExtensionDefinition + com.bitwig.extensions.controllers.expressivee.OsmoseControllerExtensionDefinition com.bitwig.extensions.controllers.expressivee.osmoseCE49.OsmoseCE49ExtensionDefinition com.bitwig.extensions.controllers.expressivee.osmoseCE61.OsmoseCE61ExtensionDefinition