Skip to content

Commit

Permalink
feat: Add an iCON V1-M script variant (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
bjoluc authored Jan 4, 2025
1 parent ab7e7a2 commit 278b689
Show file tree
Hide file tree
Showing 25 changed files with 1,083 additions and 87 deletions.
230 changes: 230 additions & 0 deletions assets/mcu-midiremote.v1m-daw

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ The following devices are explicitly supported:
- iCON:
- Platform M+ / X+ <sup>\*</sup>
- QCon Pro G2 / QCon EX G2
- V1-M / V1-X <sup>\*</sup>
- Mackie Control Universal (Pro) / XT (Pro)
- SSL UF1 <sup>\*</sup>

Expand Down Expand Up @@ -209,6 +210,25 @@ Current limitations of the MIDI Remote API:

</details>

<details>
<summary>iCON V1-M / V1-X</summary>

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!

</details>

<details>
<summary>SSL UF1</summary>

Expand Down
5 changes: 2 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",

Expand Down
9 changes: 7 additions & 2 deletions src/decorators/MidiOutputPort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
};
}

Expand Down
38 changes: 28 additions & 10 deletions src/decorators/surface-elements/LedButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class LedButtonDecorator {
private ledValue = new ContextVariable(0);

private ports?: MidiPortPair;
private channelNumber?: number;
private note?: number;

constructor(
Expand All @@ -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),
Expand All @@ -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);
};
}

/**
Expand Down
20 changes: 15 additions & 5 deletions src/decorators/surface-elements/TouchSensitiveFader.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
};
});
};
}

Expand Down
3 changes: 2 additions & 1 deletion src/device-configs/behringer_x-touch-one.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
3 changes: 2 additions & 1 deletion src/device-configs/behringer_xtouch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -78,7 +79,7 @@ const extenderPortPairConfigurator = (
};

export const deviceConfig: DeviceConfig = {
channelColorSupport: "behringer",
colorManager: BehringerColorManager,
hasIndividualScribbleStrips: true,
detectionUnits: [
{
Expand Down
Loading

0 comments on commit 278b689

Please sign in to comment.