diff options
-rw-r--r-- | res/controllers/Traktor Kontrol S4 MK3.hid.xml | 17 | ||||
-rw-r--r-- | res/controllers/Traktor-Kontrol-S4-MK3.js | 2040 |
2 files changed, 2057 insertions, 0 deletions
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 0000000000..c9852a1d40 --- /dev/null +++ b/res/controllers/Traktor Kontrol S4 MK3.hid.xml @@ -0,0 +1,17 @@ +<?xml version='1.0' encoding='utf-8'?> +<MixxxControllerPreset mixxxVersion="2.4.0" schemaVersion="1"> + <info> + <name>Traktor Kontrol S4 MK3</name> + <author>Be</author> + <description>HID Mapping for Traktor Kontrol S4 MK3</description> + <manual>native_instruments_traktor_kontrol_s4_mk3</manual> + <devices> + <product protocol="hid" vendor_id="0x17cc" product_id="0x1720" usage_page="0xff01" usage="0x1" interface_number="0x4" /> + </devices> + </info> + <controller id="Traktor"> + <scriptfiles> + <file filename="Traktor-Kontrol-S4-MK3.js" functionprefix="TraktorS4MK3"/> + </scriptfiles> + </controller> +</MixxxControllerPreset> diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js new file mode 100644 index 0000000000..2c53b65442 --- /dev/null +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -0,0 +1,2040 @@ +/// Copyright (C) 2022 Be <be@mixxx.org> +/// +/// 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 +/// the Free Software Foundation; either version 2 of the License, or +/// (at your option) any later version. The full text of the GNU +/// General Public License, version 2 can be found below. The licenses +/// of software libraries distributed together with Mixxx can be found +/// below as well. +/// +/// In addition to the terms of the GNU General Public License, the following +/// license terms apply: +/// +/// By using this mapping, you confirm that you are not Bob Ham, you are in no +/// way affiliated to Bob Ham, you are not downloading this code on behalf of +/// Bob Ham or an associate of Bob Ham. To the best of your knowledge, information +/// and belief this mapping will not make its way into the hands of Bob Ham. + +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, +}; + +/* + * USER CONFIGURABLE SETTINGS + * Adjust these to your liking + */ + +const deckColors = [ + LEDColors.red, + LEDColors.blue, + LEDColors.yellow, + LEDColors.purple, +]; + +const tempoFaderSoftTakeoverColorLow = LEDColors.white; +const tempoFaderSoftTakeoverColorHigh = LEDColors.green; + +// 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.blue, + LEDColors.yellow, + 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.orange, + LEDColors.honey, + LEDColors.yellow + 1, + + LEDColors.lime, + LEDColors.aqua, + LEDColors.green, + 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; + +/* + * HID packet parsing library + */ + +class HIDInputPacket { + constructor(reportId) { + this.reportId = reportId; + this.fields = []; + } + + registerCallback(callback, byteOffset, bitOffset, bitLength, signed) { + 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"); + } + + if (signed === undefined) { + signed = false; + } + + const field = { + callback: callback, + byteOffset: byteOffset, + bitOffset: bitOffset, + bitLength: bitLength, + oldData: 0 + }; + this.fields.push(field); + + return { + disconnect: () => { + this.fields = this.fields.filter((element) => { + return element !== field; + }); + } + }; + } + + handleInput(byteArray) { + const view = new DataView(byteArray); + if (view.getUint8(0) !== this.reportId) { + return; + } + + 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 HIDOutputPacket { + constructor(reportId, length) { + this.reportId = reportId; + this.data = Array(length).fill(0); + } + send() { + controller.send(this.data, null, this.reportId); + } +} + +/* + * Components library + */ + +class Component { + constructor(options) { + Object.assign(this, options); + if (options !== undefined && typeof options.key === "string") { + this.inKey = options.key; + this.outKey = options.key; + } + 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.inConnect(); + } + this.outConnections = []; + this.outConnect(); + } + inConnect(callback) { + if (this.inByte === undefined + || this.inBit === undefined + || this.inBitLength === undefined + || this.inPacket === undefined) { + return; + } + if (typeof callback === "function") { + this.input = callback; + } + this.inConnection = this.inPacket.registerCallback(this.input.bind(this), this.inByte, this.inBit, this.inBitLength); + } + inDisconnect() { + if (this.inConnection !== undefined) { + this.inConnection.disconnect(); + } + } + 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 (const connection of this.outConnections) { + connection.disconnect(); + } + } + outTrigger() { + for (const 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 objectInArray of obj) { + if (objectInArray instanceof Component) { + yield objectInArray; + } else if (objectInArray instanceof ComponentContainer) { + for (const doublyNestedComponent of objectInArray) { + yield doublyNestedComponent; + } + } + } + } + } + } + } + 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; + } + } +} + +/* 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]; + } + } + 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.color = this.groupsToColors[newGroup]; + 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]; + }); + } + static groupForNumber(deckNumber) { + return "[Channel" + deckNumber + "]"; + } +} + +class Button extends Component { + constructor(options) { + super(options); + this.off = 0; + if (this.longPressTimeOut === undefined) { + this.longPressTimeOut = 225; // milliseconds + } + if (this.inBitLength === undefined) { + this.inBitLength = 1; + } + } + output(value) { + const brightness = (value > 0) ? this.brightnessOn : this.brightnessOff; + 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 PowerWindowButton extends Button { + constructor(options) { + super(options); + 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; + } + } +} + +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; + } + 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; + } +} + +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 + this.brightnessOn); + } 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 = this.colorMap.getValueForNearestColor(colorCode); + this.output(engine.getValue(this.group, this.outKey)); + }); + } + } +} + +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(); + } + input(pressed) { + 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); + } + } + } else { + if (pressed) { + if (engine.getValue(this.group, "play") === 1) { + engine.setValue(this.group, "play", 0); + } else { + engine.setValue(this.group, "eject", 1); + } + } else { + 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) { + this.outConnections[0] = engine.makeConnection(this.group, "play", this.output.bind(this)); + this.outConnections[1] = engine.makeConnection(this.group, "track_loaded", this.output.bind(this)); + } + } +} + +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; + } + input(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); + } +} + +/* + * Kontrol S4 Mk3 hardware-specific constants + */ + +Pot.prototype.max = 2**12 - 1; +Pot.prototype.inBit = 0; +Pot.prototype.inBitLength = 16; + +Encoder.prototype.inBitLength = 4; + +// valid range 0 - 3, but 3 makes some colors appear whitish +Button.prototype.brightnessOff = 0; +Button.prototype.brightnessOn = 2; +Button.prototype.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 wheelRelativeMax = 2**16 - 1; +const wheelAbsoluteMax = 2879; + +const wheelTimerMax = 2**32 - 1; +const wheelTimerTicksPerSecond = 100000000; + +const baseRevolutionsPerMinute = 33 + 1/3; +const baseRevolutionsPerSecond = baseRevolutionsPerMinute / 60; +const wheelTicksPerTimerTicksToRevolutionsPerSecond = wheelTimerTicksPerSecond / wheelAbsoluteMax; + +const wheelLEDmodes = { + off: 0, + dimFlash: 1, + spot: 2, + ringFlash: 3, + dimSpot: 4, + individuallyAddressable: 5, // set byte 4 to 0 and set byes 8 - 40 to color values +}; + +const wheelModes = { + jog: 0, + vinyl: 1, + motor: 2, +}; + +// tracks state across input packets +let wheelTimer = null; +// This is a global variable so the S4Mk3Deck Components have access +// to it and it is guaranteed to be calculated before processing +// input for the Components. +let wheelTimerDelta = 0; + +/* + * Kontrol S4 Mk3 hardware specific mapping logic + */ + +// used for buttons whose LEDs only support a single color +// Don't use dim colors for these because they are hard to tell apart +// from bright colors. +const uncoloredButtonOutput = function(value) { + if (value) { + this.send(127); + } else { + this.send(0); + } +}; + +class S4Mk3EffectUnit extends ComponentContainer { + constructor(unitNumber, inPackets, outPacket, io) { + super(); + this.group = "[EffectRack1_EffectUnit" + unitNumber + "]"; + + this.mixKnob = new Pot({ + inKey: "mix", + group: this.group, + inPacket: inPackets[2], + inByte: io.mixKnob.inByte, + }); + + this.knobs = []; + this.buttons = []; + for (const index of [0, 1, 2]) { + const effectGroup = "[EffectRack1_EffectUnit" + unitNumber + "_Effect" + (index + 1) + "]"; + this.knobs[index] = new Pot({ + inKey: "meta", + group: effectGroup, + inPacket: inPackets[2], + inByte: io.knobs[index].inByte, + }); + this.buttons[index] = new PowerWindowButton({ + key: "enabled", + group: effectGroup, + output: uncoloredButtonOutput, + inPacket: inPackets[1], + inByte: io.buttons[index].inByte, + inBit: io.buttons[index].inBit, + outByte: io.buttons[index].outByte, + outPacket: outPacket, + }); + } + + for (const component of this) { + component.inConnect(); + component.outConnect(); + component.outTrigger(); + } + } +} + +class S4Mk3Deck extends Deck { + constructor(decks, colors, inPackets, outPacket, io) { + super(decks, colors); + + this.playButton = new PlayButton({ + output: uncoloredButtonOutput + }); + + this.cueButton = new CueButton(); + + const rateRanges = [0.04, 0.06, 0.08, 0.10, 0.16, 0.24, 0.5, 0.9]; + this.syncMasterButton = new ToggleButton({ + 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"); + let previousDiff = null; + let newRateRange = rateRanges[0]; + for (let i = 0; i < rateRanges.length - 1; i++) { + const currentDiff = Math.abs(rateRanges[i] - currentRateRange); + if (currentDiff < previousDiff || previousDiff === null) { + newRateRange = rateRanges[i + 1]; + } + previousDiff = currentDiff; + } + engine.setValue(this.group, "rateRange", newRateRange); + } + } + }, + }); + this.syncButton = new ToggleButton({ + key: "sync_enabled", + input: function(pressed) { + if (pressed) { + if (!this.shifted) { + script.toggleControl(this.group, this.inKey); + engine.softTakeover(this.group, "rate", true); + } 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"); + let previousDiff = null; + let newRateRange = rateRanges[0]; + for (let i = rateRanges.length - 1; i > 0; i--) { + const currentDiff = Math.abs(rateRanges[i] - currentRateRange); + if (currentDiff < previousDiff || previousDiff === null) { + newRateRange = rateRanges[i - 1]; + } + previousDiff = currentDiff; + } + engine.setValue(this.group, "rateRange", newRateRange); + } + } + }, + }); + this.tempoFader = new Pot({ + inKey: "rate", + }); + this.tempoFaderLED = new Component({ + outKey: "rate", + centered: false, + toleranceWindow: 0.001, + tempoFader: this.tempoFader, + output: function(value) { + if (this.tempoFader.hardwarePosition === null) { + return; + } + + const parameterValue = engine.getParameter(this.group, this.outKey); + const diffFromHardware = parameterValue - this.tempoFader.hardwarePosition; + if (diffFromHardware > this.toleranceWindow) { + this.send(tempoFaderSoftTakeoverColorHigh + Button.prototype.brightnessOn); + return; + } else if (diffFromHardware < (-1 * this.toleranceWindow)) { + this.send(tempoFaderSoftTakeoverColorLow + Button.prototype.brightnessOn); + return; + } + + const oldCentered = this.centered; + if (Math.abs(value) < 0.001) { + this.send(this.color + Button.prototype.brightnessOn); + // round to precisely 0 + engine.setValue(this.group, "rate", 0); + } else { + this.send(0); + } + } + }); + + this.reverseButton = new PushButton({ + key: "reverseroll", + output: uncoloredButtonOutput, + }); + this.fluxButton = new PushButton({ + key: "slip_enabled", + output: uncoloredButtonOutput, + }); + this.gridButton = new PushButton({ + key: "beats_translate_curpos", + }); + + 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] + this.brightnessOn; + // 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] + this.brightnessOn; + this.outPacket.send(); + } + }, + }); + + // set deck selection button LEDs + outPacket.data[io.deckButtonOutputByteOffset] = colors[0] + Button.prototype.brightnessOn; + outPacket.data[io.deckButtonOutputByteOffset+1] = 0; + outPacket.send(); + + this.shiftButton = new PushButton({ + deck: this, + input: function(pressed) { + if (pressed) { + this.deck.shift(); + // This button only has one color. + this.send(LEDColors.white + this.brightnessOn); + } else { + this.deck.unshift(); + this.send(LEDColors.white + this.brightnessOff); + } + }, + }); + + 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.triggerControl(this.group, "beatjump_forward"); + } else { + script.triggerControl(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 { + // FIXME: temporary hack until jog wheels are working + if (right) { + engine.setValue(this.group, "jog", 3); + // script.triggerControl(this.group, "pitch_up_small"); + } else { + engine.setValue(this.group, "jog", -3); + // 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 (!this.shifted) { + if (right) { + script.triggerControl(this.group, "loop_double"); + } else { + script.triggerControl(this.group, "loop_halve"); + } + } else { + if (right) { + script.triggerControl(this.group, "beatjump_1_forward"); + } else { + script.triggerControl(this.group, "beatjump_1_backward"); + } + } + } + }); + this.rightEncoderPress = new PushButton({ + input: function(pressed) { + if (!pressed) { + return; + } + const loopEnabled = engine.getValue(this.group, "loop_enabled"); + if (!this.shifted) { + 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); + const previewPlaying = engine.getValue("[PreviewDeck1]", "play"); + if (previewPlaying) { + if (right) { + script.triggerControl("[PreviewDeck1]", "beatjump_16_forward"); + } else { + script.triggerControl("[PreviewDeck1]", "beatjump_16_backward"); + } + } else { + engine.setValue("[Library]", "MoveVertical", right ? 1 : -1); + } + } + }); + this.libraryEncoderPress = new ToggleButton({ + inKey: "LoadSelectedTrack" + }); + this.libraryPlayButton = new PushButton({ + group: "[PreviewDeck1]", + input: function(pressed) { + if (pressed) { + if (engine.getValue(this.group, "play")) { + engine.setValue(this.group, "play", 0); + } else { + script.triggerControl(this.group, "LoadSelectedTrackAndPlay"); + } + } + }, + outKey: "play", + }); + this.libraryStarButton = new PushButton({ + group: "[Library]", + key: "MoveFocusForward", + }); + this.libraryPlaylistButton = new PushButton({ + group: "[Library]", + key: "MoveFocusBackward", + }); + this.libraryViewButton = new ToggleButton({ + group: "[Master]", |