summaryrefslogtreecommitdiffstats
path: root/res/controllers/Traktor-Kontrol-S4-MK3.js
diff options
context:
space:
mode:
Diffstat (limited to 'res/controllers/Traktor-Kontrol-S4-MK3.js')
-rw-r--r--res/controllers/Traktor-Kontrol-S4-MK3.js3157
1 files changed, 3157 insertions, 0 deletions
diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js
new file mode 100644
index 0000000000..b20056345a
--- /dev/null
+++ b/res/controllers/Traktor-Kontrol-S4-MK3.js
@@ -0,0 +1,3157 @@
+/// Created by Be <be@mixxx.org> and A. Colombier <mixxx@acolombier.dev>
+
+const LedColors = {
+ off: 0,
+ red: 4,
+ carrot: 8,
+ orange: 12,
+ honey: 16,
+ yellow: 20,
+ lime: 24,
+ green: 28,
+ aqua: 32,
+ celeste: 36,
+ sky: 40,
+ blue: 44,
+ purple: 48,
+ fuscia: 52,
+ magenta: 56,
+ azalea: 60,
+ salmon: 64,
+ white: 68,
+};
+
+
+// This define the sequence of color to use for pad button when in keyboard mode. This should make them look like an actual keyboard keyboard octave, except for C, which is green to help spotting it.
+const KeyboardColors = [
+ LedColors.green,
+ LedColors.off,
+ LedColors.white,
+ LedColors.off,
+ LedColors.white,
+ LedColors.white,
+ LedColors.off,
+ LedColors.white,
+ LedColors.off,
+ LedColors.white,
+ LedColors.off,
+ LedColors.white,
+];
+
+/*
+ * USER CONFIGURABLE SETTINGS
+ * Adjust these to your liking
+ */
+
+const DeckColors = [
+ LedColors.red,
+ LedColors.blue,
+ LedColors.yellow,
+ LedColors.purple,
+];
+
+const LibrarySortableColumns = [
+ script.LIBRARY_COLUMNS.ARTIST,
+ script.LIBRARY_COLUMNS.TITLE,
+ script.LIBRARY_COLUMNS.BPM,
+ script.LIBRARY_COLUMNS.KEY,
+ script.LIBRARY_COLUMNS.DATETIME_ADDED,
+];
+
+const LoopWheelMoveFactor = 50;
+const LoopEncoderMoveFactor = 500;
+const LoopEncoderShiftmoveFactor = 2500;
+
+const TempoFaderSoftTakeoverColorLow = LedColors.white;
+const TempoFaderSoftTakeoverColorHigh = LedColors.green;
+
+// Define whether or not to keep LED that have only one color (reverse, flux, play, shift) dimmed if they are inactive.
+// 'true' will keep them dimmed, 'false' will turn them off. Default: true
+const KeepLEDWithOneColorDimedWhenInactive = true;
+
+// Keep both deck select buttons backlit and do not fully turn off the inactive deck button.
+// 'true' will keep the unseclected deck dimmed, 'false' to fully turn it off. Default: true
+const KeepDeckSelectDimmed = true;
+
+// Define whether the keylock is mapped when doing "shift+master" (on press) or "shift+sync" (on release since long push copies the key)".
+// 'true' will use "sync+master", 'false' will use "shift+sync". Default: false
+const UseKeylockOnMaster = false;
+
+// Define whether the grid button would blink when the playback is going over a detcted beat. Can help to adjust beat grid.
+// Default: false
+const GridButtonBlinkOverBeat = false;
+
+// Wheel led blinking if reaching the end of track warning (default 30 seconds, can be changed in the settings, under "Waveforms" > "End of track warning").
+// Default: true
+const WheelLedBlinkOnTrackEnd = true;
+
+// When shifting either decks, the mixer will control microphones or auxiliary lines. If there is both a mic and an configure on the same channel, the mixer will control the auxiliary.
+// Default: false
+const MixerControlsMixAuxOnShift = false;
+
+// Define how many wheel moves are sampled to compute the speed. The more you have, the more the speed is accurate, but the
+// less responsive it gets in Mixxx. Default: 5
+const WheelSpeedSample = 3;
+
+// Make the sampler tab a beatlooproll tab instead
+// Default: false
+const UseBeatloopRollInsteadOfSampler = false;
+
+// Predefined beatlooproll sizes. Note that if you use AddLoopHalveAndDoubleOnBeatloopRollTab, the first and
+// last size will be ignored
+const BeatLoopRolls = [1/16, 1/8, 1/4, 1/2, 1, 2, 4, 8];
+
+// Make the two last button on the beatlooproll pad halve or double the loop size. This will take away the 1/16 and 8 loop size.
+// Default: true
+const AddLoopHalveAndDoubleOnBeatloopRollTab = true;
+
+// Define the speed of the jogwheel. This will impact the speed of the LED playback indicator, the sratch, and the speed of
+// the motor if enable. Recommended value are 33 + 1/3 or 45.
+// Default: 33 + 1/3
+const BaseRevolutionsPerMinute = 33 + 1/3;
+
+// Define whether or not to use motors.
+// This is a BETA feature! Please use at your own risk. Setting this off means that below settings are inactive
+// Default: false
+const UseMotors = false;
+
+// Define how many wheel moves are sampled to compute the speed when using the motor. This is helpful to mitigate delay that
+// occurs in communication as well as Mixxx limitation to 20ms latency.
+// The more you have, the more the speed is accurate.
+// less responsive it gets in Mixxx. Default: 20
+const TurnTableSpeedSample = 20;
+
+// Define how much the wheel will resist. It is a similar setting that the Grid+Wheel in Tracktor
+// Value must defined between 0 to 1. 0 is very tight, 1 is very loose.
+// Default: 0.5
+const TightnessFactor = 0.5;
+
+// Define how much force can the motor use. This defines how much the wheel will "fight" you when you block it in TT mode
+// This will also affect how quick the wheel starts spinning when enabling motor mode, or starting a deck with motor mode on
+const MaxWheelForce = 25000; // Traktor seems to cap the max value at 60000, which just sounds insane
+
+
+
+// The LEDs only support 16 base colors. Adding 1 in addition to
+// the normal 2 for Button.prototype.brightnessOn changes the color
+// slightly, so use that get 25 different colors to include the Filter
+// button as a 5th effect chain preset selector.
+const QuickEffectPresetColors = [
+ LedColors.red,
+ LedColors.green,
+ LedColors.blue,
+ LedColors.yellow,
+ LedColors.orange,
+ LedColors.purple,
+ LedColors.white,
+
+ LedColors.magenta,
+ LedColors.azalea,
+ LedColors.salmon,
+ LedColors.red + 1,
+
+ LedColors.sky,
+ LedColors.celeste,
+ LedColors.fuscia,
+ LedColors.blue + 1,
+
+ LedColors.carrot,
+ LedColors.honey,
+ LedColors.yellow + 1,
+
+ LedColors.lime,
+ LedColors.aqua,
+ LedColors.purple + 1,
+
+ LedColors.magenta + 1,
+ LedColors.azalea + 1,
+ LedColors.salmon + 1,
+ LedColors.fuscia + 1,
+];
+
+// assign samplers to the crossfader on startup
+const SamplerCrossfaderAssign = true;
+
+const MotorWindUpMilliseconds = 1200;
+const MotorWindDownMilliseconds = 900;
+
+/*
+ * HID report parsing library
+ */
+class HIDInputReport {
+ constructor(reportId) {
+ this.reportId = reportId;
+ this.fields = [];
+ }
+
+ registerCallback(callback, byteOffset, bitOffset = 0, bitLength = 1, defaultOldData = undefined) {
+ if (typeof callback !== "function") {
+ throw Error("callback must be a function");
+ }
+
+ if (!Number.isInteger(byteOffset)) {
+ throw Error("byteOffset must be 0 or a positive integer");
+ }
+ if (!Number.isInteger(bitOffset) || bitOffset < 0) {
+ throw Error("bitOffset must be 0 or a positive integer");
+ }
+ if (!Number.isInteger(bitOffset) || bitLength < 1 || bitLength > 32) {
+ throw Error("bitLength must be an integer between 1 and 32");
+ }
+
+ const field = {
+ callback: callback,
+ byteOffset: byteOffset,
+ bitOffset: bitOffset,
+ bitLength: bitLength,
+ oldData: defaultOldData
+ };
+ this.fields.push(field);
+
+ return {
+ disconnect: () => {
+ this.fields = this.fields.filter((element) => {
+ return element !== field;
+ });
+ }
+ };
+ }
+
+ handleInput(reportData) {
+ const view = new DataView(reportData);
+
+ for (const field of this.fields) {
+ const numBytes = Math.ceil(field.bitLength / 8);
+ let data;
+
+ // Little endianness is specified by the HID standard.
+ // The HID standard allows signed integers as well, but I am not aware
+ // of any HID DJ controllers which use signed integers.
+ if (numBytes === 1) {
+ data = view.getUint8(field.byteOffset);
+ } else if (numBytes === 2) {
+ data = view.getUint16(field.byteOffset, true);
+ } else if (numBytes === 3) {
+ data = view.getUint32(field.byteOffset, true) >>> 8;
+ } else if (numBytes === 4) {
+ data = view.getUint32(field.byteOffset, true);
+ } else {
+ throw Error("field bitLength must be between 1 and 32");
+ }
+
+ // The >>> 0 is required for 32 bit unsigned ints to not magically turn negative
+ // because all Numbers are really 32 bit signed floats. Because JavaScript.
+ data = ((data >> field.bitOffset) & (2 ** field.bitLength - 1)) >>> 0;
+
+ if (field.oldData !== data) {
+ field.callback(data);
+ field.oldData = data;
+ }
+ }
+ }
+}
+
+class HIDOutputReport {
+ constructor(reportId, length) {
+ this.reportId = reportId;
+ this.data = new Uint8Array(length).fill(0);
+ }
+ send() {
+ controller.sendOutputReport(this.reportId, this.data.buffer);
+ }
+}
+
+/*
+ * Components library
+ */
+
+class Component {
+ constructor(options) {
+ if (options) {
+ Object.keys(options).forEach(function(key) {
+ if (options[key] === undefined) { delete options[key]; }
+ });
+ Object.assign(this, options);
+ }
+ this.outConnections = [];
+ if (typeof this.key === "string") {
+ this.inKey = this.key;
+ this.outKey = this.key;
+ }
+ if (typeof this.unshift === "function" && this.unshift.length === 0) {
+ this.unshift();
+ }
+ this.shifted = false;
+ if (typeof this.input === "function" && this.inReport instanceof HIDInputReport && this.inReport.length === 0) {
+ this.inConnect();
+ }
+ this.outConnect();
+ }
+ inConnect(callback) {
+ if (this.inByte === undefined
+ || this.inBit === undefined
+ || this.inBitLength === undefined
+ || this.inReport === undefined) {
+ return;
+ }
+ if (typeof callback === "function") {
+ this.input = callback;
+ }
+ this.inConnection = this.inReport.registerCallback(this.input.bind(this), this.inByte, this.inBit, this.inBitLength, this.oldDataDefault);
+ }
+ inDisconnect() {
+ if (this.inConnection !== undefined) {
+ this.inConnection.disconnect();
+ }
+ }
+ send(value) {
+ if (this.outReport !== undefined && this.outByte !== undefined) {
+ this.outReport.data[this.outByte] = value;
+ this.outReport.send();
+ }
+ }
+ output(value) {
+ this.send(value);
+ }
+ outConnect() {
+ if (this.outKey !== undefined && this.group !== undefined) {
+ const connection = engine.makeConnection(this.group, this.outKey, this.output.bind(this));
+ // This is useful for case where effect would have been fully disabled in Mixxx. This appears to be the case during unit tests.
+ if (connection) {
+ this.outConnections[0] = connection;
+ } else {
+ console.warn(`Unable to connect ${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`);
+ }
+ }
+ }
+ outDisconnect() {
+ for (const connection of this.outConnections) {
+ connection.disconnect();
+ }
+ this.outConnections = [];
+ }
+ outTrigger() {
+ for (const connection of this.outConnections) {
+ connection.trigger();
+ }
+ }
+}
+class ComponentContainer extends Component {
+ constructor() {
+ super();
+ }
+ *[Symbol.iterator]() {
+ // can't use for...of here because it would create an infinite loop
+ for (const property in this) {
+ if (Object.prototype.hasOwnProperty.call(this, property)) {
+ const obj = this[property];
+ if (obj instanceof Component) {
+ yield obj;
+ } else if (Array.isArray(obj)) {
+ for (const objectInArray of obj) {
+ if (objectInArray instanceof Component) {
+ yield objectInArray;
+ }
+ }
+ }
+ }
+ }
+ }
+ reconnectComponents(callback) {
+ for (const component of this) {
+ if (typeof component.outDisconnect === "function" && component.outDisconnect.length === 0) {
+ component.outDisconnect();
+ }
+ if (typeof callback === "function" && callback.length === 1) {
+ callback.call(this, component);
+ }
+ if (typeof component.outConnect === "function" && component.outConnect.length === 0) {
+ component.outConnect();
+ }
+ component.outTrigger();
+ if (typeof component.unshift === "function" && component.unshift.length === 0) {
+ component.unshift();
+ }
+ }
+ }
+ unshift() {
+ for (const component of this) {
+ if (typeof component.unshift === "function" && component.unshift.length === 0) {
+ component.unshift();
+ }
+ component.shifted = false;
+ }
+ this.shifted = false;
+ }
+ shift() {
+ for (const component of this) {
+ if (typeof component.shift === "function" && component.shift.length === 0) {
+ component.shift();
+ }
+ component.shifted = true;
+ }
+ this.shifted = true;
+ }
+}
+
+/* eslint no-redeclare: "off" */
+class Deck extends ComponentContainer {
+ constructor(decks, colors) {
+ super();
+ if (typeof decks === "number") {
+ this.group = Deck.groupForNumber(decks);
+ } else if (Array.isArray(decks)) {
+ this.decks = decks;
+ this.currentDeckNumber = decks[0];
+ this.group = Deck.groupForNumber(decks[0]);
+ }
+ if (colors !== undefined && Array.isArray(colors)) {
+ this.groupsToColors = {};
+ let index = 0;
+ for (const deck of this.decks) {
+ this.groupsToColors[Deck.groupForNumber(deck)] = colors[index];
+ index++;
+ }
+ this.color = colors[0];
+ }
+ this.secondDeckModes = null;
+ }
+ toggleDeck() {
+ if (this.decks === undefined) {
+ throw Error("toggleDeck can only be used with Decks constructed with an Array of deck numbers, for example [1, 3]");
+ }
+
+ const currentDeckIndex = this.decks.indexOf(this.currentDeckNumber);
+ let newDeckIndex = currentDeckIndex + 1;
+ if (currentDeckIndex >= this.decks.length) {
+ newDeckIndex = 0;
+ }
+
+ this.switchDeck(Deck.groupForNumber(this.decks[newDeckIndex]));
+ }
+ switchDeck(newGroup) {
+ const currentModes = {
+ wheelMode: this.wheelMode,
+ moveMode: this.moveMode,
+ };
+
+ engine.setValue(this.group, "scratch2_enable", false);
+ this.group = newGroup;
+ this.color = this.groupsToColors[newGroup];
+
+ if (this.secondDeckModes !== null) {
+ this.wheelMode = this.secondDeckModes.wheelMode;
+ this.moveMode = this.secondDeckModes.moveMode;
+
+ if (this.wheelMode === wheelModes.motor) {
+ engine.beginTimer(MotorWindUpMilliseconds, function() {
+ engine.setValue(newGroup, "scratch2_enable", true);
+ }, true);
+ }
+ }
+
+ if (currentModes.wheelMode === wheelModes.motor) {
+ this.wheelTouch.touched = true;
+ engine.beginTimer(MotorWindDownMilliseconds, () => {
+ this.wheelTouch.touched = false;
+ }, true);
+ }
+ this.reconnectComponents(function(component) {
+ if (component.group === undefined
+ || component.group.search(script.channelRegEx) !== -1) {
+ component.group = newGroup;
+ } else if (component.group.search(script.eqRegEx) !== -1) {
+ component.group = `[EqualizerRack1_${newGroup}_Effect1]`;
+ } else if (component.group.search(script.quickEffectRegEx) !== -1) {
+ component.group = `[QuickEffectRack1_${newGroup}]`;
+ }
+
+ component.color = this.groupsToColors[newGroup];
+ });
+ this.secondDeckModes = currentModes;
+ }
+ static groupForNumber(deckNumber) {
+ return `[Channel${deckNumber}]`;
+ }
+}
+
+class Button extends Component {
+ constructor(options) {
+ options.oldDataDefault = 0;
+
+ super(options);
+
+ if (this.input === undefined) {
+ this.input = this.defaultInput;
+ if (typeof this.input === "function"
+ && this.inReport instanceof HIDInputReport
+ && this.input.length === 0) {
+ this.inConnect();
+ }
+ }
+
+ if (this.longPressTimeOutMillis === undefined) {
+ this.longPressTimeOutMillis = 225;
+ }
+ if (this.indicatorIntervalMillis === undefined) {
+ this.indicatorIntervalMillis = 350;
+ }
+ this.longPressTimer = 0;
+ this.indicatorTimer = 0;
+ this.indicatorState = false;
+ this.isLongPress = false;
+ if (this.inBitLength === undefined) {
+ this.inBitLength = 1;
+ }
+ }
+ setKey(key) {
+ this.inKey = key;
+ if (key === this.outKey) {
+ return;
+ }
+ this.outDisconnect();
+ this.outKey = key;
+ this.outConnect();
+ this.outTrigger();
+ }
+ setGroup(group) {
+ if (group === this.group) {
+ return;
+ }
+ this.outDisconnect();
+ this.group = group;
+ this.outConnect();
+ this.outTrigger();
+ }
+ output(value) {
+ if (this.indicatorTimer !== 0) {
+ return;
+ }
+ const brightness = (value > 0) ? this.brightnessOn : this.brightnessOff;
+ this.send((this.color || LedColors.white) + brightness);
+ }
+ indicatorCallback() {
+ this.indicatorState = !this.indicatorState;
+ this.send((this.indicatorColor || this.color || LedColors.white) + (this.indicatorState ? this.brightnessOn : this.brightnessOff));
+ }
+ indicator(on) {
+ if (on && this.indicatorTimer === 0) {
+ this.outDisconnect();
+ this.indicatorTimer = engine.beginTimer(this.indicatorIntervalMillis, this.indicatorCallback.bind(this));
+ } else if (!on && this.indicatorTimer !== 0) {
+ engine.stopTimer(this.indicatorTimer);
+ this.indicatorTimer = 0;
+ this.indicatorState = false;
+ this.outConnect();
+ this.outTrigger();
+ }
+ }
+ defaultInput(pressed) {
+ if (pressed) {
+ this.isLongPress = false;
+ if (typeof this.onShortPress === "function" && this.onShortPress.length === 0) { this.onShortPress(); }
+ if ((typeof this.onLongPress === "function" && this.onLongPress.length === 0) || (typeof this.onLongRelease === "function" && this.onLongRelease.length === 0)) {
+ this.longPressTimer = engine.beginTimer(this.longPressTimeOutMillis, () => {
+ this.isLongPress = true;
+ this.longPressTimer = 0;
+ if (typeof this.onLongPress !== "function") { return; }
+ this.onLongPress(this);
+ }, true);
+ }
+ } else if (this.isLongPress) {
+ if (typeof this.onLongRelease === "function" && this.onLongRelease.length === 0) { this.onLongRelease(); }
+ } else {
+ if (this.longPressTimer !== 0) {
+ engine.stopTimer(this.longPressTimer);
+ this.longPressTimer = 0;
+ }
+ if (typeof this.onShortRelease === "function" && this.onShortRelease.length === 0) { this.onShortRelease(); }
+ }
+ }
+}
+
+class PushButton extends Button {
+ constructor(options) {
+ super(options);
+ }
+ input(pressed) {
+ engine.setValue(this.group, this.inKey, pressed);
+ }
+}
+
+class ToggleButton extends Button {
+ constructor(options) {
+ super(options);
+ }
+ onShortPress() {
+ script.toggleControl(this.group, this.inKey, true);
+ }
+}
+
+class TriggerButton extends Button {
+ constructor(options) {
+ super(options);
+ }
+ onShortPress() {
+ engine.setValue(this.group, this.inKey, true);
+ }
+ onShortRelease() {
+ engine.setValue(this.group, this.inKey, false);
+ }
+}
+
+class PowerWindowButton extends Button {
+ constructor(options) {
+ super(options);
+ this.isLongPressed = false;
+ this.longPressTimer = 0;
+ }
+ onShortPress() {
+ script.toggleControl(this.group, this.inKey);
+ }
+ onLongRelease() {
+ script.toggleControl(this.group, this.inKey);
+ }
+}
+
+class PlayButton extends Button {
+ constructor(options) {
+ // Prevent accidental ejection/duplication accident
+ options.longPressTimeOutMillis = 800;
+ super(options);
+ this.inKey = "play";
+ this.outKey = "play_indicator";
+ this.outConnect();
+ }
+ onShortPress() {
+ script.toggleControl(this.group, this.inKey, true);
+ }
+ onLongPress() {
+ if (this.shifted) {
+ engine.setValue(this.group, this.inKey, false);
+ script.triggerControl(this.group, "eject");
+ } else if (!engine.getValue(this.group, this.inKey)) {
+ script.triggerControl(this.group, "CloneFromDeck");
+ }
+ }
+}
+
+class CueButton extends PushButton {
+ constructor(options) {
+ super(options);
+ this.outKey = "cue_indicator";
+ this.outConnect();
+ }
+ unshift() {
+ this.inKey = "cue_default";
+ }
+ shift() {
+ this.inKey = "start_stop";
+ }
+ input(pressed) {
+ if (this.deck.moveMode === moveModes.keyboard && !this.deck.keyboardPlayMode) {
+ this.deck.assignKeyboardPlayMode(this.group, this.inKey);
+ } else if (this.deck.wheelMode === wheelModes.motor && engine.getValue(this.group, "play") && pressed) {
+ engine.setValue(this.group, "cue_goto", pressed);
+ } else {
+ engine.setValue(this.group, this.inKey, pressed);
+ if (this.deck.wheelMode === wheelModes.motor) {
+ engine.setValue(this.group, "scratch2_enable", false);
+ engine.beginTimer(MotorWindDownMilliseconds, function() {
+ engine.setValue(this.group, "scratch2_enable", true);
+ }, true);
+ }
+ }
+ }
+}
+
+class Encoder extends Component {
+ constructor(options) {
+ super(options);
+ this.lastValue = null;
+ }
+ input(value) {
+ const oldValue = this.lastValue;
+ this.lastValue = value;
+
+ if (oldValue === null || typeof this.onChange !== "function") {
+ // This scenario happens at the controller initialisation. No real input to proceed
+ return;
+ }
+ let isRight;
+ if (oldValue === this.max && value === 0) {
+ isRight = true;
+ } else if (oldValue === 0 && value === this.max) {
+ isRight = false;
+ } else {
+ isRight = value > oldValue;
+ }
+ this.onChange(isRight);
+ }
+}
+
+/*
+ * Represent a pad button that interact with a hotcue (set, activate or clear)
+ */
+class HotcueButton extends PushButton {
+ constructor(options) {
+ super(options);
+ if (this.number === undefined || !Number.isInteger(this.number) || this.number < 1 || this.number > 32) {
+ throw Error("HotcueButton must have a number property of an integer between 1 and 32");
+ }
+ this.outKey = `hotcue_${this.number}_enabled`;
+ this.colorKey = `hotcue_${this.number}_color`;
+ this.outConnect();
+ }
+ unshift() {
+ this.inKey = `hotcue_${this.number}_activate`;
+ }
+ shift() {
+ this.inKey = `hotcue_${this.number}_clear`;
+ }
+ input(pressed) {
+ engine.setValue(this.group, "scratch2_enable", false);
+ engine.setValue(this.group, this.inKey, pressed);
+ }
+ output(value) {
+ if (value) {
+ this.send(this.color + this.brightnessOn);
+ } else {
+ this.send(LedColors.off);
+ }
+ }
+ outConnect() {
+ if (undefined !== this.group) {
+ const connection0 = engine.makeConnection(this.group, this.outKey, this.output.bind(this));
+ if (connection0) {
+ this.outConnections[0] = connection0;
+ } else {
+ console.warn(`Unable to connect ${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`);
+ }
+ const connection1 = engine.makeConnection(this.group, this.colorKey, (colorCode) => {
+ this.color = this.colorMap.getValueForNearestColor(colorCode);
+ this.output(engine.getValue(this.group, this.outKey));
+ });
+ if (connection1) {
+ this.outConnections[1] = connection1;
+ } else {
+ console.warn(`Unable to connect ${this.group}.${this.colorKey}' to the controller output. The control appears to be unavailable.`);
+ }
+ }
+ }
+}
+
+/*
+ * Represent a pad button that acts as a keyboard key. Depending the deck keyboard mode, it will either change the key, or play the cue with the button's key
+ */
+class KeyboardButton extends PushButton {
+ constructor(options) {
+ super(options);
+ if (this.number === undefined || !Number.isInteger(this.number) || this.number < 1 || this.number > 8) {
+ throw Error("KeyboardButton must have a number property of an integer between 1 and 8");
+ }
+ if (this.deck === undefined) {
+ throw Error("KeyboardButton must have a deck attached to it");
+ }
+ this.outConnect();
+ }
+ unshift() {
+ this.outTrigger();
+ }
+ shift() {
+ this.outTrigger();
+ }
+ input(pressed) {
+ const offset = this.deck.keyboardOffset - (this.shifted ? 8 : 0);
+ if (this.number + offset < 1 || this.number + offset > 24) {
+ return;
+ }
+ if (pressed) {
+ engine.setValue(this.group, "key", this.number + offset);
+ }
+ if (this.deck.keyboardPlayMode !== null) {
+ if (this.deck.keyboardPlayMode.activeKey && pressed) {
+ engine.setValue(this.deck.keyboardPlayMode.group, "cue_goto", pressed);
+ } else if (!this.deck.keyboardPlayMode.activeKey || this.deck.keyboardPlayMode.activeKey === this) {
+ script.toggleControl(this.deck.keyboardPlayMode.group, this.deck.keyboardPlayMode.action, true);
+ }
+ if (!pressed && this.deck.keyboardPlayMode.activeKey === this) {
+ this.deck.keyboardPlayMode.activeKey = undefined;
+ } else if (pressed) {
+ this.deck.keyboardPlayMode.activeKey = this;
+ }
+ }
+ }
+ output(value) {
+ const offset = this.deck.keyboardOffset - (this.shifted ? 8 : 0);
+ const colorIdx = (this.number - 1 + offset) % KeyboardColors.length;
+ const color = KeyboardColors[colorIdx];
+ if (this.number + offset < 1 || this.number + offset > 24) {
+ this.send(0);
+ } else {
+ this.send(color + (value ? this.brightnessOn : this.brightnessOff));
+ }
+ }
+ outConnect() {
+ if (undefined !== this.group) {
+ const connection = engine.makeConnection(this.group, "key", (key) => {
+ const offset = this.deck.keyboardOffset - (this.shifted ? 8 : 0);
+ this.output(key === this.number + offset);
+ });
+ if (connection) {
+ this.outConnections[0] = connection;
+ } else {
+ console.warn(`Unable to connect ${this.group}.key' to the controller output. The control appears to be unavailable.`);
+ }
+ }
+ }
+}
+
+/*
+ * Represent a pad button that will trigger a pre-defined beatloop size as set in BeatLoopRolls.
+ */
+class BeatLoopRollButton extends TriggerButton {
+ constructor(options) {
+ if (options.number === undefined || !Number.isInteger(options.number) || options.number < 0 || options.number > 7) {
+ throw Error("BeatLoopRollButton must have a number property of an integer between 0 and 7");
+ }
+ if (options.number <= 5 || !AddLoopHalveAndDoubleOnBeatloopRollTab) {
+ options.key = "beatlooproll_"+BeatLoopRolls[AddLoopHalveAndDoubleOnBeatloopRollTab ? options.number + 1 : options.number]+"_activate";
+ options.onShortPress = function() {
+ if (!this.deck.beatloopSize) {
+ this.deck.beatloopSize = engine.getValue(this.group, "beatloop_size");
+ }
+ engine.setValue(this.group, this.inKey, true);
+ };
+ options.onShortRelease = function() {
+ engine.setValue(this.group, this.inKey, false);
+ if (this.deck.beatloopSize) {
+ engine.setValue(this.group, "beatloop_size", this.deck.beatloopSize);
+ this.deck.beatloopSize = undefined;
+ }
+ };
+ } else if (options.number === 6) {
+ options.key = "loop_halve";
+ } else {
+ options.key = "loop_double";
+ }
+ super(options);
+ if (this.deck === undefined) {
+ throw Error("BeatLoopRollButton must have a deck attached to it");
+ }
+
+ this.outConnect();
+ }
+ output(value) {
+ if (this.number <= 5 || !AddLoopHalveAndDoubleOnBeatloopRollTab) {
+ this.send(LedColors.white + (value ? this.brightnessOn : this.brightnessOff));
+ } else {
+ this.send(this.color);
+ }
+ }
+}
+
+/*
+ * Represent a pad button that interact with a sampler (load, play/pause, cue, eject)
+ */
+class SamplerButton extends Button {
+ constructor(options) {
+ super(options);
+ if (this.number === undefined || !Number.isInteger(this.number) || this.number < 1 || this.number > 64) {
+ throw Error("SamplerButton must have a number property of an integer between 1 and 64");
+ }
+ this.group = `[Sampler${this.number}]`;
+ this.outConnect();
+ }
+ onShortPress() {
+ if (!this.shifted) {
+ if (engine.getValue(this.group, "track_loaded") === 0) {
+ engine.setValue(this.group, "LoadSelectedTrack", 1);
+ } else {
+ engine.setValue(this.group, "cue_gotoandplay", 1);
+ }
+ } else {
+ if (engine.getValue(this.group, "play") === 1) {
+ engine.setValue(this.group, "play", 0);
+ } else {
+ engine.setValue(this.group, "eject", 1);
+ }
+ }
+ }
+ onShortRelease() {
+ if (this.shifted) {
+ if (engine.getValue(this.group, "play") === 0) {
+ engine.setValue(this.group, "eject", 0);
+ }
+ }
+ }
+ // This function is connected to multiple Controls, so don't use the value passed in as a parameter.
+ output() {
+ if (engine.getValue(this.group, "track_loaded")) {
+ if (engine.getValue(this.group, "play")) {
+ this.send(this.color + this.brightnessOn);
+ } else {
+ this.send(this.color + this.brightnessOff);
+ }
+ } else {
+ this.send(0);
+ }
+ }
+ outConnect() {
+ if (undefined !== this.group) {
+ const connection0 = engine.makeConnection(this.group, "play", this.output.bind(this));
+ if (connection0) {
+ this.outConnections[0] = connection0;
+ } else {
+ console.warn(`Unable to connect ${this.group}.play' to the controller output. The control appears to be unavailable.`);
+ }
+ const connection1 = engine.makeConnection(this.group, "track_loaded", this.output.bind(this));
+ if (connection1) {
+ this.outConnections[1] = connection1;
+ } else {
+ console.warn(`Unable to connect ${this.group}.track_loaded' to the controller output. The control appears to be unavailable.`);
+ }
+ }
+ }
+}
+
+/*
+ * Represent a pad button that interact with a intro/extra special markers (set, activate, clear)
+ */
+class IntroOutroButton extends PushButton {
+ constructor(options) {
+ super(options);
+ if (this.cueBaseName === undefined || typeof this.cueBaseName !== "string") {
+ throw Error("must specify cueBaseName as intro_start, intro_end, outro_start, or outro_end");
+ }
+ this.outKey = `${this.cueBaseName}_enabled`;
+ this.outConnect();
+ }
+ unshift() {
+ this.inKey = `${this.cueBaseName}_activate`;
+ }
+ shift() {
+ this.inKey = `${this.cueBaseName}_clear`;
+ }
+ output(value) {
+ if (value) {
+ this.send(this.color + this.brightnessOn);
+ } else {
+ this.send(0);
+ }
+ }
+}
+
+class Pot extends Component {
+ constructor(options) {
+ super(options);
+ this.hardwarePosition = null;
+ this.shiftedHardwarePosition = null;
+
+ if (this.input === undefined) {
+ this.input = this.defaultInput;
+ }
+ }
+ setGroupKey(group, key) {
+ this.inKey = key;
+ if (key === this.outKey && group === this.group) {
+ return;
+ }
+ this.outDisconnect();
+ this.group = group;
+ this.outKey = key;
+ this.outConnect();
+ }
+ defaultInput(value) {
+ const receivingFirstValue = this.hardwarePosition === null;
+ this.hardwarePosition = value / this.max;
+ engine.setParameter(this.group, this.inKey, this.hardwarePosition);
+ if (receivingFirstValue) {
+ engine.softTakeover(this.group, this.inKey, true);
+ }
+ }
+ outDisconnect() {
+ if (this.hardwarePosition !== null) {
+ engine.softTakeover(this.group, this.inKey, true);
+ }
+ engine.softTakeoverIgnoreNextValue(this.group, this.inKey);
+ super.outDisconnect();
+ }
+}
+
+class Mixer extends ComponentContainer {
+ constructor(inReports, outReports) {
+ super();
+
+ this.outReport = outReports[128];
+
+ this.mixerColumnDeck1 = new S4Mk3MixerColumn(1, inReports, outReports[128],
+ {
+ saveGain: {inByte: 11, inBit: 0, outByte: 80},
+ effectUnit1Assign: {inByte: 2, inBit: 3, outByte: 78},
+ effectUnit2Assign: {inByte: 2, inBit: 4, outByte: 79},
+ gain: {inByte: 16},
+ eqHigh: {inByte: 44},
+ eqMid: {inByte: 46},
+ eqLow: {inByte: 48},
+ quickEffectKnob: {inByte: 64},
+ quickEffectButton: {},
+ volume: {inByte: 2},
+ pfl: {inByte: 7, inBit: 3, outByte: 77},
+ crossfaderSwitch: {inByte: 17, inBit: 4},
+ }
+ );
+ this.mixerColumnDeck2 = new S4Mk3MixerColumn(2, inReports, outReports[128],
+ {
+ saveGain: {inByte: 11, inBit: 1, outByte: 84},
+ effectUnit1Assign: {inByte: 2, inBit: 5, outByte: 82},
+ effectUnit2Assign: {inByte: 2, inBit: 6, outByte: 83},
+ gain: {inByte: 18},
+ eqHigh: {inByte: 50},
+ eqMid: {inByte: 52},