summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAntoine C <mixxx@acolombier.dev>2023-02-17 19:30:23 +0000
committerAntoine C <mixxx@acolombier.dev>2023-06-04 17:25:04 +0100
commit7170863e66c798a9786dc3d4166c7d0eac3cac97 (patch)
treeeed49f860469eeb86cefb154de4265700e758a8f
parent18f9fd7e7ed8003b536a514666d78554357b37fa (diff)
Kontrol S4 Mk3: add controller mapping
-rw-r--r--res/controllers/Traktor Kontrol S4 MK3.hid.xml2
-rw-r--r--res/controllers/Traktor-Kontrol-S4-MK3.js1886
2 files changed, 1272 insertions, 616 deletions
diff --git a/res/controllers/Traktor Kontrol S4 MK3.hid.xml b/res/controllers/Traktor Kontrol S4 MK3.hid.xml
index c9852a1d40..02b9b5b936 100644
--- a/res/controllers/Traktor Kontrol S4 MK3.hid.xml
+++ b/res/controllers/Traktor Kontrol S4 MK3.hid.xml
@@ -2,7 +2,7 @@
<MixxxControllerPreset mixxxVersion="2.4.0" schemaVersion="1">
<info>
<name>Traktor Kontrol S4 MK3</name>
- <author>Be</author>
+ <author>Be, A. Colombier</author>
<description>HID Mapping for Traktor Kontrol S4 MK3</description>
<manual>native_instruments_traktor_kontrol_s4_mk3</manual>
<devices>
diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js
index 2c53b65442..b7601607fa 100644
--- a/res/controllers/Traktor-Kontrol-S4-MK3.js
+++ b/res/controllers/Traktor-Kontrol-S4-MK3.js
@@ -1,4 +1,4 @@
-/// Copyright (C) 2022 Be <be@mixxx.org>
+/// Copyright (C) 2023 Be <be@mixxx.org> and A. Colombier <mixxx@acolombier.dev>
///
/// This mapping is free software; you can redistribute it and/or modify
/// it under the terms of the GNU General Public License as published by
@@ -37,6 +37,23 @@ const LEDColors = {
white: 68,
};
+const KeyboardColors = [
+ LEDColors.red,
+ LEDColors.orange,
+ LEDColors.yellow,
+ LEDColors.lime,
+ LEDColors.green,
+ LEDColors.aqua,
+ LEDColors.celeste,
+ LEDColors.sky,
+ LEDColors.blue,
+ LEDColors.purple,
+ LEDColors.fuscia,
+ LEDColors.azalea,
+ LEDColors.salmon,
+ LEDColors.white,
+];
+
/*
* USER CONFIGURABLE SETTINGS
* Adjust these to your liking
@@ -49,6 +66,19 @@ const deckColors = [
LEDColors.purple,
];
+// A full list can be found here: https://manual.mixxx.org/2.4/en/chapters/appendix/mixxx_controls.html#control-[Library]-sort_column
+const librarySortableColumns = [
+ 1, // Artist
+ 2, // Title
+ 15, // BPM
+ 20, // Key
+ 17, // Date added
+];
+
+const loopWheelMoveFactor = 50;
+const loopEncoderMoveFactor = 500;
+const loopEncoderShiftMoveFactor = 2500;
+
const tempoFaderSoftTakeoverColorLow = LEDColors.white;
const tempoFaderSoftTakeoverColorHigh = LEDColors.green;
@@ -95,7 +125,6 @@ const samplerCrossfaderAssign = true;
/*
* HID packet parsing library
*/
-
class HIDInputPacket {
constructor(reportId) {
this.reportId = reportId;
@@ -201,6 +230,7 @@ class HIDOutputPacket {
class Component {
constructor(options) {
Object.assign(this, options);
+ this.outConnections = [];
if (options !== undefined && typeof options.key === "string") {
this.inKey = options.key;
this.outKey = options.key;
@@ -213,14 +243,13 @@ class Component {
&& this.inPacket !== undefined && this.inPacket instanceof HIDInputPacket) {
this.inConnect();
}
- this.outConnections = [];
this.outConnect();
}
inConnect(callback) {
if (this.inByte === undefined
- || this.inBit === undefined
- || this.inBitLength === undefined
- || this.inPacket === undefined) {
+ || this.inBit === undefined
+ || this.inBitLength === undefined
+ || this.inPacket === undefined) {
return;
}
if (typeof callback === "function") {
@@ -251,6 +280,7 @@ class Component {
for (const connection of this.outConnections) {
connection.disconnect();
}
+ this.outConnections = [];
}
outTrigger() {
for (const connection of this.outConnections) {
@@ -258,28 +288,21 @@ class Component {
}
}
}
-
-class ComponentContainer {
- constructor() {}
+class ComponentContainer extends Component {
+ constructor() {
+ super();
+ }
*[Symbol.iterator]() {
- // can't use for...of here because it would create an infinite loop
+ // 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 (obj instanceof ComponentContainer) {
- for (const nestedComponent of obj) {
- yield nestedComponent;
- }
} else if (Array.isArray(obj)) {
for (const objectInArray of obj) {
if (objectInArray instanceof Component) {
yield objectInArray;
- } else if (objectInArray instanceof ComponentContainer) {
- for (const doublyNestedComponent of objectInArray) {
- yield doublyNestedComponent;
- }
}
}
}
@@ -357,7 +380,7 @@ class Deck extends ComponentContainer {
this.color = this.groupsToColors[newGroup];
this.reconnectComponents(function(component) {
if (component.group === undefined
- || component.group.search(script.channelRegEx) !== -1) {
+ || component.group.search(script.channelRegEx) !== -1) {
component.group = newGroup;
} else if (component.group.search(script.eqRegEx) !== -1) {
component.group = "[EqualizerRack1_" + newGroup + "_Effect1]";
@@ -376,17 +399,73 @@ class Deck extends ComponentContainer {
class Button extends Component {
constructor(options) {
super(options);
- this.off = 0;
+
+ if (this.input === undefined) {
+ this.input = this.defaultInput;
+ if (typeof this.input === "function"
+ && this.inPacket !== undefined && this.inPacket instanceof HIDInputPacket) {
+ this.inConnect();
+ }
+ }
+
if (this.longPressTimeOut === undefined) {
this.longPressTimeOut = 225; // milliseconds
}
+ if (this.indicatorInterval === undefined) {
+ this.indicatorInterval = 350; // milliseconds
+ }
+ this.longPressTimer = 0;
+ this.indicatorTimer = 0;
+ this.indicatorState = false;
+ this.isLongPress = false;
if (this.inBitLength === undefined) {
this.inBitLength = 1;
}
}
output(value) {
+ if (this.indicatorTimer !== 0) {
+ return;
+ }
const brightness = (value > 0) ? this.brightnessOn : this.brightnessOff;
- this.send(this.color + brightness);
+ 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.indicatorInterval, 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(); }
+ if (typeof this.onLongPress === "function" || typeof this.onLongRelease === "function") {
+ this.longPressTimer = engine.beginTimer(this.longPressTimeOut, () => {
+ 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(); }
+ } else {
+ if (this.longPressTimer !== 0) {
+ engine.stopTimer(this.longPressTimer);
+ this.longPressTimer = 0;
+ }
+ if (typeof this.onShortRelease === "function") { this.onShortRelease(); }
+ }
}
}
@@ -403,10 +482,17 @@ class ToggleButton extends Button {
constructor(options) {
super(options);
}
- input(pressed) {
- if (pressed) {
- script.toggleControl(this.group, this.inKey);
- }
+ onShortPress() {
+ script.toggleControl(this.group, this.inKey, true);
+ }
+}
+
+class TriggerButton extends Button {
+ constructor(options) {
+ super(options);
+ }
+ onShortPress() {
+ script.triggerControl(this.group, this.inKey, true);
}
}
@@ -416,33 +502,34 @@ class PowerWindowButton extends Button {
this.isLongPressed = false;
this.longPressTimer = 0;
}
- input(pressed) {
- if (pressed) {
- script.toggleControl(this.group, this.inKey);
- this.longPressTimer = engine.beginTimer(this.longPressTimeOut, () => {
- this.isLongPressed = true;
- this.longPressTimer = 0;
- }, true);
- } else {
- if (this.isLongPressed) {
- script.toggleControl(this.group, this.inKey);
- }
- if (this.longPressTimer !== 0) {
- engine.stopTimer(this.longPressTimer);
- }
- this.longPressTimer = 0;
- this.isLongPressed = false;
- }
+ onShortPress() {
+ script.toggleControl(this.group, this.inKey);
+ }
+ onLongRelease() {
+ script.toggleControl(this.group, this.inKey);
}
}
-class PlayButton extends ToggleButton {
+class PlayButton extends Button {
constructor(options) {
+ // Prevent accidental ejection/duplication accident
+ options.longPressTimeOut = 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 {
@@ -457,6 +544,13 @@ class CueButton extends PushButton {
shift() {
this.inKey = "start_stop";
}
+ input(pressed) {
+ if (this.deck.moveMode === moveModes.keyboard) {
+ this.deck.assignKeyboardPlayMode(this.group, this.inKey);
+ } else {
+ engine.setValue(this.group, this.inKey, pressed);
+ }
+ }
}
class Encoder extends Component {
@@ -464,17 +558,26 @@ class Encoder extends Component {
super(options);
this.lastValue = null;
}
- isRightTurn(value) {
- // detect wrap around
+ input(value) {
const oldValue = this.lastValue;
this.lastValue = value;
- if (oldValue === this.max && value === 0) {
- return true;
+
+ if (oldValue === null || typeof this.onChange !== "function") {
+ // This scenario happens at the controller initialisation. No real input to proceed
+ return;
}
- if (oldValue === 0 && value === this.max) {
- return false;
+ let isRight;
+ if (oldValue === this.max && value === 0) {
+ isRight = true;
+ } else if (oldValue === 0 && value === this.max) {
+ isRight = false;
+ } else {
+ isRight = value > oldValue;
}
- return value > oldValue;
+ this.onChange(isRight);
+ }
+ isRightTurn(value) {
+ // detect wrap around
}
}
@@ -494,6 +597,10 @@ class HotcueButton extends PushButton {
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);
@@ -512,6 +619,55 @@ class HotcueButton extends PushButton {
}
}
+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) {
+ script.toggleControl(this.deck.keyboardPlayMode.group, this.deck.keyboardPlayMode.action, true);
+ }
+ }
+ output(value) {
+ const offset = this.deck.keyboardOffset - (this.shifted ? 8 : 0);
+ const colorIdx = (this.number + 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) {
+ this.outConnections[0] = engine.makeConnection(this.group, "key", (key) => {
+ const offset = this.deck.keyboardOffset - (this.shifted ? 8 : 0);
+ this.output(key === this.number + offset);
+ });
+ }
+ }
+}
+
class SamplerButton extends Button {
constructor(options) {
super(options);
@@ -521,26 +677,27 @@ class SamplerButton extends Button {
this.group = "[Sampler" + this.number + "]";
this.outConnect();
}
- input(pressed) {
+ unshift() { }
+ shift() { }
+ onShortPress() {
if (!this.shifted) {
- if (pressed) {
- if (engine.getValue(this.group, "track_loaded") === 0) {
- engine.setValue(this.group, "LoadSelectedTrack", 1);
- } else {
- engine.setValue(this.group, "cue_gotoandplay", 1);
- }
+ if (engine.getValue(this.group, "track_loaded") === 0) {
+ engine.setValue(this.group, "LoadSelectedTrack", 1);
+ } else {
+ engine.setValue(this.group, "cue_gotoandplay", 1);
}
} else {
- if (pressed) {
- if (engine.getValue(this.group, "play") === 1) {
- engine.setValue(this.group, "play", 0);
- } else {
- engine.setValue(this.group, "eject", 1);
- }
+ if (engine.getValue(this.group, "play") === 1) {
+ engine.setValue(this.group, "play", 0);
} else {
- if (engine.getValue(this.group, "play") === 0) {
- engine.setValue(this.group, "eject", 0);
- }
+ engine.setValue(this.group, "eject", 1);
+ }
+ }
+ }
+ onShortRelease() {
+ if (this.shifted) {
+ if (engine.getValue(this.group, "play") === 0) {
+ engine.setValue(this.group, "eject", 0);
}
}
}
@@ -609,11 +766,312 @@ class Pot extends Component {
}
}
+class Mixer extends ComponentContainer {
+ constructor(inPackets, outPackets) {
+ super();
+
+ this.outPacket = outPackets[128];
+
+ this.mixerColumnDeck1 = new S4Mk3MixerColumn("[Channel1]", inPackets, outPackets[128],
+ {
+ saveGain: {inByte: 12, inBit: 0, outByte: 80},
+ effectUnit1Assign: {inByte: 3, inBit: 3, outByte: 78},
+ effectUnit2Assign: {inByte: 3, inBit: 4, outByte: 79},
+ gain: {inByte: 17},
+ eqHigh: {inByte: 45},
+ eqMid: {inByte: 47},
+ eqLow: {inByte: 49},
+ quickEffectKnob: {inByte: 65},
+ quickEffectButton: {},
+ volume: {inByte: 3},
+ pfl: {inByte: 8, inBit: 3, outByte: 77},
+ crossfaderSwitch: {inByte: 18, inBit: 4},
+ }
+ );
+ this.mixerColumnDeck2 = new S4Mk3MixerColumn("[Channel2]", inPackets, outPackets[128],
+ {
+ saveGain: {inByte: 12, inBit: 1, outByte: 84},
+ effectUnit1Assign: {inByte: 3, inBit: 5, outByte: 82},
+ effectUnit2Assign: {inByte: 3, inBit: 6, outByte: 83},
+ gain: {inByte: 19},
+ eqHigh: {inByte: 51},
+ eqMid: {inByte: 53},
+ eqLow: {inByte: 55},
+ quickEffectKnob: {inByte: 67},
+ volume: {inByte: 5},
+ pfl: {inByte: 8, inBit: 6, outByte: 81},
+ crossfaderSwitch: {inByte: 18, inBit: 2},
+ }
+ );
+ this.mixerColumnDeck3 = new S4Mk3MixerColumn("[Channel3]", inPackets, outPackets[128],
+ {
+ saveGain: {inByte: 3, inBit: 1, outByte: 88},
+ effectUnit1Assign: {inByte: 3, inBit: 0, outByte: 86},
+ effectUnit2Assign: {inByte: 3, inBit: 2, outByte: 87},
+ gain: {inByte: 15},
+ eqHigh: {inByte: 39},
+ eqMid: {inByte: 41},
+ eqLow: {inByte: 43},
+ quickEffectKnob: {inByte: 63},
+ volume: {inByte: 7},
+ pfl: {inByte: 8, inBit: 2, outByte: 85},
+ crossfaderSwitch: {inByte: 18, inBit: 6},
+ }
+ );
+ this.mixerColumnDeck4 = new S4Mk3MixerColumn("[Channel4]", inPackets, outPackets[128],
+ {
+ saveGain: {inByte: 12, inBit: 2, outByte: 92},
+ effectUnit1Assign: {inByte: 3, inBit: 7, outByte: 90},
+ effectUnit2Assign: {inByte: 12, inBit: 7, outByte: 91},
+ gain: {inByte: 21},
+ eqHigh: {inByte: 57},
+ eqMid: {inByte: 59},
+ eqLow: {inByte: 61},
+ quickEffectKnob: {inByte: 69},
+ volume: {inByte: 9},
+ pfl: {inByte: 8, inBit: 7, outByte: 89},
+ crossfaderSwitch: {inByte: 18, inBit: 0},
+ }
+ );
+
+ this.firstPressedFxSelector = null;
+ this.secondPressedFxSelector = null;
+ this.comboSelected = false;
+
+ const fxSelectsInputs = [
+ {inByte: 9, inBit: 5},
+ {inByte: 9, inBit: 1},
+ {inByte: 9, inBit: 6},
+ {inByte: 9, inBit: 0},
+ {inByte: 9, inBit: 7},
+ ];
+ this.fxSelects = [];
+ for (const i of [0, 1, 2, 3, 4]) {
+ this.fxSelects[i] = new FXSelect(
+ Object.assign(fxSelectsInputs[i], {
+ number: i + 1,
+ mixer: this,
+ })
+ );
+ }
+
+ const quickEffectInputs = [
+ {inByte: 8, inBit: 0, outByte: 46},
+ {inByte: 8, inBit: 5, outByte: 47},
+ {inByte: 8, inBit: 1, outByte: 48},
+ {inByte: 8, inBit: 4, outByte: 49},
+ ];
+ this.quickEffectButtons = [];
+ for (const i of [0, 1, 2, 3]) {
+ this.quickEffectButtons[i] = new QuickEffectButton(
+ Object.assign(quickEffectInputs[i], {
+ number: i + 1,
+ mixer: this,
+ })
+ );
+ }
+ this.resetFxSelectorColors();
+
+ this.quantizeButton = new Button({
+ input: function(pressed) {
+ if (pressed) {
+ this.globalQuantizeOn = !this.globalQuantizeOn;
+ for (let i = 1; i <= 4; i++) {
+ engine.setValue("[Channel" + i + "]", "quantize", this.globalQuantizeOn);
+ }
+ this.send(this.globalQuantizeOn ? 127 : 0);
+ }
+ },
+ globalQuantizeOn: false,
+ inByte: 12,
+ inBit: 6,
+ outByte: 93,
+ });
+
+ this.crossfader = new Pot({
+ group: "[Master]",
+ inKey: "crossfader",
+ inByte: 1,
+ inPacket: inPackets[2],
+ });
+ this.crossfaderCurveSwitch = new Component({
+ inByte: 19,
+ inBit: 0,
+ inBitLength: 2,
+ input: function(value) {
+ switch (value) {
+ case 0x00: // Picnic Bench / Fast Cut
+ engine.setValue("[Mixer Profile]", "xFaderMode", 0);
+ engine.setValue("[Mixer Profile]", "xFaderCalibration", 0.9);
+ engine.setValue("[Mixer Profile]", "xFaderCurve", 7.0);
+ break;
+ case 0x01: // Constant Power
+ engine.setValue("[Mixer Profile]", "xFaderMode", 1);
+ engine.setValue("[Mixer Profile]", "xFaderCalibration", 0.3);
+ engine.setValue("[Mixer Profile]", "xFaderCurve", 0.6);
+ break;
+ case 0x02: // Additive
+ engine.setValue("[Mixer Profile]", "xFaderMode", 0);
+ engine.setValue("[Mixer Profile]", "xFaderCalibration", 0.4);
+ engine.setValue("[Mixer Profile]", "xFaderCurve", 0.9);
+ }
+ },
+ });
+
+ for (const component of this) {
+ if (component.inPacket === undefined) {
+ component.inPacket = inPackets[1];
+ }
+ component.outPacket = this.outPacket;
+ component.inConnect();
+ component.outConnect();
+ component.outTrigger();
+ }
+
+ let lightQuantizeButton = true;
+ for (let i = 1; i <= 4; i++) {
+ if (!engine.getValue("[Channel" + i + "]", "quantize")) {
+ lightQuantizeButton = false;
+ }
+ }
+ this.quantizeButton.send(lightQuantizeButton ? 127 : 0);
+ this.quantizeButton.globalQuantizeOn = lightQuantizeButton;
+ }
+
+ calculatePresetNumber() {
+ if (this.firstPressedFxSelector === this.secondPressedFxSelector || this.secondPressedFxSelector === null) {
+ return this.firstPressedFxSelector;
+ }
+ let presetNumber = 5 + (4 * (this.firstPressedFxSelector - 1)) + this.secondPressedFxSelector;
+ if (this.secondPressedFxSelector > this.firstPressedFxSelector) {
+ presetNumber--;
+ }
+ return presetNumber;
+ }
+
+ resetFxSelectorColors() {
+ for (const selector of [1, 2, 3, 4, 5]) {
+ this.outPacket.data[49 + selector] = quickEffectPresetColors[selector - 1] + Button.prototype.brightnessOn;
+ }
+ console.log("Reset color");
+ this.outPacket.send();
+ }
+}
+
+class FXSelect extends Button {
+ constructor(options) {
+ super(options);
+
+ if (this.mixer === undefined) {
+ throw Error("The mixer must be specified");
+ }
+ }
+
+ onShortPress() {
+ if (this.mixer.firstPressedFxSelector === null) {
+ this.mixer.firstPressedFxSelector = this.number;
+ for (const selector of [1, 2, 3, 4, 5]) {
+ if (selector !== this.number) {
+ let presetNumber = 5 + (4 * (this.mixer.firstPressedFxSelector - 1)) + selector;
+ if (selector > this.number) {
+ presetNumber--;
+ }
+ this.outPacket.data[49 + selector] = quickEffectPresetColors[presetNumber - 1] + this.brightnessOn;
+ }
+ }
+ this.outPacket.send();
+ } else {
+ this.mixer.secondPressedFxSelector = this.number;
+ }
+
+ }
+
+ onShortRelease() {
+ // After a second selector was released, avoid loading a different preset when
+ // releasing the first pressed selector.
+ if (this.mixer.comboSelected && this.number === this.mixer.firstPressedFxSelector) {
+ this.mixer.comboSelected = false;
+ this.mixer.firstPressedFxSelector = null;
+ this.mixer.secondPressedFxSelector = null;
+ this.mixer.resetFxSelectorColors();
+ return;
+ }
+ // If mixer.firstPressedFxSelector === null, it was reset by the input handler for
+ // a QuickEffect enable button to load the preset for only one deck.
+ if (this.mixer.firstPressedFxSelector !== null) {
+ for (const deck of [1, 2, 3, 4]) {
+ const presetNumber = this.mixer.calculatePresetNumber();
+ engine.setValue("[QuickEffectRack1_[Channel" + deck + "]]", "loaded_chain_preset", presetNumber + 1);
+ }
+ }
+ if (this.mixer.firstPressedFxSelector === this.number) {
+ this.mixer.firstPressedFxSelector = null;
+ this.mixer.resetFxSelectorColors();
+ }
+ if (this.mixer.secondPressedFxSelector !== null) {
+ this.mixer.comboSelected = true;
+ }
+ this.mixer.secondPressedFxSelector = null;
+ }
+
+}
+
+
+class QuickEffectButton extends Button {
+ constructor(options) {
+ super(options);
+ if (this.mixer === undefined) {
+ throw Error("The mixer must be specified");
+ }
+ if (this.number === undefined || !Number.isInteger(this.number) || this.number < 1) {
+ throw Error("number attribute must be an integer >= 1");
+ }
+ this.group = "[QuickEffectRack1_[Channel" + this.number + "]]";
+ this.outConnect();
+ }
+ onShortPress() {
+ if (this.mixer.firstPressedFxSelector === null) {
+ script.toggleControl(this.group, "enabled");
+ } else {
+ const presetNumber = this.mixer.calculatePresetNumber();
+ this.color = quickEffectPresetColors[presetNumber - 1];
+ engine.setValue(this.group, "loaded_chain_preset", presetNumber + 1);
+ this.mixer.firstPressedFxSelector = null;
+ this.mixer.secondPressedFxSelector = null;
+ this.mixer.resetFxSelectorColors();
+ }
+ }
+ onLongRelease() {
+ if (this.mixer.firstPressedFxSelector === null) {
+ script.toggleControl(this.group, "enabled");
+ }
+ }
+ output(enabled) {
+ if (enabled) {
+ this.send(this.color + this.brightnessOn);
+ } else {
+ // It is easy to mistake the dim state for the bright state, so turn
+ // the LED fully off.
+ this.send(this.color + this.brightnessOff);
+ }
+ }
+ presetLoaded(presetNumber) {
+ this.color = quickEffectPresetColors[presetNumber - 2];
+ this.outConnections[1].trigger();
+ }
+ outConnect() {
+ if (this.group !== undefined) {
+ this.outConnections[0] = engine.makeConnection(this.group, "loaded_chain_preset", this.presetLoaded.bind(this));
+ this.outConnections[1] = engine.makeConnection(this.group, "enabled", this.output.bind(this));
+ }
+ }
+}
+
/*
* Kontrol S4 Mk3 hardware-specific constants
*/
-Pot.prototype.max = 2**12 - 1;
+Pot.prototype.max = 2 ** 12 - 1;
Pot.prototype.inBit = 0;
Pot.prototype.inBitLength = 16;
@@ -646,13 +1104,13 @@ Button.prototype.colorMap = new ColorMapper({
0xCCCCCC: LEDColors.white,
});
-const wheelRelativeMax = 2**16 - 1;
+const wheelRelativeMax = 2 ** 16 - 1;
const wheelAbsoluteMax = 2879;
-const wheelTimerMax = 2**32 - 1;
+const wheelTimerMax = 2 ** 32 - 1;
const wheelTimerTicksPerSecond = 100000000;
-const baseRevolutionsPerMinute = 33 + 1/3;
+const baseRevolutionsPerMinute = 33 + 1 / 3;
const baseRevolutionsPerSecond = baseRevolutionsPerMinute / 60;
const wheelTicksPerTimerTicksToRevolutionsPerSecond = wheelTimerTicksPerSecond / wheelAbsoluteMax;
@@ -665,10 +1123,20 @@ const wheelLEDmodes = {
individuallyAddressable: 5, // set byte 4 to 0 and set byes 8 - 40 to color values
};
+// The mode available, which the wheel can be used for.
const wheelModes = {
jog: 0,
vinyl: 1,
motor: 2,
+ loopIn: 3,
+ loopOut: 4,
+};
+
+const moveModes = {
+ beat: 0,
+ bpm: 1,
+ grid: 2,
+ keyboard: 3,
};
// tracks state across input packets
@@ -687,9 +1155,9 @@ let wheelTimerDelta = 0;
// from bright colors.
const uncoloredButtonOutput = function(value) {
if (value) {
- this.send(127);
+ this.send((this.color || LEDColors.white) + this.brightnessOn);
} else {
- this.send(0);
+ this.send((this.color || LEDColors.white) + this.brightnessOff);
}
};
@@ -697,6 +1165,8 @@ class S4Mk3EffectUnit extends ComponentContainer {
constructor(unitNumber, inPackets, outPacket, io) {
super();
this.group = "[EffectRack1_EffectUnit" + unitNumber + "]";
+ this.unitNumber = unitNumber;
+ this.focusedEffect = null;
this.mixKnob = new Pot({
inKey: "mix",
@@ -705,6 +1175,44 @@ class S4Mk3EffectUnit extends ComponentContainer {
inByte: io.mixKnob.inByte,
});
+ this.mainButton = new PowerWindowButton({
+ unit: this,
+ output: uncoloredButtonOutput,
+ inPacket: inPackets[1],
+ inByte: io.mainButton.inByte,
+ inBit: io.mainButton.inBit,
+ outByte: io.mainButton.outByte,
+ outPacket: outPacket,
+ shift: function() {
+ this.group = this.unit.group;
+ this.outKey = "group_[Master]_enable";
+ this.outConnect();
+ this.outTrigger();
+ },
+ unshift: function() {
+ this.outDisconnect();
+ this.outKey = undefined;
+ this.group = undefined;
+ uncoloredButtonOutput.call(this, false);
+ },
+ input: function(pressed) {
+ if (!this.shifted) {
+ for (const index of [0, 1, 2]) {
+ const effectGroup = "[EffectRack1_EffectUnit" + unitNumber + "_Effect" + (index + 1) + "]";
+ engine.setValue(effectGroup, "enabled", pressed);
+ }
+ this.output(pressed);
+ } else if (pressed) {
+ if (this.unit.focusedEffect !== null) {
+ this.unit.setFocusedEffect(null);
+ } else {
+ script.toggleControl(this.unit.group, "group_[Master]_enable");
+ this.shift();
+ }
+ }
+ }
+ });
+
this.knobs = [];
this.buttons = [];
for (const index of [0, 1, 2]) {
@@ -715,7 +1223,8 @@ class S4Mk3EffectUnit extends ComponentContainer {
inPacket: inPackets[2],
inByte: io.knobs[index].inByte,
});
- this.buttons[index] = new PowerWindowButton({
+ this.buttons[index] = new Button({
+ unit: this,
key: "enabled",
group: effectGroup,
output: uncoloredButtonOutput,
@@ -724,6 +1233,26 @@ class S4Mk3EffectUnit extends ComponentContainer {
inBit: io.buttons[index].inBit,
outByte: io.buttons[index].outByte,
outPacket: outPacket,
+ onShortPress: function() {
+ if (!this.shifted) {
+ script.toggleControl(this.group, this.inKey);
+ }
+ },
+ onLongPress: function() {
+ if (this.shifted) {
+ this.unit.setFocusedEffect(index);
+ }
+ },
+ onShortRelease: function() {
+ if (this.shifted) {
+ script.triggerControl(this.group, "next_effect");
+ }
+ },
+ onLongRelease: function() {
+ if (!this.shifted) {
+ script.toggleControl(this.group, this.inKey);
+ }
+ }
});
}
@@ -733,69 +1262,91 @@ class S4Mk3EffectUnit extends ComponentContainer {
component.outTrigger();
}
}
+ indicatorLoop() {
+ this.focusedEffectIndicator = !this.focusedEffectIndicator;
+ this.mainButton.output(true);
+ }
+ setFocusedEffect(effectIdx) {
+ this.mainButton.indicator(effectIdx !== null);
+ this.focusedEffect = effectIdx;
+ engine.setValue(this.group, "show_parameters", this.focusedEffect !== null);
+
+
+ const effectGroup = "[EffectRack1_EffectUnit" + this.unitNumber + "_Effect" + (this.focusedEffect + 1) + "]";
+ for (const index of [0, 1, 2]) {
+ this.buttons[index].outDisconnect();
+ this.buttons[index].group = this.focusedEffect === null ? "[EffectRack1_EffectUnit" + this.unitNumber + "_Effect" + (index + 1) + "]" : effectGroup;
+ this.buttons[index].inKey = this.focusedEffect === null ? "enabled" : "button_parameter" + (index + 1);
+ this.buttons[index].outKey = this.buttons[index].inKey;
+ this.knobs[index].group = this.buttons[index].group;
+ this.knobs[index].inKey = this.focusedEffect === null ? "meta" : "parameter" + (index + 1);
+ this.buttons[index].outConnect();
+ }
+ }
}
class S4Mk3Deck extends Deck {
- constructor(decks, colors, inPackets, outPacket, io) {
+ constructor(decks, colors, effectUnit, inPackets, outPacket, io) {
super(decks, colors);
this.playButton = new PlayButton({
output: uncoloredButtonOutput
});
- this.cueButton = new CueButton();
+ this.cueButton = new CueButton({
+ deck: this
+ });
+ this.effectUnit = effectUnit;
- const rateRanges = [0.04, 0.06, 0.08, 0.10, 0.16, 0.24, 0.5, 0.9];
- this.syncMasterButton = new ToggleButton({
+ this.syncMasterButton = new Button({
key: "sync_leader",
- input: function(pressed) {
- if (pressed) {
- if (!this.shifted) {
- script.toggleControl(this.group, this.inKey);
- } else {
- // It is possible for the rateRange to be set to a value
- // that is not in the rateRanges Array, so find the nearest
- // value in rateRanges.
- const currentRateRange = engine.getValue(this.group, "rateRange");