Skip to content

Commit

Permalink
feat: Support arbitrary device combinations – thanks @lamusician and @…
Browse files Browse the repository at this point in the history
…limageurpublic for testing!
  • Loading branch information
bjoluc committed Mar 2, 2023
1 parent 132aefb commit db82022
Show file tree
Hide file tree
Showing 19 changed files with 685 additions and 545 deletions.
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Cubase 12 MIDI Remote Script for the Behringer X-Touch / X-Touch Extender
## TL;DR

This Cubase MIDI Remote Script replaces the default Mackie Control device setup and is tailored specifically to the Behringer X-Touch.
It can be [set up](#setup) with a standalone X-Touch or with an X-Touch and an additional X-Touch Extender unit.
It can be [set up](#setup) with a standalone X-Touch or with an arbitrary combination of X-Touch and X-Touch Extender units.
Notable features include:

- Track-colored scribble strips
Expand Down
118 changes: 118 additions & 0 deletions src/Devices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { config } from "./config";
import { DecoratedDeviceSurface } from "./decorators/surface";
import { ColorManager } from "./midi/managers/ColorManager";
import { LcdManager } from "./midi/managers/LcdManager";

import { makePortPair, PortPair } from "./midi/PortPair";
import {
channelElementsWidth,
ChannelSurfaceElements,
controlSectionElementsWidth,
ControlSectionSurfaceElements,
createChannelSurfaceElements,
createControlSectionSurfaceElements,
surfaceHeight,
} from "./surface";

interface DeviceProperties {
driver: MR_DeviceDriver;
surface: DecoratedDeviceSurface;
firstChannelIndex: number;
surfaceXPosition: number;
}

/**
* A `Device` represents a physical device and manages its MIDI ports and surface elements
*/
export abstract class Device {
ports: PortPair;
colorManager: ColorManager;
lcdManager: LcdManager;

readonly firstChannelIndex: number;
channelElements: ChannelSurfaceElements;

constructor(
{ driver, firstChannelIndex, surface, surfaceXPosition }: DeviceProperties,
isExtender: boolean,
panelWidth: number
) {
this.firstChannelIndex = firstChannelIndex;

this.ports = makePortPair(driver, isExtender);
this.colorManager = new ColorManager(this);
this.lcdManager = new LcdManager(this);

// Draw device frame
surface.makeBlindPanel(surfaceXPosition, 0, panelWidth, surfaceHeight);

this.channelElements = createChannelSurfaceElements(surface, surfaceXPosition);
}
}

export class MainDevice extends Device {
static readonly surfaceWidth = channelElementsWidth + controlSectionElementsWidth;

controlSectionElements: ControlSectionSurfaceElements;

constructor(properties: DeviceProperties) {
super(properties, false, MainDevice.surfaceWidth);

this.controlSectionElements = createControlSectionSurfaceElements(
properties.surface,
properties.surfaceXPosition + channelElementsWidth
);
}
}

export class ExtenderDevice extends Device {
static readonly surfaceWidth = channelElementsWidth + 1;

constructor(properties: DeviceProperties) {
super(properties, true, ExtenderDevice.surfaceWidth);
}
}

export class Devices {
private devices: Device[] = [];

constructor(driver: MR_DeviceDriver, surface: DecoratedDeviceSurface) {
const deviceClasses = config.devices.map((deviceType) =>
deviceType === "main" ? MainDevice : ExtenderDevice
);

let nextDeviceXPosition = 0;

this.devices.push(
...deviceClasses.map((deviceClass, deviceIndex) => {
const device = new deviceClass({
firstChannelIndex: deviceIndex * 8,
driver,
surface,
surfaceXPosition: nextDeviceXPosition,
});

nextDeviceXPosition += deviceClass.surfaceWidth;

return device;
})
);

if (this.devices.length === 1) {
driver
.makeDetectionUnit()
.detectPortPair(this.devices[0].ports.input, this.devices[0].ports.output)
.expectInputNameEquals("X-Touch")
.expectOutputNameEquals("X-Touch");
}
}

getDeviceByChannelIndex(channelIndex: number) {
return this.devices[Math.floor(channelIndex / 8)];
}

forEach = this.devices.forEach.bind(this.devices);
map = this.devices.map.bind(this.devices);
flatMap = this.devices.flatMap.bind(this.devices);
filter = this.devices.filter.bind(this.devices);
}
13 changes: 9 additions & 4 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ export const config = CONFIGURATION as ScriptConfiguration;
*/
var CONFIGURATION = {
/**
* If you have an extender unit, change this to either `["extender", "main"]` (if your extender is
* placed on the left side of the main unit) or `["main", "extender"]` (if the extender is on the
* right side).
* If you have an extender unit, change this option to either `["extender", "main"]` (if your
* extender is placed on the left side of the main unit) or `["main", "extender"]` (if the
* extender is on the right side).
*
* Do you have more than one extender? Let me know and I'll add support for multiple extenders!
* You can also specify an arbitrary combination of "main" and "extender" devices here, including
* multiple X-Touch ("main") and multiple X-Touch Extender ("extender") devices. The order of the
* list below should match the order of the devices on your desk from left to right. The port
* setup in the "Add MIDI Controller Surface" dialog reflects this order for input and output
* ports, i.e., the first input and the first output port belong to the leftmost device while the
* last input and the last output port belong to the rightmost device.
*/
devices: ["main"],

Expand Down
2 changes: 1 addition & 1 deletion src/decorators/surface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EnhancedMidiOutput, PortPair } from "../midi/MidiPorts";
import { EnhancedMidiOutput, PortPair } from "../midi/PortPair";
import { CallbackCollection, ContextStateVariable, makeCallbackCollection } from "../util";

export interface LedButton extends MR_Button {
Expand Down
46 changes: 24 additions & 22 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,28 @@ import "core-js/es/array/flat-map";
import "core-js/es/string/pad-start";
import "core-js/es/string/replace-all";
import "core-js/es/object/entries";
import "core-js/es/reflect/construct";

// @ts-ignore Workaround because the core-js polyfill doesn't play nice with SWC:
Reflect.get = undefined;

import midiremoteApi from "midiremote_api_v1";
import { decoratePage } from "./decorators/page";
import { decorateSurface } from "./decorators/surface";
import { makeHostMapping } from "./mapping";
import { bindSurfaceElementsToMidi } from "./midi";
import { setupDeviceConnection } from "./midi/connection";
import { MidiPorts } from "./midi/MidiPorts";
import { createSurfaceElements } from "./surface";
import { makeTimerUtils } from "./util";
import { Devices } from "./Devices";
import { bindDeviceToMidi, makeGlobalBooleanVariables } from "./midi";
import { makeHostMapping } from "./mapping";

const driver = midiremoteApi.makeDeviceDriver("Behringer", "X-Touch", "github.com/bjoluc");

const ports = new MidiPorts(driver);
const firstMainDeviceChannelIndex = ports.getMainPorts().firstChannelIndex;
const surface = decorateSurface(driver.mSurface);

// Create devices, i.e., midi ports and surface elements for each physical device
const devices = new Devices(driver, surface);

const { activationCallbacks, midiManagers } = setupDeviceConnection(driver, ports);
const { activationCallbacks, segmentDisplayManager } = setupDeviceConnection(driver, devices);
activationCallbacks.addCallback(() => {
// @ts-expect-error The script version is filled in by esbuild
console.log("Activating cubase-xtouch-midiremote v" + SCRIPT_VERSION);
Expand All @@ -31,24 +36,21 @@ activationCallbacks.addCallback(() => {
);
});

//-----------------------------------------------------------------------------
// 2. SURFACE LAYOUT - create control elements and midi bindings
//-----------------------------------------------------------------------------
const globalBooleanVariables = makeGlobalBooleanVariables(surface);

const surface = decorateSurface(driver.mSurface);
const elements = createSurfaceElements(
surface,
ports.getChannelCount(),
firstMainDeviceChannelIndex
);
activationCallbacks.addCallback((context) => {
// Setting `runCallbacksInstantly` to `true` below is a workaround for
// https://forums.steinberg.net/t/831123.
globalBooleanVariables.areMotorsActive.set(context, true, true);
});

const page = decoratePage(driver.mMapping.makePage("Mixer"), surface);
const timerUtils = makeTimerUtils(page, surface);

bindSurfaceElementsToMidi(elements, ports, midiManagers, activationCallbacks, timerUtils);

//-----------------------------------------------------------------------------
// 3. HOST MAPPING - create mapping pages and host bindings
//-----------------------------------------------------------------------------
// Bind elements to MIDI
devices.forEach((device) => {
bindDeviceToMidi(device, globalBooleanVariables, activationCallbacks, timerUtils);
});

makeHostMapping(page, elements, firstMainDeviceChannelIndex);
// Map elements to host functions
makeHostMapping(page, devices, segmentDisplayManager, globalBooleanVariables);
78 changes: 33 additions & 45 deletions src/mapping/control.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { DecoratedFactoryMappingPage } from "../decorators/page";
import { JogWheel, LedButton, LedPushEncoder } from "../decorators/surface";
import { EncoderDisplayMode } from "../midi";
import { SurfaceElements } from "../surface";
import { ChannelSurfaceElements, ControlSectionSurfaceElements } from "../surface";

function setShiftableButtonsLedValues(
elements: SurfaceElements,
controlSectionElements: ControlSectionSurfaceElements,
context: MR_ActiveDevice,
value: number
) {
const buttons = elements.control.buttons;
const buttons = controlSectionElements.buttons;

for (const button of [buttons.edit, buttons.modify[0], buttons.modify[2], buttons.utility[2]]) {
button.mLedValue.setProcessValue(context, value);
Expand Down Expand Up @@ -60,12 +60,12 @@ function bindCursorValueControlButton(

export function bindControlButtons(
page: DecoratedFactoryMappingPage,
elements: SurfaceElements,
mixerBankZone: MR_MixerBankZone,
firstMainDeviceChannelIndex: number
controlSectionElements: ControlSectionSurfaceElements,
channelElements: ChannelSurfaceElements,
mixerBankZone: MR_MixerBankZone
) {
const host = page.mHostAccess;
const buttons = elements.control.buttons;
const buttons = controlSectionElements.buttons;

const buttonsSubPageArea = page.makeSubPageArea("Control Buttons");
const regularSubPage = buttonsSubPageArea.makeSubPage("Regular");
Expand Down Expand Up @@ -139,8 +139,8 @@ export function bindControlButtons(
bindCursorValueControlButton(
page,
buttons.automation[2],
elements.channels[firstMainDeviceChannelIndex + 7].encoder,
elements.control.jogWheel
channelElements[7].encoder,
controlSectionElements.jogWheel
);

// Project
Expand Down Expand Up @@ -178,10 +178,10 @@ export function bindControlButtons(
).mOnValueChange = (context, mapping, value) => {
if (value) {
shiftSubPage.mAction.mActivate.trigger(mapping);
setShiftableButtonsLedValues(elements, context, 1);
setShiftableButtonsLedValues(controlSectionElements, context, 1);
} else {
regularSubPage.mAction.mActivate.trigger(mapping);
setShiftableButtonsLedValues(elements, context, 0);
setShiftableButtonsLedValues(controlSectionElements, context, 0);
}
};

Expand Down Expand Up @@ -216,19 +216,22 @@ export function bindControlButtons(
.setTypeToggle();

// Navigation Buttons
const { bank, channel } = elements.control.buttons.navigation;
const { bank, channel } = buttons.navigation;
page.makeActionBinding(bank.left.mSurfaceValue, mixerBankZone.mAction.mPrevBank);
page.makeActionBinding(bank.right.mSurfaceValue, mixerBankZone.mAction.mNextBank);
page.makeActionBinding(channel.left.mSurfaceValue, mixerBankZone.mAction.mShiftLeft);
page.makeActionBinding(channel.right.mSurfaceValue, mixerBankZone.mAction.mShiftRight);
}

export function bindJogWheelSection(page: MR_FactoryMappingPage, elements: SurfaceElements) {
export function bindJogWheelSection(
page: MR_FactoryMappingPage,
controlSectionElements: ControlSectionSurfaceElements
) {
const jogWheelSubPageArea = page.makeSubPageArea("jogWeel");
const scrubSubPage = jogWheelSubPageArea.makeSubPage("scrub");
const jogSubPage = jogWheelSubPageArea.makeSubPage("jog");

const scrubButton = elements.control.buttons.scrub;
const scrubButton = controlSectionElements.buttons.scrub;

page.makeActionBinding(scrubButton.mSurfaceValue, jogWheelSubPageArea.mAction.mNext);

Expand All @@ -239,44 +242,29 @@ export function bindJogWheelSection(page: MR_FactoryMappingPage, elements: Surfa
scrubButton.mLedValue.setProcessValue(context, 0);
};

const { mJogLeftValue: jogLeft, mJogRightValue: jogRight } = elements.control.jogWheel;
const { mJogLeftValue: jogLeft, mJogRightValue: jogRight } = controlSectionElements.jogWheel;
page.makeCommandBinding(jogLeft, "Transport", "Jog Left").setSubPage(jogSubPage);
page.makeCommandBinding(jogRight, "Transport", "Jog Right").setSubPage(jogSubPage);
page.makeCommandBinding(jogLeft, "Transport", "Nudge Cursor Left").setSubPage(scrubSubPage);
page.makeCommandBinding(jogRight, "Transport", "Nudge Cursor Right").setSubPage(scrubSubPage);
}

export function bindSegmentDisplaySection(page: MR_FactoryMappingPage, elements: SurfaceElements) {
page.mHostAccess.mTransport.mTimeDisplay.mPrimary.mTransportLocator.mOnChange = (
context,
mapping,
time,
timeFormat
) => {
elements.display.onTimeUpdated(context, time, timeFormat);
};

export function bindSegmentDisplaySection(
page: MR_FactoryMappingPage,
controlSectionElements: ControlSectionSurfaceElements
) {
page.makeCommandBinding(
elements.control.buttons.timeMode.mSurfaceValue,
controlSectionElements.buttons.timeMode.mSurfaceValue,
"Transport",
"Exchange Time Formats"
);

elements.control.buttons.display.onSurfaceValueChange.addCallback((context, value) => {
if (value === 1) {
elements.display.isValueModeActive.setProcessValue(
context,
+!elements.display.isValueModeActive.getProcessValue(context)
);
}
});

// There's no "is solo mode active on any chanel" host value, is it?
// page.makeValueBinding(elements.display.leds.solo, ? )
}

export function bindDirectionButtons(page: MR_FactoryMappingPage, elements: SurfaceElements) {
const buttons = elements.control.buttons;
export function bindDirectionButtons(
page: MR_FactoryMappingPage,
controlSectionElements: ControlSectionSurfaceElements
) {
const buttons = controlSectionElements.buttons;

const subPageArea = page.makeSubPageArea("Direction Buttons");
const navigateSubPage = subPageArea.makeSubPage("Navigate");
Expand Down Expand Up @@ -321,11 +309,11 @@ export function bindDirectionButtons(page: MR_FactoryMappingPage, elements: Surf
page.makeActionBinding(directions.center.mSurfaceValue, subPageArea.mAction.mNext);
}

export function bindFootControl(page: DecoratedFactoryMappingPage, elements: SurfaceElements) {
const host = page.mHostAccess;

// Free buttons
for (const footSwitch of elements.footControl.footSwitches) {
export function bindFootControl(
page: DecoratedFactoryMappingPage,
controlSectionElements: ControlSectionSurfaceElements
) {
for (const footSwitch of controlSectionElements.footSwitches) {
page.makeCommandBinding(
footSwitch.mSurfaceValue,
"MIDI Remote",
Expand Down
Loading

0 comments on commit db82022

Please sign in to comment.