diff --git a/assets/mcu-midiremote.v1m-daw b/assets/mcu-midiremote.v1m-daw
new file mode 100644
index 0000000..30795f0
--- /dev/null
+++ b/assets/mcu-midiremote.v1m-daw
@@ -0,0 +1,230 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/readme.md b/readme.md
index 7c79218..56b57c8 100644
--- a/readme.md
+++ b/readme.md
@@ -21,6 +21,7 @@ The following devices are explicitly supported:
- iCON:
- Platform M+ / X+ \*
- QCon Pro G2 / QCon EX G2
+ - V1-M / V1-X \*
- Mackie Control Universal (Pro) / XT (Pro)
- SSL UF1 \*
@@ -209,6 +210,25 @@ Current limitations of the MIDI Remote API:
+
+iCON V1-M / V1-X
+
+The iCON V1-M has a touch screen button matrix with customizable button labels (via the iMAP software).
+The mappings of the MIDI Remote Script are available as an iMAP DAW mapping that you can [download](assets/mcu-midiremote.v1m-daw) and load into iMAP (right-click > "Load DAW mapping") so the button layout on the V1-M matches the layout of the MIDI Remote control surface.
+When you customize your mappings in the Cubase MIDI Remote Mapping Assistant, you can use the iMAP software to update the labels on the V1-M.
+The default mapping assigns each button of the first three function layers (blue, green, yellow) to a corresponding virtual button on the MIDI Remote control surface.
+Presuming the provided iMAP DAW mapping has been loaded, the following aspects of the V1-M script differ from the mapping described above:
+
+- All buttons are labelled according to their actual functions (even if these functions differ from the default MCU functions).
+- The first (blue) function layer exposes three buttons that are not available in Cubase's default MCU mapping: Edit Instrument, Reset Meters, and Click.
+- There is no additional touchscreen button for controlling the value under the mouse cursor because the controller can already do this via the Focus button top-right of the jog wheel.
+- The secondary scribble strips show track names and peak meter levels. While a track's fader is touched, its scribble strip switches to the fader's current parameter name and parameter value instead, unless the Shift button is held.
+- All encoder assign buttons are located on the second (green) function layer and there are more encoder assign buttons than traditional MCU devices have: The encoder assignments from the table in the previous section have mostly been split across individual buttons to make them easier to access. The only encoder assignments which you can page through by pressing the assign button multiple times are EQ, Sends, and Focused Insert.
+
+Lastly, thanks to iCON for supporting the development of this script variant!
+
+
+
SSL UF1
diff --git a/src/config.ts b/src/config.ts
index d283379..e3de5a4 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -96,8 +96,7 @@ var CONFIGURATION = {
*
* * `"encoders"` to make scribble strip displays pick up colors from encoders, i.e., each
* display uses the track color of the channel its encoder value belongs to. When an encoder is
- * unassigned, the scribble strip below it falls back to the corresponding mixer channel's
- * color.
+ * unassigned, its scribble strip falls back to the corresponding mixer channel's color.
*
* * `"channels"` to makes scribble strips ignore encoder colors and always use their channels'
* track colors instead. When a channel is unassigned but its encoder is assigned, the display
@@ -107,7 +106,7 @@ var CONFIGURATION = {
* always be white unless a display's channel and encoder is unassigned, in which case the
* display will revert to black.
*
- * @devices X-Touch, X-Touch One
+ * @devices X-Touch, X-Touch One, V1-M
*/
displayColorMode: "encoders",
diff --git a/src/decorators/MidiOutputPort.ts b/src/decorators/MidiOutputPort.ts
index 21654af..564ca53 100644
--- a/src/decorators/MidiOutputPort.ts
+++ b/src/decorators/MidiOutputPort.ts
@@ -16,8 +16,13 @@ class MidiOutputDecorator {
]);
};
- sendNoteOn = (context: MR_ActiveDevice, pitch: number, velocity: number | boolean) => {
- this.port.sendMidi(context, [0x90, pitch, +Boolean(velocity) * 0x7f]);
+ sendNoteOn = (
+ context: MR_ActiveDevice,
+ pitch: number,
+ velocity: number | boolean,
+ channelNumber = 0,
+ ) => {
+ this.port.sendMidi(context, [0x90 + channelNumber, pitch, +Boolean(velocity) * 0x7f]);
};
}
diff --git a/src/decorators/surface-elements/LedButton.ts b/src/decorators/surface-elements/LedButton.ts
index 06c4304..533cd1c 100644
--- a/src/decorators/surface-elements/LedButton.ts
+++ b/src/decorators/surface-elements/LedButton.ts
@@ -23,6 +23,7 @@ class LedButtonDecorator {
private ledValue = new ContextVariable(0);
private ports?: MidiPortPair;
+ private channelNumber?: number;
private note?: number;
constructor(
@@ -33,30 +34,40 @@ class LedButtonDecorator {
onSurfaceValueChange = new CallbackCollection(this.button.mSurfaceValue, "mOnProcessValueChange");
+ sendNoteOn = (context: MR_ActiveDevice, velocity: number | boolean) => {
+ if (
+ this.ports &&
+ typeof this.channelNumber !== "undefined" &&
+ typeof this.note !== "undefined"
+ ) {
+ this.ports.output.sendNoteOn(context, this.note, velocity, this.channelNumber);
+ }
+ };
+
setLedValue = (context: MR_ActiveDevice, value: number) => {
this.ledValue.set(context, value);
- if (this.ports && typeof this.note !== "undefined") {
- this.ports.output.sendNoteOn(context, this.note, value);
- }
+ this.sendNoteOn(context, value);
};
- bindToNote = (ports: MidiPortPair, note: number) => {
+ bindToNote = (ports: MidiPortPair, note: number, channelNumber = 0) => {
this.ports = ports;
+ this.channelNumber = channelNumber;
this.note = note;
- this.button.mSurfaceValue.mMidiBinding.setInputPort(ports.input).bindToNote(0, note);
+ this.button.mSurfaceValue.mMidiBinding
+ .setInputPort(ports.input)
+ .bindToNote(channelNumber, note);
this.onSurfaceValueChange.addCallback((context, newValue) => {
- ports.output.sendNoteOn(context, note, newValue || this.ledValue.get(context));
+ this.sendNoteOn(context, newValue || this.ledValue.get(context));
});
// Binding the button's mSurfaceValue to a host function may alter it to not change when the
// button is pressed. Hence, `shadowValue` is used to make the button light up while it's
// pressed.
- this.shadowValue.mMidiBinding.setInputPort(ports.input).bindToNote(0, note);
+ this.shadowValue.mMidiBinding.setInputPort(ports.input).bindToNote(channelNumber, note);
this.shadowValue.mOnProcessValueChange = (context, newValue) => {
- ports.output.sendNoteOn(
+ this.sendNoteOn(
context,
- note,
newValue ||
this.button.mSurfaceValue.getProcessValue(context) ||
this.ledValue.get(context),
@@ -67,11 +78,18 @@ class LedButtonDecorator {
// Turn the button's LED off when it becomes unassigned
this.button.mSurfaceValue.mOnTitleChange = (context, title) => {
if (title === "") {
- ports.output.sendNoteOn(context, note, 0);
+ this.sendNoteOn(context, 0);
}
};
}
};
+
+ /**
+ * Returns whether `bindToNote()` has already been called on this button.
+ */
+ isBoundToNote = () => {
+ return Boolean(this.ports);
+ };
}
/**
diff --git a/src/decorators/surface-elements/TouchSensitiveFader.ts b/src/decorators/surface-elements/TouchSensitiveFader.ts
index 0d19dfa..c86f3b4 100644
--- a/src/decorators/surface-elements/TouchSensitiveFader.ts
+++ b/src/decorators/surface-elements/TouchSensitiveFader.ts
@@ -1,12 +1,22 @@
import { MidiPortPair } from "/midi/MidiPortPair";
import { GlobalState } from "/state";
-import { ContextVariable } from "/util";
+import { CallbackCollection, ContextVariable } from "/util";
class TouchSensitiveMotorFaderDecorator {
// Workaround because `filterByValue` in the encoder bindings hides zero values from
// `mOnProcessValueChange`
private mTouchedShadowValue = this.surface.makeCustomValueVariable("faderTouchedShadow");
+ public onTouchedValueChangeCallbacks = new CallbackCollection(
+ this.mTouchedShadowValue,
+ "mOnProcessValueChange",
+ );
+
+ public onTitleChangeCallbacks = new CallbackCollection(
+ this.fader.mSurfaceValue,
+ "mOnTitleChange",
+ );
+
constructor(
private surface: MR_DeviceSurface,
private fader: MR_Fader,
@@ -33,11 +43,11 @@ class TouchSensitiveMotorFaderDecorator {
ports.output.sendMidi(context, [0xe0 + channelIndex, value & 0x7f, value >> 7]);
};
- this.mTouchedShadowValue.mOnProcessValueChange = (context, isFaderTouched) => {
+ this.onTouchedValueChangeCallbacks.addCallback((context, isFaderTouched) => {
if (!isFaderTouched) {
sendValue(context, surfaceValue.getProcessValue(context));
}
- };
+ });
areMotorsActive.addOnChangeCallback((context, areMotorsActive) => {
if (areMotorsActive) {
@@ -67,14 +77,14 @@ class TouchSensitiveMotorFaderDecorator {
surfaceValue.mOnProcessValueChange = onSurfaceValueChange;
// Send fader down when unassigned
- surfaceValue.mOnTitleChange = (context, _title1, title2) => {
+ this.onTitleChangeCallbacks.addCallback((context, _title1, title2) => {
if (title2 === "") {
surfaceValue.setProcessValue(context, 0);
// `mOnProcessValueChange` isn't run on `setProcessValue()` when the fader is not assigned
// to a mixer channel, so we manually trigger the update:
onSurfaceValueChange(context, 0);
}
- };
+ });
};
}
diff --git a/src/device-configs/behringer_x-touch-one.ts b/src/device-configs/behringer_x-touch-one.ts
index 8ebbf72..5fca19b 100644
--- a/src/device-configs/behringer_x-touch-one.ts
+++ b/src/device-configs/behringer_x-touch-one.ts
@@ -10,10 +10,11 @@ import { LedButton } from "/decorators/surface-elements/LedButton";
import { LedPushEncoder } from "/decorators/surface-elements/LedPushEncoder";
import { TouchSensitiveMotorFader } from "/decorators/surface-elements/TouchSensitiveFader";
import * as encoderPageConfigs from "/mapping/encoders/page-configs";
+import { BehringerColorManager } from "/midi/managers/colors/BehringerColorManager";
import { createElements } from "/util";
export const deviceConfig: DeviceConfig = {
- channelColorSupport: "behringer",
+ colorManager: BehringerColorManager,
hasIndividualScribbleStrips: true,
shallMouseValueModeMapAllEncoders: true,
detectionUnits: [
diff --git a/src/device-configs/behringer_xtouch.ts b/src/device-configs/behringer_xtouch.ts
index 929afb1..99702d5 100644
--- a/src/device-configs/behringer_xtouch.ts
+++ b/src/device-configs/behringer_xtouch.ts
@@ -9,6 +9,7 @@ import { Lamp } from "/decorators/surface-elements/Lamp";
import { LedButton } from "/decorators/surface-elements/LedButton";
import { LedPushEncoder } from "/decorators/surface-elements/LedPushEncoder";
import { TouchSensitiveMotorFader } from "/decorators/surface-elements/TouchSensitiveFader";
+import { BehringerColorManager } from "/midi/managers/colors/BehringerColorManager";
import { createElements } from "/util";
const channelWidth = 5;
@@ -78,7 +79,7 @@ const extenderPortPairConfigurator = (
};
export const deviceConfig: DeviceConfig = {
- channelColorSupport: "behringer",
+ colorManager: BehringerColorManager,
hasIndividualScribbleStrips: true,
detectionUnits: [
{
diff --git a/src/device-configs/icon_v1-m.ts b/src/device-configs/icon_v1-m.ts
new file mode 100644
index 0000000..db4ef6b
--- /dev/null
+++ b/src/device-configs/icon_v1-m.ts
@@ -0,0 +1,423 @@
+/**
+ * @vendor iCON
+ * @device V1-M
+ */
+
+import { ChannelSurfaceElements, DeviceConfig, MainDeviceSurface } from ".";
+import { JogWheel } from "/decorators/surface-elements/JogWheel";
+import { LedButton } from "/decorators/surface-elements/LedButton";
+import { LedPushEncoder } from "/decorators/surface-elements/LedPushEncoder";
+import { TouchSensitiveMotorFader } from "/decorators/surface-elements/TouchSensitiveFader";
+import { MainDevice } from "/devices";
+import * as pageConfigs from "/mapping/encoders/page-configs";
+import { IconColorManager } from "/midi/managers/colors/IconColorManager";
+import { createElements } from "/util";
+
+const channelWidth = 3.5;
+const channelElementsWidth = 8 * channelWidth;
+const surfaceHeight = 38;
+const deviceFramePaddingWidth = 0.8;
+
+/**
+ * Additional surface elements for main devices
+ */
+interface MainDeviceCustomElements {
+ buttonMatrix: LedButton[][][];
+}
+
+function makeChannelElements(surface: MR_DeviceSurface, x: number): ChannelSurfaceElements[] {
+ // Secondary scribble strip frame
+ // surface.makeBlindPanel(x + deviceFramePaddingWidth, 20 - 0.25, channelElementsWidth, 2.5);
+
+ return createElements(8, (index) => {
+ const currentChannelXPosition = x + deviceFramePaddingWidth + index * channelWidth;
+
+ const [recordButton, soloButton, muteButton, selectButton] = createElements(
+ 4,
+ (row) =>
+ new LedButton(surface, {
+ position: [currentChannelXPosition + 1 - 0.125, 11.5 + row * 2 - 0.125, 1.75, 1.75],
+ isChannelButton: true,
+ }),
+ );
+
+ const encoder = new LedPushEncoder(surface, currentChannelXPosition + 0.75, 9.5, 2, 2);
+
+ // VU meter
+ surface.makeBlindPanel(currentChannelXPosition + 1.3, 1.25, 0.9, 2.5);
+
+ // Primary scribble strip
+ surface.makeBlindPanel(currentChannelXPosition, 4, channelWidth, 2.5);
+ surface
+ .makeLabelField(currentChannelXPosition + 0.25, 4.25, channelWidth - 0.5, 0.75)
+ .relateTo(selectButton);
+ surface
+ .makeLabelField(currentChannelXPosition + 0.25, 4.25 + 0.75, channelWidth - 0.5, 0.75)
+ .relateTo(encoder);
+
+ // Secondary scribble strip
+ surface.makeBlindPanel(currentChannelXPosition + 0.25, 20, channelWidth - 0.5, 2);
+ surface
+ .makeLabelField(currentChannelXPosition + 0.5, 20.25, channelWidth - 1, 0.75)
+ .relateTo(selectButton);
+ surface.makeLabelField(currentChannelXPosition + 0.5, 20.25 + 0.75, channelWidth - 1, 0.75);
+
+ return {
+ index,
+ encoder,
+ scribbleStrip: {
+ trackTitle: surface.makeCustomValueVariable("Track Title"),
+ meterPeakLevel: surface.makeCustomValueVariable("Meter Peak Level"),
+ },
+ vuMeter: surface.makeCustomValueVariable("VU Meter"),
+ buttons: {
+ record: recordButton,
+ solo: soloButton,
+ mute: muteButton,
+ select: selectButton,
+ },
+
+ fader: new TouchSensitiveMotorFader(surface, currentChannelXPosition + 1, 24.5, 1.5, 11),
+ };
+ });
+}
+
+export const deviceConfig: DeviceConfig = {
+ colorManager: IconColorManager,
+ maximumMeterValue: 0xc,
+ hasIndividualScribbleStrips: true,
+ hasSecondaryScribbleStrips: true,
+
+ detectionUnits: [
+ {
+ main: (detectionPortPair) =>
+ detectionPortPair
+ .expectInputNameStartsWith("iCON V1-M")
+ .expectOutputNameStartsWith("iCON V1-M"),
+ extender: (detectionPortPair, extenderNumber) =>
+ detectionPortPair
+ .expectInputNameStartsWith(`iCON V1-X${extenderNumber}`)
+ .expectOutputNameStartsWith(`iCON V1-X${extenderNumber}`),
+ },
+ ],
+
+ createExtenderSurface(surface, x) {
+ const surfaceWidth = channelElementsWidth + deviceFramePaddingWidth * 2;
+
+ // Device frame
+ surface.makeBlindPanel(x, 0, surfaceWidth, surfaceHeight);
+
+ return {
+ width: surfaceWidth,
+ channelElements: makeChannelElements(surface, x),
+ };
+ },
+
+ createMainSurface(surface, x): MainDeviceSurface {
+ const surfaceWidth = channelElementsWidth + 19;
+
+ // Device frame
+ surface.makeBlindPanel(x, 0, surfaceWidth, surfaceHeight);
+
+ const channelElements = makeChannelElements(surface, x);
+ x += deviceFramePaddingWidth + channelElementsWidth;
+
+ // Main VU meters
+ surface.makeBlindPanel(x + 1.3, 1.25, 0.9, 2.5);
+ surface.makeBlindPanel(x + 2.3, 1.25, 0.9, 2.5);
+
+ // Time display
+ surface.makeBlindPanel(x + 4.75, 4.75, 10.25, 1.5);
+
+ // DAW and Function Layer buttons
+ createElements(8, (buttonIndex) => {
+ surface
+ .makeBlindPanel(x + 2 + buttonIndex * 1.65 + +(buttonIndex > 2) * 0.75, 9.125, 1.5, 1.5)
+ .setShapeCircle();
+ });
+
+ // Button matrix
+ const buttonMatrixControlLayerZone = surface.makeControlLayerZone("Touch Buttons");
+ const buttonMatrix = createElements(3, (layerIndex) => {
+ const controlLayer = buttonMatrixControlLayerZone.makeControlLayer(
+ "Layer " + (layerIndex < 3 ? layerIndex + 1 : "U" + (layerIndex - 3)),
+ );
+
+ return createElements(4, (row) =>
+ createElements(6, (column) =>
+ new LedButton(surface, {
+ position: [x + 1.25 + column * 2.5, 12.25 + row * 2, 2.5, 2],
+ }).setControlLayer(controlLayer),
+ ),
+ );
+ });
+
+ const lowerButtonMatrix = createElements(2, (row) =>
+ createElements(
+ 6,
+ (column) =>
+ new LedButton(surface, {
+ position: [x + 3.5 + column * 2.25, 22.75 + row * 1.75, 2.125, 1.5],
+ }),
+ ),
+ );
+
+ const transportButtons: LedButton[] = [];
+ let nextTransportButtonXPosition = x + 3.5;
+ for (const buttonWidth of [1.575, 1.575, 1.575, 2.005, 3.01, 3.01]) {
+ transportButtons.push(
+ new LedButton(surface, {
+ position: [nextTransportButtonXPosition, 22.75 + 2 * 1.75, buttonWidth, 1.5],
+ }),
+ );
+ nextTransportButtonXPosition += buttonWidth + 0.125;
+ }
+
+ return {
+ width: surfaceWidth,
+ channelElements,
+ controlSectionElements: {
+ mainFader: new TouchSensitiveMotorFader(surface, x + 1, 24.5, 1.5, 11),
+ mainVuMeters: {
+ left: surface.makeCustomValueVariable("Main VU Meter L"),
+ right: surface.makeCustomValueVariable("Main VU Meter R"),
+ },
+
+ jogWheel: new JogWheel(surface, x + 6.675, 29, 7, 7),
+
+ buttons: {
+ navigation: {
+ channel: { left: lowerButtonMatrix[0][0], right: lowerButtonMatrix[0][1] },
+ bank: { left: lowerButtonMatrix[0][2], right: lowerButtonMatrix[0][3] },
+ },
+ flip: lowerButtonMatrix[0][4],
+
+ display: buttonMatrix[0][0][4],
+ timeMode: buttonMatrix[0][0][5],
+ scrub: buttonMatrix[0][1][4],
+ edit: buttonMatrix[0][0][0],
+
+ encoderAssign: {
+ pan: buttonMatrix[1][0][0],
+ eq: buttonMatrix[1][1][0],
+ },
+
+ modify: {
+ undo: buttonMatrix[0][2][0],
+ redo: buttonMatrix[0][2][1],
+ save: buttonMatrix[0][3][0],
+ revert: buttonMatrix[0][3][1],
+ },
+
+ automation: {
+ read: lowerButtonMatrix[1][1],
+ write: lowerButtonMatrix[1][3],
+ motor: buttonMatrix[0][1][5],
+ mixer: buttonMatrix[0][0][2],
+ project: buttonMatrix[0][0][3],
+ },
+
+ utility: {
+ instrument: buttonMatrix[0][1][0],
+ main: buttonMatrix[0][1][1],
+ soloDefeat: buttonMatrix[0][2][2],
+ shift: buttonMatrix[0][3][5],
+ },
+
+ transport: {
+ rewind: transportButtons[0],
+ forward: transportButtons[1],
+ cycle: transportButtons[2],
+ stop: transportButtons[3],
+ play: transportButtons[4],
+ record: transportButtons[5],
+
+ punch: buttonMatrix[0][1][3],
+ markers: {
+ previous: buttonMatrix[0][2][3],
+ add: buttonMatrix[0][2][4],
+ next: buttonMatrix[0][2][5],
+ },
+ left: buttonMatrix[0][3][3],
+ right: buttonMatrix[0][3][4],
+ },
+ },
+
+ footSwitch1: surface.makeButton(x + 2.75 + 6 * 1.65, 1.25, 1.5, 1.5).setShapeCircle(),
+ footSwitch2: surface.makeButton(x + 2.75 + 7 * 1.65, 1.25, 1.5, 1.5).setShapeCircle(),
+ },
+
+ customElements: {
+ buttonMatrix,
+ },
+ };
+ },
+
+ enhanceMapping({ devices, page, lifecycleCallbacks }) {
+ const mainDevices = devices.filter(
+ (device) => device instanceof MainDevice,
+ ) as MainDevice[];
+
+ // Map remaining button matrix buttons for each main device
+ for (const device of mainDevices) {
+ const { ports } = device;
+ const buttonMatrix = device.customElements.buttonMatrix;
+
+ // Bind unbound buttons in Layers 1-3 to MIDI notes on channel 2
+ const channel2Buttons: LedButton[] = [];
+ for (const [layerId, layer] of buttonMatrix.entries()) {
+ for (const [rowId, row] of layer.entries()) {
+ for (const [columnId, button] of row.entries()) {
+ if (!button.isBoundToNote()) {
+ channel2Buttons.push(button);
+ button.bindToNote(ports, layerId * 24 + rowId * 6 + columnId, 1); // Channel 2
+ }
+ }
+ }
+ }
+
+ // Reset non-MCU (channel 2) buttons on (de)activation
+ const resetChannel2Buttons = (context: MR_ActiveDevice) => {
+ for (const button of channel2Buttons) {
+ button.sendNoteOn(context, 0);
+ }
+ };
+
+ lifecycleCallbacks.addActivationCallback(resetChannel2Buttons);
+ lifecycleCallbacks.addDeactivationCallback(resetChannel2Buttons);
+
+ // Host mappings
+ // Edit instrument
+ page
+ .makeValueBinding(
+ buttonMatrix[0][0][1].mSurfaceValue,
+ page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mInstrumentOpen,
+ )
+ .setTypeToggle();
+
+ // Reset meters
+ page.makeCommandBinding(buttonMatrix[0][1][2].mSurfaceValue, "Mixer", "Meters: Reset");
+
+ // Click
+ page
+ .makeValueBinding(
+ buttonMatrix[0][3][2].mSurfaceValue,
+ page.mHostAccess.mTransport.mValue.mMetronomeActive,
+ )
+ .setTypeToggle();
+ }
+ },
+
+ getSupplementaryShiftButtons(device: MainDevice) {
+ const buttonMatrix = device.customElements.buttonMatrix;
+ return [buttonMatrix[1][3][5], buttonMatrix[2][3][5]];
+ },
+
+ configureEncoderMappings(defaultEncoderMapping, page) {
+ const makeActivatorButtonSelector = (row: number, column: number) => (device: MainDevice) =>
+ (device as MainDevice).customElements.buttonMatrix[1][row][column];
+
+ const hostAccess = page.mHostAccess;
+ return [
+ // The default six MCU encoder assign button mappings are included for backwards compatibility
+ // with the default iMAP Cubase button functions:
+ ...defaultEncoderMapping,
+
+ // These are additional, fine-grained encoder mappings:
+
+ {
+ pages: [pageConfigs.monitor],
+ activatorButtonSelector: makeActivatorButtonSelector(0, 1),
+ },
+ {
+ pages: [pageConfigs.inputGain],
+ activatorButtonSelector: makeActivatorButtonSelector(0, 2),
+ },
+ {
+ pages: [pageConfigs.inputPhase],
+ activatorButtonSelector: makeActivatorButtonSelector(0, 3),
+ },
+ {
+ pages: [pageConfigs.lowCut],
+ activatorButtonSelector: makeActivatorButtonSelector(0, 4),
+ },
+ {
+ pages: [pageConfigs.highCut],
+ activatorButtonSelector: makeActivatorButtonSelector(0, 5),
+ },
+
+ {
+ pages: [pageConfigs.sends(hostAccess)],
+ activatorButtonSelector: makeActivatorButtonSelector(1, 1),
+ },
+ {
+ pages: pageConfigs.allAvailableCuePages,
+ activatorButtonSelector: makeActivatorButtonSelector(1, 2),
+ },
+ {
+ activatorButtonSelector: makeActivatorButtonSelector(1, 3),
+ pages: [pageConfigs.vstQuickControls(hostAccess)],
+ },
+ {
+ activatorButtonSelector: makeActivatorButtonSelector(1, 4),
+ pages: [pageConfigs.trackQuickControls(hostAccess)],
+ },
+ {
+ activatorButtonSelector: makeActivatorButtonSelector(1, 5),
+ pages: [pageConfigs.focusedQuickControls(hostAccess)],
+ },
+
+ // Strip effects
+ ...[
+ pageConfigs.stripEffectGate(hostAccess),
+ pageConfigs.stripEffectCompressor(hostAccess),
+ pageConfigs.stripEffectTools(hostAccess),
+ pageConfigs.stripEffectSaturator(hostAccess),
+ pageConfigs.stripEffectLimiter(hostAccess),
+ ].map((pageConfig, buttonColumn) => ({
+ pages: [pageConfig],
+ activatorButtonSelector: makeActivatorButtonSelector(2, buttonColumn),
+ })),
+
+ // Focused insert (with additional slot controls)
+ {
+ activatorButtonSelector: makeActivatorButtonSelector(3, 0),
+ pages: [
+ pageConfigs.focusedInsertEffect(
+ hostAccess,
+ (insertEffectViewer, _encoderPage, { page, mainDevices }) => {
+ insertEffectViewer.excludeEmptySlots();
+
+ for (const device of mainDevices as MainDevice[]) {
+ const buttonMatrix = device.customElements.buttonMatrix;
+
+ // "|< Slot"
+ page.makeActionBinding(
+ buttonMatrix[1][3][1].mSurfaceValue,
+ insertEffectViewer.mAction.mReset,
+ );
+
+ // "< Slot"
+ page.makeActionBinding(
+ buttonMatrix[1][3][2].mSurfaceValue,
+ insertEffectViewer.mAction.mPrev,
+ );
+
+ // "Open Insert"
+ page
+ .makeValueBinding(buttonMatrix[1][3][3].mSurfaceValue, insertEffectViewer.mEdit)
+ .setTypeToggle();
+
+ // "Slot >"
+ page.makeActionBinding(
+ buttonMatrix[1][3][4].mSurfaceValue,
+ insertEffectViewer.mAction.mNext,
+ );
+ }
+ },
+ ),
+ ],
+ },
+ ];
+ },
+};
diff --git a/src/device-configs/index.d.ts b/src/device-configs/index.d.ts
index 83fe251..b968841 100644
--- a/src/device-configs/index.d.ts
+++ b/src/device-configs/index.d.ts
@@ -1,4 +1,4 @@
-import { Except } from "type-fest";
+import { Class, Except, SetRequired } from "type-fest";
import { JogWheel } from "/decorators/surface-elements/JogWheel";
import { Lamp } from "/decorators/surface-elements/Lamp";
import { LedButton } from "/decorators/surface-elements/LedButton";
@@ -7,6 +7,7 @@ import { TouchSensitiveMotorFader } from "/decorators/surface-elements/TouchSens
import { Device, MainDevice } from "/devices";
import { EncoderMappingConfig } from "/mapping/encoders/EncoderMapper";
import { SegmentDisplayManager } from "/midi/managers/SegmentDisplayManager";
+import { ColorManager } from "/midi/managers/colors/ColorManager";
import { GlobalState } from "/state";
import { LifecycleCallbacks } from "/util";
@@ -42,8 +43,10 @@ export interface DeviceSurface {
channelElements: ChannelSurfaceElements[];
}
-export interface MainDeviceSurface extends DeviceSurface {
+export interface MainDeviceSurface = {}>
+ extends DeviceSurface {
controlSectionElements: PartialControlSectionSurfaceElements;
+ customElements?: MainDeviceCustomElements;
}
export interface ChannelSurfaceElements {
@@ -51,6 +54,12 @@ export interface ChannelSurfaceElements {
encoder: LedPushEncoder;
scribbleStrip: {
trackTitle: MR_SurfaceCustomValueVariable;
+
+ /**
+ * An optional custom value variable to show meter peak levels on devices with secondary
+ * scribble strips
+ **/
+ meterPeakLevel?: MR_SurfaceCustomValueVariable;
};
vuMeter: MR_SurfaceCustomValueVariable;
buttons: {
@@ -142,30 +151,52 @@ export interface PartialControlSectionButtons {
export type ControlSectionButtons = RequireAllElements;
+interface PartialDisplayLeds {
+ smpte?: Lamp;
+ beats?: Lamp;
+ solo?: Lamp;
+}
+
+type DispalyLeds = RequireAllElements;
+
export interface PartialControlSectionSurfaceElements {
mainFader: TouchSensitiveMotorFader;
+ mainVuMeters?: {
+ left: MR_SurfaceCustomValueVariable;
+ right: MR_SurfaceCustomValueVariable;
+ };
+
jogWheel: JogWheel;
buttons?: PartialControlSectionButtons;
- displayLeds?: {
- smpte?: Lamp;
- beats?: Lamp;
- solo?: Lamp;
- };
+ displayLeds?: PartialDisplayLEDs;
expressionPedal?: MR_Knob;
footSwitch1?: MR_Button;
footSwitch2?: MR_Button;
}
-export type ControlSectionSurfaceElements =
- RequireAllElements;
+export interface ControlSectionSurfaceElements
+ extends SetRequired<
+ PartialControlSectionSurfaceElements,
+ "expressionPedal" | "footSwitch1" | "footSwitch2"
+ > {
+ buttons: RequireAllElements;
+ displayLeds: RequireAllElements;
+}
-export type ControlSectionSurfaceElementsDefaultsFactory =
- DefaultElementsFactory;
+export type ControlSectionSurfaceElementsDefaultsFactory = DefaultElementsFactory<
+ Except
+>;
export interface DeviceConfig {
- channelColorSupport?: "behringer";
+ colorManager?: Class;
+
+ /**
+ * If the maximum meter value (sent on clip) should deviate from the default (0xe), specify it
+ * here.
+ */
+ maximumMeterValue?: number;
/**
* Whether the device has per-channel scribble strip displays, i.e. no display padding characters
@@ -175,6 +206,13 @@ export interface DeviceConfig {
*/
hasIndividualScribbleStrips?: boolean;
+ /**
+ * Whether the device has additional secondary scribble strip displays.
+ *
+ * @default false
+ */
+ hasSecondaryScribbleStrips?: boolean;
+
/**
* Whether all encoders shall be mapped in mouse value control mode. This option is intended for
* devices with only one physical channel.
@@ -227,10 +265,10 @@ export interface DeviceConfig {
getMouseValueModeButton?(device: MainDevice): LedButton;
/**
- * This optional function receives the default {@link EncoderMappingConfig} and returns an
- * `EncoderMappingConfig` that will be applied instead of the default.
+ * This optional function receives the default list of {@link EncoderMappingConfig}s and returns a
+ * list of `EncoderMappingConfig`s that will be applied instead of the default.
*
- * The default config is defined in {@link file://./../mapping/encoders/index.ts}
+ * The default mappings are defined in {@link file://./../mapping/encoders/index.ts}
*/
configureEncoderMappings?(
defaultEncoderMappings: EncoderMappingConfig[],
diff --git a/src/devices/Device.ts b/src/devices/Device.ts
index c0b2467..4332e24 100644
--- a/src/devices/Device.ts
+++ b/src/devices/Device.ts
@@ -1,7 +1,7 @@
import { ChannelSurfaceElements, DeviceSurface } from "../device-configs";
import { deviceConfig } from "/config";
import { MidiPortPair } from "/midi/MidiPortPair";
-import { ColorManager } from "/midi/managers/ColorManager";
+import { ColorManager } from "/midi/managers/colors/ColorManager";
import { LcdManager } from "/midi/managers/lcd";
import { GlobalState } from "/state";
import { TimerUtils } from "/util";
@@ -31,8 +31,8 @@ export abstract class Device {
this.ports = new MidiPortPair(driver, isExtender);
this.lcdManager = new LcdManager(this, globalState, timerUtils);
- if (deviceConfig.channelColorSupport === "behringer") {
- this.colorManager = new ColorManager(this);
+ if (deviceConfig.colorManager) {
+ this.colorManager = new deviceConfig.colorManager(this);
}
}
}
diff --git a/src/devices/MainDevice.ts b/src/devices/MainDevice.ts
index 8b5390c..ad7238b 100644
--- a/src/devices/MainDevice.ts
+++ b/src/devices/MainDevice.ts
@@ -10,8 +10,9 @@ import { LedButton } from "/decorators/surface-elements/LedButton";
import { GlobalState } from "/state";
import { TimerUtils, applyDefaultsFactory, createElements } from "/util";
-export class MainDevice extends Device {
+export class MainDevice = {}> extends Device {
controlSectionElements: ControlSectionSurfaceElements;
+ customElements: CustomElements;
constructor(
driver: MR_DeviceDriver,
@@ -28,6 +29,7 @@ export class MainDevice extends Device {
surface,
deviceSurface.controlSectionElements,
);
+ this.customElements = (deviceSurface.customElements ?? {}) as CustomElements;
}
/**
diff --git a/src/mapping/control.ts b/src/mapping/control.ts
index c6a1655..a637876 100644
--- a/src/mapping/control.ts
+++ b/src/mapping/control.ts
@@ -20,6 +20,7 @@ function setShiftableButtonsLedValues(
buttons.transport.rewind,
buttons.transport.forward,
buttons.navigation.bank.left,
+ buttons.display,
]) {
button.setLedValue(context, value);
}
diff --git a/src/mapping/encoders/EncoderMapper.ts b/src/mapping/encoders/EncoderMapper.ts
index a8610ab..ecbe1d6 100644
--- a/src/mapping/encoders/EncoderMapper.ts
+++ b/src/mapping/encoders/EncoderMapper.ts
@@ -1,4 +1,4 @@
-import { EncoderMappingDependencies, EncoderPage, EncoderPageConfig } from "./EncoderPage";
+import { EncoderMappingDependencies, EncoderPageConfig } from "./EncoderPage";
import { EncoderPageGroup } from "./EncoderPageGroup";
import { LedButton } from "/decorators/surface-elements/LedButton";
import { Device, MainDevice } from "/devices";
@@ -6,11 +6,10 @@ import { SegmentDisplayManager } from "/midi/managers/SegmentDisplayManager";
import { GlobalState } from "/state";
/**
- * The joint configuration for all "encoder assignments". Each encoder assignment maps a number of
- * encoder pages to a specified button. Each encoder page specifies host mappings ("assignments")
- * for an arbitrary number of encoders.
+ * The configuration object for an encoder mapping. An encoder mapping maps a number of encoder
+ * pages to a specified button.
*/
-export type EncoderMappingConfig = {
+export interface EncoderMappingConfig {
/**
* A function that – given a `MainDevice` – returns the device's button that will be mapped to the
* provided encoder pages.
@@ -18,7 +17,7 @@ export type EncoderMappingConfig = {
activatorButtonSelector: (device: MainDevice) => LedButton;
pages: EncoderPageConfig[];
-};
+}
export class EncoderMapper {
private readonly dependencies: EncoderMappingDependencies;
diff --git a/src/mapping/encoders/EncoderPage.ts b/src/mapping/encoders/EncoderPage.ts
index 5c7c42c..6e73aea 100644
--- a/src/mapping/encoders/EncoderPage.ts
+++ b/src/mapping/encoders/EncoderPage.ts
@@ -50,6 +50,10 @@ export type EncoderAssignmentConfigs =
| EncoderAssignmentConfig[]
| ((channel: MR_MixerBankChannel, channelIndex: number) => EncoderAssignmentConfig);
+/**
+ * An encoder page specifies host mappings ("encoder assignments") for an arbitrary number of
+ * encoders.
+ */
export interface EncoderPageConfig {
name: string;
assignments: EncoderAssignmentConfigs;
diff --git a/src/mapping/encoders/index.ts b/src/mapping/encoders/index.ts
index 2ed6a5d..8baed4c 100644
--- a/src/mapping/encoders/index.ts
+++ b/src/mapping/encoders/index.ts
@@ -54,7 +54,7 @@ export function bindEncoders(
// Send
{
activatorButtonSelector: (device) => selectAssignButtons(device).send,
- pages: [pageConfigs.sends(hostAccess), ...pageConfigs.allAvailableCueSlotPages],
+ pages: [pageConfigs.sends(hostAccess), ...pageConfigs.allAvailableCuePages],
},
// Plug-In
diff --git a/src/mapping/encoders/page-configs.ts b/src/mapping/encoders/page-configs.ts
index 7ee5931..64ba3ff 100644
--- a/src/mapping/encoders/page-configs.ts
+++ b/src/mapping/encoders/page-configs.ts
@@ -1,5 +1,10 @@
import { mDefaults } from "midiremote_api_v1";
-import { EncoderAssignmentConfig, EncoderPageConfig } from "./EncoderPage";
+import {
+ EncoderAssignmentConfig,
+ EncoderMappingDependencies,
+ EncoderPage,
+ EncoderPageConfig,
+} from "./EncoderPage";
import { config } from "/config";
import { EncoderDisplayMode, LedPushEncoder } from "/decorators/surface-elements/LedPushEncoder";
import { createElements } from "/util";
@@ -285,7 +290,14 @@ export const stripEffectSaturator = (hostAccess: MR_HostAccess) =>
export const stripEffectLimiter = (hostAccess: MR_HostAccess) =>
makeStripEffectEncoderPageConfig("Limiter", getStripEffectAssignments(hostAccess)["limiter"]);
-export const focusedInsertEffect = (hostAccess: MR_HostAccess): EncoderPageConfig => {
+export const focusedInsertEffect = (
+ hostAccess: MR_HostAccess,
+ enhanceMapping?: (
+ insertEffectViewer: MR_HostInsertEffectViewer,
+ encoderPage: EncoderPage,
+ mappingDependencies: EncoderMappingDependencies,
+ ) => void,
+): EncoderPageConfig => {
const insertEffectsViewer = hostAccess.mTrackSelection.mMixerChannel.mInsertAndStripEffects
.makeInsertEffectViewer("Inserts")
.followPluginWindowInFocus();
@@ -303,7 +315,8 @@ export const focusedInsertEffect = (hostAccess: MR_HostAccess): EncoderPageConfi
},
areAssignmentsChannelRelated: false,
- enhanceMapping(encoderPage, pageGroup, { page, mainDevices, globalState }) {
+ enhanceMapping(encoderPage, pageGroup, mappingDependencies) {
+ const { page, mainDevices, globalState } = mappingDependencies;
const subPages = encoderPage.subPages;
const actions = parameterBankZone.mAction;
@@ -336,6 +349,10 @@ export const focusedInsertEffect = (hostAccess: MR_HostAccess): EncoderPageConfi
}
});
}
+
+ if (enhanceMapping) {
+ enhanceMapping(insertEffectsViewer, encoderPage, mappingDependencies);
+ }
},
};
};
@@ -369,7 +386,7 @@ export const sendSlot = (slotId: number): EncoderPageConfig => ({
areAssignmentsChannelRelated: true,
});
-export const cueSlot = (slotId: number): EncoderPageConfig => ({
+export const cue = (slotId: number): EncoderPageConfig => ({
name: "Cue",
assignments: (channel) => {
const cueSlot = channel.mCueSends.getByIndex(slotId);
@@ -385,7 +402,4 @@ export const cueSlot = (slotId: number): EncoderPageConfig => ({
areAssignmentsChannelRelated: true,
});
-export const allAvailableCueSlotPages = createElements(
- mDefaults.getMaxControlRoomCueChannels(),
- cueSlot,
-);
+export const allAvailableCuePages = createElements(mDefaults.getMaxControlRoomCueChannels(), cue);
diff --git a/src/mapping/index.ts b/src/mapping/index.ts
index 0a6bcbc..b02a85b 100755
--- a/src/mapping/index.ts
+++ b/src/mapping/index.ts
@@ -61,26 +61,43 @@ export function makeHostMapping(
// Fader
page.makeValueBinding(channelElements.fader.mSurfaceValue, channel.mValue.mVolume);
+
+ // Peak level display
+ if (channelElements.scribbleStrip.meterPeakLevel) {
+ page.makeValueBinding(
+ channelElements.scribbleStrip.meterPeakLevel,
+ channel.mValue.mVUMeterPeak,
+ );
+ }
}
return channel;
});
+ const mainChannel = page.mHostAccess.mMixConsole
+ .makeMixerBankZone()
+ .includeOutputChannels()
+ .makeMixerBankChannel();
+
for (const device of devices) {
if (device instanceof MainDevice) {
const controlSectionElements = device.controlSectionElements;
- // Main fader
+ // Main Fader
page.makeValueBinding(
controlSectionElements.mainFader.mSurfaceValue,
config.mapMainFaderToControlRoom
? page.mHostAccess.mControlRoom.mMainChannel.mLevelValue
- : page.mHostAccess.mMixConsole
- .makeMixerBankZone()
- .includeOutputChannels()
- .makeMixerBankChannel().mValue.mVolume,
+ : mainChannel.mValue.mVolume,
);
+ // Main VU Meters
+ const mainVuMeters = device.controlSectionElements.mainVuMeters;
+ if (mainVuMeters) {
+ page.makeValueBinding(mainVuMeters.left, mainChannel.mValue.mVUMeter);
+ page.makeValueBinding(mainVuMeters.right, mainChannel.mValue.mVUMeter);
+ }
+
// Display buttons, 1-8, Modify, Automation, Utility, Transport, Navigation, Jog wheel
bindControlSection(page, device, mixerBankZone, globalState);
}
diff --git a/src/midi/index.ts b/src/midi/index.ts
index 1f933db..471a055 100644
--- a/src/midi/index.ts
+++ b/src/midi/index.ts
@@ -1,4 +1,4 @@
-import { RgbColor } from "./managers/ColorManager";
+import { RgbColor } from "./managers/colors/ColorManager";
import { SegmentDisplayManager } from "./managers/SegmentDisplayManager";
import { sendChannelMeterMode, sendGlobalMeterModeOrientation, sendMeterLevel } from "./util";
import { config, deviceConfig } from "/config";
@@ -72,9 +72,10 @@ function bindVuMeter(
vuMeter: MR_SurfaceCustomValueVariable,
outputPort: MidiOutputPort,
meterId: number,
+ midiChannel = 0,
) {
const sendLevel = (context: MR_ActiveDevice, level: number) => {
- sendMeterLevel(context, outputPort, meterId, level);
+ sendMeterLevel(context, outputPort, meterId, level, midiChannel);
};
let isMeterUnassigned = false;
@@ -82,7 +83,9 @@ function bindVuMeter(
if (!isMeterUnassigned || newValue === 0) {
// Apply a log scale twice to make the meters look more like Cubase's MixConsole meters
const meterLevel = Math.ceil(
- (1 + Math.log10(0.1 + 0.9 * (1 + Math.log10(0.1 + 0.9 * newValue)))) * 0xe - 0.25,
+ (1 + Math.log10(0.1 + 0.9 * (1 + Math.log10(0.1 + 0.9 * newValue)))) *
+ (deviceConfig.maximumMeterValue ?? 0xe) -
+ 0.25,
);
sendLevel(context, meterLevel);
@@ -106,8 +109,8 @@ function bindChannelElements(device: Device, globalState: GlobalState) {
// Push Encoder
channel.encoder.bindToMidi(ports, channelIndex);
- // Display colors – only supported by the X-Touch
- if (deviceConfig.channelColorSupport === "behringer") {
+ // Display colors
+ if (deviceConfig.colorManager) {
const encoderColor = new ContextVariable({ isAssigned: false, r: 0, g: 0, b: 0 });
channel.encoder.mEncoderValue.mOnColorChange = (context, r, g, b, _a, isAssigned) => {
encoderColor.set(context, { isAssigned, r, g, b });
@@ -174,6 +177,24 @@ function bindChannelElements(device: Device, globalState: GlobalState) {
setIsMeterUnassigned(context, title2 === "");
};
+ if (deviceConfig.hasSecondaryScribbleStrips && channel.scribbleStrip.meterPeakLevel) {
+ channel.scribbleStrip.meterPeakLevel.mOnDisplayValueChange = (context, value) => {
+ channelTextManager.onMeterPeakLevelChange(context, value);
+ };
+
+ channel.fader.mSurfaceValue.mOnDisplayValueChange = (context, value) => {
+ channelTextManager.onFaderParameterValueChange(context, value);
+ };
+
+ channel.fader.onTitleChangeCallbacks.addCallback((context, _title, parameterName) => {
+ channelTextManager.onFaderParameterNameChange(context, parameterName);
+ });
+
+ channel.fader.onTouchedValueChangeCallbacks.addCallback((context, isFaderTouched) => {
+ channelTextManager.onFaderTouchedChange(context, Boolean(isFaderTouched));
+ });
+ }
+
/** Clears the channel meter's overload indicator */
const clearOverload = (context: MR_ActiveDevice) => {
sendMeterLevel(context, ports.output, channelIndex, 0xf);
@@ -306,4 +327,10 @@ function bindControlSectionElements(device: MainDevice, globalState: GlobalState
.setInputPort(ports.input)
.bindToControlChange(0, 0x2e)
.setTypeAbsolute();
+
+ // Main VU Meters
+ if (elements.mainVuMeters) {
+ bindVuMeter(elements.mainVuMeters.left, ports.output, 0, 1);
+ bindVuMeter(elements.mainVuMeters.right, ports.output, 1, 1);
+ }
}
diff --git a/src/midi/managers/ColorManager.ts b/src/midi/managers/colors/BehringerColorManager.ts
similarity index 79%
rename from src/midi/managers/ColorManager.ts
rename to src/midi/managers/colors/BehringerColorManager.ts
index d86be20..92ef0db 100644
--- a/src/midi/managers/ColorManager.ts
+++ b/src/midi/managers/colors/BehringerColorManager.ts
@@ -1,8 +1,9 @@
import { closest as determineClosestColor } from "color-diff";
+import { ColorManager, RgbColor } from "./ColorManager";
import { Device } from "/devices";
import { ContextVariable, createElements } from "/util";
-export enum ScribbleStripColor {
+enum ScribbleStripColor {
black = 0x00,
red = 0x01,
green = 0x02,
@@ -13,8 +14,6 @@ export enum ScribbleStripColor {
white = 0x07,
}
-export type RgbColor = { r: number; g: number; b: number };
-
type DeviceColorDefinition = { R: number; G: number; B: number; code: ScribbleStripColor };
const scribbleStripColorsRGB: DeviceColorDefinition[] = [
@@ -28,7 +27,7 @@ const scribbleStripColorsRGB: DeviceColorDefinition[] = [
{ code: ScribbleStripColor.white, R: 0xcc, G: 0xcc, B: 0xcc },
];
-export class ColorManager {
+export class BehringerColorManager implements ColorManager {
private static rgbToScribbleStripColor({ r, g, b }: RgbColor) {
return (
determineClosestColor(
@@ -44,10 +43,6 @@ export class ColorManager {
this.colors = createElements(8, () => new ContextVariable(ScribbleStripColor.black));
}
- /**
- * Sends all colors to the device in a SysEx message. Unless on initialization, you don't need to
- * call this method because it is automatically done by `setChannelColor()`.
- */
sendColors(context: MR_ActiveDevice) {
this.device.ports.output.sendSysex(context, [
0x72,
@@ -56,7 +51,11 @@ export class ColorManager {
]);
}
- setChannelColor(context: MR_ActiveDevice, channelIndex: number, color: ScribbleStripColor) {
+ private setChannelColor(
+ context: MR_ActiveDevice,
+ channelIndex: number,
+ color: ScribbleStripColor,
+ ) {
const colorVariable = this.colors[channelIndex];
if (colorVariable.get(context) !== color) {
colorVariable.set(context, color);
@@ -65,7 +64,11 @@ export class ColorManager {
}
setChannelColorRgb(context: MR_ActiveDevice, channelIndex: number, color: RgbColor) {
- this.setChannelColor(context, channelIndex, ColorManager.rgbToScribbleStripColor(color));
+ this.setChannelColor(
+ context,
+ channelIndex,
+ BehringerColorManager.rgbToScribbleStripColor(color),
+ );
}
resetColors(context: MR_ActiveDevice) {
diff --git a/src/midi/managers/colors/ColorManager.d.ts b/src/midi/managers/colors/ColorManager.d.ts
new file mode 100644
index 0000000..4820b9c
--- /dev/null
+++ b/src/midi/managers/colors/ColorManager.d.ts
@@ -0,0 +1,13 @@
+export type RgbColor = { r: number; g: number; b: number };
+
+export interface ColorManager {
+ /**
+ * Sends all colors to the device in a SysEx message. Unless on initialization, you don't need to
+ * call this method because it is automatically done by `setChannelColorRgb()`.
+ */
+ sendColors(context: MR_ActiveDevice): void;
+
+ setChannelColorRgb(context: MR_ActiveDevice, channelIndex: number, color: RgbColor): void;
+
+ resetColors(context: MR_ActiveDevice): void;
+}
diff --git a/src/midi/managers/colors/IconColorManager.ts b/src/midi/managers/colors/IconColorManager.ts
new file mode 100644
index 0000000..0d4bd8c
--- /dev/null
+++ b/src/midi/managers/colors/IconColorManager.ts
@@ -0,0 +1,44 @@
+import { ColorManager, RgbColor } from "./ColorManager";
+import { Device } from "/devices";
+import { ContextVariable, createElements } from "/util";
+
+export class IconColorManager implements ColorManager {
+ private colors: Array>;
+
+ constructor(private device: Device) {
+ this.colors = createElements(8, () => new ContextVariable({ r: 0, g: 0, b: 0 }));
+ }
+
+ sendColors(context: MR_ActiveDevice) {
+ this.device.ports.output.sendMidi(context, [
+ 0xf0,
+ 0x00,
+ 0x02,
+ 0x4e,
+ 0x16,
+ 0x14,
+ ...this.colors.flatMap((color) => {
+ const { r, g, b } = color.get(context);
+ return [Math.round(r * 127), Math.round(g * 127), Math.round(b * 127)];
+ }),
+ 0xf7,
+ ]);
+ }
+
+ setChannelColorRgb(context: MR_ActiveDevice, channelIndex: number, color: RgbColor) {
+ const colorVariable = this.colors[channelIndex];
+ const previousColor = colorVariable.get(context);
+
+ if (previousColor.r !== color.r || previousColor.g !== color.g || previousColor.b !== color.b) {
+ colorVariable.set(context, color);
+ this.sendColors(context);
+ }
+ }
+
+ resetColors(context: MR_ActiveDevice) {
+ for (const color of this.colors) {
+ color.set(context, { r: 0, g: 0, b: 0 });
+ }
+ this.sendColors(context);
+ }
+}
diff --git a/src/midi/managers/lcd/ChannelTextManager.ts b/src/midi/managers/lcd/ChannelTextManager.ts
index a8aeebb..942ab49 100644
--- a/src/midi/managers/lcd/ChannelTextManager.ts
+++ b/src/midi/managers/lcd/ChannelTextManager.ts
@@ -36,15 +36,12 @@ export class ChannelTextManager {
* Given a <= `ChannelTextManager.channelWidth` characters long string, returns a left-padded
* version of it that appears centered on an `ChannelTextManager.channelWidth`-character display.
*/
- private static centerString(input: string) {
- if (input.length >= ChannelTextManager.channelWidth) {
+ private static centerString(input: string, width = ChannelTextManager.channelWidth) {
+ if (input.length >= width) {
return input;
}
- return (
- LcdManager.makeSpaces(Math.floor((ChannelTextManager.channelWidth - input.length) / 2)) +
- input
- );
+ return LcdManager.makeSpaces(Math.floor((width - input.length) / 2)) + input;
}
/**
@@ -147,6 +144,12 @@ export class ChannelTextManager {
private channelName = new ContextVariable("");
+ private meterPeakLevel = new ContextVariable("");
+ private faderParameterValue = new ContextVariable("");
+ private faderParameterName = new ContextVariable("");
+ private isFaderTouched = new ContextVariable(false);
+ private isFaderParameterDisplayed = new ContextVariable(false);
+
/** Whether the parameter controlled by the channel's encoder belongs to that channel */
public isParameterChannelRelated = true;
@@ -162,6 +165,12 @@ export class ChannelTextManager {
globalState.areDisplayRowsFlipped.addOnChangeCallback(this.updateTrackTitleDisplay.bind(this));
globalState.selectedTrackName.addOnChangeCallback(this.onSelectedTrackChange.bind(this));
+ if (deviceConfig.hasSecondaryScribbleStrips) {
+ globalState.isShiftModeActive.addOnChangeCallback(
+ this.updateIsFaderParameterDisplayed.bind(this),
+ );
+ }
+
if (DEVICE_NAME === "MCU Pro") {
// Handle metering mode changes
globalState.isGlobalLcdMeterModeVertical.addOnChangeCallback(
@@ -250,6 +259,57 @@ export class ChannelTextManager {
}
this.sendText(context, row, this.channelName.get(context));
+ this.updateSecondaryTrackTitleDisplay(context);
+ }
+
+ /**
+ * Updates the track title displayed on the first row of the channel's secondary display, if the
+ * device has secondary displays.
+ */
+ private updateSecondaryTrackTitleDisplay(context: MR_ActiveDevice) {
+ if (deviceConfig.hasSecondaryScribbleStrips) {
+ this.sendText(
+ context,
+ 2,
+ ChannelTextManager.centerString(
+ this.isFaderParameterDisplayed.get(context)
+ ? this.faderParameterName.get(context)
+ : this.channelName.get(context),
+ ),
+ );
+ }
+ }
+
+ private updateIsFaderParameterDisplayed(context: MR_ActiveDevice) {
+ const previousValue = this.isFaderParameterDisplayed.get(context);
+ const newValue =
+ this.isFaderTouched.get(context) && !this.globalState.isShiftModeActive.get(context);
+
+ if (newValue !== previousValue) {
+ this.isFaderParameterDisplayed.set(context, newValue);
+ this.updateSecondaryTrackTitleDisplay(context);
+ this.updateSupplementaryInfo(context);
+ }
+ }
+
+ /**
+ * Updates the string displayed on the second row of the channel's secondary display, if the
+ * device has secondary displays.
+ */
+ private updateSupplementaryInfo(context: MR_ActiveDevice) {
+ if (deviceConfig.hasSecondaryScribbleStrips) {
+ this.sendText(
+ context,
+ 3,
+ ChannelTextManager.centerString(
+ ChannelTextManager.abbreviateString(
+ this.isFaderParameterDisplayed.get(context)
+ ? this.faderParameterValue.get(context)
+ : this.meterPeakLevel.get(context),
+ ),
+ ),
+ );
+ }
}
setParameterNameBuilder(builder?: EncoderParameterNameBuilder) {
@@ -353,8 +413,41 @@ export class ChannelTextManager {
}
}
+ /** This callback is not called externally, but only from within this class */
onParameterChange(context: MR_ActiveDevice) {
this.lastParameterChangeTime = performance.now();
this.disableLocalValueDisplayMode(context);
}
+
+ onMeterPeakLevelChange(context: MR_ActiveDevice, level: string) {
+ this.meterPeakLevel.set(context, level);
+ if (!this.isFaderParameterDisplayed.get(context)) {
+ this.updateSupplementaryInfo(context);
+ }
+ }
+
+ onFaderParameterValueChange(context: MR_ActiveDevice, value: string) {
+ this.faderParameterValue.set(context, ChannelTextManager.stripNonAsciiCharacters(value));
+ if (this.isFaderParameterDisplayed.get(context)) {
+ this.updateSupplementaryInfo(context);
+ }
+ }
+
+ onFaderParameterNameChange(context: MR_ActiveDevice, name: string) {
+ this.faderParameterName.set(
+ context,
+ ChannelTextManager.abbreviateString(
+ ChannelTextManager.stripNonAsciiCharacters(ChannelTextManager.translateParameterName(name)),
+ ),
+ );
+
+ if (this.isFaderParameterDisplayed.get(context)) {
+ this.updateSecondaryTrackTitleDisplay(context);
+ }
+ }
+
+ onFaderTouchedChange(context: MR_ActiveDevice, isFaderTouched: boolean) {
+ this.isFaderTouched.set(context, isFaderTouched);
+ this.updateIsFaderParameterDisplayed(context);
+ }
}
diff --git a/src/midi/managers/lcd/LcdManager.ts b/src/midi/managers/lcd/LcdManager.ts
index 73cf7c3..6272d96 100644
--- a/src/midi/managers/lcd/LcdManager.ts
+++ b/src/midi/managers/lcd/LcdManager.ts
@@ -1,4 +1,5 @@
import { ChannelTextManager } from "./ChannelTextManager";
+import { deviceConfig } from "/config";
import { Device } from "/devices";
import { GlobalState } from "/state";
import { TimerUtils, createElements } from "/util";
@@ -34,9 +35,29 @@ export class LcdManager {
);
}
- private sendText(context: MR_ActiveDevice, startIndex: number, text: string) {
+ private sendText(
+ context: MR_ActiveDevice,
+ startIndex: number,
+ text: string,
+ targetSecondaryDisplay = false,
+ ) {
const chars = LcdManager.asciiStringToCharArray(text.slice(0, 112));
- this.device.ports.output.sendSysex(context, [0x12, startIndex, ...chars]);
+
+ if (targetSecondaryDisplay) {
+ this.device.ports.output.sendMidi(context, [
+ 0xf0,
+ 0x00,
+ 0x02,
+ 0x4e,
+ 0x15,
+ 0x13,
+ startIndex,
+ ...chars,
+ 0xf7,
+ ]);
+ } else {
+ this.device.ports.output.sendSysex(context, [0x12, startIndex, ...chars]);
+ }
}
private sendChannelText(
@@ -48,10 +69,22 @@ export class LcdManager {
while (text.length < 7) {
text += " ";
}
- this.sendText(context, row * 56 + (channelIndex % 8) * 7, text);
+
+ let isSecondaryDisplayRow = false;
+ if (row > 1) {
+ isSecondaryDisplayRow = true;
+ row -= 2;
+ }
+
+ this.sendText(context, row * 56 + (channelIndex % 8) * 7, text, isSecondaryDisplayRow);
}
clearDisplays(context: MR_ActiveDevice) {
- this.sendText(context, 0, LcdManager.makeSpaces(112));
+ const spaces = LcdManager.makeSpaces(112);
+ this.sendText(context, 0, spaces);
+
+ if (deviceConfig.hasSecondaryScribbleStrips) {
+ this.sendText(context, 0, spaces, true);
+ }
}
}
diff --git a/src/midi/util.ts b/src/midi/util.ts
index d60a2f1..5a5b1b5 100644
--- a/src/midi/util.ts
+++ b/src/midi/util.ts
@@ -33,6 +33,7 @@ export function sendMeterLevel(
outputPort: MidiOutputPort,
channelIndex: number,
meterLevel: number,
+ midiChannel = 0,
) {
- outputPort.sendMidi(context, [0xd0, (channelIndex << 4) + meterLevel]);
+ outputPort.sendMidi(context, [0xd0 + midiChannel, (channelIndex << 4) + meterLevel]);
}