diff --git a/res/controllers/Traktor Kontrol S4 MK3.hid.xml b/res/controllers/Traktor Kontrol S4 MK3.hid.xml new file mode 100644 index 000000000000..c9852a1d40da --- /dev/null +++ b/res/controllers/Traktor Kontrol S4 MK3.hid.xml @@ -0,0 +1,17 @@ + + + + Traktor Kontrol S4 MK3 + Be + HID Mapping for Traktor Kontrol S4 MK3 + native_instruments_traktor_kontrol_s4_mk3 + + + + + + + + + + diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js new file mode 100644 index 000000000000..f8e954247e78 --- /dev/null +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -0,0 +1,1107 @@ +/* eslint no-redeclare: "off", no-unused-vars: "off" */ + +const longPressTimeOut = 225; + +// valid range 0 - 3 +const buttonBrightnessOff = 0; +const buttonBrightnessOn = 2; + +// add 0 - 3 to each color to control brightness +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 +}; + +const quickEffectPresetColors = [ + null, + LEDColors.red, + LEDColors.blue, + LEDColors.yellow, + LEDColors.purple, + + LEDColors.magenta, + LEDColors.azalea, + LEDColors.salmon, + + LEDColors.sky, + LEDColors.celeste, + LEDColors.fuscia, + + LEDColors.carrot, + LEDColors.orange, + LEDColors.honey, + + LEDColors.lime, + LEDColors.aqua, + LEDColors.green, +]; + +const colorMap = new ColorMapper({ + 0xCC0000: LEDColors.red, + 0xCC5E00: LEDColors.carrot, + 0xCC7800: LEDColors.orange, + 0xCC9200: LEDColors.honey, + + 0xCCCC00: LEDColors.yellow, + 0x81CC00: LEDColors.lime, + 0x00CC00: LEDColors.green, + 0x00CC49: LEDColors.aqua, + + 0x00CCCC: LEDColors.celeste, + 0x0091CC: LEDColors.sky, + 0x0000CC: LEDColors.blue, + 0xCC00CC: LEDColors.purple, + + 0xCC0091: LEDColors.fuscia, + 0xCC0079: LEDColors.magenta, + 0xCC477E: LEDColors.azalea, + 0xCC4761: LEDColors.salmon, + + 0xCCCCCC: LEDColors.white, +}); + +const potMax = 2**12 - 1; + +class HIDInputPacket { + constructor(reportId) { + this.reportId = reportId; + this.fields = []; + } + + registerCallback(callback, byteOffset, bitOffset, bitLength) { + if (typeof callback !== "function") { + throw Error("callback must be a function"); + } + + if (byteOffset === undefined || typeof byteOffset !== "number" || !Number.isInteger(byteOffset)) { + throw Error("byteOffset must be 0 or a positive integer"); + } + + if (bitOffset === undefined) { + bitOffset = 0; + } + if (typeof bitOffset !== "number" || bitOffset < 0 || !Number.isInteger(bitOffset)) { + throw Error("bitOffset must be 0 or a positive integer"); + } + + if (bitLength === undefined) { + bitLength = 1; + } + if (typeof bitLength !== "number" || bitLength < 1 || !Number.isInteger(bitOffset) || bitLength > 32) { + throw Error("bitLength must be an integer between 1 and 32"); + } + + this.fields.push({ + callback: callback, + byteOffset: byteOffset, + bitOffset: bitOffset, + bitLength: bitLength, + oldData: 0 + }); + } + + handleInput(byteArray) { + for (let field of this.fields) { + const view = new DataView(byteArray); + const numBytes = Math.ceil(field.bitLength / 8); + let data; + + if (numBytes === 1) { + data = view.getUint8(field.byteOffset); + } else if (numBytes === 2) { + data = view.getUint16(field.byteOffset); + } else if (numBytes === 3) { + data = view.getUint32(field.byteOffset) >>> 8; + } else if (numBytes === 4) { + data = view.getUint32(field.byteOffset); + } else { + throw Error("field bitLength must be between 1 and 32"); + } + + data = (data >>> field.bitOffset) & (2 ** field.bitLength - 1); + + if (field.oldData !== data) { + field.callback(data); + field.oldData = data; + } + } + } +} + +class HIDOutputPacket { + constructor(reportId, length) { + this.reportId = reportId; + this.data = Array(length).fill(0); + this.paused = false; + } + + send() { + if (!this.paused) { + controller.send(this.data, null, this.reportId); + } + } +} + +class Component { + constructor(options) { + Object.assign(this, options); + if (this.unshift !== undefined && typeof this.unshift === "function") { + this.unshift(); + } + this.shifted = false; + if (this.input !== undefined && typeof this.input === "function" + && this.inPacket !== undefined && this.inPacket instanceof HIDInputPacket) { + this.registerInput(); + } + this.outConnections = []; + this.outConnect(); + } + registerInput(callback) { + if (this.inByte === undefined + || this.inBit === undefined + || this.inBitLength === undefined + || this.inPacket === undefined) { + return; + } + if (typeof callback === "function") { + this.input = callback; + } + this.inPacket.registerCallback(this.input.bind(this), this.inByte, this.inBit, this.inBitLength); + } + send(value) { + if (this.outPacket !== undefined && this.outByte !== undefined) { + this.outPacket.data[this.outByte] = value; + this.outPacket.send(); + } + } + output(value) { + this.send(value); + } + outConnect() { + if (this.outKey !== undefined && this.group !== undefined) { + this.outConnections[0] = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); + } + } + outDisconnect() { + for (let connection of this.outConnections) { + connection.disconnect(); + } + } + outTrigger() { + for (let connection of this.outConnections) { + connection.trigger(); + } + } +} + +class ComponentContainer { + constructor() {} + *[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 (obj instanceof ComponentContainer) { + for (const nestedComponent of obj) { + yield nestedComponent; + } + // } else if (Array.isArray(obj)) { + // for (const nestedComponent of obj) { + //FIXME why is this always false + // if (nestedComponent instanceof Component) { + // yield nestedComponent; + // } + // } + } + } + } + } + reconnectComponents(callback) { + for (const component of this) { + if (component.outDisconnect !== undefined && typeof component.outDisconnect === "function") { + component.outDisconnect(); + } + if (callback !== undefined && typeof callback === "function") { + callback.call(this, component); + } + if (component.outConnect !== undefined && typeof component.outConnect === "function") { + component.outConnect(); + } + component.outTrigger(); + } + } + unshift() { + for (const component of this) { + if (component.unshift !== undefined && typeof component.unshift === "function") { + component.unshift(); + } + component.shifted = false; + } + } + shift() { + for (const component of this) { + if (component.shift !== undefined && typeof component.shift === "function") { + component.shift(); + } + component.shifted = true; + } + } +} + +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]); + console.log(this.group); + } + 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]; + } + } + 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) { + this.group = newGroup; + this.reconnectComponents(function(component) { + component.group = newGroup; + component.color = this.groupsToColors[newGroup]; + }); + } + static groupForNumber(deckNumber) { + return "[Channel" + deckNumber + "]"; + } +} + +class Button extends Component { + constructor(options) { + super(options); + this.off = 0; + if (this.inBitLength === undefined) { + this.inBitLength = 1; + } + } + output(value) { + const brightness = (value > 0) ? buttonBrightnessOn : buttonBrightnessOff; + this.send(this.color + brightness); + } +} + +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); + } + input(pressed) { + if (pressed) { + script.toggleControl(this.group, this.inKey); + } + } +} + +class PlayButton extends ToggleButton { + constructor(options) { + super(options); + this.inKey = "play"; + this.outKey = "play_indicator"; + this.outConnect(); + } +} + +class CueButton extends PushButton { + constructor(options) { + super(options); + this.outKey = "cue_indicator"; + this.outConnect(); + } + unshift() { + this.inKey = "cue_default"; + } + shift() { + this.inKey = "start_stop"; + } +} + +class Encoder extends Component { + constructor(options) { + super(options); + this.lastValue = null; + // specific to this hardware + this.max = 14; + } + isRightTurn(value) { + // detect wrap around + const oldValue = this.lastValue; + this.lastValue = value; + if (oldValue === this.max && value === 0) { + return true; + } + if (oldValue === 0 && value === this.max) { + return false; + } + return value > oldValue; + } +} + +// This implementation is specific to the Kontrol S4 Mk3 and not designed to be generalizable. +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"; + } + output(value) { + if (value) { + this.send(this.color + buttonBrightnessOn); + } else { + this.send(0); + } + } + outConnect() { + if (undefined !== this.group) { + this.outConnections[0] = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); + this.outConnections[1] = engine.makeConnection(this.group, this.colorKey, (colorCode) => { + this.color = colorMap.getValueForNearestColor(colorCode); + this.output(engine.getValue(this.group, this.outKey)); + }); + } + } +} + +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"; + } +} + +class Pot extends Component { + constructor(options) { + super(options); + this.firstValueReceived = false; + } + input(value) { + engine.setParameter(this.group, this.inKey, value/this.max); + if (!this.firstValueReceived) { + this.firstValueReceived = true; + this.connect(); + } + } + connect() { + engine.softTakeover(this.group, this.inKey, true); + } + disconnect() { + engine.softTakeoverIgnoreNextValue(this.group, this.inKey); + } +} + +class S4Mk3Deck extends Deck { + constructor(decks, colors, inputPacket, outputPacket, io) { + super(decks, colors); + + this.playButton = new PlayButton({ + output: function(value) { + // Unfortunately the hardware only supports green but not other colors + if (value) { + this.send(127); + } else { + // Turn the LED off completely because the dim green state + // is too easy to confuse with the bright green state. Light from + // the cue button bleeds through in the off state so the play button + // is not completely dark. + this.send(0); + } + }, + }); + + this.cueButton = new CueButton(); + this.syncButton = new ToggleButton({ + inKey: "sync_enabled", + outKey: "sync_enabled" + }); + + this.deckButtonLeft = new Button({ + deck: this, + input: function(value) { + if (value) { + this.deck.switchDeck(Deck.groupForNumber(decks[0])); + this.outPacket.data[io.deckButtonOutputByteOffset] = colors[0] + buttonBrightnessOn; + // turn off the other deck selection button's LED + this.outPacket.data[io.deckButtonOutputByteOffset+1] = 0; + this.outPacket.send(); + } + }, + }); + this.deckButtonRight = new Button({ + deck: this, + input: function(value) { + if (value) { + this.deck.switchDeck(Deck.groupForNumber(decks[1])); + // turn off the other deck selection button's LED + this.outPacket.data[io.deckButtonOutputByteOffset] = 0; + this.outPacket.data[io.deckButtonOutputByteOffset+1] = colors[1] + buttonBrightnessOn; + this.outPacket.send(); + } + }, + }); + + // set deck selection button LEDs + outputPacket.data[io.deckButtonOutputByteOffset] = colors[0] + buttonBrightnessOn; + outputPacket.data[io.deckButtonOutputByteOffset+1] = 0; + outputPacket.send(); + + this.tempoFader = new Pot({ + inKey: "rate", + max: potMax + }); + + this.tempoFaderCenteredLED = new Component({ + outKey: "rate", + centered: false, + output: function(value) { + const oldCentered = this.centered; + if (Math.abs(value) < 0.001) { + this.centered = true; + } else { + this.centered = false; + } + + // Avoid excessive output packets + if (oldCentered !== this.centered) { + if (this.centered) { + this.send(this.color + buttonBrightnessOn); + // round to precisely 0 + engine.setValue(this.group, "rate", 0); + } else { + this.send(0); + } + } + } + }); + + this.shiftButton = new PushButton({ + deck: this, + input: function(pressed) { + if (pressed) { + this.deck.shift(); + // This button only has one color. + this.send(LEDColors.white + buttonBrightnessOn); + } else { + this.deck.unshift(); + this.send(0); + } + }, + }); + + this.leftEncoder = new Encoder({ + deck: this, + input: function(value) { + const right = this.isRightTurn(value); + if (!this.shifted) { + if (!this.deck.leftEncoderPress.pressed) { + if (right) { + script.toggleControl(this.group, "beatjump_forward"); + } else { + script.toggleControl(this.group, "beatjump_backward"); + } + } else { + let beatjumpSize = engine.getValue(this.group, "beatjump_size"); + if (right) { + beatjumpSize *= 2; + } else { + beatjumpSize /= 2; + } + engine.setValue(this.group, "beatjump_size", beatjumpSize); + } + } else { + if (right) { + script.triggerControl(this.group, "pitch_up_small"); + } else { + script.triggerControl(this.group, "pitch_down_small"); + } + } + } + }); + this.leftEncoderPress = new PushButton({ + input: function(pressed) { + this.pressed = pressed; + if (pressed) { + script.toggleControl(this.group, "pitch_adjust_set_default"); + } + }, + }); + + this.rightEncoder = new Encoder({ + input: function(value) { + const right = this.isRightTurn(value); + if (right) { + script.toggleControl(this.group, "loop_double"); + } else { + script.toggleControl(this.group, "loop_halve"); + } + } + }); + this.rightEncoderPress = new PushButton({ + input: function(pressed) { + if (!pressed) { + return; + } + const loopEnabled = engine.getValue(this.group, "loop_enabled"); + if (!this.shifted) { + if (loopEnabled) { + script.triggerControl(this.group, "reloop_toggle"); + } else { + script.triggerControl(this.group, "beatloop_activate"); + } + } else { + if (loopEnabled) { + script.triggerControl(this.group, "reloop_andstop"); + } else { + script.triggerControl(this.group, "reloop_toggle"); + } + } + }, + }); + + this.libraryEncoder = new Encoder({ + input: function(value) { + const right = this.isRightTurn(value); + if (right) { + script.toggleControl("[Playlist]", "SelectNextTrack"); + } else { + script.toggleControl("[Playlist]", "SelectPrevTrack"); + } + } + }); + this.libraryEncoderPress = new ToggleButton({ + inKey: "LoadSelectedTrack" + }); + + this.pads = new ComponentContainer(); + const defaultPadLayer = [ + new IntroOutroButton({ + cueBaseName: "intro_start", + }), + new IntroOutroButton({ + cueBaseName: "intro_end", + }), + new IntroOutroButton({ + cueBaseName: "outro_start", + }), + new IntroOutroButton({ + cueBaseName: "outro_end", + }), + new HotcueButton({ + number: 1 + }), + new HotcueButton({ + number: 2 + }), + new HotcueButton({ + number: 3 + }), + new HotcueButton({ + number: 4 + }) + ]; + let i = 0; + for (const pad of defaultPadLayer) { + if (!(pad instanceof HotcueButton)) { + pad.color = this.color; + } + pad.group = this.group; + Object.assign(pad, io.pads[i]); + pad.inPacket = inputPacket; + pad.outPacket = outputPacket; + pad.registerInput(); + pad.outConnect(); + pad.outTrigger(); + this.pads[i] = pad; + i++; + } + + for (const property in this) { + if (Object.prototype.hasOwnProperty.call(this, property)) { + const component = this[property]; + if (component instanceof Component) { + component.inPacket = inputPacket; + component.outPacket = outputPacket; + component.group = this.group; + if (component.color === undefined) { + component.color = this.color; + } + Object.assign(component, io[property]); + component.registerInput(); + component.outConnect(); + component.outTrigger(); + } + } + } + } +} + +class S4MK3 { + constructor() { + this.inputPackets = []; + this.inputPackets[1] = new HIDInputPacket(1); + + this.outPackets = []; + this.outPackets[128] = new HIDOutputPacket(128, 94); + + // There is no consistent offset between the left and right deck, + // so every single components' IO needs to be specified individually + // for both decks. + this.leftDeck = new S4Mk3Deck( + [1, 3], [LEDColors.red, LEDColors.yellow], + this.inputPackets[1], this.outPackets[128], + { + playButton: {inByte: 5, inBit: 0, outByte: 55}, + cueButton: {inByte: 5, inBit: 1, outByte: 8}, + syncButton: {inByte: 6, inBit: 7, outByte: 14}, + deckButtonLeft: {inByte: 6, inBit: 2}, + deckButtonRight: {inByte: 6, inBit: 3}, + deckButtonOutputByteOffset: 12, + tempoFaderCenteredLED: {outByte: 11}, + shiftButton: {inByte: 6, inBit: 1, outByte: 59}, + leftEncoder: {inByte: 20, inBit: 0, inBitLength: 4}, + leftEncoderPress: {inByte: 7, inBit: 2}, + rightEncoder: {inByte: 20, inBit: 4, inBitLength: 4}, + rightEncoderPress: {inByte: 7, inBit: 5}, + libraryEncoder: {inByte: 21, inBit: 0, inBitLength: 4}, + libraryEncoderPress: {inByte: 1, inBit: 1}, + pads: [ + {inByte: 4, inBit: 5, outByte: 0}, + {inByte: 4, inBit: 4, outByte: 1}, + {inByte: 4, inBit: 7, outByte: 2}, + {inByte: 4, inBit: 6, outByte: 3}, + + {inByte: 4, inBit: 3, outByte: 4}, + {inByte: 4, inBit: 2, outByte: 5}, + {inByte: 4, inBit: 1, outByte: 6}, + {inByte: 4, inBit: 0, outByte: 7}, + ], + } + ); + + this.rightDeck = new S4Mk3Deck( + [2, 4], [LEDColors.blue, LEDColors.purple], + this.inputPackets[1], this.outPackets[128], + { + playButton: {inByte: 13, inBit: 0, outByte: 66}, + cueButton: {inByte: 15, inBit: 5, outByte: 31}, + syncButton: {inByte: 15, inBit: 4, outByte: 37}, + deckButtonLeft: {inByte: 15, inBit: 2}, + deckButtonRight: {inByte: 15, inBit: 3}, + deckButtonOutputByteOffset: 35, + tempoFaderCenteredLED: {outByte: 34}, + shiftButton: {inByte: 15, inBit: 1, outByte: 70}, + leftEncoder: {inByte: 21, inBit: 4, inBitLength: 4}, + leftEncoderPress: {inByte: 16, inBit: 5}, + rightEncoder: {inByte: 22, inBit: 0, inBitLength: 4}, + rightEncoderPress: {inByte: 16, inBit: 2}, + libraryEncoder: {inByte: 22, inBit: 4, inBitLength: 4}, + libraryEncoderPress: {inByte: 11, inBit: 1}, + pads: [ + {inByte: 14, inBit: 5, outByte: 23}, + {inByte: 14, inBit: 4, outByte: 24}, + {inByte: 14, inBit: 7, outByte: 25}, + {inByte: 14, inBit: 6, outByte: 26}, + + {inByte: 14, inBit: 3, outByte: 27}, + {inByte: 14, inBit: 2, outByte: 28}, + {inByte: 14, inBit: 1, outByte: 29}, + {inByte: 14, inBit: 0, outByte: 30}, + ], + } + ); + + const mixer = new ComponentContainer(); + mixer.firstPressedFxSelector = null; + mixer.secondPressedFxSelector = null; + const calculatePresetNumber = function() { + if (mixer.firstPressedFxSelector === mixer.secondPressedFxSelector || mixer.secondPressedFxSelector === null) { + return mixer.firstPressedFxSelector; + } + let presetNumber = 4 + (3 * (mixer.firstPressedFxSelector - 1)) + mixer.secondPressedFxSelector; + if (mixer.secondPressedFxSelector > mixer.firstPressedFxSelector) { + presetNumber--; + } + return presetNumber; + }; + mixer.comboSelected = false; + const resetFxSelectorColors = () => { + const packet = this.outPackets[128]; + for (const deck of [1, 2, 3, 4]) { + packet.data[49 + deck] = quickEffectPresetColors[deck] + buttonBrightnessOn; + } + packet.send(); + }; + const fxSelectInput = function(pressed) { + if (pressed) { + if (mixer.firstPressedFxSelector === null) { + mixer.firstPressedFxSelector = this.number; + for (const selector of [1, 2, 3, 4]) { + if (selector !== this.number) { + let presetNumber = 4 + (3 * (mixer.firstPressedFxSelector - 1)) + selector; + if (selector > this.number) { + presetNumber--; + } + this.outPacket.data[49 + selector] = quickEffectPresetColors[presetNumber] + buttonBrightnessOn; + } + } + this.outPacket.send(); + } else { + mixer.secondPressedFxSelector = this.number; + } + } else { + // After a second selector was released, avoid loading a different preset when + // releasing the first pressed selector. + if (mixer.comboSelected && this.number === mixer.firstPressedFxSelector) { + mixer.comboSelected = false; + mixer.firstPressedFxSelector = null; + mixer.secondPressedFxSelector = null; + 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 (mixer.firstPressedFxSelector !== null) { + for (const deck of [1, 2, 3, 4]) { + engine.setValue("[QuickEffectRack1_[Channel" + deck + "]]", "loaded_preset", calculatePresetNumber()); + } + } + if (mixer.firstPressedFxSelector === this.number) { + mixer.firstPressedFxSelector = null; + resetFxSelectorColors(); + } + if (mixer.secondPressedFxSelector !== null) { + mixer.comboSelected = true; + } + mixer.secondPressedFxSelector = null; + } + }; + mixer.fxSelect1 = new Button({ + inByte: 9, + inBit: 5, + inBitLength: 1, + number: 1, + input: fxSelectInput, + }); + mixer.fxSelect2 = new Button({ + inByte: 9, + inBit: 1, + inBitLength: 1, + number: 2, + input: fxSelectInput, + }); + mixer.fxSelect3 = new Button({ + inByte: 9, + inBit: 6, + inBitLength: 1, + number: 3, + input: fxSelectInput, + }); + mixer.fxSelect4 = new Button({ + inByte: 9, + inBit: 0, + inBitLength: 1, + number: 4, + input: fxSelectInput, + }); + + mixer.pfl1 = new ToggleButton({ + group: "[Channel1]", + inKey: "pfl", + outKey: "pfl", + inByte: 8, + inBit: 3, + inBitLength: 1, + outByte: 77, + // Unfortunately this button only has one color + color: LEDColors.white + }); + mixer.pfl2 = new ToggleButton({ + group: "[Channel2]", + inKey: "pfl", + outKey: "pfl", + inByte: 8, + inBit: 6, + inBitLength: 1, + outByte: 81, + // Unfortunately this button only has one color + color: LEDColors.white + }); + mixer.pfl3 = new ToggleButton({ + group: "[Channel3]", + inKey: "pfl", + outKey: "pfl", + inByte: 8, + inBit: 2, + inBitLength: 1, + outByte: 85, + // Unfortunately this button only has one color + color: LEDColors.white + }); + mixer.pfl4 = new ToggleButton({ + group: "[Channel4]", + inKey: "pfl", + outKey: "pfl", + inByte: 8, + inBit: 7, + inBitLength: 1, + outByte: 89, + // Unfortunately this button only has one color + color: LEDColors.white + }); + + const quickEffectButton = class extends Button { + constructor(options) { + super(options); + 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(); + this.isLongPressed = false; + this.longPressTimer = 0; + } + input(pressed) { + if (mixer.firstPressedFxSelector === null) { + if (pressed) { + script.toggleControl(this.group, "enabled"); + this.longPressTimer = engine.beginTimer(longPressTimeOut, () => { + this.isLongPressed = true; + this.longPressTimer = 0; + }, true); + } else { + if (this.isLongPressed) { + script.toggleControl(this.group, "enabled"); + } + if (this.longPressTimer !== 0) { + engine.stopTimer(this.longPressTimer); + } + this.longPressTimer = 0; + this.isLongPressed = false; + } + } else { + if (pressed) { + const presetNumber = calculatePresetNumber(); + this.color = quickEffectPresetColors[presetNumber]; + engine.setValue(this.group, "loaded_preset", presetNumber); + mixer.firstPressedFxSelector = null; + mixer.secondPressedFxSelector = null; + resetFxSelectorColors(); + } + } + } + presetLoaded(presetNumber) { + this.color = quickEffectPresetColors[presetNumber]; + this.outConnections[1].trigger(); + } + outConnect() { + if (this.group !== undefined) { + this.outConnections[0] = engine.makeConnection(this.group, "loaded_preset", this.presetLoaded.bind(this)); + this.outConnections[1] = engine.makeConnection(this.group, "enabled", this.output.bind(this)); + } + } + }; + mixer.quickEffectButton1 = new quickEffectButton({ + number: 1, + inByte: 8, + inBit: 0, + outByte: 46 + }); + mixer.quickEffectButton2 = new quickEffectButton({ + number: 2, + inByte: 8, + inBit: 5, + outByte: 47 + }); + mixer.quickEffectButton3 = new quickEffectButton({ + number: 3, + inByte: 8, + inBit: 1, + outByte: 48 + }); + mixer.quickEffectButton4 = new quickEffectButton({ + number: 4, + inByte: 8, + inBit: 4, + outByte: 49 + }); + resetFxSelectorColors(); + + for (const component of mixer) { + component.inPacket = this.inputPackets[1]; + component.outPacket = this.outPackets[128]; + component.registerInput(); + component.outConnect(); + component.outTrigger(); + } + + engine.softTakeover("[QuickEffectRack1_[Channel1]]", "super1", true); + engine.softTakeover("[QuickEffectRack1_[Channel2]]", "super1", true); + engine.softTakeover("[QuickEffectRack1_[Channel3]]", "super1", true); + engine.softTakeover("[QuickEffectRack1_[Channel4]]", "super1", true); + } + incomingData(data) { + const reportId = data[0]; + // slice off the reportId + if (reportId === 1) { + let string = ""; + for (let byte of data) { + if (byte === 0) { + // special case because Math.log(0) === Infinity + string = string + "0".repeat(8) + ","; + } else { + const numOfZeroes = 7 - Math.floor(Math.log(byte) / Math.log(2)); + string = string + "0".repeat(numOfZeroes) + byte.toString(2) + ","; + } + } + print(string); + + this.inputPackets[1].handleInput(data.buffer); + } else if (reportId === 2) { + const buffer = data.buffer.slice(1); + const view = new Uint16Array(buffer, 0, buffer.byteLength/2); + + engine.setParameter("[Master]", "crossfader", view[0]/potMax); + + engine.setParameter("[Channel1]", "volume", view[1]/potMax); + engine.setParameter("[Channel2]", "volume", view[2]/potMax); + engine.setParameter("[Channel3]", "volume", view[3]/potMax); + engine.setParameter("[Channel4]", "volume", view[4]/potMax); + + engine.setParameter("[Channel2]", "rate", view[5]/potMax); + this.leftDeck.tempoFader.input(view[6]); + + engine.setParameter("[Channel3]", "pregain", view[7]/potMax); + engine.setParameter("[Channel1]", "pregain", view[8]/potMax); + engine.setParameter("[Channel2]", "pregain", view[9]/potMax); + engine.setParameter("[Channel4]", "pregain", view[10]/potMax); + + // These control the controller's audio interface in hardware, so do not map them. + // engine.setParameter('[Master]', 'gain', view[11]/potMax); + // engine.setParameter('[Master]', 'booth_gain', view[12]/potMax); + // engine.setParameter('[Master]', 'headMix', view[13]/potMax); + // engine.setParameter('[Master]', 'headVolume', view[14]/potMax); + + engine.setParameter("[EffectRack1_EffectUnit1]", "mix", view[15]/potMax); + engine.setParameter("[EffectRack1_EffectUnit1_Effect1]", "meta", view[16]/potMax); + engine.setParameter("[EffectRack1_EffectUnit1_Effect2]", "meta", view[17]/potMax); + engine.setParameter("[EffectRack1_EffectUnit1_Effect3]", "meta", view[18]/potMax); + + engine.setParameter("[EqualizerRack1_[Channel3]_Effect1]", "parameter3", view[19]/potMax); + engine.setParameter("[EqualizerRack1_[Channel3]_Effect1]", "parameter2", view[20]/potMax); + engine.setParameter("[EqualizerRack1_[Channel3]_Effect1]", "parameter1", view[21]/potMax); + + engine.setParameter("[EqualizerRack1_[Channel1]_Effect1]", "parameter3", view[22]/potMax); + engine.setParameter("[EqualizerRack1_[Channel1]_Effect1]", "parameter2", view[23]/potMax); + engine.setParameter("[EqualizerRack1_[Channel1]_Effect1]", "parameter1", view[24]/potMax); + + engine.setParameter("[EqualizerRack1_[Channel2]_Effect1]", "parameter3", view[25]/potMax); + engine.setParameter("[EqualizerRack1_[Channel2]_Effect1]", "parameter2", view[26]/potMax); + engine.setParameter("[EqualizerRack1_[Channel2]_Effect1]", "parameter1", view[27]/potMax); + + engine.setParameter("[EqualizerRack1_[Channel4]_Effect1]", "parameter3", view[28]/potMax); + engine.setParameter("[EqualizerRack1_[Channel4]_Effect1]", "parameter2", view[29]/potMax); + engine.setParameter("[EqualizerRack1_[Channel4]_Effect1]", "parameter1", view[30]/potMax); + + engine.setParameter("[QuickEffectRack1_[Channel3]]", "super1", view[31]/potMax); + engine.setParameter("[QuickEffectRack1_[Channel1]]", "super1", view[32]/potMax); + engine.setParameter("[QuickEffectRack1_[Channel2]]", "super1", view[33]/potMax); + engine.setParameter("[QuickEffectRack1_[Channel4]]", "super1", view[34]/potMax); + + engine.setParameter("[EffectRack1_EffectUnit2]", "mix", view[35]/potMax); + engine.setParameter("[EffectRack1_EffectUnit2_Effect1]", "meta", view[36]/potMax); + engine.setParameter("[EffectRack1_EffectUnit2_Effect2]", "meta", view[37]/potMax); + engine.setParameter("[EffectRack1_EffectUnit2_Effect3]", "meta", view[38]/potMax); + } + } + init() { + // controller.send([ + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0 + // ], null, 128); + } + shutdown() { + controller.send(new Array(94).fill(0), null, 128); + } +} + +var TraktorS4MK3 = new S4MK3();