From 18f9fd7e7ed8003b536a514666d78554357b37fa Mon Sep 17 00:00:00 2001 From: Be Date: Sat, 3 Jul 2021 22:14:06 -0500 Subject: [PATCH 01/19] Kontrol S4 Mk3: initial commit --- .../Traktor Kontrol S4 MK3.hid.xml | 17 + res/controllers/Traktor-Kontrol-S4-MK3.js | 2040 +++++++++++++++++ 2 files changed, 2057 insertions(+) create mode 100644 res/controllers/Traktor Kontrol S4 MK3.hid.xml create mode 100644 res/controllers/Traktor-Kontrol-S4-MK3.js 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..2c53b65442d7 --- /dev/null +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -0,0 +1,2040 @@ +/// Copyright (C) 2022 Be +/// +/// 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]", + inKey: "maximize_library", + }); + + this.pads = Array(8).fill(new Component()); + 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 + }) + ]; + const hotcuePage2 = Array(8).fill({}); + const hotcuePage3 = Array(8).fill({}); + const samplerPage1 = Array(8).fill({}); + const samplerPage2 = Array(8).fill({}); + let i = 0; + /* eslint no-unused-vars: "off" */ + for (const pad of hotcuePage2) { + // start with hotcue 5; hotcues 1-4 are in defaultPadLayer + hotcuePage2[i] = new HotcueButton({number: i + 1}); + hotcuePage3[i] = new HotcueButton({number: i + 13}); + let samplerNumber = i + 1; + if (samplerNumber > 4) { + samplerNumber += 4; + } + if (decks[0] > 1) { + samplerNumber += 4; + } + samplerPage1[i] = new SamplerButton({number: samplerNumber}); + samplerPage2[i] = new SamplerButton({number: samplerNumber + 16}); + if (samplerCrossfaderAssign) { + engine.setValue( + "[Sampler" + samplerNumber + "]", + "orientation", + (decks[0] === 1) ? 0 : 2 + ); + } + i++; + } + + const switchPadLayer = (deck, newLayer) => { + let index = 0; + for (let pad of deck.pads) { + pad.outDisconnect(); + pad.inDisconnect(); + + pad = newLayer[index]; + Object.assign(pad, io.pads[index]); + if (!(pad instanceof HotcueButton)) { + pad.color = deck.color; + } + // don't change the group of SamplerButtons + if (!(pad instanceof SamplerButton)) { + pad.group = deck.group; + } + if (pad.inPacket === undefined) { + pad.inPacket = inPackets[1]; + } + pad.outPacket = outPacket; + pad.inConnect(); + pad.outConnect(); + pad.outTrigger(); + deck.pads[index] = pad; + index++; + } + }; + + this.padLayers = { + defaultLayer: 0, + hotcuePage2: 1, + hotcuePage3: 2, + samplerPage1: 3, + samplerPage2: 4, + }; + switchPadLayer(this, defaultPadLayer); + this.currentPadLayer = this.padLayers.defaultLayer; + + this.hotcuePadModeButton = new Button({ + deck: this, + input: function(pressed) { + if (!this.shifted) { + if (pressed) { + if (this.deck.currentPadLayer !== this.deck.padLayers.hotcuePage2) { + switchPadLayer(this.deck, hotcuePage2); + this.deck.currentPadLayer = this.deck.padLayers.hotcuePage2; + } else { + switchPadLayer(this.deck, defaultPadLayer); + this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; + } + this.deck.lightPadMode(); + } + } else { + engine.setValue(this.deck.group, "loop_in", pressed); + } + }, + // make sure loop_in gets reset to 0 if shift is released before this button + unshift: function() { + if (engine.getValue(this.deck.group, "loop_in") === 1) { + engine.setValue(this.deck.group, "loop_in", 0); + } + }, + // hack to switch the LED color when changing decks + outTrigger: function() { + this.deck.lightPadMode(); + } + }); + this.recordPadModeButton = new Button({ + deck: this, + input: function(pressed) { + if (!this.shifted) { + if (pressed) { + if (this.deck.currentPadLayer !== this.deck.padLayers.hotcuePage3) { + switchPadLayer(this.deck, hotcuePage3); + this.deck.currentPadLayer = this.deck.padLayers.hotcuePage3; + } else { + switchPadLayer(this.deck, defaultPadLayer); + this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; + } + this.deck.lightPadMode(); + } + } else { + engine.setValue(this.deck.group, "loop_out", pressed); + } + }, + // make sure loop_out gets reset to 0 if shift is released before this button + unshift: function() { + if (engine.getValue(this.deck.group, "loop_out") === 1) { + engine.setValue(this.deck.group, "loop_out", 0); + } + } + }); + this.samplesPadModeButton = new Button({ + deck: this, + input: function(pressed) { + if (pressed) { + if (this.deck.currentPadLayer !== this.deck.padLayers.samplerPage1) { + switchPadLayer(this.deck, samplerPage1); + this.deck.currentPadLayer = this.deck.padLayers.samplerPage1; + } else { + switchPadLayer(this.deck, defaultPadLayer); + this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; + } + this.deck.lightPadMode(); + } + }, + }); + this.mutePadModeButton = new Button({ + deck: this, + input: function(pressed) { + if (pressed) { + if (this.deck.currentPadLayer !== this.deck.padLayers.samplerPage2) { + switchPadLayer(this.deck, samplerPage2); + this.deck.currentPadLayer = this.deck.padLayers.samplerPage2; + } else { + switchPadLayer(this.deck, defaultPadLayer); + this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; + } + this.deck.lightPadMode(); + } + }, + }); + + this.stemsPadModeButton = new ToggleButton({ + key: "keylock", + }); + + this.lightPadMode = function() { + const hotcuePadModeLEDOn = this.currentPadLayer === this.padLayers.hotcuePage2; + this.hotcuePadModeButton.send(this.color + (hotcuePadModeLEDOn ? this.brightnessOn : this.brightnessOff)); + + // unfortunately the other pad mode buttons only have one LED color + const recordPadModeLEDOn = this.currentPadLayer === this.padLayers.hotcuePage3; + this.recordPadModeButton.send(recordPadModeLEDOn ? 127 : 0); + + const samplesPadModeLEDOn = this.currentPadLayer === this.padLayers.samplerPage1; + this.samplesPadModeButton.send(samplesPadModeLEDOn ? 127 : 0); + + const mutePadModeButtonLEDOn = this.currentPadLayer === this.padLayers.samplerPage2; + this.mutePadModeButton.send(mutePadModeButtonLEDOn ? 127 : 0); + }; + + this.wheelMode = wheelModes.vinyl; + let motorWindDownTimer = 0; + const motorWindDownTimerCallback = () => { + engine.stopTimer(motorWindDownTimer); + motorWindDownTimer = 0; + }; + const motorWindDownMilliseconds = 900; + this.turntableButton = new Button({ + deck: this, + input: function(press) { + if (press) { + if (this.deck.wheelMode === wheelModes.motor) { + this.deck.wheelMode = wheelModes.vinyl; + motorWindDownTimer = engine.beginTimer(motorWindDownMilliseconds, motorWindDownTimerCallback, true); + // engine.setValue(this.group, "scratch2_enable", false); + } else { + this.deck.wheelMode = wheelModes.motor; + // engine.setValue(this.group, "scratch2_enable", true); + } + this.outTrigger(); + } + }, + outTrigger: function() { + const motorOn = this.deck.wheelMode === wheelModes.motor; + this.send(this.color + (motorOn ? this.brightnessOn : this.brightnessOff)); + const vinylModeOn = this.deck.wheelMode === wheelModes.vinyl; + this.deck.jogButton.send(this.color + (vinylModeOn ? this.brightnessOn : this.brightnessOff)); + }, + }); + this.jogButton = new Button({ + deck: this, + input: function(press) { + if (press) { + if (this.deck.wheelMode === wheelModes.vinyl) { + this.deck.wheelMode = wheelModes.jog; + } else { + if (this.deck.wheelMode === wheelModes.motor) { + motorWindDownTimer = engine.beginTimer(motorWindDownMilliseconds, motorWindDownTimerCallback, true); + } + this.deck.wheelMode = wheelModes.vinyl; + } + engine.setValue(this.group, "scratch2_enable", false); + this.outTrigger(); + } + }, + outTrigger: function() { + const vinylOn = this.deck.wheelMode === wheelModes.vinyl; + this.send(this.color + (vinylOn ? this.brightnessOn : this.brightnessOff)); + const motorOn = this.deck.wheelMode === wheelModes.motor; + this.deck.turntableButton.send(this.color + (motorOn ? this.brightnessOn : this.brightnessOff)); + }, + }); + + this.wheelTouch = new Button({ + touched: false, + deck: this, + input: function(touched) { + this.touched = touched; + if (this.deck.wheelMode !== wheelModes.jog) { + if (touched) { + engine.setValue(this.group, "scratch2_enable", true); + } else { + // The wheel keeps spinning + engine.beginTimer(600, () => { + engine.setValue(this.group, "scratch2_enable", false); + }, true); + } + } + }, + }); + + // The relative and absolute position inputs have the same resolution but direction + // cannot be determined reliably with the absolute position because it is easily + // possible to spin the wheel fast enough that it spins more than half a revolution + // between input packets. So there is no need to process the absolution position + // at all; the relative position is sufficient. + this.wheelRelative = new Component({ + oldValue: null, + deck: this, + input: function(value) { + const oldValue = this.oldValue; + this.oldValue = value; + let diff = value - oldValue; + if (diff === 0 || oldValue === null || motorWindDownTimer !== 0) { + return; + } + + if (diff > wheelRelativeMax / 2) { + diff = (wheelRelativeMax - value + oldValue) * -1; + } else if (diff < -1 * (wheelRelativeMax / 2)) { + diff = wheelRelativeMax - oldValue + value; + } + + const wheelVelocity = diff / wheelTimerDelta * wheelTicksPerTimerTicksToRevolutionsPerSecond; + // if (this.group === "[Channel1]") { + // console.log(value + "\t" + diff + "\t" + wheelTimerDelta + "\t" + wheelVelocity + "\t" + wheelVelocity / baseRevolutionsPerSecond); + // } + if (engine.getValue(this.group, "scratch2_enable")) { + engine.setValue(this.group, "scratch2", wheelVelocity / baseRevolutionsPerSecond); + } else { + if (this.deck.wheelMode === wheelModes.motor + || (this.deck.wheelMode === wheelModes.jog && this.deck.wheelTouch.touched) + ) { + return; + } + engine.setValue(this.group, "jog", wheelVelocity * 4); + } + }, + }); + + this.wheelLED = new Component({ + outKey: "playposition", + output: function(fractionOfTrack) { + const durationSeconds = engine.getValue(this.group, "duration"); + const positionSeconds = fractionOfTrack * durationSeconds; + const revolutions = positionSeconds * baseRevolutionsPerSecond; + const fractionalRevolution = revolutions - Math.floor(revolutions); + const LEDposition = fractionalRevolution * wheelAbsoluteMax; + + const wheelOutput = Array(40).fill(0); + wheelOutput[0] = decks[0] - 1; + wheelOutput[1] = wheelLEDmodes.spot; + wheelOutput[2] = LEDposition & (2**8 - 1); + wheelOutput[3] = LEDposition >> 8; + wheelOutput[4] = this.color + Button.prototype.brightnessOn; + controller.send(wheelOutput, null, 50, true); + } + }); + + for (const property in this) { + if (Object.prototype.hasOwnProperty.call(this, property)) { + const component = this[property]; + if (component instanceof Component) { + Object.assign(component, io[property]); + if (component.inPacket === undefined) { + component.inPacket = inPackets[1]; + } + component.outPacket = outPacket; + if (component.group === undefined) { + component.group = this.group; + } + if (component.color === undefined) { + component.color = this.color; + } + if (component instanceof Encoder) { + component.max = 2**component.inBitLength - 1; + } + component.inConnect(); + component.outConnect(); + component.outTrigger(); + } + } + } + this.shiftButton.send(LEDColors.white + this.brightnessOff); + } +} + +class S4Mk3MixerColumn extends ComponentContainer { + constructor(group, inPackets, outPacket, io) { + super(); + + this.group = group; + + this.gain = new Pot({ + inKey: "pregain", + }); + this.eqHigh = new Pot({ + group: "[EqualizerRack1_" + group + "_Effect1]", + inKey: "parameter3", + }); + this.eqMid = new Pot({ + group: "[EqualizerRack1_" + group + "_Effect1]", + inKey: "parameter2", + }); + this.eqLow = new Pot({ + group: "[EqualizerRack1_" + group + "_Effect1]", + inKey: "parameter1", + }); + this.quickEffectKnob = new Pot({ + group: "[QuickEffectRack1_" + group + "]", + inKey: "super1", + }); + this.volume = new Pot({ + inKey: "volume", + }); + + this.pfl = new ToggleButton({ + inKey: "pfl", + outKey: "pfl", + output: uncoloredButtonOutput, + }); + + this.effectUnit1Assign = new PowerWindowButton({ + group: "[EffectRack1_EffectUnit1]", + key: "group_" + this.group + "_enable", + output: uncoloredButtonOutput, + }); + + this.effectUnit2Assign = new PowerWindowButton({ + group: "[EffectRack1_EffectUnit2]", + key: "group_" + this.group + "_enable", + output: uncoloredButtonOutput, + }); + + // FIXME: Why is output not working for these? + this.saveGain = new PushButton({ + key: "update_replaygain_from_pregain", + output: uncoloredButtonOutput, + }); + + this.crossfaderSwitch = new Component({ + inBitLength: 2, + input: function(value) { + if (value === 0) { + engine.setValue(this.group, "orientation", 2); + } else if (value === 1) { + engine.setValue(this.group, "orientation", 1); + } else if (value === 2) { + engine.setValue(this.group, "orientation", 0); + } + }, + }); + + for (const property in this) { + if (Object.prototype.hasOwnProperty.call(this, property)) { + const component = this[property]; + if (component instanceof Component) { + Object.assign(component, io[property]); + if (component instanceof Pot) { + component.inPacket = inPackets[2]; + } else { + component.inPacket = inPackets[1]; + } + component.outPacket = outPacket; + + if (component.group === undefined) { + component.group = this.group; + } + + component.inConnect(); + component.outConnect(); + component.outTrigger(); + } + } + } + } +} + +const packetToBinaryString = (data) => { + let string = ""; + for (const 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) + ","; + } + } + // remove trailing comma + return string.slice(0, -1); +}; + +class S4MK3 { + constructor() { + if (engine.getValue("[Master]", "num_samplers") < 32) { + engine.setValue("[Master]", "num_samplers", 32); + } + + this.inPackets = []; + this.inPackets[1] = new HIDInputPacket(1); + this.inPackets[2] = new HIDInputPacket(2); + this.inPackets[3] = new HIDInputPacket(3); + + this.outPackets = []; + this.outPackets[128] = new HIDOutputPacket(128, 94); + + this.effectUnit1 = new S4Mk3EffectUnit(1, this.inPackets, this.outPackets[128], + { + mixKnob: {inByte: 31}, + knobs: [ + {inByte: 33}, + {inByte: 35}, + {inByte: 37}, + ], + buttons: [ + {inByte: 2, inBit: 7, outByte: 63}, + {inByte: 2, inBit: 3, outByte: 64}, + {inByte: 2, inBit: 2, outByte: 65}, + ], + } + ); + this.effectUnit2 = new S4Mk3EffectUnit(2, this.inPackets, this.outPackets[128], + { + mixKnob: {inByte: 71}, + knobs: [ + {inByte: 73}, + {inByte: 75}, + {inByte: 77}, + ], + buttons: [ + {inByte: 10, inBit: 5, outByte: 74}, + {inByte: 10, inBit: 6, outByte: 75}, + {inByte: 10, inBit: 7, outByte: 76}, + ], + } + ); + + // 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], [deckColors[0], deckColors[2]], + this.inPackets, this.outPackets[128], + { + playButton: {inByte: 5, inBit: 0, outByte: 55}, + cueButton: {inByte: 5, inBit: 1, outByte: 8}, + syncButton: {inByte: 6, inBit: 7, outByte: 14}, + syncMasterButton: {inByte: 1, inBit: 0, outByte: 15}, + hotcuePadModeButton: {inByte: 5, inBit: 2, outByte: 9}, + recordPadModeButton: {inByte: 5, inBit: 3, outByte: 56}, + samplesPadModeButton: {inByte: 5, inBit: 4, outByte: 57}, + mutePadModeButton: {inByte: 5, inBit: 5, outByte: 58}, + stemsPadModeButton: {inByte: 6, inBit: 0, outByte: 10}, + deckButtonLeft: {inByte: 6, inBit: 2}, + deckButtonRight: {inByte: 6, inBit: 3}, + deckButtonOutputByteOffset: 12, + tempoFaderLED: {outByte: 11}, + shiftButton: {inByte: 6, inBit: 1, outByte: 59}, + leftEncoder: {inByte: 20, inBit: 0}, + leftEncoderPress: {inByte: 7, inBit: 2}, + rightEncoder: {inByte: 20, inBit: 4}, + rightEncoderPress: {inByte: 7, inBit: 5}, + libraryEncoder: {inByte: 21, inBit: 0}, + libraryEncoderPress: {inByte: 1, inBit: 1}, + turntableButton: {inByte: 6, inBit: 5, outByte: 17}, + jogButton: {inByte: 6, inBit: 4, outByte: 16}, + gridButton: {inByte: 6, inBit: 6, outByte: 18}, + reverseButton: {inByte: 2, inBit: 4, outByte: 60}, + fluxButton: {inByte: 2, inBit: 5, outByte: 61}, + libraryPlayButton: {inByte: 1, inBit: 5, outByte: 22}, + libraryStarButton: {inByte: 1, inBit: 4, outByte: 21}, + libraryPlaylistButton: {inByte: 2, inBit: 1, outByte: 20}, + libraryViewButton: {inByte: 2, inBit: 0, outByte: 19}, + 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}, + ], + tempoFader: {inByte: 13, inBit: 0, inBitLength: 16, inPacket: this.inPackets[2]}, + wheelRelative: {inByte: 12, inBit: 0, inBitLength: 16, inPacket: this.inPackets[3]}, + wheelAbsolute: {inByte: 16, inBit: 0, inBitLength: 16, inPacket: this.inPackets[3]}, + wheelTouch: {inByte: 17, inBit: 4}, + } + ); + + this.rightDeck = new S4Mk3Deck( + [2, 4], [deckColors[1], deckColors[3]], + this.inPackets, this.outPackets[128], + { + playButton: {inByte: 13, inBit: 0, outByte: 66}, + cueButton: {inByte: 15, inBit: 5, outByte: 31}, + syncButton: {inByte: 15, inBit: 4, outByte: 37}, + syncMasterButton: {inByte: 11, inBit: 0, outByte: 38}, + hotcuePadModeButton: {inByte: 13, inBit: 2, outByte: 32}, + recordPadModeButton: {inByte: 13, inBit: 3, outByte: 67}, + samplesPadModeButton: {inByte: 13, inBit: 4, outByte: 68}, + mutePadModeButton: {inByte: 13, inBit: 5, outByte: 69}, + stemsPadModeButton: {inByte: 13, inBit: 1, outByte: 33}, + deckButtonLeft: {inByte: 15, inBit: 2}, + deckButtonRight: {inByte: 15, inBit: 3}, + deckButtonOutputByteOffset: 35, + tempoFaderLED: {outByte: 34}, + shiftButton: {inByte: 15, inBit: 1, outByte: 70}, + leftEncoder: {inByte: 21, inBit: 4}, + leftEncoderPress: {inByte: 16, inBit: 5}, + rightEncoder: {inByte: 22, inBit: 0}, + rightEncoderPress: {inByte: 16, inBit: 2}, + libraryEncoder: {inByte: 22, inBit: 4}, + libraryEncoderPress: {inByte: 11, inBit: 1}, + turntableButton: {inByte: 15, inBit: 6, outByte: 40}, + jogButton: {inByte: 15, inBit: 0, outByte: 39}, + gridButton: {inByte: 15, inBit: 7, outByte: 41}, + reverseButton: {inByte: 11, inBit: 4, outByte: 71}, + fluxButton: {inByte: 11, inBit: 5, outByte: 72}, + libraryPlayButton: {inByte: 10, inBit: 2, outByte: 45}, + libraryStarButton: {inByte: 10, inBit: 1, outByte: 44}, + libraryPlaylistButton: {inByte: 10, inBit: 3, outByte: 43}, + libraryViewButton: {inByte: 10, inBit: 0, outByte: 42}, + 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}, + ], + tempoFader: {inByte: 11, inBit: 0, inBitLength: 16, inPacket: this.inPackets[2]}, + wheelRelative: {inByte: 40, inBit: 0, inBitLength: 16, inPacket: this.inPackets[3]}, + wheelAbsolute: {inByte: 44, inBit: 0, inBitLength: 16, inPacket: this.inPackets[3]}, + wheelTouch: {inByte: 17, inBit: 5}, + } + ); + + this.mixerColumnDeck1 = new S4Mk3MixerColumn("[Channel1]", this.inPackets, this.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]", this.inPackets, this.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]", this.inPackets, this.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]", this.inPackets, this.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}, + } + ); + + // The interaction between the FX SELECT buttons and the QuickEffect enable buttons is rather complex. + // It is easier to have this separate from the S4Mk3MixerColumn class and the FX SELECT buttons are not + // really in the mixer columns. + 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 = 5 + (4 * (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 selector of [1, 2, 3, 4, 5]) { + packet.data[49 + selector] = quickEffectPresetColors[selector - 1] + Button.prototype.brightnessOn; + } + packet.send(); + }; + const fxSelectInput = function(pressed) { + if (pressed) { + if (mixer.firstPressedFxSelector === null) { + mixer.firstPressedFxSelector = this.number; + for (const selector of [1, 2, 3, 4, 5]) { + if (selector !== this.number) { + let presetNumber = 5 + (4 * (mixer.firstPressedFxSelector - 1)) + selector; + if (selector > this.number) { + presetNumber--; + } + this.outPacket.data[49 + selector] = quickEffectPresetColors[presetNumber - 1] + this.brightnessOn; + } + } + 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_chain_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, + number: 1, + input: fxSelectInput, + }); + mixer.fxSelect2 = new Button({ + inByte: 9, + inBit: 1, + number: 2, + input: fxSelectInput, + }); + mixer.fxSelect3 = new Button({ + inByte: 9, + inBit: 6, + number: 3, + input: fxSelectInput, + }); + mixer.fxSelect4 = new Button({ + inByte: 9, + inBit: 0, + number: 4, + input: fxSelectInput, + }); + mixer.fxSelectFilter = new Button({ + inByte: 9, + inBit: 7, + number: 5, + input: fxSelectInput, + }); + + 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(this.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 - 1]; + engine.setValue(this.group, "loaded_chain_preset", presetNumber); + mixer.firstPressedFxSelector = null; + mixer.secondPressedFxSelector = null; + resetFxSelectorColors(); + } + } + } + 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(0); + } + } + presetLoaded(presetNumber) { + this.color = quickEffectPresetColors[presetNumber - 1]; + 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)); + } + } + }; + 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(); + + mixer.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, + }); + + mixer.crossfader = new Pot({ + group: "[Master]", + inKey: "crossfader", + inByte: 1, + inPacket: this.inPackets[2], + }); + mixer.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 mixer) { + if (component.inPacket === undefined) { + component.inPacket = this.inPackets[1]; + } + component.outPacket = this.outPackets[128]; + component.inConnect(); + component.outConnect(); + component.outTrigger(); + } + + let lightQuantizeButton = true; + for (let i = 1; i <= 4; i++) { + if (!engine.getValue("[Channel" + i + "]", "quantize")) { + lightQuantizeButton = false; + } + } + mixer.quantizeButton.send(lightQuantizeButton ? 127 : 0); + mixer.quantizeButton.globalQuantizeOn = lightQuantizeButton; + + /* eslint no-unused-vars: "off" */ + const meterConnection = engine.makeConnection("[Master]", "guiTick50ms", function(_value) { + const deckMeters = Array(78).fill(0); + // Each column has 14 segments, but treat the top one specially for the clip indicator. + const deckSegments = 13; + for (let deckNum = 1; deckNum <= 4; deckNum++) { + const deckGroup = "[Channel" + deckNum + "]"; + const deckLevel = engine.getValue(deckGroup, "VuMeter"); + const columnBaseIndex = (deckNum - 1) * (deckSegments + 2); + const scaledLevel = deckLevel * deckSegments; + const segmentsToLightFully = Math.floor(scaledLevel); + const partialSegmentValue = scaledLevel - segmentsToLightFully; + if (segmentsToLightFully > 0) { + // There are 3 brightness levels per segment: off, dim, and full. + for (let i = 0; i <= segmentsToLightFully; i++) { + deckMeters[columnBaseIndex + i] = 127; + } + if (partialSegmentValue > 0.5 && segmentsToLightFully < deckSegments) { + deckMeters[columnBaseIndex + segmentsToLightFully + 1] = 125; + } + } + if (engine.getValue(deckGroup, "PeakIndicator")) { + deckMeters[columnBaseIndex + deckSegments + 1] = 127; + } + } + // There are more bytes in the packet which seem like they should be for the main + // mix meters, but setting those bytes does not do anything, except for lighting + // the clip lights on the main mix meters. + controller.send(deckMeters, null, 129); + }); + + const motorTimer = engine.beginTimer(20, () => { + const baseRate = 6068; + let velocityLeft = 0; + let velocityRight = 0; + const S4Mk3 = this; + if (this.leftDeck.wheelMode === wheelModes.motor + && engine.getValue(this.leftDeck.group, "play")) { + velocityLeft = baseRate * engine.getValue(S4Mk3.leftDeck.group, "rate_ratio"); + } + if (this.rightDeck.wheelMode === wheelModes.motor + && engine.getValue(this.rightDeck.group, "play")) { + velocityRight = baseRate * engine.getValue(S4Mk3.rightDeck.group, "rate_ratio"); + } + // byte 2 > 127 rotates backward + const motor = [1, 32, 1, velocityLeft & (2**8 - 1), velocityLeft >> 8, + 1, 32, 1, velocityRight & (2**8 - 1), velocityRight >> 8]; + controller.send(motor, null, 49, true); + }); + } + incomingData(data) { + const reportId = data[0]; + if (reportId === 1) { + this.inPackets[1].handleInput(data.buffer); + } else if (reportId === 2) { + this.inPackets[2].handleInput(data.buffer); + // The master volume, booth volume, headphone mix, and headphone volume knobs + // control the controller's audio interface in hardware, so they are not mapped. + } else if (reportId === 3) { + // The 32 bit unsigned ints at bytes 8 and 36 always have exactly the same value, + // so only process one of them. This must be processed before the wheel positions. + const oldWheelTimer = wheelTimer; + const view = new DataView(data.buffer); + wheelTimer = view.getUint32(8, true); + // Processing first value; no previous value to compare with. + if (oldWheelTimer === null) { + return; + } + wheelTimerDelta = wheelTimer - oldWheelTimer; + if (wheelTimerDelta < 0) { + wheelTimerDelta += wheelTimerMax; + } + + this.leftDeck.wheelRelative.input(view.getUint16(12, true)); + this.rightDeck.wheelRelative.input(view.getUint16(40, true)); + } + } + init() { + // sending these magic packets is required for the jog wheel LEDs to work + const wheelLEDinitPacket = Array(26).fill(0); + wheelLEDinitPacket[1] = 1; + wheelLEDinitPacket[2] = 3; + controller.send(wheelLEDinitPacket, null, 48); + wheelLEDinitPacket[0] = 1; + // hack around https://github.com/mixxxdj/mixxx/issues/10828 + engine.beginTimer(35, () => { controller.send(wheelLEDinitPacket, null, 48); }, true); + + // get state of knobs and faders + this.incomingData(new Uint8Array(controller.getInputReport(2))); + } + shutdown() { + // button LEDs + controller.send(new Array(94).fill(0), null, 128); + + // meter LEDs + controller.send(new Array(78).fill(0), null, 129); + + const wheelOutput = Array(40).fill(0); + // left wheel LEDs + controller.send(wheelOutput, null, 50); + // right wheel LEDs + wheelOutput[0] = 1; + controller.send(wheelOutput, null, 50); + } +} + +/* eslint no-unused-vars: "off", no-var: "off" */ +var TraktorS4MK3 = new S4MK3(); From 7170863e66c798a9786dc3d4166c7d0eac3cac97 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Fri, 17 Feb 2023 19:30:23 +0000 Subject: [PATCH 02/19] Kontrol S4 Mk3: add controller mapping --- .../Traktor Kontrol S4 MK3.hid.xml | 2 +- res/controllers/Traktor-Kontrol-S4-MK3.js | 1986 +++++++++++------ 2 files changed, 1322 insertions(+), 666 deletions(-) diff --git a/res/controllers/Traktor Kontrol S4 MK3.hid.xml b/res/controllers/Traktor Kontrol S4 MK3.hid.xml index c9852a1d40da..02b9b5b93639 100644 --- a/res/controllers/Traktor Kontrol S4 MK3.hid.xml +++ b/res/controllers/Traktor Kontrol S4 MK3.hid.xml @@ -2,7 +2,7 @@ Traktor Kontrol S4 MK3 - Be + Be, A. Colombier 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 index 2c53b65442d7..b7601607fa29 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 +/// Copyright (C) 2023 Be and A. Colombier /// /// 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,67 +766,378 @@ class Pot extends Component { } } -/* - * 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, +class Mixer extends ComponentContainer { + constructor(inPackets, outPackets) { + super(); - 0xCC0091: LEDColors.fuscia, - 0xCC0079: LEDColors.magenta, - 0xCC477E: LEDColors.azalea, - 0xCC4761: LEDColors.salmon, + this.outPacket = outPackets[128]; - 0xCCCCCC: LEDColors.white, -}); + 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}, + } + ); -const wheelRelativeMax = 2**16 - 1; -const wheelAbsoluteMax = 2879; + this.firstPressedFxSelector = null; + this.secondPressedFxSelector = null; + this.comboSelected = false; -const wheelTimerMax = 2**32 - 1; -const wheelTimerTicksPerSecond = 100000000; + 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 baseRevolutionsPerMinute = 33 + 1/3; -const baseRevolutionsPerSecond = baseRevolutionsPerMinute / 60; -const wheelTicksPerTimerTicksToRevolutionsPerSecond = wheelTimerTicksPerSecond / wheelAbsoluteMax; + 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(); -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 -}; + 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, + }); -const wheelModes = { - jog: 0, - vinyl: 1, - motor: 2, -}; + 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.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 +}; + +// 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 let wheelTimer = null; @@ -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"); - 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); - } + defaultRange: 0.08, + output: uncoloredButtonOutput, + onShortRelease: function() { + script.toggleControl(this.group, this.inKey); + }, + onLongPress: function() { + const currentRange = engine.getValue(this.group, "rateRange"); + if (currentRange < 1.0) { + engine.setValue(this.group, "rateRange", 1.0); + this.indicator(true); + } else { + engine.setValue(this.group, "rateRange", this.defaultRange); + this.indicator(false); } }, }); - this.syncButton = new ToggleButton({ + this.syncButton = new Button({ 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); - } + output: uncoloredButtonOutput, + onLongPress: function() { + if (this.shifted) { + engine.setValue(this.group, "sync_key", true); + engine.setValue(this.group, "sync_key", false); + } else { + script.triggerControl(this.group, "beatsync_tempo"); } }, + onShortRelease: function() { + script.toggleControl(this.group, this.inKey); + if (!this.shifted) { + engine.softTakeover(this.group, "rate", true); + } + }, + shift: function() { + this.outDisconnect(); + this.inKey = "keylock"; + this.outKey = "keylock"; + this.outConnect(); + this.outTrigger(); + }, + unshift: function() { + this.outDisconnect(); + this.inKey = "sync_enabled"; + this.outKey = "sync_enabled"; + this.outConnect(); + this.outTrigger(); + }, }); this.tempoFader = new Pot({ inKey: "rate", @@ -831,16 +1382,213 @@ class S4Mk3Deck extends Deck { } }); - this.reverseButton = new PushButton({ + this.reverseButton = new Button({ key: "reverseroll", + deck: this, + previousWheelMode: null, + loopModeConnection: null, + unshift: function() { + this.outDisconnect(); + this.outKey = "reverseroll"; + this.outConnect(); + this.outTrigger(); + + }, + shift: function() { + this.outDisconnect(); + this.outKey = "loop_enabled"; + this.outConnect(); + this.outTrigger(); + }, output: uncoloredButtonOutput, + onShortRelease: function() { + if (!this.shifted) { + engine.setValue(this.group, this.key, false); + } + }, + onShortPress: function() { + this.indicator(false); + if (this.shifted) { + const loopEnabled = engine.getValue(this.group, "loop_enabled"); + // If there is currently no loop, we set the loop in of a new loop + if (!loopEnabled) { + engine.setValue(this.group, "loop_end_position", -1); + engine.setValue(this.group, "loop_in", true); + this.indicator(true); + // Else, we enter/exit the loop in wheel mode + } else if (this.previousWheelMode === null) { + this.previousWheelMode = this.deck.wheelMode; + this.deck.wheelMode = wheelModes.loopIn; + + if (this.loopModeConnection === null) { + this.loopModeConnection = engine.makeConnection(this.group, this.outKey, (loopEnabled) => { + if (loopEnabled) { return; } + + this.indicator(false); + const wheelOutput = Array(40).fill(0); + wheelOutput[0] = decks[0] - 1; + engine.beginTimer(decks[0] * 35, () => { + controller.send(wheelOutput, null, 50, true); + this.deck.wheelMode = this.previousWheelMode; + this.previousWheelMode = null; + }, true); + this.loopModeConnection.disconnect(); + this.loopModeConnection = null; + }); + } + + const wheelOutput = Array(40).fill(0); + wheelOutput[0] = decks[0] - 1; + wheelOutput[1] = wheelLEDmodes.ringFlash; + wheelOutput[4] = this.color + Button.prototype.brightnessOn; + + // hack around https://github.com/mixxxdj/mixxx/issues/10828 + // This isn't directly needed, but because we used this hack for + // the track progression, we must make sure we are in sync with it's + // delayed updated + engine.beginTimer(decks[0] * 35, () => { controller.send(wheelOutput, null, 50, true); }, true); + + this.indicator(true); + } else if (this.previousWheelMode !== null) { + if (this.loopModeConnection !== null) { + this.loopModeConnection.disconnect(); + this.loopModeConnection = null; + } + const wheelOutput = Array(40).fill(0); + wheelOutput[0] = decks[0] - 1; + engine.beginTimer(decks[0] * 35, () => { + controller.send(wheelOutput, null, 50, true); + this.deck.wheelMode = this.previousWheelMode; + this.previousWheelMode = null; + }, true); + } + } else { + engine.setValue(this.group, this.key, true); + } + } }); - this.fluxButton = new PushButton({ + this.fluxButton = new Button({ key: "slip_enabled", + deck: this, + previousWheelMode: null, + loopModeConnection: null, + unshift: function() { + this.outDisconnect(); + this.outKey = "slip_enabled"; + this.outConnect(); + this.outTrigger(); + + }, + shift: function() { + this.outDisconnect(); + this.outKey = "loop_enabled"; + this.outConnect(); + this.outTrigger(); + }, + outConnect: function() { + if (this.outKey !== undefined && this.group !== undefined) { + this.outConnections[0] = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); + } + }, output: uncoloredButtonOutput, + onShortRelease: function() { + if (!this.shifted) { + engine.setValue(this.group, this.key, false); + engine.setValue(this.group, "scratch2_enable", false); + } + }, + onShortPress: function() { + this.indicator(false); + if (this.shifted) { + const loopEnabled = engine.getValue(this.group, "loop_enabled"); + // If there is currently no loop, we set the loop in of a new loop + if (!loopEnabled) { + engine.setValue(this.group, "loop_out", true); + this.deck.reverseButton.indicator(false); + // Else, we enter/exit the loop in wheel mode + } else if (this.previousWheelMode === null) { + this.previousWheelMode = this.deck.wheelMode; + this.deck.wheelMode = wheelModes.loopOut; + if (this.loopModeConnection === null) { + this.loopModeConnection = engine.makeConnection(this.group, this.outKey, (loopEnabled) => { + if (loopEnabled) { return; } + + this.indicator(false); + const wheelOutput = Array(40).fill(0); + wheelOutput[0] = decks[0] - 1; + engine.beginTimer(decks[0] * 35, () => { + controller.send(wheelOutput, null, 50, true); + this.deck.wheelMode = this.previousWheelMode; + this.previousWheelMode = null; + }, true); + this.loopModeConnection.disconnect(); + this.loopModeConnection = null; + }); + } + + const wheelOutput = Array(40).fill(0); + wheelOutput[0] = decks[0] - 1; + wheelOutput[1] = wheelLEDmodes.ringFlash; + wheelOutput[4] = this.color + Button.prototype.brightnessOn; + + // hack around https://github.com/mixxxdj/mixxx/issues/10828 + // This isn't directly needed, but because we used this hack for + // the track progression, we must make sure we are in sync with it's + // delayed updated + engine.beginTimer(decks[0] * 35, () => { + controller.send(wheelOutput, null, 50, true); + }, true); + + this.indicator(true); + } else if (this.previousWheelMode !== null) { + if (this.loopModeConnection !== null) { + this.loopModeConnection.disconnect(); + this.loopModeConnection = null; + } + const wheelOutput = Array(40).fill(0); + wheelOutput[0] = decks[0] - 1; + engine.beginTimer(decks[0] * 35, () => { + controller.send(wheelOutput, null, 50, true); + this.deck.wheelMode = this.previousWheelMode; + this.previousWheelMode = null; + }, true); + } + } else { + engine.setValue(this.group, this.key, true); + } + } }); - this.gridButton = new PushButton({ - key: "beats_translate_curpos", + this.gridButton = new Button({ + key: "beat_active", + deck: this, + previousMoveMode: null, + onShortPress: function() { + this.deck.libraryEncoder.gridButtonPressed = true; + }, + onLongPress: function() { + this.deck.libraryEncoder.gridButtonPressed = true; + this.previousMoveMode = this.deck.moveMode; + + if (this.shifted) { + this.deck.moveMode = moveModes.grid; + } else { + this.deck.moveMode = moveModes.bpm; + } + + this.indicator(true); + }, + onLongRelease: function() { + this.deck.libraryEncoder.gridButtonPressed = false; + if (this.previousMoveMode !== null) { + this.deck.moveMode = this.previousMoveMode; + this.previousMoveMode = null; + } + this.indicator(false); + }, + onShortRelease: function() { + this.deck.libraryEncoder.gridButtonPressed = false; + script.triggerControl(this.group, "beats_translate_curpos"); + }, }); this.deckButtonLeft = new Button({ @@ -850,7 +1598,7 @@ class S4Mk3Deck extends Deck { 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.data[io.deckButtonOutputByteOffset + 1] = 0; this.outPacket.send(); } }, @@ -862,7 +1610,7 @@ class S4Mk3Deck extends Deck { 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.data[io.deckButtonOutputByteOffset + 1] = colors[1] + this.brightnessOn; this.outPacket.send(); } }, @@ -870,7 +1618,7 @@ class S4Mk3Deck extends Deck { // set deck selection button LEDs outPacket.data[io.deckButtonOutputByteOffset] = colors[0] + Button.prototype.brightnessOn; - outPacket.data[io.deckButtonOutputByteOffset+1] = 0; + outPacket.data[io.deckButtonOutputByteOffset + 1] = 0; outPacket.send(); this.shiftButton = new PushButton({ @@ -889,33 +1637,51 @@ class S4Mk3Deck extends Deck { 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"); + onChange: function(right) { + + switch (this.deck.moveMode) { + case moveModes.grid: + script.triggerControl(this.group, right ? "beats_adjust_faster" : "beats_adjust_slower"); + break; + case moveModes.keyboard: + if ( + this.deck.keyboard[0].offset === (right ? 16 : 0) + ) { + return; + } + this.deck.keyboardOffset += (right ? 1 : -1); + this.deck.keyboard.forEach(function(pad) { + pad.outTrigger(); + }); + break; + case moveModes.bpm: + script.triggerControl(this.group, right ? "beats_translate_later" : "beats_translate_earlier"); + break; + default: + if (!this.shifted) { + if (!this.deck.leftEncoderPress.pressed) { + if (right) { + script.triggerControl(this.group, "beatjump_forward"); + } else { + script.triggerControl(this.group, "beatjump_backward"); + } } else { - script.triggerControl(this.group, "beatjump_backward"); + let beatjumpSize = engine.getValue(this.group, "beatjump_size"); + if (right) { + beatjumpSize *= 2; + } else { + beatjumpSize /= 2; + } + engine.setValue(this.group, "beatjump_size", beatjumpSize); } } else { - let beatjumpSize = engine.getValue(this.group, "beatjump_size"); if (right) { - beatjumpSize *= 2; + script.triggerControl(this.group, "pitch_up_small"); } else { - beatjumpSize /= 2; + script.triggerControl(this.group, "pitch_down_small"); } - 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"); } + break; } } }); @@ -929,20 +1695,18 @@ class S4Mk3Deck extends Deck { }); 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"); - } + deck: this, + onChange: function(right) { + if (this.deck.wheelMode === wheelModes.loopIn || this.deck.wheelMode === wheelModes.loopOut) { + const moveFactor = this.shifted ? loopEncoderShiftMoveFactor : loopEncoderMoveFactor; + const valueIn = engine.getValue(this.group, "loop_start_position") + (right ? moveFactor : -moveFactor); + const valueOut = engine.getValue(this.group, "loop_end_position") + (right ? moveFactor : -moveFactor); + engine.setValue(this.group, "loop_start_position", valueIn); + engine.setValue(this.group, "loop_end_position", valueOut); + } else if (this.shifted) { + script.triggerControl(this.group, right ? "loop_move_1_forward" : "loop_move_1_backward"); } else { - if (right) { - script.triggerControl(this.group, "beatjump_1_forward"); - } else { - script.triggerControl(this.group, "beatjump_1_backward"); - } + script.triggerControl(this.group, right ? "loop_double" : "loop_halve"); } } }); @@ -955,59 +1719,114 @@ class S4Mk3Deck extends Deck { 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"); - } + 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"); + libraryPlayButtonPressed: false, + gridButtonPressed: false, + starButtonPressed: false, + libraryViewButtonPressed: false, + currentSortedColumnIdx: -1, + onChange: function(right) { + if (this.libraryViewButtonPressed) { + this.currentSortedColumnIdx = (this.currentSortedColumnIdx + (right ? 1 : -1)) % librarySortableColumns.length; + engine.setValue("[Library]", "sort_column", librarySortableColumns[this.currentSortedColumnIdx]); + } else if (this.starButtonPressed) { + if (this.shifted) { + // FIXME doesn't exist, feature request needed + script.triggerControl(this.group, right ? "track_color_prev" : "track_color_next"); } else { - script.triggerControl("[PreviewDeck1]", "beatjump_16_backward"); + script.triggerControl(this.group, right ? "stars_up" : "stars_down"); } + } else if (this.gridButtonPressed) { + script.triggerControl(this.group, right ? "waveform_zoom_up" : "waveform_zoom_down"); + } else if (this.libraryPlayButtonPressed) { + script.triggerControl("[PreviewDeck1]", right ? "beatjump_16_forward" : "beatjump_16_backward"); } else { + // FIXME there is a bug where this action has no effect when the Mixxx window has no focused. Bug to be reported + engine.setValue("[Library]", "focused_widget", this.shifted ? 2 : 3); engine.setValue("[Library]", "MoveVertical", right ? 1 : -1); } } }); - this.libraryEncoderPress = new ToggleButton({ - inKey: "LoadSelectedTrack" + this.libraryEncoderPress = new Button({ + libraryViewButtonPressed: false, + onShortPress: function(pressed) { + if (this.libraryViewButtonPressed) { + script.toggleControl("[Library]", "sort_order"); + } else if (this.shifted) { + script.triggerControl("[Library]", "GoToItem"); + } else { + script.triggerControl(this.group, "LoadSelectedTrack"); + } + }, + // FIXME not supported, feature request + // onLongPress: function(){ + // script.triggerControl("[Library]", "search_related_track", engine.getValue("[Library]", "sort_column")); + // } }); this.libraryPlayButton = new PushButton({ group: "[PreviewDeck1]", + libraryEncoder: this.libraryEncoder, input: function(pressed) { if (pressed) { - if (engine.getValue(this.group, "play")) { - engine.setValue(this.group, "play", 0); - } else { - script.triggerControl(this.group, "LoadSelectedTrackAndPlay"); - } + script.triggerControl(this.group, "LoadSelectedTrackAndPlay"); + } else { + engine.setValue(this.group, "play", 0); + script.triggerControl(this.group, "eject"); } + this.libraryEncoder.libraryPlayButtonPressed = pressed; }, outKey: "play", }); - this.libraryStarButton = new PushButton({ + this.libraryStarButton = new Button({ group: "[Library]", key: "MoveFocusForward", + libraryEncoder: this.libraryEncoder, + onShortRelease: function() { + script.triggerControl(this.group, this.shifted ? "track_color_prev" : "track_color_next"); + }, + onLongPress: function() { + this.libraryEncoder.starButtonPressed = true; + }, + onLongRelease: function() { + this.libraryEncoder.starButtonPressed = false; + }, }); - this.libraryPlaylistButton = new PushButton({ - group: "[Library]", - key: "MoveFocusBackward", - }); - this.libraryViewButton = new ToggleButton({ + // FIXME there isn not feature about playlist at the moment, feature request + // this.libraryPlaylistButton = new Button({ + // onShortRelease: function(){ + // const current_selected_playlist = engine.getValue("[Library]", "playlist_selected"); + // engine.setValue("[Library]", this.shifted ? "remove_selected_track_to_playlist" : "add_selected_track_to_playlist", current_selected_playlist); + // }, + // onLongPress: function(){ + + // } + // }); + this.libraryViewButton = new Button({ group: "[Master]", - inKey: "maximize_library", + key: "maximize_library", + libraryEncoder: this.libraryEncoder, + libraryEncoderPress: this.libraryEncoderPress, + onShortRelease: function() { + script.toggleControl(this.group, this.inKey, true); + }, + onLongPress: function() { + this.libraryEncoder.libraryViewButtonPressed = true; + this.libraryEncoderPress.libraryViewButtonPressed = true; + }, + onLongRelease: function() { + this.libraryEncoder.libraryViewButtonPressed = false; + this.libraryEncoderPress.libraryViewButtonPressed = false; + } }); + this.keyboardPlayMode = null; + this.keyboardOffset = 9; + this.pads = Array(8).fill(new Component()); const defaultPadLayer = [ new IntroOutroButton({ @@ -1037,8 +1856,8 @@ class S4Mk3Deck extends Deck { ]; const hotcuePage2 = Array(8).fill({}); const hotcuePage3 = Array(8).fill({}); - const samplerPage1 = Array(8).fill({}); - const samplerPage2 = Array(8).fill({}); + const samplerPage = Array(8).fill({}); + this.keyboard = Array(8).fill({}); let i = 0; /* eslint no-unused-vars: "off" */ for (const pad of hotcuePage2) { @@ -1052,8 +1871,7 @@ class S4Mk3Deck extends Deck { if (decks[0] > 1) { samplerNumber += 4; } - samplerPage1[i] = new SamplerButton({number: samplerNumber}); - samplerPage2[i] = new SamplerButton({number: samplerNumber + 16}); + samplerPage[i] = new SamplerButton({number: samplerNumber}); if (samplerCrossfaderAssign) { engine.setValue( "[Sampler" + samplerNumber + "]", @@ -1061,6 +1879,10 @@ class S4Mk3Deck extends Deck { (decks[0] === 1) ? 0 : 2 ); } + this.keyboard[i] = new KeyboardButton({ + number: i + 1, + deck: this, + }); i++; } @@ -1074,6 +1896,7 @@ class S4Mk3Deck extends Deck { Object.assign(pad, io.pads[index]); if (!(pad instanceof HotcueButton)) { pad.color = deck.color; + pad.unshift(); } // don't change the group of SamplerButtons if (!(pad instanceof SamplerButton)) { @@ -1095,122 +1918,136 @@ class S4Mk3Deck extends Deck { defaultLayer: 0, hotcuePage2: 1, hotcuePage3: 2, - samplerPage1: 3, - samplerPage2: 4, + samplerPage: 3, + keyboard: 5, }; switchPadLayer(this, defaultPadLayer); this.currentPadLayer = this.padLayers.defaultLayer; this.hotcuePadModeButton = new Button({ deck: this, - input: function(pressed) { + onShortPress: function() { if (!this.shifted) { - if (pressed) { - if (this.deck.currentPadLayer !== this.deck.padLayers.hotcuePage2) { - switchPadLayer(this.deck, hotcuePage2); - this.deck.currentPadLayer = this.deck.padLayers.hotcuePage2; - } else { - switchPadLayer(this.deck, defaultPadLayer); - this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; - } - this.deck.lightPadMode(); + if (this.deck.currentPadLayer !== this.deck.padLayers.hotcuePage2) { + switchPadLayer(this.deck, hotcuePage2); + this.deck.currentPadLayer = this.deck.padLayers.hotcuePage2; + } else { + switchPadLayer(this.deck, defaultPadLayer); + this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; } + this.deck.lightPadMode(); } else { - engine.setValue(this.deck.group, "loop_in", pressed); - } - }, - // make sure loop_in gets reset to 0 if shift is released before this button - unshift: function() { - if (engine.getValue(this.deck.group, "loop_in") === 1) { - engine.setValue(this.deck.group, "loop_in", 0); + switchPadLayer(this.deck, hotcuePage3); + this.deck.currentPadLayer = this.deck.padLayers.hotcuePage3; + this.deck.lightPadMode(); } + }, // hack to switch the LED color when changing decks outTrigger: function() { this.deck.lightPadMode(); } }); - this.recordPadModeButton = new Button({ + // this.recordPadModeButton = new Button({ + // deck: this, + // input: function(pressed) { + // if (!this.shifted) { + // if (pressed) { + // if (this.deck.currentPadLayer !== this.deck.padLayers.hotcuePage3) { + // switchPadLayer(this.deck, hotcuePage3); + // this.deck.currentPadLayer = this.deck.padLayers.hotcuePage3; + // } else { + // switchPadLayer(this.deck, defaultPadLayer); + // this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; + // } + // this.deck.lightPadMode(); + // } + // } else { + // engine.setValue(this.deck.group, "loop_out", pressed); + // } + // }, + // // make sure loop_out gets reset to 0 if shift is released before this button + // unshift: function() { + // if (engine.getValue(this.deck.group, "loop_out") === 1) { + // engine.setValue(this.deck.group, "loop_out", 0); + // } + // } + // }); + this.samplesPadModeButton = new Button({ deck: this, - input: function(pressed) { - if (!this.shifted) { - if (pressed) { - if (this.deck.currentPadLayer !== this.deck.padLayers.hotcuePage3) { - switchPadLayer(this.deck, hotcuePage3); - this.deck.currentPadLayer = this.deck.padLayers.hotcuePage3; - } else { - switchPadLayer(this.deck, defaultPadLayer); - this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; - } - this.deck.lightPadMode(); - } + onShortPress: function() { + if (this.deck.currentPadLayer !== this.deck.padLayers.samplerPage) { + switchPadLayer(this.deck, samplerPage); + engine.setValue("[Samplers]", "show_samplers", true); + this.deck.currentPadLayer = this.deck.padLayers.samplerPage; } else { - engine.setValue(this.deck.group, "loop_out", pressed); + switchPadLayer(this.deck, defaultPadLayer); + engine.setValue("[Samplers]", "show_samplers", false); + this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; } + this.deck.lightPadMode(); }, - // make sure loop_out gets reset to 0 if shift is released before this button - unshift: function() { - if (engine.getValue(this.deck.group, "loop_out") === 1) { - engine.setValue(this.deck.group, "loop_out", 0); - } - } }); - this.samplesPadModeButton = new Button({ + // this.mutePadModeButton = new Button({ + // deck: this, + // input: function(pressed) { + // if (pressed) { + // if (this.deck.currentPadLayer !== this.deck.padLayers.samplerPage2) { + // switchPadLayer(this.deck, samplerPage2); + // this.deck.currentPadLayer = this.deck.padLayers.samplerPage2; + // } else { + // switchPadLayer(this.deck, defaultPadLayer); + // this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; + // } + // this.deck.lightPadMode(); + // } + // }, + // }); + + this.stemsPadModeButton = new Button({ deck: this, - input: function(pressed) { - if (pressed) { - if (this.deck.currentPadLayer !== this.deck.padLayers.samplerPage1) { - switchPadLayer(this.deck, samplerPage1); - this.deck.currentPadLayer = this.deck.padLayers.samplerPage1; - } else { - switchPadLayer(this.deck, defaultPadLayer); - this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; - } + previousMoveMode: null, + onLongPress: function() { + if (this.deck.keyboardPlayMode !== null) { + this.deck.keyboardPlayMode = null; this.deck.lightPadMode(); } }, - }); - this.mutePadModeButton = new Button({ - deck: this, - input: function(pressed) { - if (pressed) { - if (this.deck.currentPadLayer !== this.deck.padLayers.samplerPage2) { - switchPadLayer(this.deck, samplerPage2); - this.deck.currentPadLayer = this.deck.padLayers.samplerPage2; - } else { - switchPadLayer(this.deck, defaultPadLayer); - this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; - } - this.deck.lightPadMode(); + onShortPress: function() { + if (this.previousMoveMode === null) { + this.previousMoveMode = this.deck.moveMode; + this.deck.moveMode = moveModes.keyboard; + } + }, + onShortRelease: function() { + if (this.previousMoveMode !== null) { + this.deck.moveMode = this.previousMoveMode; + this.previousMoveMode = null; + } + if (this.deck.currentPadLayer === this.deck.padLayers.keyboard) { + switchPadLayer(this.deck, defaultPadLayer); + this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; + } else if (this.deck.currentPadLayer !== this.deck.padLayers.keyboard) { + switchPadLayer(this.deck, this.deck.keyboard); + this.deck.currentPadLayer = this.deck.padLayers.keyboard; + } + this.deck.lightPadMode(); + }, + onLongRelease: function() { + if (this.previousMoveMode !== null) { + this.deck.moveMode = this.previousMoveMode; + this.previousMoveMode = null; } }, }); - this.stemsPadModeButton = new ToggleButton({ - key: "keylock", - }); - - this.lightPadMode = function() { - const hotcuePadModeLEDOn = this.currentPadLayer === this.padLayers.hotcuePage2; - this.hotcuePadModeButton.send(this.color + (hotcuePadModeLEDOn ? this.brightnessOn : this.brightnessOff)); - - // unfortunately the other pad mode buttons only have one LED color - const recordPadModeLEDOn = this.currentPadLayer === this.padLayers.hotcuePage3; - this.recordPadModeButton.send(recordPadModeLEDOn ? 127 : 0); - - const samplesPadModeLEDOn = this.currentPadLayer === this.padLayers.samplerPage1; - this.samplesPadModeButton.send(samplesPadModeLEDOn ? 127 : 0); - - const mutePadModeButtonLEDOn = this.currentPadLayer === this.padLayers.samplerPage2; - this.mutePadModeButton.send(mutePadModeButtonLEDOn ? 127 : 0); - }; - this.wheelMode = wheelModes.vinyl; let motorWindDownTimer = 0; const motorWindDownTimerCallback = () => { engine.stopTimer(motorWindDownTimer); motorWindDownTimer = 0; }; + const motorWindUpMilliseconds = 1200; const motorWindDownMilliseconds = 900; this.turntableButton = new Button({ deck: this, @@ -1219,10 +2056,14 @@ class S4Mk3Deck extends Deck { if (this.deck.wheelMode === wheelModes.motor) { this.deck.wheelMode = wheelModes.vinyl; motorWindDownTimer = engine.beginTimer(motorWindDownMilliseconds, motorWindDownTimerCallback, true); - // engine.setValue(this.group, "scratch2_enable", false); + engine.setValue(this.group, "scratch2_enable", false); } else { this.deck.wheelMode = wheelModes.motor; - // engine.setValue(this.group, "scratch2_enable", true); + const group = this.group; + engine.beginTimer(motorWindUpMilliseconds, function() { + // console.log("JOG tt on for "+group); + engine.setValue(group, "scratch2_enable", true); + }, true); } this.outTrigger(); } @@ -1263,17 +2104,29 @@ class S4Mk3Deck extends Deck { deck: this, input: function(touched) { this.touched = touched; - if (this.deck.wheelMode !== wheelModes.jog) { + if (this.deck.wheelMode === wheelModes.vinyl || this.deck.wheelMode === wheelModes.motor) { if (touched) { engine.setValue(this.group, "scratch2_enable", true); } else { - // The wheel keeps spinning - engine.beginTimer(600, () => { - engine.setValue(this.group, "scratch2_enable", false); - }, true); + this.stopScratchWhenOver(); } } }, + stopScratchWhenOver: function() { + if (this.touched && (this.deck.wheelMode === wheelModes.motor || this.deck.wheelMode === wheelModes.vinyl)) { + return; + } + + if (engine.getValue(this.group, "play") && + engine.getValue(this.group, "scratch2") < 1.5 * baseRevolutionsPerSecond && + engine.getValue(this.group, "scratch2") > 0) { + engine.setValue(this.group, "scratch2_enable", false); + } else if (engine.getValue(this.group, "scratch2") === 0) { + engine.setValue(this.group, "scratch2_enable", false); + } else { + engine.beginTimer(100, this.stopScratchWhenOver.bind(this), true); + } + } }); // The relative and absolute position inputs have the same resolution but direction @@ -1284,13 +2137,24 @@ class S4Mk3Deck extends Deck { this.wheelRelative = new Component({ oldValue: null, deck: this, + // We use a rolling average on a sample of speed received. An alternative could + // be to compute precise speed as soon as two points have been received. While the + // alternative is likely going to reduce the delay. it may introduce imprefection due + // to delays that could occurred at various level, so we stick with the naive average for now + stack: [], + stackIdx: 0, + // There is a second sampling group, larger, that improve precision but increase delay, which + // is used in TT mode + stackAvg: [], + stackAvgIdx: 0, input: function(value) { const oldValue = this.oldValue; this.oldValue = value; - let diff = value - oldValue; - if (diff === 0 || oldValue === null || motorWindDownTimer !== 0) { + if (oldValue === null) { + // This is to avoid the issue where the first time, we diff with 0, leading to the absolute value return; } + let diff = value - oldValue; if (diff > wheelRelativeMax / 2) { diff = (wheelRelativeMax - value + oldValue) * -1; @@ -1298,26 +2162,74 @@ class S4Mk3Deck extends Deck { diff = wheelRelativeMax - oldValue + value; } - const wheelVelocity = diff / wheelTimerDelta * wheelTicksPerTimerTicksToRevolutionsPerSecond; - // if (this.group === "[Channel1]") { - // console.log(value + "\t" + diff + "\t" + wheelTimerDelta + "\t" + wheelVelocity + "\t" + wheelVelocity / baseRevolutionsPerSecond); - // } - if (engine.getValue(this.group, "scratch2_enable")) { - engine.setValue(this.group, "scratch2", wheelVelocity / baseRevolutionsPerSecond); - } else { - if (this.deck.wheelMode === wheelModes.motor - || (this.deck.wheelMode === wheelModes.jog && this.deck.wheelTouch.touched) - ) { - return; + this.stack[this.stackIdx] = diff / wheelTimerDelta; + this.stackIdx = (this.stackIdx + 1) % 5; + + const avgSpeed = (this.stack.reduce((ps, v) => ps + v, 0) / this.stack.length) * wheelTicksPerTimerTicksToRevolutionsPerSecond; + + this.stackAvg[this.stackAvgIdx] = avgSpeed; + this.stackAvgIdx = (this.stackAvgIdx + 1) % 40; + + const ttAvgSpeed = this.stackAvg.reduce((ps, v) => ps + v, 0) / this.stackAvg.length; + + if (avgSpeed === 0 && + engine.getValue(this.group, "scratch2") === 0 && + engine.getValue(this.group, "jog") === 0 && + this.deck.wheelMode !== wheelModes.motor) { + return; + } + + switch (this.deck.wheelMode) { + case wheelModes.motor: + // console.log(this.group+"\t"+ttAvgSpeed / baseRevolutionsPerSecond); + engine.setValue(this.group, "scratch2", ttAvgSpeed / baseRevolutionsPerSecond); + break; + case wheelModes.loopIn: + engine.setValue( + this.group, + "loop_start_position", + Math.min( + engine.getValue( + this.group, + "loop_start_position" + ) + (avgSpeed * loopWheelMoveFactor), + engine.getValue( + this.group, + "loop_end_position" + ) - loopWheelMoveFactor + ) + ); + break; + case wheelModes.loopOut: + engine.setValue( + this.group, + "loop_end_position", + engine.getValue( + this.group, + "loop_end_position" + ) + (avgSpeed * loopWheelMoveFactor) + ); + break; + case wheelModes.vinyl: + if (this.deck.wheelTouch.touched || engine.getValue(this.group, "scratch2") !== 0) { + engine.setValue(this.group, "scratch2", avgSpeed); + } else { + engine.setValue(this.group, "jog", avgSpeed); } - engine.setValue(this.group, "jog", wheelVelocity * 4); + break; + default: + engine.setValue(this.group, "jog", avgSpeed); } }, }); this.wheelLED = new Component({ + deck: this, outKey: "playposition", output: function(fractionOfTrack) { + if (this.deck.wheelMode > wheelModes.motor) { + return; + } const durationSeconds = engine.getValue(this.group, "duration"); const positionSeconds = fractionOfTrack * durationSeconds; const revolutions = positionSeconds * baseRevolutionsPerSecond; @@ -1327,10 +2239,13 @@ class S4Mk3Deck extends Deck { const wheelOutput = Array(40).fill(0); wheelOutput[0] = decks[0] - 1; wheelOutput[1] = wheelLEDmodes.spot; - wheelOutput[2] = LEDposition & (2**8 - 1); + wheelOutput[2] = LEDposition & (2 ** 8 - 1); wheelOutput[3] = LEDposition >> 8; wheelOutput[4] = this.color + Button.prototype.brightnessOn; - controller.send(wheelOutput, null, 50, true); + + // FIXME glitch, likely related to https://github.com/mixxxdj/mixxx/issues/10828 + engine.beginTimer(decks[0] * 35, () => { controller.send(wheelOutput, null, 50, true); }, true); + } }); @@ -1350,7 +2265,7 @@ class S4Mk3Deck extends Deck { component.color = this.color; } if (component instanceof Encoder) { - component.max = 2**component.inBitLength - 1; + component.max = 2 ** component.inBitLength - 1; } component.inConnect(); component.outConnect(); @@ -1360,6 +2275,40 @@ class S4Mk3Deck extends Deck { } this.shiftButton.send(LEDColors.white + this.brightnessOff); } + + assignKeyboardPlayMode(group, action) { + this.keyboardPlayMode = { + group: group, + action: action, + }; + this.lightPadMode(); + } + + lightPadMode() { + if (this.currentPadLayer === this.padLayers.hotcuePage2) { + this.hotcuePadModeButton.send(this.hotcuePadModeButton.color + this.hotcuePadModeButton.brightnessOn); + } else if (this.currentPadLayer === this.padLayers.hotcuePage3) { + this.hotcuePadModeButton.send(LEDColors.white + this.hotcuePadModeButton.brightnessOn); + } else { + this.hotcuePadModeButton.send(this.hotcuePadModeButton.color + this.hotcuePadModeButton.brightnessOff); + } + + // unfortunately the other pad mode buttons only have one LED color + // const recordPadModeLEDOn = this.currentPadLayer === this.padLayers.hotcuePage3; + // this.recordPadModeButton.send(recordPadModeLEDOn ? 127 : 0); + + const samplesPadModeLEDOn = this.currentPadLayer === this.padLayers.samplerPage; + this.samplesPadModeButton.send(samplesPadModeLEDOn ? 127 : 0); + + // this.mutePadModeButtonLEDOn = this.currentPadLayer === this.padLayers.samplerPage2; + // const mutedModeButton.send(mutePadModeButtonLEDOn ? 127 : 0); + if (this.keyboardPlayMode !== null) { + this.stemsPadModeButton.send(LEDColors.green + this.stemsPadModeButton.brightnessOn); + } else { + const keyboardPadModeLEDOn = this.currentPadLayer === this.padLayers.keyboard; + this.stemsPadModeButton.send(this.stemsPadModeButton.color + (keyboardPadModeLEDOn ? this.stemsPadModeButton.brightnessOn : this.stemsPadModeButton.brightnessOff)); + } + } } class S4Mk3MixerColumn extends ComponentContainer { @@ -1412,6 +2361,7 @@ class S4Mk3MixerColumn extends ComponentContainer { // FIXME: Why is output not working for these? this.saveGain = new PushButton({ key: "update_replaygain_from_pregain", + group: group, output: uncoloredButtonOutput, }); @@ -1470,8 +2420,8 @@ const packetToBinaryString = (data) => { class S4MK3 { constructor() { - if (engine.getValue("[Master]", "num_samplers") < 32) { - engine.setValue("[Master]", "num_samplers", 32); + if (engine.getValue("[Master]", "num_samplers") < 16) { + engine.setValue("[Master]", "num_samplers", 16); } this.inPackets = []; @@ -1479,12 +2429,17 @@ class S4MK3 { this.inPackets[2] = new HIDInputPacket(2); this.inPackets[3] = new HIDInputPacket(3); + // There are various of other HID report which doesn't seem to have any + // immediate use but it is likely that some useful settings may be found + // in them such as the wheel tension. + this.outPackets = []; this.outPackets[128] = new HIDOutputPacket(128, 94); this.effectUnit1 = new S4Mk3EffectUnit(1, this.inPackets, this.outPackets[128], { mixKnob: {inByte: 31}, + mainButton: {inByte: 2, inBit: 6, outByte: 62}, knobs: [ {inByte: 33}, {inByte: 35}, @@ -1500,6 +2455,7 @@ class S4MK3 { this.effectUnit2 = new S4Mk3EffectUnit(2, this.inPackets, this.outPackets[128], { mixKnob: {inByte: 71}, + mainButton: {inByte: 10, inBit: 4, outByte: 73}, knobs: [ {inByte: 73}, {inByte: 75}, @@ -1517,7 +2473,7 @@ class S4MK3 { // so every single components' IO needs to be specified individually // for both decks. this.leftDeck = new S4Mk3Deck( - [1, 3], [deckColors[0], deckColors[2]], + [1, 3], [deckColors[0], deckColors[2]], this.effectUnit1, this.inPackets, this.outPackets[128], { playButton: {inByte: 5, inBit: 0, outByte: 55}, @@ -1568,7 +2524,7 @@ class S4MK3 { ); this.rightDeck = new S4Mk3Deck( - [2, 4], [deckColors[1], deckColors[3]], + [2, 4], [deckColors[1], deckColors[3]], this.effectUnit2, this.inPackets, this.outPackets[128], { playButton: {inByte: 13, inBit: 0, outByte: 66}, @@ -1618,317 +2574,10 @@ class S4MK3 { } ); - this.mixerColumnDeck1 = new S4Mk3MixerColumn("[Channel1]", this.inPackets, this.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]", this.inPackets, this.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]", this.inPackets, this.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]", this.inPackets, this.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}, - } - ); - // The interaction between the FX SELECT buttons and the QuickEffect enable buttons is rather complex. // It is easier to have this separate from the S4Mk3MixerColumn class and the FX SELECT buttons are not // really in the mixer columns. - 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 = 5 + (4 * (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 selector of [1, 2, 3, 4, 5]) { - packet.data[49 + selector] = quickEffectPresetColors[selector - 1] + Button.prototype.brightnessOn; - } - packet.send(); - }; - const fxSelectInput = function(pressed) { - if (pressed) { - if (mixer.firstPressedFxSelector === null) { - mixer.firstPressedFxSelector = this.number; - for (const selector of [1, 2, 3, 4, 5]) { - if (selector !== this.number) { - let presetNumber = 5 + (4 * (mixer.firstPressedFxSelector - 1)) + selector; - if (selector > this.number) { - presetNumber--; - } - this.outPacket.data[49 + selector] = quickEffectPresetColors[presetNumber - 1] + this.brightnessOn; - } - } - 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_chain_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, - number: 1, - input: fxSelectInput, - }); - mixer.fxSelect2 = new Button({ - inByte: 9, - inBit: 1, - number: 2, - input: fxSelectInput, - }); - mixer.fxSelect3 = new Button({ - inByte: 9, - inBit: 6, - number: 3, - input: fxSelectInput, - }); - mixer.fxSelect4 = new Button({ - inByte: 9, - inBit: 0, - number: 4, - input: fxSelectInput, - }); - mixer.fxSelectFilter = new Button({ - inByte: 9, - inBit: 7, - number: 5, - input: fxSelectInput, - }); - - 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(this.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 - 1]; - engine.setValue(this.group, "loaded_chain_preset", presetNumber); - mixer.firstPressedFxSelector = null; - mixer.secondPressedFxSelector = null; - resetFxSelectorColors(); - } - } - } - 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(0); - } - } - presetLoaded(presetNumber) { - this.color = quickEffectPresetColors[presetNumber - 1]; - 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)); - } - } - }; - 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(); - - mixer.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, - }); - - mixer.crossfader = new Pot({ - group: "[Master]", - inKey: "crossfader", - inByte: 1, - inPacket: this.inPackets[2], - }); - mixer.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 mixer) { - if (component.inPacket === undefined) { - component.inPacket = this.inPackets[1]; - } - component.outPacket = this.outPackets[128]; - component.inConnect(); - component.outConnect(); - component.outTrigger(); - } - - let lightQuantizeButton = true; - for (let i = 1; i <= 4; i++) { - if (!engine.getValue("[Channel" + i + "]", "quantize")) { - lightQuantizeButton = false; - } - } - mixer.quantizeButton.send(lightQuantizeButton ? 127 : 0); - mixer.quantizeButton.globalQuantizeOn = lightQuantizeButton; + this.mixer = new Mixer(this.inPackets, this.outPackets); /* eslint no-unused-vars: "off" */ const meterConnection = engine.makeConnection("[Master]", "guiTick50ms", function(_value) { @@ -1943,7 +2592,7 @@ class S4MK3 { const segmentsToLightFully = Math.floor(scaledLevel); const partialSegmentValue = scaledLevel - segmentsToLightFully; if (segmentsToLightFully > 0) { - // There are 3 brightness levels per segment: off, dim, and full. + // There are 3 brightness levels per segment: off, dim, and full. for (let i = 0; i <= segmentsToLightFully; i++) { deckMeters[columnBaseIndex + i] = 127; } @@ -1960,23 +2609,24 @@ class S4MK3 { // the clip lights on the main mix meters. controller.send(deckMeters, null, 129); }); - const motorTimer = engine.beginTimer(20, () => { - const baseRate = 6068; + const leftMinRate = 1560; + const rightMinRate = 1420; let velocityLeft = 0; let velocityRight = 0; const S4Mk3 = this; if (this.leftDeck.wheelMode === wheelModes.motor - && engine.getValue(this.leftDeck.group, "play")) { - velocityLeft = baseRate * engine.getValue(S4Mk3.leftDeck.group, "rate_ratio"); + && engine.getValue(S4Mk3.leftDeck.group, "play")) { + velocityLeft = wheelAbsoluteMax * engine.getValue(S4Mk3.leftDeck.group, "rate_ratio") + leftMinRate; } if (this.rightDeck.wheelMode === wheelModes.motor - && engine.getValue(this.rightDeck.group, "play")) { - velocityRight = baseRate * engine.getValue(S4Mk3.rightDeck.group, "rate_ratio"); + && engine.getValue(S4Mk3.rightDeck.group, "play")) { + velocityRight = wheelAbsoluteMax * engine.getValue(S4Mk3.rightDeck.group, "rate_ratio") + rightMinRate; } + // byte 2 > 127 rotates backward - const motor = [1, 32, 1, velocityLeft & (2**8 - 1), velocityLeft >> 8, - 1, 32, 1, velocityRight & (2**8 - 1), velocityRight >> 8]; + const motor = [1, 32, 1, velocityLeft & (2 ** 8 - 1), velocityLeft >> 8, + 1, 32, 1, velocityRight & (2 ** 8 - 1), velocityRight >> 8]; controller.send(motor, null, 49, true); }); } @@ -2017,6 +2667,10 @@ class S4MK3 { // hack around https://github.com/mixxxdj/mixxx/issues/10828 engine.beginTimer(35, () => { controller.send(wheelLEDinitPacket, null, 48); }, true); + // Init wheel timer data + wheelTimer = null; + wheelTimerDelta = 0; + // get state of knobs and faders this.incomingData(new Uint8Array(controller.getInputReport(2))); } @@ -2029,6 +2683,8 @@ class S4MK3 { const wheelOutput = Array(40).fill(0); // left wheel LEDs + // FIXME this data gets ignored due to https://github.com/mixxxdj/mixxx/issues/10828 + // Unfortunately, because this is the teardown function, we cannot use a timer to delay the send controller.send(wheelOutput, null, 50); // right wheel LEDs wheelOutput[0] = 1; From 29cd34f664ce05dcfb674ebd01ee0af4b00deac8 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Sat, 18 Feb 2023 13:18:07 +0000 Subject: [PATCH 03/19] Kontrol S4 Mk3: adding settings for various behavior and small refactor --- res/controllers/Traktor-Kontrol-S4-MK3.js | 220 +++++++++++++--------- 1 file changed, 132 insertions(+), 88 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index b7601607fa29..0494a2cc1844 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -82,6 +82,18 @@ 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) dimmed if they are inactive. +// 'true' will keep them dimmed, 'false' will turn them off. Default: true +const keepLEDWithOneColorDimedWhenInactive = 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: true +const gridButtonBlinkOverBeat = true; + // 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 @@ -229,11 +241,16 @@ class HIDOutputPacket { class Component { constructor(options) { - Object.assign(this, options); + if (options) { + Object.keys(options).forEach(function(key) { + if (options[key] === undefined) { delete options[key]; } + }); + Object.assign(this, options); + } this.outConnections = []; - if (options !== undefined && typeof options.key === "string") { - this.inKey = options.key; - this.outKey = options.key; + if (typeof this.key === "string") { + this.inKey = this.key; + this.outKey = this.key; } if (this.unshift !== undefined && typeof this.unshift === "function") { this.unshift(); @@ -273,7 +290,13 @@ class Component { } outConnect() { if (this.outKey !== undefined && this.group !== undefined) { - this.outConnections[0] = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); + 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 unaivailable."); + } } } outDisconnect() { @@ -422,6 +445,16 @@ class Button extends Component { this.inBitLength = 1; } } + setKey(key) { + this.inKey = key; + if (key === this.outKey) { + return; + } + this.outDisconnect(); + this.outKey = key; + this.outConnect(); + this.outTrigger(); + } output(value) { if (this.indicatorTimer !== 0) { return; @@ -610,11 +643,21 @@ class HotcueButton extends PushButton { } 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) => { + 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 unaivailable."); + } + 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 unaivailable."); + } } } } @@ -660,10 +703,15 @@ class KeyboardButton extends PushButton { } outConnect() { if (undefined !== this.group) { - this.outConnections[0] = engine.makeConnection(this.group, "key", (key) => { + 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 unaivailable."); + } } } } @@ -715,8 +763,18 @@ class SamplerButton extends Button { } 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)); + 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 unaivailable."); + } + 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 unaivailable."); + } } } } @@ -1061,8 +1119,18 @@ class QuickEffectButton extends Button { } 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)); + const connection0 = engine.makeConnection(this.group, "loaded_chain_preset", this.presetLoaded.bind(this)); + if (connection0) { + this.outConnections[0] = connection0; + } else { + console.warn("Unable to connect '"+this.group+".loaded_chain_preset' to the controller output. The control appears to be unaivailable."); + } + const connection1 = engine.makeConnection(this.group, "enabled", this.output.bind(this)); + if (connection1) { + this.outConnections[1] = connection1; + } else { + console.warn("Unable to connect '"+this.group+".enabled' to the controller output. The control appears to be unaivailable."); + } } } } @@ -1080,6 +1148,13 @@ 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.uncoloredOutput = function(value) { + if (this.indicatorTimer !== 0) { + return; + } + const color = (value > 0) ? (this.color || LEDColors.white) + this.brightnessOn : LEDColors.off; + this.send(color); +}; Button.prototype.colorMap = new ColorMapper({ 0xCC0000: LEDColors.red, 0xCC5E00: LEDColors.carrot, @@ -1150,17 +1225,6 @@ 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((this.color || LEDColors.white) + this.brightnessOn); - } else { - this.send((this.color || LEDColors.white) + this.brightnessOff); - } -}; - class S4Mk3EffectUnit extends ComponentContainer { constructor(unitNumber, inPackets, outPacket, io) { super(); @@ -1177,7 +1241,6 @@ class S4Mk3EffectUnit extends ComponentContainer { this.mainButton = new PowerWindowButton({ unit: this, - output: uncoloredButtonOutput, inPacket: inPackets[1], inByte: io.mainButton.inByte, inBit: io.mainButton.inBit, @@ -1193,7 +1256,7 @@ class S4Mk3EffectUnit extends ComponentContainer { this.outDisconnect(); this.outKey = undefined; this.group = undefined; - uncoloredButtonOutput.call(this, false); + this.output(false); }, input: function(pressed) { if (!this.shifted) { @@ -1227,7 +1290,6 @@ class S4Mk3EffectUnit extends ComponentContainer { unit: this, key: "enabled", group: effectGroup, - output: uncoloredButtonOutput, inPacket: inPackets[1], inByte: io.buttons[index].inByte, inBit: io.buttons[index].inBit, @@ -1290,7 +1352,7 @@ class S4Mk3Deck extends Deck { super(decks, colors); this.playButton = new PlayButton({ - output: uncoloredButtonOutput + output: keepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput }); this.cueButton = new CueButton({ @@ -1301,7 +1363,12 @@ class S4Mk3Deck extends Deck { this.syncMasterButton = new Button({ key: "sync_leader", defaultRange: 0.08, - output: uncoloredButtonOutput, + shift: useKeylockOnMaster ? function() { + this.setKey("keylock"); + } : undefined, + unshift: useKeylockOnMaster ? function() { + this.setKey("sync_leader"); + } : undefined, onShortRelease: function() { script.toggleControl(this.group, this.inKey); }, @@ -1318,7 +1385,6 @@ class S4Mk3Deck extends Deck { }); this.syncButton = new Button({ key: "sync_enabled", - output: uncoloredButtonOutput, onLongPress: function() { if (this.shifted) { engine.setValue(this.group, "sync_key", true); @@ -1333,20 +1399,12 @@ class S4Mk3Deck extends Deck { engine.softTakeover(this.group, "rate", true); } }, - shift: function() { - this.outDisconnect(); - this.inKey = "keylock"; - this.outKey = "keylock"; - this.outConnect(); - this.outTrigger(); - }, - unshift: function() { - this.outDisconnect(); - this.inKey = "sync_enabled"; - this.outKey = "sync_enabled"; - this.outConnect(); - this.outTrigger(); - }, + shift: !useKeylockOnMaster ? function() { + this.setKey("keylock"); + } : undefined, + unshift: !useKeylockOnMaster ? function() { + this.setKey("sync_enabled"); + } : undefined, }); this.tempoFader = new Pot({ inKey: "rate", @@ -1388,19 +1446,13 @@ class S4Mk3Deck extends Deck { previousWheelMode: null, loopModeConnection: null, unshift: function() { - this.outDisconnect(); - this.outKey = "reverseroll"; - this.outConnect(); - this.outTrigger(); + this.setKey("reverseroll"); }, shift: function() { - this.outDisconnect(); - this.outKey = "loop_enabled"; - this.outConnect(); - this.outTrigger(); + this.setKey("loop_enabled"); }, - output: uncoloredButtonOutput, + output: keepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput, onShortRelease: function() { if (!this.shifted) { engine.setValue(this.group, this.key, false); @@ -1473,24 +1525,23 @@ class S4Mk3Deck extends Deck { previousWheelMode: null, loopModeConnection: null, unshift: function() { - this.outDisconnect(); - this.outKey = "slip_enabled"; - this.outConnect(); - this.outTrigger(); + this.setKey("slip_enabled"); }, shift: function() { - this.outDisconnect(); - this.outKey = "loop_enabled"; - this.outConnect(); - this.outTrigger(); + this.setKey("loop_enabled"); }, outConnect: function() { if (this.outKey !== undefined && this.group !== undefined) { - this.outConnections[0] = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); + const connection = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); + 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 unaivailable."); + } } }, - output: uncoloredButtonOutput, + output: keepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput, onShortRelease: function() { if (!this.shifted) { engine.setValue(this.group, this.key, false); @@ -1559,7 +1610,7 @@ class S4Mk3Deck extends Deck { } }); this.gridButton = new Button({ - key: "beat_active", + key: gridButtonBlinkOverBeat ? "beat_active" : undefined, deck: this, previousMoveMode: null, onShortPress: function() { @@ -2185,30 +2236,27 @@ class S4Mk3Deck extends Deck { engine.setValue(this.group, "scratch2", ttAvgSpeed / baseRevolutionsPerSecond); break; case wheelModes.loopIn: - engine.setValue( - this.group, - "loop_start_position", - Math.min( - engine.getValue( - this.group, - "loop_start_position" - ) + (avgSpeed * loopWheelMoveFactor), - engine.getValue( - this.group, - "loop_end_position" - ) - loopWheelMoveFactor - ) - ); + { + const loopStartPosition = engine.getValue(this.group, "loop_start_position"); + const loopEndPosition = engine.getValue(this.group, "loop_end_position"); + const value = Math.min(loopStartPosition + (avgSpeed * loopWheelMoveFactor), loopEndPosition - loopWheelMoveFactor); + engine.setValue( + this.group, + "loop_start_position", + value + ); + } break; case wheelModes.loopOut: - engine.setValue( - this.group, - "loop_end_position", - engine.getValue( + { + const loopEndPosition = engine.getValue(this.group, "loop_end_position"); + const value = loopEndPosition + (avgSpeed * loopWheelMoveFactor); + engine.setValue( this.group, - "loop_end_position" - ) + (avgSpeed * loopWheelMoveFactor) - ); + "loop_end_position", + value + ); + } break; case wheelModes.vinyl: if (this.deck.wheelTouch.touched || engine.getValue(this.group, "scratch2") !== 0) { @@ -2343,26 +2391,22 @@ class S4Mk3MixerColumn extends ComponentContainer { this.pfl = new ToggleButton({ inKey: "pfl", outKey: "pfl", - output: uncoloredButtonOutput, }); this.effectUnit1Assign = new PowerWindowButton({ group: "[EffectRack1_EffectUnit1]", key: "group_" + this.group + "_enable", - output: uncoloredButtonOutput, }); this.effectUnit2Assign = new PowerWindowButton({ group: "[EffectRack1_EffectUnit2]", key: "group_" + this.group + "_enable", - output: uncoloredButtonOutput, }); // FIXME: Why is output not working for these? this.saveGain = new PushButton({ key: "update_replaygain_from_pregain", group: group, - output: uncoloredButtonOutput, }); this.crossfaderSwitch = new Component({ From ff1761d6f6a1840832f0cbac32e0da8957aa00ab Mon Sep 17 00:00:00 2001 From: Antoine C Date: Sun, 19 Feb 2023 16:20:36 +0000 Subject: [PATCH 04/19] Kontrol S4 Mk3: adding context-menu control using playlist button --- res/controllers/Traktor-Kontrol-S4-MK3.js | 65 +++++++++++++++++------ 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index 0494a2cc1844..c9854f0f7936 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -1011,7 +1011,6 @@ class Mixer extends ComponentContainer { 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(); } } @@ -1780,6 +1779,7 @@ class S4Mk3Deck extends Deck { gridButtonPressed: false, starButtonPressed: false, libraryViewButtonPressed: false, + libraryPlaylistButtonPressed: false, currentSortedColumnIdx: -1, onChange: function(right) { if (this.libraryViewButtonPressed) { @@ -1797,7 +1797,7 @@ class S4Mk3Deck extends Deck { } else if (this.libraryPlayButtonPressed) { script.triggerControl("[PreviewDeck1]", right ? "beatjump_16_forward" : "beatjump_16_backward"); } else { - // FIXME there is a bug where this action has no effect when the Mixxx window has no focused. Bug to be reported + // FIXME there is a bug where this action has no effect when the Mixxx window has no focused. https://github.com/mixxxdj/mixxx/issues/11285 engine.setValue("[Library]", "focused_widget", this.shifted ? 2 : 3); engine.setValue("[Library]", "MoveVertical", right ? 1 : -1); } @@ -1808,10 +1808,14 @@ class S4Mk3Deck extends Deck { onShortPress: function(pressed) { if (this.libraryViewButtonPressed) { script.toggleControl("[Library]", "sort_order"); - } else if (this.shifted) { - script.triggerControl("[Library]", "GoToItem"); } else { - script.triggerControl(this.group, "LoadSelectedTrack"); + const currentlyFocusWidget = engine.getValue("[Library]", "focused_widget"); + // 3 == Tracks table or root views of library features + if (this.shifted || currentlyFocusWidget !== 3) { + script.triggerControl("[Library]", "GoToItem"); + } else { + script.triggerControl(this.group, "LoadSelectedTrack"); + } } }, // FIXME not supported, feature request @@ -1835,7 +1839,6 @@ class S4Mk3Deck extends Deck { }); this.libraryStarButton = new Button({ group: "[Library]", - key: "MoveFocusForward", libraryEncoder: this.libraryEncoder, onShortRelease: function() { script.triggerControl(this.group, this.shifted ? "track_color_prev" : "track_color_next"); @@ -1847,16 +1850,46 @@ class S4Mk3Deck extends Deck { this.libraryEncoder.starButtonPressed = false; }, }); - // FIXME there isn not feature about playlist at the moment, feature request - // this.libraryPlaylistButton = new Button({ - // onShortRelease: function(){ - // const current_selected_playlist = engine.getValue("[Library]", "playlist_selected"); - // engine.setValue("[Library]", this.shifted ? "remove_selected_track_to_playlist" : "add_selected_track_to_playlist", current_selected_playlist); - // }, - // onLongPress: function(){ + // FIXME there is no feature about playlist at the moment, so we use this button to control the context menu, which has playlist control + this.libraryPlaylistButton = new Button({ + group: "[Library]", + libraryEncoder: this.libraryEncoder, + outConnect: function() { + const connection = engine.makeConnection(this.group, "focused_widget", (widget) => { + // 4 == Context menu + this.output(widget === 4); + }); + // 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+".focused_widget' to the controller output. The control appears to be unaivailable."); + } + }, + onShortRelease: function() { + const currentlyFocusWidget = engine.getValue("[Library]", "focused_widget"); + // 3 == Tracks table or root views of library features + // 4 == Context menu + if (currentlyFocusWidget !== 3 && currentlyFocusWidget !== 4) { + return; + } + script.toggleControl("[Library]", "show_track_menu"); + this.libraryEncoder.libraryPlayButtonPressed = false; - // } - // }); + if (currentlyFocusWidget === 4) { + engine.setValue("[Library]", "focused_widget", 3); + } + }, + onShortPress: function() { + this.libraryEncoder.libraryPlayButtonPressed = true; + }, + onLongRelease: function() { + this.libraryEncoder.libraryPlayButtonPressed = false; + }, + onLongPress: function() { + engine.setValue("[Library]", "clear_search", 1); + } + }); this.libraryViewButton = new Button({ group: "[Master]", key: "maximize_library", @@ -2112,7 +2145,6 @@ class S4Mk3Deck extends Deck { this.deck.wheelMode = wheelModes.motor; const group = this.group; engine.beginTimer(motorWindUpMilliseconds, function() { - // console.log("JOG tt on for "+group); engine.setValue(group, "scratch2_enable", true); }, true); } @@ -2232,7 +2264,6 @@ class S4Mk3Deck extends Deck { switch (this.deck.wheelMode) { case wheelModes.motor: - // console.log(this.group+"\t"+ttAvgSpeed / baseRevolutionsPerSecond); engine.setValue(this.group, "scratch2", ttAvgSpeed / baseRevolutionsPerSecond); break; case wheelModes.loopIn: From 9a61748ba93231217b129f7f8507b7eb4cd8127a Mon Sep 17 00:00:00 2001 From: Antoine C Date: Tue, 21 Feb 2023 20:08:52 +0000 Subject: [PATCH 05/19] Kontrol S4 Mk3: fixes and motor control enhancement --- res/controllers/Traktor-Kontrol-S4-MK3.js | 323 ++++++++++++++++++---- 1 file changed, 271 insertions(+), 52 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index c9854f0f7936..21a769be4578 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -94,6 +94,33 @@ const useKeylockOnMaster = false; // Default: true const gridButtonBlinkOverBeat = true; +// 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 = 5; + +// 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 = true; + +// 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: 40 +const turnTableSpeedSample = 40; + +// 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 impact resistance of the wheel if you are using a tight setting (tightnessFactor< 0.5) +// Default: 24000. +const maxWheelForce = 24000; + + + // 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 @@ -134,6 +161,9 @@ const quickEffectPresetColors = [ // assign samplers to the crossfader on startup const samplerCrossfaderAssign = true; +const motorWindUpMilliseconds = 600; +const motorWindDownMilliseconds = 900; + /* * HID packet parsing library */ @@ -295,7 +325,7 @@ class Component { 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 unaivailable."); + console.warn("Unable to connect '" + this.group + "." + this.outKey + "' to the controller output. The control appears to be unaivailable."); } } } @@ -384,6 +414,7 @@ class Deck extends ComponentContainer { } this.color = colors[0]; } + this.secondDeckModes = null; } toggleDeck() { if (this.decks === undefined) { @@ -399,8 +430,32 @@ class Deck extends ComponentContainer { 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) { @@ -413,6 +468,7 @@ class Deck extends ComponentContainer { component.color = this.groupsToColors[newGroup]; }); + this.secondDeckModes = currentModes; } static groupForNumber(deckNumber) { return "[Channel" + deckNumber + "]"; @@ -647,7 +703,7 @@ class HotcueButton extends PushButton { 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 unaivailable."); + console.warn("Unable to connect '" + this.group + "." + this.outKey + "' to the controller output. The control appears to be unaivailable."); } const connection1 = engine.makeConnection(this.group, this.colorKey, (colorCode) => { this.color = this.colorMap.getValueForNearestColor(colorCode); @@ -656,7 +712,7 @@ class HotcueButton extends PushButton { 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 unaivailable."); + console.warn("Unable to connect '" + this.group + "." + this.colorKey + "' to the controller output. The control appears to be unaivailable."); } } } @@ -710,7 +766,42 @@ class KeyboardButton extends PushButton { if (connection) { this.outConnections[0] = connection; } else { - console.warn("Unable to connect '"+this.group+".key' to the controller output. The control appears to be unaivailable."); + console.warn("Unable to connect '" + this.group + ".key' to the controller output. The control appears to be unaivailable."); + } + } + } +} + +class BeatLoopRollButton extends Button { + constructor(options) { + super(options); + if (this.number === undefined || !Number.isInteger(this.number) || this.number < 1 || this.number > 8) { + throw Error("BeatLoopRollButton must have a number property of an integer between 1 and 8"); + } + if (this.deck === undefined) { + throw Error("BeatLoopRollButton must have a deck attached to it"); + } + this.outConnect(); + } + unshift() { + this.outTrigger(); + } + shift() { + this.outTrigger(); + } + output(value) { + // In us + } + 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 unaivailable."); } } } @@ -767,13 +858,13 @@ class SamplerButton extends Button { if (connection0) { this.outConnections[0] = connection0; } else { - console.warn("Unable to connect '"+this.group+".play' to the controller output. The control appears to be unaivailable."); + console.warn("Unable to connect '" + this.group + ".play' to the controller output. The control appears to be unaivailable."); } 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 unaivailable."); + console.warn("Unable to connect '" + this.group + ".track_loaded' to the controller output. The control appears to be unaivailable."); } } } @@ -1122,13 +1213,13 @@ class QuickEffectButton extends Button { if (connection0) { this.outConnections[0] = connection0; } else { - console.warn("Unable to connect '"+this.group+".loaded_chain_preset' to the controller output. The control appears to be unaivailable."); + console.warn("Unable to connect '" + this.group + ".loaded_chain_preset' to the controller output. The control appears to be unaivailable."); } const connection1 = engine.makeConnection(this.group, "enabled", this.output.bind(this)); if (connection1) { this.outConnections[1] = connection1; } else { - console.warn("Unable to connect '"+this.group+".enabled' to the controller output. The control appears to be unaivailable."); + console.warn("Unable to connect '" + this.group + ".enabled' to the controller output. The control appears to be unaivailable."); } } } @@ -1466,7 +1557,7 @@ class S4Mk3Deck extends Deck { engine.setValue(this.group, "loop_end_position", -1); engine.setValue(this.group, "loop_in", true); this.indicator(true); - // Else, we enter/exit the loop in wheel mode + // Else, we enter/exit the loop in wheel mode } else if (this.previousWheelMode === null) { this.previousWheelMode = this.deck.wheelMode; this.deck.wheelMode = wheelModes.loopIn; @@ -1536,7 +1627,7 @@ class S4Mk3Deck extends Deck { 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 unaivailable."); + console.warn("Unable to connect '" + this.group + "." + this.outKey + "' to the controller output. The control appears to be unaivailable."); } } }, @@ -1555,7 +1646,7 @@ class S4Mk3Deck extends Deck { if (!loopEnabled) { engine.setValue(this.group, "loop_out", true); this.deck.reverseButton.indicator(false); - // Else, we enter/exit the loop in wheel mode + // Else, we enter/exit the loop in wheel mode } else if (this.previousWheelMode === null) { this.previousWheelMode = this.deck.wheelMode; this.deck.wheelMode = wheelModes.loopOut; @@ -1798,8 +1889,18 @@ class S4Mk3Deck extends Deck { script.triggerControl("[PreviewDeck1]", right ? "beatjump_16_forward" : "beatjump_16_backward"); } else { // FIXME there is a bug where this action has no effect when the Mixxx window has no focused. https://github.com/mixxxdj/mixxx/issues/11285 - engine.setValue("[Library]", "focused_widget", this.shifted ? 2 : 3); - engine.setValue("[Library]", "MoveVertical", right ? 1 : -1); + // As a workaround, we are using deprecated control, hoping the bug will be fixed before the controls get removed + const currentlyFocusWidget = engine.getValue("[Library]", "focused_widget"); + if (currentlyFocusWidget === 0) { + if (this.shifted) { + script.triggerControl("[Playlist]", right ? "SelectNextPlaylist" : "SelectPrevPlaylist"); + } else { + script.triggerControl("[Playlist]", right ? "SelectNextTrack" : "SelectPrevTrack"); + } + } else { + engine.setValue("[Library]", "focused_widget", this.shifted ? 2 : 3); + engine.setValue("[Library]", "MoveVertical", right ? 1 : -1); + } } } }); @@ -1811,10 +1912,12 @@ class S4Mk3Deck extends Deck { } else { const currentlyFocusWidget = engine.getValue("[Library]", "focused_widget"); // 3 == Tracks table or root views of library features - if (this.shifted || currentlyFocusWidget !== 3) { - script.triggerControl("[Library]", "GoToItem"); - } else { + if (this.shifted && currentlyFocusWidget === 0) { + script.triggerControl("[Playlist]", "ToggleSelectedSidebarItem"); + } else if (currentlyFocusWidget === 3 || currentlyFocusWidget === 0) { script.triggerControl(this.group, "LoadSelectedTrack"); + } else { + script.triggerControl("[Library]", "GoToItem"); } } }, @@ -1863,7 +1966,7 @@ class S4Mk3Deck extends Deck { if (connection) { this.outConnections[0] = connection; } else { - console.warn("Unable to connect '"+this.group+".focused_widget' to the controller output. The control appears to be unaivailable."); + console.warn("Unable to connect '" + this.group + ".focused_widget' to the controller output. The control appears to be unaivailable."); } }, onShortRelease: function() { @@ -2131,9 +2234,7 @@ class S4Mk3Deck extends Deck { engine.stopTimer(motorWindDownTimer); motorWindDownTimer = 0; }; - const motorWindUpMilliseconds = 1200; - const motorWindDownMilliseconds = 900; - this.turntableButton = new Button({ + this.turntableButton = useMotors ? new Button({ deck: this, input: function(press) { if (press) { @@ -2157,7 +2258,7 @@ class S4Mk3Deck extends Deck { const vinylModeOn = this.deck.wheelMode === wheelModes.vinyl; this.deck.jogButton.send(this.color + (vinylModeOn ? this.brightnessOn : this.brightnessOff)); }, - }); + }) : undefined; this.jogButton = new Button({ deck: this, input: function(press) { @@ -2196,7 +2297,7 @@ class S4Mk3Deck extends Deck { } }, stopScratchWhenOver: function() { - if (this.touched && (this.deck.wheelMode === wheelModes.motor || this.deck.wheelMode === wheelModes.vinyl)) { + if (this.touched || this.deck.wheelMode === wheelModes.motor) { return; } @@ -2226,10 +2327,12 @@ class S4Mk3Deck extends Deck { // to delays that could occurred at various level, so we stick with the naive average for now stack: [], stackIdx: 0, + avgSpeed: 0, // There is a second sampling group, larger, that improve precision but increase delay, which // is used in TT mode stackAvg: [], stackAvgIdx: 0, + ttAvgSpeed: 0, input: function(value) { const oldValue = this.oldValue; this.oldValue = value; @@ -2237,6 +2340,7 @@ class S4Mk3Deck extends Deck { // This is to avoid the issue where the first time, we diff with 0, leading to the absolute value return; } + let diff = value - oldValue; if (diff > wheelRelativeMax / 2) { @@ -2246,16 +2350,16 @@ class S4Mk3Deck extends Deck { } this.stack[this.stackIdx] = diff / wheelTimerDelta; - this.stackIdx = (this.stackIdx + 1) % 5; + this.stackIdx = (this.stackIdx + 1) % wheelSpeedSample; - const avgSpeed = (this.stack.reduce((ps, v) => ps + v, 0) / this.stack.length) * wheelTicksPerTimerTicksToRevolutionsPerSecond; + this.avgSpeed = (this.stack.reduce((ps, v) => ps + v, 0) / this.stack.length) * wheelTicksPerTimerTicksToRevolutionsPerSecond; - this.stackAvg[this.stackAvgIdx] = avgSpeed; - this.stackAvgIdx = (this.stackAvgIdx + 1) % 40; + this.stackAvg[this.stackAvgIdx] = this.avgSpeed; + this.stackAvgIdx = (this.stackAvgIdx + 1) % turnTableSpeedSample; - const ttAvgSpeed = this.stackAvg.reduce((ps, v) => ps + v, 0) / this.stackAvg.length; + this.ttAvgSpeed = this.stackAvg.reduce((ps, v) => ps + v, 0) / this.stackAvg.length; - if (avgSpeed === 0 && + if (this.avgSpeed === 0 && engine.getValue(this.group, "scratch2") === 0 && engine.getValue(this.group, "jog") === 0 && this.deck.wheelMode !== wheelModes.motor) { @@ -2264,13 +2368,14 @@ class S4Mk3Deck extends Deck { switch (this.deck.wheelMode) { case wheelModes.motor: - engine.setValue(this.group, "scratch2", ttAvgSpeed / baseRevolutionsPerSecond); + // engine.setValue(this.group, "scratch2", 1.0); + engine.setValue(this.group, "scratch2", this.ttAvgSpeed / baseRevolutionsPerSecond); break; case wheelModes.loopIn: { const loopStartPosition = engine.getValue(this.group, "loop_start_position"); const loopEndPosition = engine.getValue(this.group, "loop_end_position"); - const value = Math.min(loopStartPosition + (avgSpeed * loopWheelMoveFactor), loopEndPosition - loopWheelMoveFactor); + const value = Math.min(loopStartPosition + (this.avgSpeed * loopWheelMoveFactor), loopEndPosition - loopWheelMoveFactor); engine.setValue( this.group, "loop_start_position", @@ -2281,7 +2386,7 @@ class S4Mk3Deck extends Deck { case wheelModes.loopOut: { const loopEndPosition = engine.getValue(this.group, "loop_end_position"); - const value = loopEndPosition + (avgSpeed * loopWheelMoveFactor); + const value = loopEndPosition + (this.avgSpeed * loopWheelMoveFactor); engine.setValue( this.group, "loop_end_position", @@ -2291,13 +2396,13 @@ class S4Mk3Deck extends Deck { break; case wheelModes.vinyl: if (this.deck.wheelTouch.touched || engine.getValue(this.group, "scratch2") !== 0) { - engine.setValue(this.group, "scratch2", avgSpeed); + engine.setValue(this.group, "scratch2", this.avgSpeed); } else { - engine.setValue(this.group, "jog", avgSpeed); + engine.setValue(this.group, "jog", this.avgSpeed); } break; default: - engine.setValue(this.group, "jog", avgSpeed); + engine.setValue(this.group, "jog", this.avgSpeed); } }, }); @@ -2318,7 +2423,7 @@ class S4Mk3Deck extends Deck { const wheelOutput = Array(40).fill(0); wheelOutput[0] = decks[0] - 1; wheelOutput[1] = wheelLEDmodes.spot; - wheelOutput[2] = LEDposition & (2 ** 8 - 1); + wheelOutput[2] = LEDposition & 0xff; wheelOutput[3] = LEDposition >> 8; wheelOutput[4] = this.color + Button.prototype.brightnessOn; @@ -2684,26 +2789,140 @@ class S4MK3 { // the clip lights on the main mix meters. controller.send(deckMeters, null, 129); }); - const motorTimer = engine.beginTimer(20, () => { - const leftMinRate = 1560; - const rightMinRate = 1420; - let velocityLeft = 0; - let velocityRight = 0; - const S4Mk3 = this; - if (this.leftDeck.wheelMode === wheelModes.motor - && engine.getValue(S4Mk3.leftDeck.group, "play")) { - velocityLeft = wheelAbsoluteMax * engine.getValue(S4Mk3.leftDeck.group, "rate_ratio") + leftMinRate; + if (useMotors) { + engine.beginTimer(20, this.motorCallback.bind(this)); + } + + } + motorCallback() { + const motorData = [ + 1, 0x20, 1, 0, 0, + 1, 0x20, 1, 0, 0, + + ]; + const velocityFactor = 4500; + const maxVelocity = 10; + + let velocityLeft = 0; + let velocityRight = 0; + + let expectedLeftSpeed = 0; + let expectedRightSpeed = 0; + + if (this.leftDeck.wheelMode === wheelModes.motor + && engine.getValue(this.leftDeck.group, "play")) { + expectedLeftSpeed = engine.getValue(this.leftDeck.group, "rate_ratio"); + } + + if (this.rightDeck.wheelMode === wheelModes.motor + && engine.getValue(this.rightDeck.group, "play")) { + expectedRightSpeed = engine.getValue(this.rightDeck.group, "rate_ratio"); + } + + const currentLeftSpeed = (this.leftDeck.wheelRelative.avgSpeed + this.leftDeck.wheelRelative.ttAvgSpeed) / (2 * baseRevolutionsPerSecond); + const currentRightSpeed = (this.rightDeck.wheelRelative.avgSpeed + this.rightDeck.wheelRelative.ttAvgSpeed) / (2 * baseRevolutionsPerSecond); + + if (expectedLeftSpeed) { + velocityLeft = expectedLeftSpeed + Math.min( + maxVelocity, + Math.max( + -maxVelocity, + (expectedLeftSpeed - currentLeftSpeed) * 2 + ) + ); + } else { + if (tightnessFactor > 0.5) { + // Super loose + const reduceFactor = (Math.min(0.5, tightnessFactor - 0.5) / 0.5) * 0.7; + velocityLeft = currentLeftSpeed * reduceFactor; + } else if (tightnessFactor < 0.5) { + // Super tight + const reduceFactor = (Math.min(0, tightnessFactor) * 4); + velocityLeft = expectedLeftSpeed + Math.min( + maxVelocity, + Math.max( + -maxVelocity, + (expectedLeftSpeed - currentLeftSpeed) * 2 + ) + ); + } - if (this.rightDeck.wheelMode === wheelModes.motor - && engine.getValue(S4Mk3.rightDeck.group, "play")) { - velocityRight = wheelAbsoluteMax * engine.getValue(S4Mk3.rightDeck.group, "rate_ratio") + rightMinRate; + } + + if (expectedRightSpeed) { + velocityRight = expectedRightSpeed + Math.min( + maxVelocity, + Math.max( + -maxVelocity, + (expectedRightSpeed - currentRightSpeed) + ) + ); + } else { + if (tightnessFactor > 0.5) { + // Super loose + const reduceFactor = (Math.min(0.5, tightnessFactor - 0.5) / 0.5) * 0.7; + velocityRight = currentRightSpeed * reduceFactor; + } else if (tightnessFactor < 0.5) { + // Super tight + const reduceFactor = (Math.min(0, tightnessFactor) * 4); + velocityRight = expectedRightSpeed + Math.min( + maxVelocity, + Math.max( + -maxVelocity, + (expectedRightSpeed - currentRightSpeed) * 2 + ) + ); + } + } - // byte 2 > 127 rotates backward - const motor = [1, 32, 1, velocityLeft & (2 ** 8 - 1), velocityLeft >> 8, - 1, 32, 1, velocityRight & (2 ** 8 - 1), velocityRight >> 8]; - controller.send(motor, null, 49, true); - }); + if (velocityLeft < 0) { + motorData[1] = 0xe0; + motorData[2] = 0xfe; + velocityLeft = -velocityLeft; + } + + if (velocityRight < 0) { + motorData[6] = 0xe0; + motorData[7] = 0xfe; + velocityRight = -velocityRight; + } + + + if (expectedLeftSpeed) { + velocityLeft = Math.pow(velocityLeft, 2) * velocityFactor; + } else { + velocityLeft = velocityLeft * velocityFactor; + } + + if (expectedRightSpeed) { + velocityRight = Math.pow(velocityRight, 2) * velocityFactor; + } else { + velocityRight = velocityRight * velocityFactor; + } + + velocityLeft = Math.min( + maxWheelForce, + Math.floor(velocityLeft) + ); + + velocityRight = Math.min( + maxWheelForce, + Math.floor(velocityRight) + ); + + motorData[3] = velocityLeft & 0xff; + motorData[4] = velocityLeft >> 8; + + motorData[8] = velocityRight & 0xff; + motorData[9] = velocityRight >> 8; + + //// byte 2 > 127 rotates backward + if (Math.round(currentLeftSpeed * 100) !== Math.round(expectedLeftSpeed * 100)) { + console.log(expectedLeftSpeed + " " + Math.round(currentLeftSpeed * 100) + " -> " + velocityLeft + "\t" + expectedRightSpeed + " -> " + currentRightSpeed); + } + + controller.send(motorData, null, 49, true); } incomingData(data) { const reportId = data[0]; From c5aa244b9b00f855f4dace6dfb793de7b59a0fdf Mon Sep 17 00:00:00 2001 From: Antoine C Date: Wed, 22 Feb 2023 13:25:01 +0000 Subject: [PATCH 06/19] Kontrol S4 Mk3: better motor stability and smoother support for 45 RPM mode --- res/controllers/Traktor-Kontrol-S4-MK3.js | 193 ++++++++++++++-------- 1 file changed, 121 insertions(+), 72 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index 21a769be4578..50d2cfd444de 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -98,6 +98,15 @@ const gridButtonBlinkOverBeat = true; // less responsive it gets in Mixxx. Default: 5 const wheelSpeedSample = 5; +// Make the sampler tab a beatlooproll tab instead +// Default: false +const useBeatloopRoolInsteadOfSampler = false; + +// 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 @@ -115,9 +124,8 @@ const turnTableSpeedSample = 40; 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 impact resistance of the wheel if you are using a tight setting (tightnessFactor< 0.5) -// Default: 24000. -const maxWheelForce = 24000; +// This will also im +const maxWheelForce = 25000; // Traktor seems to cap the max value at 60000, which just sounds insane @@ -161,7 +169,7 @@ const quickEffectPresetColors = [ // assign samplers to the crossfader on startup const samplerCrossfaderAssign = true; -const motorWindUpMilliseconds = 600; +const motorWindUpMilliseconds = 1200; const motorWindDownMilliseconds = 900; /* @@ -501,6 +509,8 @@ class Button extends Component { this.inBitLength = 1; } } + unshift() {} + shift() {} setKey(key) { this.inKey = key; if (key === this.outKey) { @@ -581,7 +591,10 @@ class TriggerButton extends Button { super(options); } onShortPress() { - script.triggerControl(this.group, this.inKey, true); + engine.setValue(this.group, this.inKey, true); + } + onShortRelease() { + engine.setValue(this.group, this.inKey, false); } } @@ -636,8 +649,16 @@ class CueButton extends PushButton { input(pressed) { if (this.deck.moveMode === moveModes.keyboard) { 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); + } } } } @@ -772,38 +793,22 @@ class KeyboardButton extends PushButton { } } -class BeatLoopRollButton extends Button { +const beatLoopRolls = [0.0625, 0.125, 0.25, 0.5, 1, 2, 4, 8]; +class BeatLoopRollButton extends TriggerButton { constructor(options) { - super(options); - if (this.number === undefined || !Number.isInteger(this.number) || this.number < 1 || this.number > 8) { - throw Error("BeatLoopRollButton must have a number property of an integer between 1 and 8"); + 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"); } + options.key = "beatlooproll_"+beatLoopRolls[options.number]+"_activate"; + super(options); if (this.deck === undefined) { throw Error("BeatLoopRollButton must have a deck attached to it"); } + this.outConnect(); } - unshift() { - this.outTrigger(); - } - shift() { - this.outTrigger(); - } output(value) { - // In us - } - 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 unaivailable."); - } - } + this.send(LEDColors.white + (value ? this.brightnessOn : this.brightnessOff)); } } @@ -816,8 +821,6 @@ class SamplerButton extends Button { this.group = "[Sampler" + this.number + "]"; this.outConnect(); } - unshift() { } - shift() { } onShortPress() { if (!this.shifted) { if (engine.getValue(this.group, "track_loaded") === 0) { @@ -1275,7 +1278,6 @@ const wheelAbsoluteMax = 2879; const wheelTimerMax = 2 ** 32 - 1; const wheelTimerTicksPerSecond = 100000000; -const baseRevolutionsPerMinute = 33 + 1 / 3; const baseRevolutionsPerSecond = baseRevolutionsPerMinute / 60; const wheelTicksPerTimerTicksToRevolutionsPerSecond = wheelTimerTicksPerSecond / wheelAbsoluteMax; @@ -2043,7 +2045,7 @@ class S4Mk3Deck extends Deck { ]; const hotcuePage2 = Array(8).fill({}); const hotcuePage3 = Array(8).fill({}); - const samplerPage = Array(8).fill({}); + const samplerOrBeatloopRoolPage = Array(8).fill({}); this.keyboard = Array(8).fill({}); let i = 0; /* eslint no-unused-vars: "off" */ @@ -2051,20 +2053,30 @@ class S4Mk3Deck extends Deck { // start with hotcue 5; hotcues 1-4 are in defaultPadLayer hotcuePage2[i] = new HotcueButton({number: i + 1}); hotcuePage3[i] = new HotcueButton({number: i + 13}); - let samplerNumber = i + 1; - if (samplerNumber > 4) { - samplerNumber += 4; - } - if (decks[0] > 1) { - samplerNumber += 4; - } - samplerPage[i] = new SamplerButton({number: samplerNumber}); - if (samplerCrossfaderAssign) { - engine.setValue( - "[Sampler" + samplerNumber + "]", - "orientation", - (decks[0] === 1) ? 0 : 2 - ); + if (useBeatloopRoolInsteadOfSampler) { + samplerOrBeatloopRoolPage[i] = new BeatLoopRollButton({ + number: i, + deck: this, + }); + + } else { + let samplerNumber = i + 1; + if (samplerNumber > 4) { + samplerNumber += 4; + } + if (decks[0] > 1) { + samplerNumber += 4; + } + samplerOrBeatloopRoolPage[i] = new SamplerButton({ + number: samplerNumber, + }); + if (samplerCrossfaderAssign) { + engine.setValue( + "[Sampler" + samplerNumber + "]", + "orientation", + (decks[0] === 1) ? 0 : 2 + ); + } } this.keyboard[i] = new KeyboardButton({ number: i + 1, @@ -2083,7 +2095,6 @@ class S4Mk3Deck extends Deck { Object.assign(pad, io.pads[index]); if (!(pad instanceof HotcueButton)) { pad.color = deck.color; - pad.unshift(); } // don't change the group of SamplerButtons if (!(pad instanceof SamplerButton)) { @@ -2092,6 +2103,7 @@ class S4Mk3Deck extends Deck { if (pad.inPacket === undefined) { pad.inPacket = inPackets[1]; } + pad.unshift(); pad.outPacket = outPacket; pad.inConnect(); pad.outConnect(); @@ -2164,7 +2176,7 @@ class S4Mk3Deck extends Deck { deck: this, onShortPress: function() { if (this.deck.currentPadLayer !== this.deck.padLayers.samplerPage) { - switchPadLayer(this.deck, samplerPage); + switchPadLayer(this.deck, samplerOrBeatloopRoolPage); engine.setValue("[Samplers]", "show_samplers", true); this.deck.currentPadLayer = this.deck.padLayers.samplerPage; } else { @@ -2368,7 +2380,6 @@ class S4Mk3Deck extends Deck { switch (this.deck.wheelMode) { case wheelModes.motor: - // engine.setValue(this.group, "scratch2", 1.0); engine.setValue(this.group, "scratch2", this.ttAvgSpeed / baseRevolutionsPerSecond); break; case wheelModes.loopIn: @@ -2791,6 +2802,18 @@ class S4MK3 { }); if (useMotors) { engine.beginTimer(20, this.motorCallback.bind(this)); + this.leftVelocityFactor = wheelAbsoluteMax * baseRevolutionsPerSecond * 2; + this.rightVelocityFactor = wheelAbsoluteMax * baseRevolutionsPerSecond * 2; + + this.leftFactor = [this.leftVelocityFactor]; + this.leftFactorIdx = 1; + this.rightFactor = [this.rightVelocityFactor]; + this.rightFactorIdx = 1; + + this.averageLeftCorrectness = []; + this.averageLeftCorrectnessIdx = 0; + this.averageRightCorrectness = []; + this.averageRightCorrectnessIdx = 0; } } @@ -2800,7 +2823,6 @@ class S4MK3 { 1, 0x20, 1, 0, 0, ]; - const velocityFactor = 4500; const maxVelocity = 10; let velocityLeft = 0; @@ -2819,15 +2841,15 @@ class S4MK3 { expectedRightSpeed = engine.getValue(this.rightDeck.group, "rate_ratio"); } - const currentLeftSpeed = (this.leftDeck.wheelRelative.avgSpeed + this.leftDeck.wheelRelative.ttAvgSpeed) / (2 * baseRevolutionsPerSecond); - const currentRightSpeed = (this.rightDeck.wheelRelative.avgSpeed + this.rightDeck.wheelRelative.ttAvgSpeed) / (2 * baseRevolutionsPerSecond); + const currentLeftSpeed = this.leftDeck.wheelRelative.avgSpeed / baseRevolutionsPerSecond; + const currentRightSpeed = this.rightDeck.wheelRelative.avgSpeed / baseRevolutionsPerSecond; if (expectedLeftSpeed) { velocityLeft = expectedLeftSpeed + Math.min( maxVelocity, Math.max( -maxVelocity, - (expectedLeftSpeed - currentLeftSpeed) * 2 + (expectedLeftSpeed - currentLeftSpeed) ) ); } else { @@ -2837,12 +2859,12 @@ class S4MK3 { velocityLeft = currentLeftSpeed * reduceFactor; } else if (tightnessFactor < 0.5) { // Super tight - const reduceFactor = (Math.min(0, tightnessFactor) * 4); + const reduceFactor = (2 - Math.max(0, tightnessFactor) * 4); velocityLeft = expectedLeftSpeed + Math.min( maxVelocity, Math.max( -maxVelocity, - (expectedLeftSpeed - currentLeftSpeed) * 2 + (expectedLeftSpeed - currentLeftSpeed) * reduceFactor ) ); @@ -2864,12 +2886,13 @@ class S4MK3 { velocityRight = currentRightSpeed * reduceFactor; } else if (tightnessFactor < 0.5) { // Super tight - const reduceFactor = (Math.min(0, tightnessFactor) * 4); + const reduceFactor = (2 - Math.max(0, tightnessFactor) * 4); + console.log(reduceFactor); velocityRight = expectedRightSpeed + Math.min( maxVelocity, Math.max( -maxVelocity, - (expectedRightSpeed - currentRightSpeed) * 2 + (expectedRightSpeed - currentRightSpeed) * reduceFactor ) ); @@ -2888,17 +2911,49 @@ class S4MK3 { velocityRight = -velocityRight; } + const roundedCurrentLeftSpeed = Math.round(currentLeftSpeed * 100); + const roundedCurrentRightSpeed = Math.round(currentRightSpeed * 100); - if (expectedLeftSpeed) { - velocityLeft = Math.pow(velocityLeft, 2) * velocityFactor; - } else { - velocityLeft = velocityLeft * velocityFactor; + velocityLeft = velocityLeft * this.leftVelocityFactor; + velocityRight = velocityRight * this.rightVelocityFactor; + + const minNormalFactor = 0.8 * wheelAbsoluteMax * baseRevolutionsPerSecond * 2; + const maxNormalFactor = 1.2 * wheelAbsoluteMax * baseRevolutionsPerSecond * 2; + + if (velocityLeft > minNormalFactor && velocityLeft < maxNormalFactor) { + this.averageLeftCorrectness[this.averageLeftCorrectnessIdx] = roundedCurrentLeftSpeed; + this.averageLeftCorrectnessIdx = (this.averageLeftCorrectnessIdx + 1) % 10; + const averageCorrectness = Math.round(this.averageLeftCorrectness.reduce((a, b) => a+b, 0) / this.averageLeftCorrectness.length); + this.leftFactor[this.leftFactorIdx] = velocityLeft; + this.leftFactorIdx = (this.leftFactorIdx + 1) % 10; + const averageFactor = Math.round(this.leftFactor.reduce((a, b) => a+b, 0) / this.leftFactor.length); + + + if ((averageCorrectness < 100 && velocityLeft > this.leftVelocityFactor) || (averageCorrectness > 100 && velocityLeft < this.leftVelocityFactor)) { + this.leftVelocityFactor = averageFactor; + } } - if (expectedRightSpeed) { - velocityRight = Math.pow(velocityRight, 2) * velocityFactor; - } else { - velocityRight = velocityRight * velocityFactor; + if (velocityRight > minNormalFactor && velocityRight < maxNormalFactor) { + this.averageRightCorrectness[this.averageRightCorrectnessIdx] = roundedCurrentRightSpeed / (expectedRightSpeed || 0.001); + this.averageRightCorrectnessIdx = (this.averageRightCorrectnessIdx + 1) % 20; + const averageCorrectness = Math.round(this.averageRightCorrectness.reduce((a, b) => a+b, 0) / this.averageRightCorrectness.length); + this.rightFactor[this.rightFactorIdx] = velocityRight; + this.rightFactorIdx = (this.rightFactorIdx + 1) % 20; + const averageFactor = Math.round(this.rightFactor.reduce((a, b) => a+b, 0) / this.rightFactor.length); + + + if ((averageCorrectness < 100 && velocityRight > this.rightVelocityFactor) || (averageCorrectness > 100 && velocityRight < this.rightVelocityFactor)) { + this.rightVelocityFactor = averageFactor; + } + } + + if (velocityLeft) { + velocityLeft += wheelAbsoluteMax / 2; + } + + if (velocityRight) { + velocityRight += wheelAbsoluteMax / 2; } velocityLeft = Math.min( @@ -2916,12 +2971,6 @@ class S4MK3 { motorData[8] = velocityRight & 0xff; motorData[9] = velocityRight >> 8; - - //// byte 2 > 127 rotates backward - if (Math.round(currentLeftSpeed * 100) !== Math.round(expectedLeftSpeed * 100)) { - console.log(expectedLeftSpeed + " " + Math.round(currentLeftSpeed * 100) + " -> " + velocityLeft + "\t" + expectedRightSpeed + " -> " + currentRightSpeed); - } - controller.send(motorData, null, 49, true); } incomingData(data) { From b72e8ca69ce402c1f32a90bcd88ede969f3a819a Mon Sep 17 00:00:00 2001 From: Antoine C Date: Wed, 22 Feb 2023 19:28:25 +0000 Subject: [PATCH 07/19] Kontrol S4 Mk3: fix loop mode exit and aligning with doc --- res/controllers/Traktor-Kontrol-S4-MK3.js | 109 +++++++++++----------- 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index 50d2cfd444de..6df442cb56b2 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -90,7 +90,7 @@ const keepLEDWithOneColorDimedWhenInactive = true; // '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. +// Define whether the grid button would blink when the playback is going over a detected beat. Can help to adjust beat grid. // Default: true const gridButtonBlinkOverBeat = true; @@ -686,9 +686,6 @@ class Encoder extends Component { } this.onChange(isRight); } - isRightTurn(value) { - // detect wrap around - } } class HotcueButton extends PushButton { @@ -1550,6 +1547,27 @@ class S4Mk3Deck extends Deck { engine.setValue(this.group, this.key, false); } }, + loopModeOff: function(skipRestore) { + if (this.previousWheelMode !== null) { + this.indicator(false); + const wheelOutput = Array(40).fill(0); + wheelOutput[0] = decks[0] - 1; + const that = this; + controller.send(wheelOutput, null, 50, true); + if (!skipRestore) { + that.deck.wheelMode = that.previousWheelMode; + } + that.previousWheelMode = null; + if (this.loopModeConnection !== null) { + this.loopModeConnection.disconnect(); + this.loopModeConnection = null; + } + } + }, + onLoopChange: function(loopEnabled) { + if (loopEnabled) { return; } + this.loopModeOff(); + }, onShortPress: function() { this.indicator(false); if (this.shifted) { @@ -1561,24 +1579,13 @@ class S4Mk3Deck extends Deck { this.indicator(true); // Else, we enter/exit the loop in wheel mode } else if (this.previousWheelMode === null) { + this.deck.fluxButton.loopModeOff(); + engine.setValue(this.group, "scratch2_enable", false); this.previousWheelMode = this.deck.wheelMode; this.deck.wheelMode = wheelModes.loopIn; if (this.loopModeConnection === null) { - this.loopModeConnection = engine.makeConnection(this.group, this.outKey, (loopEnabled) => { - if (loopEnabled) { return; } - - this.indicator(false); - const wheelOutput = Array(40).fill(0); - wheelOutput[0] = decks[0] - 1; - engine.beginTimer(decks[0] * 35, () => { - controller.send(wheelOutput, null, 50, true); - this.deck.wheelMode = this.previousWheelMode; - this.previousWheelMode = null; - }, true); - this.loopModeConnection.disconnect(); - this.loopModeConnection = null; - }); + this.loopModeConnection = engine.makeConnection(this.group, this.outKey, this.onLoopChange.bind(this)); } const wheelOutput = Array(40).fill(0); @@ -1594,17 +1601,7 @@ class S4Mk3Deck extends Deck { this.indicator(true); } else if (this.previousWheelMode !== null) { - if (this.loopModeConnection !== null) { - this.loopModeConnection.disconnect(); - this.loopModeConnection = null; - } - const wheelOutput = Array(40).fill(0); - wheelOutput[0] = decks[0] - 1; - engine.beginTimer(decks[0] * 35, () => { - controller.send(wheelOutput, null, 50, true); - this.deck.wheelMode = this.previousWheelMode; - this.previousWheelMode = null; - }, true); + this.loopModeOff(); } } else { engine.setValue(this.group, this.key, true); @@ -1640,6 +1637,27 @@ class S4Mk3Deck extends Deck { engine.setValue(this.group, "scratch2_enable", false); } }, + loopModeOff: function(skipRestore) { + if (this.previousWheelMode !== null) { + this.indicator(false); + const wheelOutput = Array(40).fill(0); + wheelOutput[0] = decks[0] - 1; + const that = this; + controller.send(wheelOutput, null, 50, true); + if (!skipRestore) { + that.deck.wheelMode = that.previousWheelMode; + } + that.previousWheelMode = null; + if (this.loopModeConnection !== null) { + this.loopModeConnection.disconnect(); + this.loopModeConnection = null; + } + } + }, + onLoopChange: function(loopEnabled) { + if (loopEnabled) { return; } + this.loopModeOff(); + }, onShortPress: function() { this.indicator(false); if (this.shifted) { @@ -1650,23 +1668,12 @@ class S4Mk3Deck extends Deck { this.deck.reverseButton.indicator(false); // Else, we enter/exit the loop in wheel mode } else if (this.previousWheelMode === null) { + this.deck.reverseButton.loopModeOff(); + engine.setValue(this.group, "scratch2_enable", false); this.previousWheelMode = this.deck.wheelMode; this.deck.wheelMode = wheelModes.loopOut; if (this.loopModeConnection === null) { - this.loopModeConnection = engine.makeConnection(this.group, this.outKey, (loopEnabled) => { - if (loopEnabled) { return; } - - this.indicator(false); - const wheelOutput = Array(40).fill(0); - wheelOutput[0] = decks[0] - 1; - engine.beginTimer(decks[0] * 35, () => { - controller.send(wheelOutput, null, 50, true); - this.deck.wheelMode = this.previousWheelMode; - this.previousWheelMode = null; - }, true); - this.loopModeConnection.disconnect(); - this.loopModeConnection = null; - }); + this.loopModeConnection = engine.makeConnection(this.group, this.outKey, this.onLoopChange.bind(this)); } const wheelOutput = Array(40).fill(0); @@ -1684,17 +1691,7 @@ class S4Mk3Deck extends Deck { this.indicator(true); } else if (this.previousWheelMode !== null) { - if (this.loopModeConnection !== null) { - this.loopModeConnection.disconnect(); - this.loopModeConnection = null; - } - const wheelOutput = Array(40).fill(0); - wheelOutput[0] = decks[0] - 1; - engine.beginTimer(decks[0] * 35, () => { - controller.send(wheelOutput, null, 50, true); - this.deck.wheelMode = this.previousWheelMode; - this.previousWheelMode = null; - }, true); + this.loopModeOff(); } } else { engine.setValue(this.group, this.key, true); @@ -2250,6 +2247,8 @@ class S4Mk3Deck extends Deck { deck: this, input: function(press) { if (press) { + this.deck.reverseButton.loopModeOff(true); + this.deck.fluxButton.loopModeOff(true); if (this.deck.wheelMode === wheelModes.motor) { this.deck.wheelMode = wheelModes.vinyl; motorWindDownTimer = engine.beginTimer(motorWindDownMilliseconds, motorWindDownTimerCallback, true); @@ -2275,6 +2274,8 @@ class S4Mk3Deck extends Deck { deck: this, input: function(press) { if (press) { + this.deck.reverseButton.loopModeOff(true); + this.deck.fluxButton.loopModeOff(true); if (this.deck.wheelMode === wheelModes.vinyl) { this.deck.wheelMode = wheelModes.jog; } else { From 4fdd8957ab374cf1a694bdde6aac7a0db526a51a Mon Sep 17 00:00:00 2001 From: Antoine C Date: Mon, 27 Feb 2023 10:27:26 +0000 Subject: [PATCH 08/19] Kontrol S4 Mk3: styling update --- res/controllers/Traktor-Kontrol-S4-MK3.js | 386 +++++++++++----------- 1 file changed, 188 insertions(+), 198 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index 6df442cb56b2..615af1d6f0ff 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -16,7 +16,7 @@ /// 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 = { +const LedColors = { off: 0, red: 4, carrot: 8, @@ -37,21 +37,44 @@ const LEDColors = { white: 68, }; +// A full list can be found here: https://manual.mixxx.org/2.4/en/chapters/appendix/mixxx_controls.html#control-[Library]-sort_column +const LibraryColumns = { + Artist: 1, + Title: 2, + Album: 3, + Albumartist: 4, + Year: 5, + Genre: 6, + Composer: 7, + Grouping: 8, + Tracknumber: 9, + Filetype: 10, + NativeLocation: 11, + Comment: 12, + Duration: 13, + Bitrate: 14, + BPM: 15, + ReplayGain: 16, + DatetimeAdded: 17, + TimesPlayed: 18, + Rating: 19, +}; + 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, + 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, ]; /* @@ -59,73 +82,72 @@ const KeyboardColors = [ * Adjust these to your liking */ -const deckColors = [ - LEDColors.red, - LEDColors.blue, - LEDColors.yellow, - LEDColors.purple, +const DeckColors = [ + LedColors.red, + LedColors.blue, + LedColors.yellow, + 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 LibrarySortableColumns = [ + LibraryColumns.Artist, + LibraryColumns.Title, + LibraryColumns.BPM, + LibraryColumns.Key, + LibraryColumns.DatetimeAdded, ]; -const loopWheelMoveFactor = 50; -const loopEncoderMoveFactor = 500; -const loopEncoderShiftMoveFactor = 2500; +const LOOP_WHEEL_MOVE_FACTOR = 50; // LOOP_WHEEL_MOVE_FACTOR +const LOOP_ENCODER_MOVE_FACTOR = 500; // LOOP_ENCODER_MOVE_FACTOR +const LOOP_ENCODER_SHIFTMOVE_FACTOR = 2500; // LOOP_ENCODER_SHIFTMOVE_FACTOR -const tempoFaderSoftTakeoverColorLow = LEDColors.white; -const tempoFaderSoftTakeoverColorHigh = LEDColors.green; +const TempoFaderSoftTakeoverColorLow = LedColors.white; +const TempoFaderSoftTakeoverColorHigh = LedColors.green; // Define whether or not to keep LED that have only one color (reverse, flux, play) dimmed if they are inactive. // 'true' will keep them dimmed, 'false' will turn them off. Default: true -const keepLEDWithOneColorDimedWhenInactive = true; +const KeepLEDWithOneColorDimedWhenInactive = 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; +const UseKeylockOnMaster = false; -// Define whether the grid button would blink when the playback is going over a detected beat. Can help to adjust beat grid. -// Default: true -const gridButtonBlinkOverBeat = true; +// 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; // 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 = 5; +const WheelSpeedSample = 5; // Make the sampler tab a beatlooproll tab instead // Default: false -const useBeatloopRoolInsteadOfSampler = false; +const UseBeatloopRoolInsteadOfSampler = false; // 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; +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 = true; +const UseMotors = true; // 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: 40 -const turnTableSpeedSample = 40; +const TurnTableSpeedSample = 40; // 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; +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 im -const maxWheelForce = 25000; // Traktor seems to cap the max value at 60000, which just sounds insane +const MaxWheelForce = 25000; // Traktor seems to cap the max value at 60000, which just sounds insane @@ -133,44 +155,44 @@ const maxWheelForce = 25000; // Traktor seems to cap the max value at 60000, wh // 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, +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; +const SamplerCrossfaderAssign = true; -const motorWindUpMilliseconds = 1200; -const motorWindDownMilliseconds = 900; +const MotorWindUpMilliseconds = 1200; +const MotorWindDownMilliseconds = 900; /* * HID packet parsing library @@ -333,7 +355,7 @@ class Component { 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 unaivailable."); + console.warn("Unable to connect '" + this.group + "." + this.outKey + "' to the controller output. The control appears to be unavailable."); } } } @@ -452,7 +474,7 @@ class Deck extends ComponentContainer { this.moveMode = this.secondDeckModes.moveMode; if (this.wheelMode === wheelModes.motor) { - engine.beginTimer(motorWindUpMilliseconds, function() { + engine.beginTimer(MotorWindUpMilliseconds, function() { engine.setValue(newGroup, "scratch2_enable", true); }, true); } @@ -460,7 +482,7 @@ class Deck extends ComponentContainer { if (currentModes.wheelMode === wheelModes.motor) { this.wheelTouch.touched = true; - engine.beginTimer(motorWindDownMilliseconds, () => { + engine.beginTimer(MotorWindDownMilliseconds, () => { this.wheelTouch.touched = false; }, true); } @@ -526,11 +548,11 @@ class Button extends Component { return; } const brightness = (value > 0) ? this.brightnessOn : this.brightnessOff; - this.send((this.color || LEDColors.white) + 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)); + this.send((this.indicatorColor || this.color || LedColors.white) + (this.indicatorState ? this.brightnessOn : this.brightnessOff)); } indicator(on) { if (on && this.indicatorTimer === 0) { @@ -655,7 +677,7 @@ class CueButton extends PushButton { 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.beginTimer(MotorWindDownMilliseconds, function() { engine.setValue(this.group, "scratch2_enable", true); }, true); } @@ -721,7 +743,7 @@ class HotcueButton extends PushButton { 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 unaivailable."); + 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); @@ -730,7 +752,7 @@ class HotcueButton extends PushButton { 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 unaivailable."); + console.warn("Unable to connect '" + this.group + "." + this.colorKey + "' to the controller output. The control appears to be unavailable."); } } } @@ -784,7 +806,7 @@ class KeyboardButton extends PushButton { if (connection) { this.outConnections[0] = connection; } else { - console.warn("Unable to connect '" + this.group + ".key' to the controller output. The control appears to be unaivailable."); + console.warn("Unable to connect '" + this.group + ".key' to the controller output. The control appears to be unavailable."); } } } @@ -805,7 +827,7 @@ class BeatLoopRollButton extends TriggerButton { this.outConnect(); } output(value) { - this.send(LEDColors.white + (value ? this.brightnessOn : this.brightnessOff)); + this.send(LedColors.white + (value ? this.brightnessOn : this.brightnessOff)); } } @@ -858,13 +880,13 @@ class SamplerButton extends Button { if (connection0) { this.outConnections[0] = connection0; } else { - console.warn("Unable to connect '" + this.group + ".play' to the controller output. The control appears to be unaivailable."); + 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 unaivailable."); + console.warn("Unable to connect '" + this.group + ".track_loaded' to the controller output. The control appears to be unavailable."); } } } @@ -1100,7 +1122,7 @@ class Mixer extends ComponentContainer { resetFxSelectorColors() { for (const selector of [1, 2, 3, 4, 5]) { - this.outPacket.data[49 + selector] = quickEffectPresetColors[selector - 1] + Button.prototype.brightnessOn; + this.outPacket.data[49 + selector] = QuickEffectPresetColors[selector - 1] + Button.prototype.brightnessOn; } this.outPacket.send(); } @@ -1124,7 +1146,7 @@ class FXSelect extends Button { if (selector > this.number) { presetNumber--; } - this.outPacket.data[49 + selector] = quickEffectPresetColors[presetNumber - 1] + this.brightnessOn; + this.outPacket.data[49 + selector] = QuickEffectPresetColors[presetNumber - 1] + this.brightnessOn; } } this.outPacket.send(); @@ -1182,7 +1204,7 @@ class QuickEffectButton extends Button { script.toggleControl(this.group, "enabled"); } else { const presetNumber = this.mixer.calculatePresetNumber(); - this.color = quickEffectPresetColors[presetNumber - 1]; + this.color = QuickEffectPresetColors[presetNumber - 1]; engine.setValue(this.group, "loaded_chain_preset", presetNumber + 1); this.mixer.firstPressedFxSelector = null; this.mixer.secondPressedFxSelector = null; @@ -1204,7 +1226,7 @@ class QuickEffectButton extends Button { } } presetLoaded(presetNumber) { - this.color = quickEffectPresetColors[presetNumber - 2]; + this.color = QuickEffectPresetColors[presetNumber - 2]; this.outConnections[1].trigger(); } outConnect() { @@ -1213,13 +1235,13 @@ class QuickEffectButton extends Button { if (connection0) { this.outConnections[0] = connection0; } else { - console.warn("Unable to connect '" + this.group + ".loaded_chain_preset' to the controller output. The control appears to be unaivailable."); + console.warn("Unable to connect '" + this.group + ".loaded_chain_preset' to the controller output. The control appears to be unavailable."); } const connection1 = engine.makeConnection(this.group, "enabled", this.output.bind(this)); if (connection1) { this.outConnections[1] = connection1; } else { - console.warn("Unable to connect '" + this.group + ".enabled' to the controller output. The control appears to be unaivailable."); + console.warn("Unable to connect '" + this.group + ".enabled' to the controller output. The control appears to be unavailable."); } } } @@ -1242,31 +1264,31 @@ Button.prototype.uncoloredOutput = function(value) { if (this.indicatorTimer !== 0) { return; } - const color = (value > 0) ? (this.color || LEDColors.white) + this.brightnessOn : LEDColors.off; + const color = (value > 0) ? (this.color || LedColors.white) + this.brightnessOn : LedColors.off; this.send(color); }; 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, + 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; @@ -1275,7 +1297,7 @@ const wheelAbsoluteMax = 2879; const wheelTimerMax = 2 ** 32 - 1; const wheelTimerTicksPerSecond = 100000000; -const baseRevolutionsPerSecond = baseRevolutionsPerMinute / 60; +const baseRevolutionsPerSecond = BaseRevolutionsPerMinute / 60; const wheelTicksPerTimerTicksToRevolutionsPerSecond = wheelTimerTicksPerSecond / wheelAbsoluteMax; const wheelLEDmodes = { @@ -1441,7 +1463,7 @@ class S4Mk3Deck extends Deck { super(decks, colors); this.playButton = new PlayButton({ - output: keepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput + output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput }); this.cueButton = new CueButton({ @@ -1452,10 +1474,10 @@ class S4Mk3Deck extends Deck { this.syncMasterButton = new Button({ key: "sync_leader", defaultRange: 0.08, - shift: useKeylockOnMaster ? function() { + shift: UseKeylockOnMaster ? function() { this.setKey("keylock"); } : undefined, - unshift: useKeylockOnMaster ? function() { + unshift: UseKeylockOnMaster ? function() { this.setKey("sync_leader"); } : undefined, onShortRelease: function() { @@ -1488,10 +1510,10 @@ class S4Mk3Deck extends Deck { engine.softTakeover(this.group, "rate", true); } }, - shift: !useKeylockOnMaster ? function() { + shift: !UseKeylockOnMaster ? function() { this.setKey("keylock"); } : undefined, - unshift: !useKeylockOnMaster ? function() { + unshift: !UseKeylockOnMaster ? function() { this.setKey("sync_enabled"); } : undefined, }); @@ -1511,10 +1533,10 @@ class S4Mk3Deck extends Deck { const parameterValue = engine.getParameter(this.group, this.outKey); const diffFromHardware = parameterValue - this.tempoFader.hardwarePosition; if (diffFromHardware > this.toleranceWindow) { - this.send(tempoFaderSoftTakeoverColorHigh + Button.prototype.brightnessOn); + this.send(TempoFaderSoftTakeoverColorHigh + Button.prototype.brightnessOn); return; } else if (diffFromHardware < (-1 * this.toleranceWindow)) { - this.send(tempoFaderSoftTakeoverColorLow + Button.prototype.brightnessOn); + this.send(TempoFaderSoftTakeoverColorLow + Button.prototype.brightnessOn); return; } @@ -1541,7 +1563,7 @@ class S4Mk3Deck extends Deck { shift: function() { this.setKey("loop_enabled"); }, - output: keepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput, + output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput, onShortRelease: function() { if (!this.shifted) { engine.setValue(this.group, this.key, false); @@ -1626,11 +1648,11 @@ class S4Mk3Deck extends Deck { 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 unaivailable."); + console.warn("Unable to connect '" + this.group + "." + this.outKey + "' to the controller output. The control appears to be unavailable."); } } }, - output: keepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput, + output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput, onShortRelease: function() { if (!this.shifted) { engine.setValue(this.group, this.key, false); @@ -1699,7 +1721,7 @@ class S4Mk3Deck extends Deck { } }); this.gridButton = new Button({ - key: gridButtonBlinkOverBeat ? "beat_active" : undefined, + key: GridButtonBlinkOverBeat ? "beat_active" : undefined, deck: this, previousMoveMode: null, onShortPress: function() { @@ -1767,10 +1789,10 @@ class S4Mk3Deck extends Deck { if (pressed) { this.deck.shift(); // This button only has one color. - this.send(LEDColors.white + this.brightnessOn); + this.send(LedColors.white + this.brightnessOn); } else { this.deck.unshift(); - this.send(LEDColors.white + this.brightnessOff); + this.send(LedColors.white + this.brightnessOff); } }, }); @@ -1838,7 +1860,7 @@ class S4Mk3Deck extends Deck { deck: this, onChange: function(right) { if (this.deck.wheelMode === wheelModes.loopIn || this.deck.wheelMode === wheelModes.loopOut) { - const moveFactor = this.shifted ? loopEncoderShiftMoveFactor : loopEncoderMoveFactor; + const moveFactor = this.shifted ? LOOP_ENCODER_SHIFTMOVE_FACTOR : LOOP_ENCODER_MOVE_FACTOR; const valueIn = engine.getValue(this.group, "loop_start_position") + (right ? moveFactor : -moveFactor); const valueOut = engine.getValue(this.group, "loop_end_position") + (right ? moveFactor : -moveFactor); engine.setValue(this.group, "loop_start_position", valueIn); @@ -1873,8 +1895,8 @@ class S4Mk3Deck extends Deck { currentSortedColumnIdx: -1, onChange: function(right) { if (this.libraryViewButtonPressed) { - this.currentSortedColumnIdx = (this.currentSortedColumnIdx + (right ? 1 : -1)) % librarySortableColumns.length; - engine.setValue("[Library]", "sort_column", librarySortableColumns[this.currentSortedColumnIdx]); + this.currentSortedColumnIdx = (this.currentSortedColumnIdx + (right ? 1 : -1)) % LibrarySortableColumns.length; + engine.setValue("[Library]", "sort_column", LibrarySortableColumns[this.currentSortedColumnIdx]); } else if (this.starButtonPressed) { if (this.shifted) { // FIXME doesn't exist, feature request needed @@ -1965,7 +1987,7 @@ class S4Mk3Deck extends Deck { if (connection) { this.outConnections[0] = connection; } else { - console.warn("Unable to connect '" + this.group + ".focused_widget' to the controller output. The control appears to be unaivailable."); + console.warn("Unable to connect '" + this.group + ".focused_widget' to the controller output. The control appears to be unavailable."); } }, onShortRelease: function() { @@ -2050,7 +2072,7 @@ class S4Mk3Deck extends Deck { // start with hotcue 5; hotcues 1-4 are in defaultPadLayer hotcuePage2[i] = new HotcueButton({number: i + 1}); hotcuePage3[i] = new HotcueButton({number: i + 13}); - if (useBeatloopRoolInsteadOfSampler) { + if (UseBeatloopRoolInsteadOfSampler) { samplerOrBeatloopRoolPage[i] = new BeatLoopRollButton({ number: i, deck: this, @@ -2067,7 +2089,7 @@ class S4Mk3Deck extends Deck { samplerOrBeatloopRoolPage[i] = new SamplerButton({ number: samplerNumber, }); - if (samplerCrossfaderAssign) { + if (SamplerCrossfaderAssign) { engine.setValue( "[Sampler" + samplerNumber + "]", "orientation", @@ -2144,30 +2166,9 @@ class S4Mk3Deck extends Deck { this.deck.lightPadMode(); } }); + // The record button doesn't have a mapping by default, but you can add yours here // this.recordPadModeButton = new Button({ - // deck: this, - // input: function(pressed) { - // if (!this.shifted) { - // if (pressed) { - // if (this.deck.currentPadLayer !== this.deck.padLayers.hotcuePage3) { - // switchPadLayer(this.deck, hotcuePage3); - // this.deck.currentPadLayer = this.deck.padLayers.hotcuePage3; - // } else { - // switchPadLayer(this.deck, defaultPadLayer); - // this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; - // } - // this.deck.lightPadMode(); - // } - // } else { - // engine.setValue(this.deck.group, "loop_out", pressed); - // } - // }, - // // make sure loop_out gets reset to 0 if shift is released before this button - // unshift: function() { - // if (engine.getValue(this.deck.group, "loop_out") === 1) { - // engine.setValue(this.deck.group, "loop_out", 0); - // } - // } + // ... // }); this.samplesPadModeButton = new Button({ deck: this, @@ -2184,20 +2185,9 @@ class S4Mk3Deck extends Deck { this.deck.lightPadMode(); }, }); + // The mute button doesn't have a mapping by default, but you can add yours here // this.mutePadModeButton = new Button({ - // deck: this, - // input: function(pressed) { - // if (pressed) { - // if (this.deck.currentPadLayer !== this.deck.padLayers.samplerPage2) { - // switchPadLayer(this.deck, samplerPage2); - // this.deck.currentPadLayer = this.deck.padLayers.samplerPage2; - // } else { - // switchPadLayer(this.deck, defaultPadLayer); - // this.deck.currentPadLayer = this.deck.padLayers.defaultLayer; - // } - // this.deck.lightPadMode(); - // } - // }, + // ... // }); this.stemsPadModeButton = new Button({ @@ -2243,7 +2233,7 @@ class S4Mk3Deck extends Deck { engine.stopTimer(motorWindDownTimer); motorWindDownTimer = 0; }; - this.turntableButton = useMotors ? new Button({ + this.turntableButton = UseMotors ? new Button({ deck: this, input: function(press) { if (press) { @@ -2251,12 +2241,12 @@ class S4Mk3Deck extends Deck { this.deck.fluxButton.loopModeOff(true); if (this.deck.wheelMode === wheelModes.motor) { this.deck.wheelMode = wheelModes.vinyl; - motorWindDownTimer = engine.beginTimer(motorWindDownMilliseconds, motorWindDownTimerCallback, true); + motorWindDownTimer = engine.beginTimer(MotorWindDownMilliseconds, motorWindDownTimerCallback, true); engine.setValue(this.group, "scratch2_enable", false); } else { this.deck.wheelMode = wheelModes.motor; const group = this.group; - engine.beginTimer(motorWindUpMilliseconds, function() { + engine.beginTimer(MotorWindUpMilliseconds, function() { engine.setValue(group, "scratch2_enable", true); }, true); } @@ -2280,7 +2270,7 @@ class S4Mk3Deck extends Deck { this.deck.wheelMode = wheelModes.jog; } else { if (this.deck.wheelMode === wheelModes.motor) { - motorWindDownTimer = engine.beginTimer(motorWindDownMilliseconds, motorWindDownTimerCallback, true); + motorWindDownTimer = engine.beginTimer(MotorWindDownMilliseconds, motorWindDownTimerCallback, true); } this.deck.wheelMode = wheelModes.vinyl; } @@ -2363,12 +2353,12 @@ class S4Mk3Deck extends Deck { } this.stack[this.stackIdx] = diff / wheelTimerDelta; - this.stackIdx = (this.stackIdx + 1) % wheelSpeedSample; + this.stackIdx = (this.stackIdx + 1) % WheelSpeedSample; this.avgSpeed = (this.stack.reduce((ps, v) => ps + v, 0) / this.stack.length) * wheelTicksPerTimerTicksToRevolutionsPerSecond; this.stackAvg[this.stackAvgIdx] = this.avgSpeed; - this.stackAvgIdx = (this.stackAvgIdx + 1) % turnTableSpeedSample; + this.stackAvgIdx = (this.stackAvgIdx + 1) % TurnTableSpeedSample; this.ttAvgSpeed = this.stackAvg.reduce((ps, v) => ps + v, 0) / this.stackAvg.length; @@ -2387,7 +2377,7 @@ class S4Mk3Deck extends Deck { { const loopStartPosition = engine.getValue(this.group, "loop_start_position"); const loopEndPosition = engine.getValue(this.group, "loop_end_position"); - const value = Math.min(loopStartPosition + (this.avgSpeed * loopWheelMoveFactor), loopEndPosition - loopWheelMoveFactor); + const value = Math.min(loopStartPosition + (this.avgSpeed * LOOP_WHEEL_MOVE_FACTOR), loopEndPosition - LOOP_WHEEL_MOVE_FACTOR); engine.setValue( this.group, "loop_start_position", @@ -2398,7 +2388,7 @@ class S4Mk3Deck extends Deck { case wheelModes.loopOut: { const loopEndPosition = engine.getValue(this.group, "loop_end_position"); - const value = loopEndPosition + (this.avgSpeed * loopWheelMoveFactor); + const value = loopEndPosition + (this.avgSpeed * LOOP_WHEEL_MOVE_FACTOR); engine.setValue( this.group, "loop_end_position", @@ -2469,7 +2459,7 @@ class S4Mk3Deck extends Deck { } } } - this.shiftButton.send(LEDColors.white + this.brightnessOff); + this.shiftButton.send(LedColors.white + this.brightnessOff); } assignKeyboardPlayMode(group, action) { @@ -2484,7 +2474,7 @@ class S4Mk3Deck extends Deck { if (this.currentPadLayer === this.padLayers.hotcuePage2) { this.hotcuePadModeButton.send(this.hotcuePadModeButton.color + this.hotcuePadModeButton.brightnessOn); } else if (this.currentPadLayer === this.padLayers.hotcuePage3) { - this.hotcuePadModeButton.send(LEDColors.white + this.hotcuePadModeButton.brightnessOn); + this.hotcuePadModeButton.send(LedColors.white + this.hotcuePadModeButton.brightnessOn); } else { this.hotcuePadModeButton.send(this.hotcuePadModeButton.color + this.hotcuePadModeButton.brightnessOff); } @@ -2499,7 +2489,7 @@ class S4Mk3Deck extends Deck { // this.mutePadModeButtonLEDOn = this.currentPadLayer === this.padLayers.samplerPage2; // const mutedModeButton.send(mutePadModeButtonLEDOn ? 127 : 0); if (this.keyboardPlayMode !== null) { - this.stemsPadModeButton.send(LEDColors.green + this.stemsPadModeButton.brightnessOn); + this.stemsPadModeButton.send(LedColors.green + this.stemsPadModeButton.brightnessOn); } else { const keyboardPadModeLEDOn = this.currentPadLayer === this.padLayers.keyboard; this.stemsPadModeButton.send(this.stemsPadModeButton.color + (keyboardPadModeLEDOn ? this.stemsPadModeButton.brightnessOn : this.stemsPadModeButton.brightnessOff)); @@ -2665,7 +2655,7 @@ class S4MK3 { // so every single components' IO needs to be specified individually // for both decks. this.leftDeck = new S4Mk3Deck( - [1, 3], [deckColors[0], deckColors[2]], this.effectUnit1, + [1, 3], [DeckColors[0], DeckColors[2]], this.effectUnit1, this.inPackets, this.outPackets[128], { playButton: {inByte: 5, inBit: 0, outByte: 55}, @@ -2716,7 +2706,7 @@ class S4MK3 { ); this.rightDeck = new S4Mk3Deck( - [2, 4], [deckColors[1], deckColors[3]], this.effectUnit2, + [2, 4], [DeckColors[1], DeckColors[3]], this.effectUnit2, this.inPackets, this.outPackets[128], { playButton: {inByte: 13, inBit: 0, outByte: 66}, @@ -2801,7 +2791,7 @@ class S4MK3 { // the clip lights on the main mix meters. controller.send(deckMeters, null, 129); }); - if (useMotors) { + if (UseMotors) { engine.beginTimer(20, this.motorCallback.bind(this)); this.leftVelocityFactor = wheelAbsoluteMax * baseRevolutionsPerSecond * 2; this.rightVelocityFactor = wheelAbsoluteMax * baseRevolutionsPerSecond * 2; @@ -2854,13 +2844,13 @@ class S4MK3 { ) ); } else { - if (tightnessFactor > 0.5) { + if (TightnessFactor > 0.5) { // Super loose - const reduceFactor = (Math.min(0.5, tightnessFactor - 0.5) / 0.5) * 0.7; + const reduceFactor = (Math.min(0.5, TightnessFactor - 0.5) / 0.5) * 0.7; velocityLeft = currentLeftSpeed * reduceFactor; - } else if (tightnessFactor < 0.5) { + } else if (TightnessFactor < 0.5) { // Super tight - const reduceFactor = (2 - Math.max(0, tightnessFactor) * 4); + const reduceFactor = (2 - Math.max(0, TightnessFactor) * 4); velocityLeft = expectedLeftSpeed + Math.min( maxVelocity, Math.max( @@ -2881,13 +2871,13 @@ class S4MK3 { ) ); } else { - if (tightnessFactor > 0.5) { + if (TightnessFactor > 0.5) { // Super loose - const reduceFactor = (Math.min(0.5, tightnessFactor - 0.5) / 0.5) * 0.7; + const reduceFactor = (Math.min(0.5, TightnessFactor - 0.5) / 0.5) * 0.7; velocityRight = currentRightSpeed * reduceFactor; - } else if (tightnessFactor < 0.5) { + } else if (TightnessFactor < 0.5) { // Super tight - const reduceFactor = (2 - Math.max(0, tightnessFactor) * 4); + const reduceFactor = (2 - Math.max(0, TightnessFactor) * 4); console.log(reduceFactor); velocityRight = expectedRightSpeed + Math.min( maxVelocity, @@ -2958,12 +2948,12 @@ class S4MK3 { } velocityLeft = Math.min( - maxWheelForce, + MaxWheelForce, Math.floor(velocityLeft) ); velocityRight = Math.min( - maxWheelForce, + MaxWheelForce, Math.floor(velocityRight) ); From 9df5a734363915659044f248ba6d35d1f8f03d0d Mon Sep 17 00:00:00 2001 From: Antoine C Date: Mon, 6 Mar 2023 22:10:52 +0000 Subject: [PATCH 09/19] Kontrol S4 Mk3: default setting tweaks and LED bugfix --- res/controllers/Traktor-Kontrol-S4-MK3.js | 63 +++++++++++++++++------ 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index 615af1d6f0ff..3721a2cbeb05 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -104,7 +104,7 @@ const LOOP_ENCODER_SHIFTMOVE_FACTOR = 2500; // LOOP_ENCODER_SHIFTMOVE_FACTOR const TempoFaderSoftTakeoverColorLow = LedColors.white; const TempoFaderSoftTakeoverColorHigh = LedColors.green; -// Define whether or not to keep LED that have only one color (reverse, flux, play) dimmed if they are inactive. +// 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; @@ -117,8 +117,8 @@ const UseKeylockOnMaster = false; const GridButtonBlinkOverBeat = 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 = 5; +// less responsive it gets in Mixxx. Default: 3 +const WheelSpeedSample = 3; // Make the sampler tab a beatlooproll tab instead // Default: false @@ -137,8 +137,8 @@ const UseMotors = true; // 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: 40 -const TurnTableSpeedSample = 40; +// 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. @@ -404,6 +404,9 @@ class ComponentContainer extends Component { component.outConnect(); } component.outTrigger(); + if (component.unshift !== undefined && typeof component.unshift === "function") { + component.unshift(); + } } } unshift() { @@ -531,8 +534,6 @@ class Button extends Component { this.inBitLength = 1; } } - unshift() {} - shift() {} setKey(key) { this.inKey = key; if (key === this.outKey) { @@ -543,6 +544,15 @@ class Button extends Component { 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; @@ -1407,7 +1417,7 @@ class S4Mk3EffectUnit extends ComponentContainer { outByte: io.buttons[index].outByte, outPacket: outPacket, onShortPress: function() { - if (!this.shifted) { + if (!this.shifted || this.unit.focusedEffect !== null) { script.toggleControl(this.group, this.inKey); } }, @@ -1417,7 +1427,7 @@ class S4Mk3EffectUnit extends ComponentContainer { } }, onShortRelease: function() { - if (this.shifted) { + if (this.shifted && this.unit.focusedEffect === null) { script.triggerControl(this.group, "next_effect"); } }, @@ -1447,9 +1457,18 @@ class S4Mk3EffectUnit extends ComponentContainer { const effectGroup = "[EffectRack1_EffectUnit" + this.unitNumber + "_Effect" + (this.focusedEffect + 1) + "]"; for (const index of [0, 1, 2]) { + const unfocusGroup = "[EffectRack1_EffectUnit" + this.unitNumber + "_Effect" + (index + 1) + "]"; this.buttons[index].outDisconnect(); - this.buttons[index].group = this.focusedEffect === null ? "[EffectRack1_EffectUnit" + this.unitNumber + "_Effect" + (index + 1) + "]" : effectGroup; + this.buttons[index].group = this.focusedEffect === null ? unfocusGroup : effectGroup; this.buttons[index].inKey = this.focusedEffect === null ? "enabled" : "button_parameter" + (index + 1); + this.buttons[index].shift = this.focusedEffect === null ? undefined : function() { + this.setGroup(unfocusGroup); + this.setKey("enabled"); + }; + this.buttons[index].unshift = this.focusedEffect === null ? undefined : function() { + this.setGroup(effectGroup); + this.setKey("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); @@ -1724,6 +1743,9 @@ class S4Mk3Deck extends Deck { key: GridButtonBlinkOverBeat ? "beat_active" : undefined, deck: this, previousMoveMode: null, + unshift: !GridButtonBlinkOverBeat ? function() { + this.output(false); + } : undefined, onShortPress: function() { this.deck.libraryEncoder.gridButtonPressed = true; }, @@ -1785,16 +1807,20 @@ class S4Mk3Deck extends Deck { this.shiftButton = new PushButton({ deck: this, + output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput, + unshift: function() { + this.output(false); + }, + shift: function() { + this.output(true); + }, 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({ @@ -2122,7 +2148,6 @@ class S4Mk3Deck extends Deck { if (pad.inPacket === undefined) { pad.inPacket = inPackets[1]; } - pad.unshift(); pad.outPacket = outPacket; pad.inConnect(); pad.outConnect(); @@ -2225,6 +2250,10 @@ class S4Mk3Deck extends Deck { this.previousMoveMode = null; } }, + // hack to switch the LED color when changing decks + outTrigger: function() { + this.deck.lightPadMode(); + } }); this.wheelMode = wheelModes.vinyl; @@ -2456,10 +2485,12 @@ class S4Mk3Deck extends Deck { component.inConnect(); component.outConnect(); component.outTrigger(); + if (this.unshift !== undefined && typeof this.unshift === "function") { + this.unshift(); + } } } } - this.shiftButton.send(LedColors.white + this.brightnessOff); } assignKeyboardPlayMode(group, action) { From b6cc0f73a88e6c92f542a1bb9c7d16127f2baa4b Mon Sep 17 00:00:00 2001 From: Antoine C Date: Thu, 9 Mar 2023 19:49:42 +0000 Subject: [PATCH 10/19] Kontrol S4 Mk3: additionnal effect mappings and init fix --- res/controllers/Traktor-Kontrol-S4-MK3.js | 87 ++++++++++++----------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index 3721a2cbeb05..15b8983a8e3f 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -203,7 +203,7 @@ class HIDInputPacket { this.fields = []; } - registerCallback(callback, byteOffset, bitOffset, bitLength, signed) { + registerCallback(callback, byteOffset, bitOffset, bitLength, defaultOldData) { if (typeof callback !== "function") { throw Error("callback must be a function"); } @@ -226,16 +226,12 @@ class HIDInputPacket { 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 + oldData: defaultOldData }; this.fields.push(field); @@ -248,9 +244,10 @@ class HIDInputPacket { }; } - handleInput(byteArray) { + handleInput(byteArray, bufferHasNoReportID) { + const offset = bufferHasNoReportID ? -1 : 0; const view = new DataView(byteArray); - if (view.getUint8(0) !== this.reportId) { + if (!bufferHasNoReportID && view.getUint8(0) !== this.reportId) { return; } @@ -262,13 +259,13 @@ class HIDInputPacket { // 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); + data = view.getUint8(field.byteOffset + offset); } else if (numBytes === 2) { - data = view.getUint16(field.byteOffset, true); + data = view.getUint16(field.byteOffset + offset, true); } else if (numBytes === 3) { - data = view.getUint32(field.byteOffset, true) >>> 8; + data = view.getUint32(field.byteOffset + offset, true) >>> 8; } else if (numBytes === 4) { - data = view.getUint32(field.byteOffset, true); + data = view.getUint32(field.byteOffset + offset, true); } else { throw Error("field bitLength must be between 1 and 32"); } @@ -332,7 +329,7 @@ class Component { if (typeof callback === "function") { this.input = callback; } - this.inConnection = this.inPacket.registerCallback(this.input.bind(this), this.inByte, this.inBit, this.inBitLength); + this.inConnection = this.inPacket.registerCallback(this.input.bind(this), this.inByte, this.inBit, this.inBitLength, this.oldDataDefault); } inDisconnect() { if (this.inConnection !== undefined) { @@ -510,6 +507,8 @@ class Deck extends ComponentContainer { class Button extends Component { constructor(options) { + options.oldDataDefault = 0; + super(options); if (this.input === undefined) { @@ -930,6 +929,17 @@ class Pot extends Component { constructor(options) { super(options); this.hardwarePosition = null; + this.shiftedHardwarePosition = null; + } + setGroupKey(group, key) { + this.inKey = key; + if (key === this.outKey && group === this.group) { + return; + } + this.outDisconnect(); + this.group = group; + this.outKey = key; + this.outConnect(); } input(value) { const receivingFirstValue = this.hardwarePosition === null; @@ -944,6 +954,7 @@ class Pot extends Component { engine.softTakeover(this.group, this.inKey, true); } engine.softTakeoverIgnoreNextValue(this.group, this.inKey); + super.outDisconnect(); } } @@ -1472,6 +1483,12 @@ class S4Mk3EffectUnit extends ComponentContainer { 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.knobs[index].shift = this.focusedEffect === null ? undefined : function() { + this.setGroupKey(unfocusGroup, "meta"); + }; + this.knobs[index].unshift = this.focusedEffect === null ? undefined : function() { + this.setGroupKey(effectGroup, "parameter" + (index + 1)); + }; this.buttons[index].outConnect(); } } @@ -1634,11 +1651,7 @@ class S4Mk3Deck extends Deck { wheelOutput[1] = wheelLEDmodes.ringFlash; wheelOutput[4] = this.color + Button.prototype.brightnessOn; - // hack around https://github.com/mixxxdj/mixxx/issues/10828 - // This isn't directly needed, but because we used this hack for - // the track progression, we must make sure we are in sync with it's - // delayed updated - engine.beginTimer(decks[0] * 35, () => { controller.send(wheelOutput, null, 50, true); }, true); + controller.send(wheelOutput, null, 50, true); this.indicator(true); } else if (this.previousWheelMode !== null) { @@ -1722,13 +1735,7 @@ class S4Mk3Deck extends Deck { wheelOutput[1] = wheelLEDmodes.ringFlash; wheelOutput[4] = this.color + Button.prototype.brightnessOn; - // hack around https://github.com/mixxxdj/mixxx/issues/10828 - // This isn't directly needed, but because we used this hack for - // the track progression, we must make sure we are in sync with it's - // delayed updated - engine.beginTimer(decks[0] * 35, () => { - controller.send(wheelOutput, null, 50, true); - }, true); + controller.send(wheelOutput, null, 50, true); this.indicator(true); } else if (this.previousWheelMode !== null) { @@ -2458,8 +2465,7 @@ class S4Mk3Deck extends Deck { wheelOutput[3] = LEDposition >> 8; wheelOutput[4] = this.color + Button.prototype.brightnessOn; - // FIXME glitch, likely related to https://github.com/mixxxdj/mixxx/issues/10828 - engine.beginTimer(decks[0] * 35, () => { controller.send(wheelOutput, null, 50, true); }, true); + controller.send(wheelOutput, null, 50, true); } }); @@ -2574,8 +2580,7 @@ class S4Mk3MixerColumn extends ComponentContainer { // FIXME: Why is output not working for these? this.saveGain = new PushButton({ - key: "update_replaygain_from_pregain", - group: group, + key: "update_replaygain_from_pregain" }); this.crossfaderSwitch = new Component({ @@ -2995,12 +3000,12 @@ class S4MK3 { motorData[9] = velocityRight >> 8; controller.send(motorData, null, 49, true); } - incomingData(data) { - const reportId = data[0]; + incomingData(data, _, forceReportId) { + const reportId = forceReportId || data[0]; if (reportId === 1) { - this.inPackets[1].handleInput(data.buffer); + this.inPackets[1].handleInput(data.buffer, !!forceReportId); } else if (reportId === 2) { - this.inPackets[2].handleInput(data.buffer); + this.inPackets[2].handleInput(data.buffer, !!forceReportId); // The master volume, booth volume, headphone mix, and headphone volume knobs // control the controller's audio interface in hardware, so they are not mapped. } else if (reportId === 3) { @@ -3020,6 +3025,8 @@ class S4MK3 { this.leftDeck.wheelRelative.input(view.getUint16(12, true)); this.rightDeck.wheelRelative.input(view.getUint16(40, true)); + } else { + console.warn("Unsupported HID repord with ID "+ reportId + ". Contains: "+data); } } init() { @@ -3027,17 +3034,17 @@ class S4MK3 { const wheelLEDinitPacket = Array(26).fill(0); wheelLEDinitPacket[1] = 1; wheelLEDinitPacket[2] = 3; - controller.send(wheelLEDinitPacket, null, 48); + controller.send(wheelLEDinitPacket, null, 48, true); wheelLEDinitPacket[0] = 1; - // hack around https://github.com/mixxxdj/mixxx/issues/10828 - engine.beginTimer(35, () => { controller.send(wheelLEDinitPacket, null, 48); }, true); + controller.send(wheelLEDinitPacket, null, 48); // Init wheel timer data wheelTimer = null; wheelTimerDelta = 0; // get state of knobs and faders - this.incomingData(new Uint8Array(controller.getInputReport(2))); + this.incomingData(new Uint8Array(controller.getInputReport(1)), null, 1); + this.incomingData(new Uint8Array(controller.getInputReport(2)), null, 2); } shutdown() { // button LEDs @@ -3048,12 +3055,10 @@ class S4MK3 { const wheelOutput = Array(40).fill(0); // left wheel LEDs - // FIXME this data gets ignored due to https://github.com/mixxxdj/mixxx/issues/10828 - // Unfortunately, because this is the teardown function, we cannot use a timer to delay the send - controller.send(wheelOutput, null, 50); + controller.send(wheelOutput, null, 50, true); // right wheel LEDs wheelOutput[0] = 1; - controller.send(wheelOutput, null, 50); + controller.send(wheelOutput, null, 50, true); } } From 66f82756c76b182063465f2b23ff43c23409fca0 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Sun, 26 Mar 2023 18:52:32 +0100 Subject: [PATCH 11/19] Kontrol S4 Mk3: PR feedback --- res/controllers/Traktor-Kontrol-S4-MK3.js | 386 ++++++++++++---------- 1 file changed, 208 insertions(+), 178 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index 15b8983a8e3f..499f8db91a55 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -1,20 +1,4 @@ -/// Copyright (C) 2023 Be and A. Colombier -/// -/// 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. +/// Created by Be and A. Colombier const LedColors = { off: 0, @@ -60,20 +44,19 @@ const LibraryColumns = { Rating: 19, }; +// 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.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.off, + LedColors.white, + LedColors.off, + LedColors.white, + LedColors.white, + LedColors.off, + LedColors.white, + LedColors.off, + LedColors.white, + LedColors.off, LedColors.white, ]; @@ -97,9 +80,9 @@ const LibrarySortableColumns = [ LibraryColumns.DatetimeAdded, ]; -const LOOP_WHEEL_MOVE_FACTOR = 50; // LOOP_WHEEL_MOVE_FACTOR -const LOOP_ENCODER_MOVE_FACTOR = 500; // LOOP_ENCODER_MOVE_FACTOR -const LOOP_ENCODER_SHIFTMOVE_FACTOR = 2500; // LOOP_ENCODER_SHIFTMOVE_FACTOR +const LoopWheelMoveFactor = 50; +const LoopEncoderMoveFactor = 500; +const LoopEncoderShiftmoveFactor = 2500; const TempoFaderSoftTakeoverColorLow = LedColors.white; const TempoFaderSoftTakeoverColorHigh = LedColors.green; @@ -108,6 +91,10 @@ const TempoFaderSoftTakeoverColorHigh = LedColors.green; // '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; @@ -117,12 +104,20 @@ const UseKeylockOnMaster = false; const GridButtonBlinkOverBeat = 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: 3 +// less responsive it gets in Mixxx. Default: 5 const WheelSpeedSample = 3; // Make the sampler tab a beatlooproll tab instead // Default: false -const UseBeatloopRoolInsteadOfSampler = false; +const UseBeatloopRollInsteadOfSampler = true; + +// 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. @@ -157,8 +152,10 @@ const MaxWheelForce = 25000; // Traktor seems to cap the max value at 60000, wh // button as a 5th effect chain preset selector. const QuickEffectPresetColors = [ LedColors.red, + LedColors.green, LedColors.blue, LedColors.yellow, + LedColors.orange, LedColors.purple, LedColors.white, @@ -173,13 +170,11 @@ const QuickEffectPresetColors = [ LedColors.blue + 1, LedColors.carrot, - LedColors.orange, LedColors.honey, LedColors.yellow + 1, LedColors.lime, LedColors.aqua, - LedColors.green, LedColors.purple + 1, LedColors.magenta + 1, @@ -195,15 +190,15 @@ const MotorWindUpMilliseconds = 1200; const MotorWindDownMilliseconds = 900; /* - * HID packet parsing library + * HID report parsing library */ -class HIDInputPacket { +class HIDInputReport { constructor(reportId) { this.reportId = reportId; this.fields = []; } - registerCallback(callback, byteOffset, bitOffset, bitLength, defaultOldData) { + registerCallback(callback, byteOffset, bitOffset = 0, bitLength = 1, defaultOldData = undefined) { if (typeof callback !== "function") { throw Error("callback must be a function"); } @@ -212,16 +207,11 @@ class HIDInputPacket { 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"); } @@ -244,10 +234,9 @@ class HIDInputPacket { }; } - handleInput(byteArray, bufferHasNoReportID) { - const offset = bufferHasNoReportID ? -1 : 0; + handleInput(byteArray) { const view = new DataView(byteArray); - if (!bufferHasNoReportID && view.getUint8(0) !== this.reportId) { + if (view.getUint8(0) !== this.reportId) { return; } @@ -259,13 +248,13 @@ class HIDInputPacket { // 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 + offset); + data = view.getUint8(field.byteOffset); } else if (numBytes === 2) { - data = view.getUint16(field.byteOffset + offset, true); + data = view.getUint16(field.byteOffset, true); } else if (numBytes === 3) { - data = view.getUint32(field.byteOffset + offset, true) >>> 8; + data = view.getUint32(field.byteOffset, true) >>> 8; } else if (numBytes === 4) { - data = view.getUint32(field.byteOffset + offset, true); + data = view.getUint32(field.byteOffset, true); } else { throw Error("field bitLength must be between 1 and 32"); } @@ -282,7 +271,7 @@ class HIDInputPacket { } } -class HIDOutputPacket { +class HIDOutputReport { constructor(reportId, length) { this.reportId = reportId; this.data = Array(length).fill(0); @@ -314,7 +303,7 @@ class Component { } this.shifted = false; if (this.input !== undefined && typeof this.input === "function" - && this.inPacket !== undefined && this.inPacket instanceof HIDInputPacket) { + && this.inReport !== undefined && this.inReport instanceof HIDInputReport) { this.inConnect(); } this.outConnect(); @@ -323,13 +312,13 @@ class Component { if (this.inByte === undefined || this.inBit === undefined || this.inBitLength === undefined - || this.inPacket === undefined) { + || this.inReport === undefined) { return; } if (typeof callback === "function") { this.input = callback; } - this.inConnection = this.inPacket.registerCallback(this.input.bind(this), this.inByte, this.inBit, this.inBitLength, this.oldDataDefault); + this.inConnection = this.inReport.registerCallback(this.input.bind(this), this.inByte, this.inBit, this.inBitLength, this.oldDataDefault); } inDisconnect() { if (this.inConnection !== undefined) { @@ -337,9 +326,9 @@ class Component { } } send(value) { - if (this.outPacket !== undefined && this.outByte !== undefined) { - this.outPacket.data[this.outByte] = value; - this.outPacket.send(); + if (this.outReport !== undefined && this.outByte !== undefined) { + this.outReport.data[this.outByte] = value; + this.outReport.send(); } } output(value) { @@ -514,16 +503,16 @@ class Button extends Component { if (this.input === undefined) { this.input = this.defaultInput; if (typeof this.input === "function" - && this.inPacket !== undefined && this.inPacket instanceof HIDInputPacket) { + && this.inReport !== undefined && this.inReport instanceof HIDInputReport) { this.inConnect(); } } - if (this.longPressTimeOut === undefined) { - this.longPressTimeOut = 225; // milliseconds + if (this.longPressTimeOutMillis === undefined) { + this.longPressTimeOutMillis = 225; } - if (this.indicatorInterval === undefined) { - this.indicatorInterval = 350; // milliseconds + if (this.indicatorIntervalMillis === undefined) { + this.indicatorIntervalMillis = 350; } this.longPressTimer = 0; this.indicatorTimer = 0; @@ -566,7 +555,7 @@ class Button extends Component { indicator(on) { if (on && this.indicatorTimer === 0) { this.outDisconnect(); - this.indicatorTimer = engine.beginTimer(this.indicatorInterval, this.indicatorCallback.bind(this)); + this.indicatorTimer = engine.beginTimer(this.indicatorIntervalMillis, this.indicatorCallback.bind(this)); } else if (!on && this.indicatorTimer !== 0) { engine.stopTimer(this.indicatorTimer); this.indicatorTimer = 0; @@ -580,7 +569,7 @@ class Button extends Component { 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.longPressTimer = engine.beginTimer(this.longPressTimeOutMillis, () => { this.isLongPress = true; this.longPressTimer = 0; if (typeof this.onLongPress !== "function") { return; } @@ -646,7 +635,7 @@ class PowerWindowButton extends Button { class PlayButton extends Button { constructor(options) { // Prevent accidental ejection/duplication accident - options.longPressTimeOut = 800; + options.longPressTimeOutMillis = 800; super(options); this.inKey = "play"; this.outKey = "play_indicator"; @@ -678,7 +667,7 @@ class CueButton extends PushButton { this.inKey = "start_stop"; } input(pressed) { - if (this.deck.moveMode === moveModes.keyboard) { + 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); @@ -719,6 +708,9 @@ class Encoder extends Component { } } +/* + * Represent a pad button that interact with a hotcue (set, activate or clear) + */ class HotcueButton extends PushButton { constructor(options) { super(options); @@ -743,7 +735,7 @@ class HotcueButton extends PushButton { if (value) { this.send(this.color + this.brightnessOn); } else { - this.send(0); + this.send(LedColors.off); } } outConnect() { @@ -767,6 +759,9 @@ class HotcueButton extends PushButton { } } +/* + * 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); @@ -793,12 +788,21 @@ class KeyboardButton extends PushButton { engine.setValue(this.group, "key", this.number + offset); } if (this.deck.keyboardPlayMode !== null) { - script.toggleControl(this.deck.keyboardPlayMode.group, this.deck.keyboardPlayMode.action, true); + 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 + offset) % KeyboardColors.length; + const colorIdx = (this.number - 1 + offset) % KeyboardColors.length; const color = KeyboardColors[colorIdx]; if (this.number + offset < 1 || this.number + offset > 24) { this.send(0); @@ -821,13 +825,31 @@ class KeyboardButton extends PushButton { } } -const beatLoopRolls = [0.0625, 0.125, 0.25, 0.5, 1, 2, 4, 8]; +/* + * 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"); } - options.key = "beatlooproll_"+beatLoopRolls[options.number]+"_activate"; + if (options.number <= 5 || !AddLoopHalveAndDoubleOnBeatloopRollTab) { + options.key = "beatlooproll_"+BeatLoopRolls[AddLoopHalveAndDoubleOnBeatloopRollTab ? options.number + 1 : options.number]+"_activate"; + options.onShortPress = function() { + this.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.beatloopSize) { + engine.setValue(this.group, "beatloop_size", this.beatloopSize); + } + }; + } 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"); @@ -836,10 +858,17 @@ class BeatLoopRollButton extends TriggerButton { this.outConnect(); } output(value) { - this.send(LedColors.white + (value ? this.brightnessOn : this.brightnessOff)); + 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); @@ -901,6 +930,9 @@ class SamplerButton extends Button { } } +/* + * Represent a pad button that interact with a intro/extra special markers (set, activate, clear) + */ class IntroOutroButton extends PushButton { constructor(options) { super(options); @@ -959,12 +991,12 @@ class Pot extends Component { } class Mixer extends ComponentContainer { - constructor(inPackets, outPackets) { + constructor(inReports, outReports) { super(); - this.outPacket = outPackets[128]; + this.outReport = outReports[128]; - this.mixerColumnDeck1 = new S4Mk3MixerColumn("[Channel1]", inPackets, outPackets[128], + this.mixerColumnDeck1 = new S4Mk3MixerColumn("[Channel1]", inReports, outReports[128], { saveGain: {inByte: 12, inBit: 0, outByte: 80}, effectUnit1Assign: {inByte: 3, inBit: 3, outByte: 78}, @@ -980,7 +1012,7 @@ class Mixer extends ComponentContainer { crossfaderSwitch: {inByte: 18, inBit: 4}, } ); - this.mixerColumnDeck2 = new S4Mk3MixerColumn("[Channel2]", inPackets, outPackets[128], + this.mixerColumnDeck2 = new S4Mk3MixerColumn("[Channel2]", inReports, outReports[128], { saveGain: {inByte: 12, inBit: 1, outByte: 84}, effectUnit1Assign: {inByte: 3, inBit: 5, outByte: 82}, @@ -995,7 +1027,7 @@ class Mixer extends ComponentContainer { crossfaderSwitch: {inByte: 18, inBit: 2}, } ); - this.mixerColumnDeck3 = new S4Mk3MixerColumn("[Channel3]", inPackets, outPackets[128], + this.mixerColumnDeck3 = new S4Mk3MixerColumn("[Channel3]", inReports, outReports[128], { saveGain: {inByte: 3, inBit: 1, outByte: 88}, effectUnit1Assign: {inByte: 3, inBit: 0, outByte: 86}, @@ -1010,7 +1042,7 @@ class Mixer extends ComponentContainer { crossfaderSwitch: {inByte: 18, inBit: 6}, } ); - this.mixerColumnDeck4 = new S4Mk3MixerColumn("[Channel4]", inPackets, outPackets[128], + this.mixerColumnDeck4 = new S4Mk3MixerColumn("[Channel4]", inReports, outReports[128], { saveGain: {inByte: 12, inBit: 2, outByte: 92}, effectUnit1Assign: {inByte: 3, inBit: 7, outByte: 90}, @@ -1038,6 +1070,7 @@ class Mixer extends ComponentContainer { {inByte: 9, inBit: 7}, ]; this.fxSelects = []; + // FX SELECT buttons: Filter, 1, 2, 3, 4 for (const i of [0, 1, 2, 3, 4]) { this.fxSelects[i] = new FXSelect( Object.assign(fxSelectsInputs[i], { @@ -1054,6 +1087,7 @@ class Mixer extends ComponentContainer { {inByte: 8, inBit: 4, outByte: 49}, ]; this.quickEffectButtons = []; + // FX SELECT buttons: 1, 2, 3, 4 for (const i of [0, 1, 2, 3]) { this.quickEffectButtons[i] = new QuickEffectButton( Object.assign(quickEffectInputs[i], { @@ -1068,8 +1102,8 @@ class Mixer extends ComponentContainer { input: function(pressed) { if (pressed) { this.globalQuantizeOn = !this.globalQuantizeOn; - for (let i = 1; i <= 4; i++) { - engine.setValue("[Channel" + i + "]", "quantize", this.globalQuantizeOn); + for (let deckIdx = 1; deckIdx <= 4; deckIdx++) { + engine.setValue("[Channel" + deckIdx + "]", "quantize", this.globalQuantizeOn); } this.send(this.globalQuantizeOn ? 127 : 0); } @@ -1084,7 +1118,7 @@ class Mixer extends ComponentContainer { group: "[Master]", inKey: "crossfader", inByte: 1, - inPacket: inPackets[2], + inReport: inReports[2], }); this.crossfaderCurveSwitch = new Component({ inByte: 19, @@ -1111,18 +1145,18 @@ class Mixer extends ComponentContainer { }); for (const component of this) { - if (component.inPacket === undefined) { - component.inPacket = inPackets[1]; + if (component.inReport === undefined) { + component.inReport = inReports[1]; } - component.outPacket = this.outPacket; + component.outReport = this.outReport; component.inConnect(); component.outConnect(); component.outTrigger(); } let lightQuantizeButton = true; - for (let i = 1; i <= 4; i++) { - if (!engine.getValue("[Channel" + i + "]", "quantize")) { + for (let deckIdx = 1; deckIdx <= 4; deckIdx++) { + if (!engine.getValue("[Channel" + deckIdx + "]", "quantize")) { lightQuantizeButton = false; } } @@ -1143,9 +1177,9 @@ class Mixer extends ComponentContainer { resetFxSelectorColors() { for (const selector of [1, 2, 3, 4, 5]) { - this.outPacket.data[49 + selector] = QuickEffectPresetColors[selector - 1] + Button.prototype.brightnessOn; + this.outReport.data[49 + selector] = QuickEffectPresetColors[selector - 1] + Button.prototype.brightnessOn; } - this.outPacket.send(); + this.outReport.send(); } } @@ -1167,10 +1201,10 @@ class FXSelect extends Button { if (selector > this.number) { presetNumber--; } - this.outPacket.data[49 + selector] = QuickEffectPresetColors[presetNumber - 1] + this.brightnessOn; + this.outReport.data[49 + selector] = QuickEffectPresetColors[presetNumber - 1] + this.brightnessOn; } } - this.outPacket.send(); + this.outReport.send(); } else { this.mixer.secondPressedFxSelector = this.number; } @@ -1316,7 +1350,7 @@ const wheelRelativeMax = 2 ** 16 - 1; const wheelAbsoluteMax = 2879; const wheelTimerMax = 2 ** 32 - 1; -const wheelTimerTicksPerSecond = 100000000; +const wheelTimerTicksPerSecond = 100000000; // One tick every 10ns const baseRevolutionsPerSecond = BaseRevolutionsPerMinute / 60; const wheelTicksPerTimerTicksToRevolutionsPerSecond = wheelTimerTicksPerSecond / wheelAbsoluteMax; @@ -1346,7 +1380,7 @@ const moveModes = { keyboard: 3, }; -// tracks state across input packets +// tracks state across input reports 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 @@ -1358,7 +1392,7 @@ let wheelTimerDelta = 0; */ class S4Mk3EffectUnit extends ComponentContainer { - constructor(unitNumber, inPackets, outPacket, io) { + constructor(unitNumber, inReports, outReport, io) { super(); this.group = "[EffectRack1_EffectUnit" + unitNumber + "]"; this.unitNumber = unitNumber; @@ -1367,17 +1401,17 @@ class S4Mk3EffectUnit extends ComponentContainer { this.mixKnob = new Pot({ inKey: "mix", group: this.group, - inPacket: inPackets[2], + inReport: inReports[2], inByte: io.mixKnob.inByte, }); this.mainButton = new PowerWindowButton({ unit: this, - inPacket: inPackets[1], + inReport: inReports[1], inByte: io.mainButton.inByte, inBit: io.mainButton.inBit, outByte: io.mainButton.outByte, - outPacket: outPacket, + outReport: outReport, shift: function() { this.group = this.unit.group; this.outKey = "group_[Master]_enable"; @@ -1415,18 +1449,18 @@ class S4Mk3EffectUnit extends ComponentContainer { this.knobs[index] = new Pot({ inKey: "meta", group: effectGroup, - inPacket: inPackets[2], + inReport: inReports[2], inByte: io.knobs[index].inByte, }); this.buttons[index] = new Button({ unit: this, key: "enabled", group: effectGroup, - inPacket: inPackets[1], + inReport: inReports[1], inByte: io.buttons[index].inByte, inBit: io.buttons[index].inBit, outByte: io.buttons[index].outByte, - outPacket: outPacket, + outReport: outReport, onShortPress: function() { if (!this.shifted || this.unit.focusedEffect !== null) { script.toggleControl(this.group, this.inKey); @@ -1495,7 +1529,7 @@ class S4Mk3EffectUnit extends ComponentContainer { } class S4Mk3Deck extends Deck { - constructor(decks, colors, effectUnit, inPackets, outPacket, io) { + constructor(decks, colors, effectUnit, inReports, outReport, io) { super(decks, colors); this.playButton = new PlayButton({ @@ -1755,6 +1789,10 @@ class S4Mk3Deck extends Deck { } : undefined, onShortPress: function() { this.deck.libraryEncoder.gridButtonPressed = true; + + if (this.shift) { + engine.setValue(this.group, "bpm_tap", true); + } }, onLongPress: function() { this.deck.libraryEncoder.gridButtonPressed = true; @@ -1779,6 +1817,10 @@ class S4Mk3Deck extends Deck { onShortRelease: function() { this.deck.libraryEncoder.gridButtonPressed = false; script.triggerControl(this.group, "beats_translate_curpos"); + + if (this.shift) { + engine.setValue(this.group, "bpm_tap", false); + } }, }); @@ -1787,10 +1829,10 @@ class S4Mk3Deck extends Deck { input: function(value) { if (value) { this.deck.switchDeck(Deck.groupForNumber(decks[0])); - this.outPacket.data[io.deckButtonOutputByteOffset] = colors[0] + this.brightnessOn; + this.outReport.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.outReport.data[io.deckButtonOutputByteOffset + 1] = KeepDeckSelectDimmed ? colors[1] + this.brightnessOff : 0; + this.outReport.send(); } }, }); @@ -1800,17 +1842,17 @@ class S4Mk3Deck extends Deck { 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(); + this.outReport.data[io.deckButtonOutputByteOffset] = KeepDeckSelectDimmed ? colors[0] + this.brightnessOff : 0; + this.outReport.data[io.deckButtonOutputByteOffset + 1] = colors[1] + this.brightnessOn; + this.outReport.send(); } }, }); // set deck selection button LEDs - outPacket.data[io.deckButtonOutputByteOffset] = colors[0] + Button.prototype.brightnessOn; - outPacket.data[io.deckButtonOutputByteOffset + 1] = 0; - outPacket.send(); + outReport.data[io.deckButtonOutputByteOffset] = colors[0] + Button.prototype.brightnessOn; + outReport.data[io.deckButtonOutputByteOffset + 1] = KeepDeckSelectDimmed ? colors[1] + Button.prototype.brightnessOff : 0; + outReport.send(); this.shiftButton = new PushButton({ deck: this, @@ -1893,7 +1935,7 @@ class S4Mk3Deck extends Deck { deck: this, onChange: function(right) { if (this.deck.wheelMode === wheelModes.loopIn || this.deck.wheelMode === wheelModes.loopOut) { - const moveFactor = this.shifted ? LOOP_ENCODER_SHIFTMOVE_FACTOR : LOOP_ENCODER_MOVE_FACTOR; + const moveFactor = this.shifted ? LoopEncoderShiftmoveFactor : LoopEncoderMoveFactor; const valueIn = engine.getValue(this.group, "loop_start_position") + (right ? moveFactor : -moveFactor); const valueOut = engine.getValue(this.group, "loop_end_position") + (right ? moveFactor : -moveFactor); engine.setValue(this.group, "loop_start_position", valueIn); @@ -2097,7 +2139,7 @@ class S4Mk3Deck extends Deck { ]; const hotcuePage2 = Array(8).fill({}); const hotcuePage3 = Array(8).fill({}); - const samplerOrBeatloopRoolPage = Array(8).fill({}); + const samplerOrBeatloopRollPage = Array(8).fill({}); this.keyboard = Array(8).fill({}); let i = 0; /* eslint no-unused-vars: "off" */ @@ -2105,8 +2147,8 @@ class S4Mk3Deck extends Deck { // start with hotcue 5; hotcues 1-4 are in defaultPadLayer hotcuePage2[i] = new HotcueButton({number: i + 1}); hotcuePage3[i] = new HotcueButton({number: i + 13}); - if (UseBeatloopRoolInsteadOfSampler) { - samplerOrBeatloopRoolPage[i] = new BeatLoopRollButton({ + if (UseBeatloopRollInsteadOfSampler) { + samplerOrBeatloopRollPage[i] = new BeatLoopRollButton({ number: i, deck: this, }); @@ -2119,7 +2161,7 @@ class S4Mk3Deck extends Deck { if (decks[0] > 1) { samplerNumber += 4; } - samplerOrBeatloopRoolPage[i] = new SamplerButton({ + samplerOrBeatloopRollPage[i] = new SamplerButton({ number: samplerNumber, }); if (SamplerCrossfaderAssign) { @@ -2152,10 +2194,10 @@ class S4Mk3Deck extends Deck { if (!(pad instanceof SamplerButton)) { pad.group = deck.group; } - if (pad.inPacket === undefined) { - pad.inPacket = inPackets[1]; + if (pad.inReport === undefined) { + pad.inReport = inReports[1]; } - pad.outPacket = outPacket; + pad.outReport = outReport; pad.inConnect(); pad.outConnect(); pad.outTrigger(); @@ -2206,7 +2248,7 @@ class S4Mk3Deck extends Deck { deck: this, onShortPress: function() { if (this.deck.currentPadLayer !== this.deck.padLayers.samplerPage) { - switchPadLayer(this.deck, samplerOrBeatloopRoolPage); + switchPadLayer(this.deck, samplerOrBeatloopRollPage); engine.setValue("[Samplers]", "show_samplers", true); this.deck.currentPadLayer = this.deck.padLayers.samplerPage; } else { @@ -2238,7 +2280,7 @@ class S4Mk3Deck extends Deck { } }, onShortRelease: function() { - if (this.previousMoveMode !== null) { + if (this.previousMoveMode !== null && !this.deck.keyboardPlayMode) { this.deck.moveMode = this.previousMoveMode; this.previousMoveMode = null; } @@ -2252,7 +2294,7 @@ class S4Mk3Deck extends Deck { this.deck.lightPadMode(); }, onLongRelease: function() { - if (this.previousMoveMode !== null) { + if (this.previousMoveMode !== null && !this.deck.keyboardPlayMode) { this.deck.moveMode = this.previousMoveMode; this.previousMoveMode = null; } @@ -2355,7 +2397,7 @@ class S4Mk3Deck extends Deck { // The relative and absolute position inputs have the same resolution but direction // cannot be determined reliably with the absolute position because it is easily // possible to spin the wheel fast enough that it spins more than half a revolution - // between input packets. So there is no need to process the absolution position + // between input reports. So there is no need to process the absolution position // at all; the relative position is sufficient. this.wheelRelative = new Component({ oldValue: null, @@ -2413,7 +2455,7 @@ class S4Mk3Deck extends Deck { { const loopStartPosition = engine.getValue(this.group, "loop_start_position"); const loopEndPosition = engine.getValue(this.group, "loop_end_position"); - const value = Math.min(loopStartPosition + (this.avgSpeed * LOOP_WHEEL_MOVE_FACTOR), loopEndPosition - LOOP_WHEEL_MOVE_FACTOR); + const value = Math.min(loopStartPosition + (this.avgSpeed * LoopWheelMoveFactor), loopEndPosition - LoopWheelMoveFactor); engine.setValue( this.group, "loop_start_position", @@ -2424,7 +2466,7 @@ class S4Mk3Deck extends Deck { case wheelModes.loopOut: { const loopEndPosition = engine.getValue(this.group, "loop_end_position"); - const value = loopEndPosition + (this.avgSpeed * LOOP_WHEEL_MOVE_FACTOR); + const value = loopEndPosition + (this.avgSpeed * LoopWheelMoveFactor); engine.setValue( this.group, "loop_end_position", @@ -2475,10 +2517,10 @@ class S4Mk3Deck extends Deck { const component = this[property]; if (component instanceof Component) { Object.assign(component, io[property]); - if (component.inPacket === undefined) { - component.inPacket = inPackets[1]; + if (component.inReport === undefined) { + component.inReport = inReports[1]; } - component.outPacket = outPacket; + component.outReport = outReport; if (component.group === undefined) { component.group = this.group; } @@ -2535,7 +2577,7 @@ class S4Mk3Deck extends Deck { } class S4Mk3MixerColumn extends ComponentContainer { - constructor(group, inPackets, outPacket, io) { + constructor(group, inReports, outReport, io) { super(); this.group = group; @@ -2602,11 +2644,11 @@ class S4Mk3MixerColumn extends ComponentContainer { if (component instanceof Component) { Object.assign(component, io[property]); if (component instanceof Pot) { - component.inPacket = inPackets[2]; + component.inReport = inReports[2]; } else { - component.inPacket = inPackets[1]; + component.inReport = inReports[1]; } - component.outPacket = outPacket; + component.outReport = outReport; if (component.group === undefined) { component.group = this.group; @@ -2621,40 +2663,25 @@ class S4Mk3MixerColumn extends ComponentContainer { } } -const packetToBinaryString = (data) => { - let string = ""; - for (const 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) + ","; - } - } - // remove trailing comma - return string.slice(0, -1); -}; - class S4MK3 { constructor() { if (engine.getValue("[Master]", "num_samplers") < 16) { engine.setValue("[Master]", "num_samplers", 16); } - this.inPackets = []; - this.inPackets[1] = new HIDInputPacket(1); - this.inPackets[2] = new HIDInputPacket(2); - this.inPackets[3] = new HIDInputPacket(3); + this.inReports = []; + this.inReports[1] = new HIDInputReport(1); + this.inReports[2] = new HIDInputReport(2); + this.inReports[3] = new HIDInputReport(3); // There are various of other HID report which doesn't seem to have any // immediate use but it is likely that some useful settings may be found // in them such as the wheel tension. - this.outPackets = []; - this.outPackets[128] = new HIDOutputPacket(128, 94); + this.outReports = []; + this.outReports[128] = new HIDOutputReport(128, 94); - this.effectUnit1 = new S4Mk3EffectUnit(1, this.inPackets, this.outPackets[128], + this.effectUnit1 = new S4Mk3EffectUnit(1, this.inReports, this.outReports[128], { mixKnob: {inByte: 31}, mainButton: {inByte: 2, inBit: 6, outByte: 62}, @@ -2670,7 +2697,7 @@ class S4MK3 { ], } ); - this.effectUnit2 = new S4Mk3EffectUnit(2, this.inPackets, this.outPackets[128], + this.effectUnit2 = new S4Mk3EffectUnit(2, this.inReports, this.outReports[128], { mixKnob: {inByte: 71}, mainButton: {inByte: 10, inBit: 4, outByte: 73}, @@ -2692,7 +2719,7 @@ class S4MK3 { // for both decks. this.leftDeck = new S4Mk3Deck( [1, 3], [DeckColors[0], DeckColors[2]], this.effectUnit1, - this.inPackets, this.outPackets[128], + this.inReports, this.outReports[128], { playButton: {inByte: 5, inBit: 0, outByte: 55}, cueButton: {inByte: 5, inBit: 1, outByte: 8}, @@ -2734,16 +2761,16 @@ class S4MK3 { {inByte: 4, inBit: 1, outByte: 6}, {inByte: 4, inBit: 0, outByte: 7}, ], - tempoFader: {inByte: 13, inBit: 0, inBitLength: 16, inPacket: this.inPackets[2]}, - wheelRelative: {inByte: 12, inBit: 0, inBitLength: 16, inPacket: this.inPackets[3]}, - wheelAbsolute: {inByte: 16, inBit: 0, inBitLength: 16, inPacket: this.inPackets[3]}, + tempoFader: {inByte: 13, inBit: 0, inBitLength: 16, inReport: this.inReports[2]}, + wheelRelative: {inByte: 12, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, + wheelAbsolute: {inByte: 16, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, wheelTouch: {inByte: 17, inBit: 4}, } ); this.rightDeck = new S4Mk3Deck( [2, 4], [DeckColors[1], DeckColors[3]], this.effectUnit2, - this.inPackets, this.outPackets[128], + this.inReports, this.outReports[128], { playButton: {inByte: 13, inBit: 0, outByte: 66}, cueButton: {inByte: 15, inBit: 5, outByte: 31}, @@ -2785,9 +2812,9 @@ class S4MK3 { {inByte: 14, inBit: 1, outByte: 29}, {inByte: 14, inBit: 0, outByte: 30}, ], - tempoFader: {inByte: 11, inBit: 0, inBitLength: 16, inPacket: this.inPackets[2]}, - wheelRelative: {inByte: 40, inBit: 0, inBitLength: 16, inPacket: this.inPackets[3]}, - wheelAbsolute: {inByte: 44, inBit: 0, inBitLength: 16, inPacket: this.inPackets[3]}, + tempoFader: {inByte: 11, inBit: 0, inBitLength: 16, inReport: this.inReports[2]}, + wheelRelative: {inByte: 40, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, + wheelAbsolute: {inByte: 44, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, wheelTouch: {inByte: 17, inBit: 5}, } ); @@ -2795,7 +2822,7 @@ class S4MK3 { // The interaction between the FX SELECT buttons and the QuickEffect enable buttons is rather complex. // It is easier to have this separate from the S4Mk3MixerColumn class and the FX SELECT buttons are not // really in the mixer columns. - this.mixer = new Mixer(this.inPackets, this.outPackets); + this.mixer = new Mixer(this.inReports, this.outReports); /* eslint no-unused-vars: "off" */ const meterConnection = engine.makeConnection("[Master]", "guiTick50ms", function(_value) { @@ -2822,7 +2849,7 @@ class S4MK3 { deckMeters[columnBaseIndex + deckSegments + 1] = 127; } } - // There are more bytes in the packet which seem like they should be for the main + // There are more bytes in the report which seem like they should be for the main // mix meters, but setting those bytes does not do anything, except for lighting // the clip lights on the main mix meters. controller.send(deckMeters, null, 129); @@ -3000,12 +3027,12 @@ class S4MK3 { motorData[9] = velocityRight >> 8; controller.send(motorData, null, 49, true); } - incomingData(data, _, forceReportId) { - const reportId = forceReportId || data[0]; + incomingData(data) { + const reportId = data[0]; if (reportId === 1) { - this.inPackets[1].handleInput(data.buffer, !!forceReportId); + this.inReports[1].handleInput(data.buffer); } else if (reportId === 2) { - this.inPackets[2].handleInput(data.buffer, !!forceReportId); + this.inReports[2].handleInput(data.buffer); // The master volume, booth volume, headphone mix, and headphone volume knobs // control the controller's audio interface in hardware, so they are not mapped. } else if (reportId === 3) { @@ -3030,21 +3057,24 @@ class S4MK3 { } } init() { - // sending these magic packets is required for the jog wheel LEDs to work - const wheelLEDinitPacket = Array(26).fill(0); - wheelLEDinitPacket[1] = 1; - wheelLEDinitPacket[2] = 3; - controller.send(wheelLEDinitPacket, null, 48, true); - wheelLEDinitPacket[0] = 1; - controller.send(wheelLEDinitPacket, null, 48); + // sending these magic reports is required for the jog wheel LEDs to work + const wheelLEDinitReport = Array(26).fill(0); + wheelLEDinitReport[1] = 1; + wheelLEDinitReport[2] = 3; + controller.send(wheelLEDinitReport, null, 48, true); + wheelLEDinitReport[0] = 1; + controller.send(wheelLEDinitReport, null, 48); // Init wheel timer data wheelTimer = null; wheelTimerDelta = 0; // get state of knobs and faders - this.incomingData(new Uint8Array(controller.getInputReport(1)), null, 1); - this.incomingData(new Uint8Array(controller.getInputReport(2)), null, 2); + // this.incomingData(new Uint8Array(controller.getInputReport(1)), null, 1); + // this.incomingData(new Uint8Array(controller.getInputReport(2)), null, 2); + + this.incomingData(new Uint8Array([0x01, ...Uint8Array.from(new Uint8Array(controller.getInputReport(0x01)))])); + this.incomingData(new Uint8Array([0x02, ...Uint8Array.from(new Uint8Array(controller.getInputReport(0x02)))])); } shutdown() { // button LEDs From 71870fe0649a1a26e45d26a51c8f97fa8fc731a9 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Sun, 26 Mar 2023 19:04:27 +0100 Subject: [PATCH 12/19] Kontrol S4 Mk3: adding end of track warning feature --- res/controllers/Traktor-Kontrol-S4-MK3.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index 499f8db91a55..c55fdbc4d024 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -103,6 +103,10 @@ const UseKeylockOnMaster = false; // 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; + // 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; @@ -2502,9 +2506,14 @@ class S4Mk3Deck extends Deck { const wheelOutput = Array(40).fill(0); wheelOutput[0] = decks[0] - 1; - wheelOutput[1] = wheelLEDmodes.spot; - wheelOutput[2] = LEDposition & 0xff; - wheelOutput[3] = LEDposition >> 8; + + if (engine.getValue(this.group, "end_of_track") && WheelLedBlinkOnTrackEnd) { + wheelOutput[1] = wheelLEDmodes.ringFlash; + } else { + wheelOutput[1] = wheelLEDmodes.spot; + wheelOutput[2] = LEDposition & 0xff; + wheelOutput[3] = LEDposition >> 8; + } wheelOutput[4] = this.color + Button.prototype.brightnessOn; controller.send(wheelOutput, null, 50, true); @@ -3070,9 +3079,6 @@ class S4MK3 { wheelTimerDelta = 0; // get state of knobs and faders - // this.incomingData(new Uint8Array(controller.getInputReport(1)), null, 1); - // this.incomingData(new Uint8Array(controller.getInputReport(2)), null, 2); - this.incomingData(new Uint8Array([0x01, ...Uint8Array.from(new Uint8Array(controller.getInputReport(0x01)))])); this.incomingData(new Uint8Array([0x02, ...Uint8Array.from(new Uint8Array(controller.getInputReport(0x02)))])); } From 951e8e87f6b8d4474aa0565457cf83e6291b5078 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Sun, 2 Apr 2023 17:59:56 +0100 Subject: [PATCH 13/19] Kontrol S4 Mk3: PR feedback --- res/controllers/Traktor-Kontrol-S4-MK3.js | 31 ++++++++++------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index c55fdbc4d024..ea4a2ce7a3cb 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -238,11 +238,8 @@ class HIDInputReport { }; } - handleInput(byteArray) { - const view = new DataView(byteArray); - if (view.getUint8(0) !== this.reportId) { - return; - } + handleInput(reportData) { + const view = new DataView(reportData); for (const field of this.fields) { const numBytes = Math.ceil(field.bitLength / 8); @@ -251,14 +248,15 @@ class HIDInputReport { // 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. + // Note that `field.byteOffset` is an absolute offset from the report data, which includes the report ID on the first byte, however `reportData` omits the report ID, thus the "minus one" offset if (numBytes === 1) { - data = view.getUint8(field.byteOffset); + data = view.getUint8(field.byteOffset - 1); } else if (numBytes === 2) { - data = view.getUint16(field.byteOffset, true); + data = view.getUint16(field.byteOffset - 1, true); } else if (numBytes === 3) { - data = view.getUint32(field.byteOffset, true) >>> 8; + data = view.getUint32(field.byteOffset - 1, true) >>> 8; } else if (numBytes === 4) { - data = view.getUint32(field.byteOffset, true); + data = view.getUint32(field.byteOffset - 1, true); } else { throw Error("field bitLength must be between 1 and 32"); } @@ -2680,6 +2678,8 @@ class S4MK3 { this.inReports = []; this.inReports[1] = new HIDInputReport(1); + // The master volume, booth volume, headphone mix, and headphone volume knobs + // control the controller's audio interface in hardware, so they are not mapped. this.inReports[2] = new HIDInputReport(2); this.inReports[3] = new HIDInputReport(3); @@ -3038,12 +3038,8 @@ class S4MK3 { } incomingData(data) { const reportId = data[0]; - if (reportId === 1) { - this.inReports[1].handleInput(data.buffer); - } else if (reportId === 2) { - this.inReports[2].handleInput(data.buffer); - // The master volume, booth volume, headphone mix, and headphone volume knobs - // control the controller's audio interface in hardware, so they are not mapped. + if (reportId in this.inReports && reportId !== 3) { + this.inReports[reportId].handleInput(data.buffer.slice(1)); } else if (reportId === 3) { // The 32 bit unsigned ints at bytes 8 and 36 always have exactly the same value, // so only process one of them. This must be processed before the wheel positions. @@ -3079,8 +3075,9 @@ class S4MK3 { wheelTimerDelta = 0; // get state of knobs and faders - this.incomingData(new Uint8Array([0x01, ...Uint8Array.from(new Uint8Array(controller.getInputReport(0x01)))])); - this.incomingData(new Uint8Array([0x02, ...Uint8Array.from(new Uint8Array(controller.getInputReport(0x02)))])); + for (const repordId of [0x01, 0x02]) { + this.inReports[repordId].handleInput(controller.getInputReport(repordId)); + } } shutdown() { // button LEDs From e1f7efc80a1e583ea10eb25c4a9df777c59b1342 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Sun, 2 Apr 2023 21:31:55 +0100 Subject: [PATCH 14/19] Kontrol S4 Mk3: adding input mixer feature --- res/controllers/Traktor-Kontrol-S4-MK3.js | 121 ++++++++++++++++++---- 1 file changed, 101 insertions(+), 20 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index ea4a2ce7a3cb..f668a246120a 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -107,6 +107,10 @@ const GridButtonBlinkOverBeat = false; // 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: true +const MixerControlsMixAnxOnShift = true; + // 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; @@ -404,6 +408,7 @@ class ComponentContainer extends Component { } component.shifted = false; } + this.shifted = false; } shift() { for (const component of this) { @@ -412,6 +417,7 @@ class ComponentContainer extends Component { } component.shifted = true; } + this.shifted = true; } } @@ -964,6 +970,10 @@ class Pot extends Component { super(options); this.hardwarePosition = null; this.shiftedHardwarePosition = null; + + if (this.input === undefined) { + this.input = this.defaultInput; + } } setGroupKey(group, key) { this.inKey = key; @@ -975,7 +985,7 @@ class Pot extends Component { this.outKey = key; this.outConnect(); } - input(value) { + defaultInput(value) { const receivingFirstValue = this.hardwarePosition === null; this.hardwarePosition = value / this.max; engine.setParameter(this.group, this.inKey, this.hardwarePosition); @@ -998,7 +1008,7 @@ class Mixer extends ComponentContainer { this.outReport = outReports[128]; - this.mixerColumnDeck1 = new S4Mk3MixerColumn("[Channel1]", inReports, outReports[128], + this.mixerColumnDeck1 = new S4Mk3MixerColumn(1, inReports, outReports[128], { saveGain: {inByte: 12, inBit: 0, outByte: 80}, effectUnit1Assign: {inByte: 3, inBit: 3, outByte: 78}, @@ -1014,7 +1024,7 @@ class Mixer extends ComponentContainer { crossfaderSwitch: {inByte: 18, inBit: 4}, } ); - this.mixerColumnDeck2 = new S4Mk3MixerColumn("[Channel2]", inReports, outReports[128], + this.mixerColumnDeck2 = new S4Mk3MixerColumn(2, inReports, outReports[128], { saveGain: {inByte: 12, inBit: 1, outByte: 84}, effectUnit1Assign: {inByte: 3, inBit: 5, outByte: 82}, @@ -1029,7 +1039,7 @@ class Mixer extends ComponentContainer { crossfaderSwitch: {inByte: 18, inBit: 2}, } ); - this.mixerColumnDeck3 = new S4Mk3MixerColumn("[Channel3]", inReports, outReports[128], + this.mixerColumnDeck3 = new S4Mk3MixerColumn(3, inReports, outReports[128], { saveGain: {inByte: 3, inBit: 1, outByte: 88}, effectUnit1Assign: {inByte: 3, inBit: 0, outByte: 86}, @@ -1044,7 +1054,7 @@ class Mixer extends ComponentContainer { crossfaderSwitch: {inByte: 18, inBit: 6}, } ); - this.mixerColumnDeck4 = new S4Mk3MixerColumn("[Channel4]", inReports, outReports[128], + this.mixerColumnDeck4 = new S4Mk3MixerColumn(4, inReports, outReports[128], { saveGain: {inByte: 12, inBit: 2, outByte: 92}, effectUnit1Assign: {inByte: 3, inBit: 7, outByte: 90}, @@ -1531,7 +1541,7 @@ class S4Mk3EffectUnit extends ComponentContainer { } class S4Mk3Deck extends Deck { - constructor(decks, colors, effectUnit, inReports, outReport, io) { + constructor(decks, colors, effectUnit, mixer, inReports, outReport, io) { super(decks, colors); this.playButton = new PlayButton({ @@ -1541,7 +1551,9 @@ class S4Mk3Deck extends Deck { this.cueButton = new CueButton({ deck: this }); + this.effectUnit = effectUnit; + this.mixer = mixer; this.syncMasterButton = new Button({ key: "sync_leader", @@ -2584,32 +2596,44 @@ class S4Mk3Deck extends Deck { } class S4Mk3MixerColumn extends ComponentContainer { - constructor(group, inReports, outReport, io) { + constructor(idx, inReports, outReport, io) { super(); - this.group = group; + this.idx = idx; + this.group = "[Channel" + idx + "]"; this.gain = new Pot({ inKey: "pregain", }); this.eqHigh = new Pot({ - group: "[EqualizerRack1_" + group + "_Effect1]", + group: "[EqualizerRack1_" + this.group + "_Effect1]", inKey: "parameter3", }); this.eqMid = new Pot({ - group: "[EqualizerRack1_" + group + "_Effect1]", + group: "[EqualizerRack1_" + this.group + "_Effect1]", inKey: "parameter2", }); this.eqLow = new Pot({ - group: "[EqualizerRack1_" + group + "_Effect1]", + group: "[EqualizerRack1_" + this.group + "_Effect1]", inKey: "parameter1", }); this.quickEffectKnob = new Pot({ - group: "[QuickEffectRack1_" + group + "]", + group: "[QuickEffectRack1_" + this.group + "]", inKey: "super1", }); this.volume = new Pot({ inKey: "volume", + mixer: this, + input: MixerControlsMixAnxOnShift ? function(value) { + if (this.mixer.shifted) { + const controlKey = (this.group === "[Microphone" + this.mixer.idx + "]" || this.group === "[Microphone]") ? "talkover" : "master"; + const isPlaying = engine.getValue(this.group, controlKey); + if ((value !== 0) !== isPlaying) { + engine.setValue(this.group, controlKey, value !== 0); + } + } + this.defaultInput(value); + } : undefined }); this.pfl = new ToggleButton({ @@ -2667,6 +2691,55 @@ class S4Mk3MixerColumn extends ComponentContainer { } } } + + if (MixerControlsMixAnxOnShift) { + this.shift = function() { + engine.setValue("[Microphone]", "show_microphone", true); + this.updateGroup(true); + }; + + this.unshift = function() { + engine.setValue("[Microphone]", "show_microphone", false); + this.updateGroup(false); + }; + } + } + + updateGroup(shifted) { + let alternativeInput = null; + if (engine.getValue("[Auxiliary" + this.idx + "]", "input_configured")) { + alternativeInput = "[Auxiliary" + this.idx + "]"; + } else if (engine.getValue(this.idx !== 1 ? "[Microphone" + this.idx + "]" : "[Microphone]", "input_configured")) { + alternativeInput = this.idx !== 1 ? "[Microphone" + this.idx + "]" : "[Microphone]"; + } + + if (!alternativeInput) { + return; + } + this.group = shifted ? alternativeInput : "[Channel" + this.idx + "]"; + for (const property of ["gain", "volume", "pfl", "crossfaderSwitch"]) { + const component = this[property]; + if (component instanceof Component) { + component.outDisconnect(); + component.inDisconnect(); + component.group = this.group; + component.inConnect(); + component.outConnect(); + component.outTrigger(); + } + } + for (const property of ["effectUnit1Assign", "effectUnit2Assign"]) { + const component = this[property]; + if (component instanceof Component) { + component.outDisconnect(); + component.inDisconnect(); + component.inKey = "group_" + this.group + "_enable"; + component.outKey = "group_" + this.group + "_enable"; + component.inConnect(); + component.outConnect(); + component.outTrigger(); + } + } } } @@ -2723,11 +2796,16 @@ class S4MK3 { } ); + // The interaction between the FX SELECT buttons and the QuickEffect enable buttons is rather complex. + // It is easier to have this separate from the S4Mk3MixerColumn dhe FX SELECT buttons are not + // really in the mixer columns. + this.mixer = new Mixer(this.inReports, this.outReports); + // 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], [DeckColors[0], DeckColors[2]], this.effectUnit1, + [1, 3], [DeckColors[0], DeckColors[2]], this.effectUnit1, this.mixer, this.inReports, this.outReports[128], { playButton: {inByte: 5, inBit: 0, outByte: 55}, @@ -2778,7 +2856,7 @@ class S4MK3 { ); this.rightDeck = new S4Mk3Deck( - [2, 4], [DeckColors[1], DeckColors[3]], this.effectUnit2, + [2, 4], [DeckColors[1], DeckColors[3]], this.effectUnit2, this.mixer, this.inReports, this.outReports[128], { playButton: {inByte: 13, inBit: 0, outByte: 66}, @@ -2828,18 +2906,21 @@ class S4MK3 { } ); - // The interaction between the FX SELECT buttons and the QuickEffect enable buttons is rather complex. - // It is easier to have this separate from the S4Mk3MixerColumn class and the FX SELECT buttons are not - // really in the mixer columns. - this.mixer = new Mixer(this.inReports, this.outReports); - + const that = this; /* eslint no-unused-vars: "off" */ const meterConnection = engine.makeConnection("[Master]", "guiTick50ms", function(_value) { const deckMeters = Array(78).fill(0); // Each column has 14 segments, but treat the top one specially for the clip indicator. const deckSegments = 13; for (let deckNum = 1; deckNum <= 4; deckNum++) { - const deckGroup = "[Channel" + deckNum + "]"; + let deckGroup = "[Channel" + deckNum + "]"; + if (that.leftDeck.shifted || that.rightDeck.shifted) { + if (engine.getValue("[Auxiliary" + deckNum + "]", "input_configured")) { + deckGroup = "[Auxiliary" + deckNum + "]"; + } else if (engine.getValue(deckNum !== 1 ? "[Microphone" + deckNum + "]" : "[Microphone]", "input_configured")) { + deckGroup = deckNum !== 1 ? "[Microphone" + deckNum + "]" : "[Microphone]"; + } + } const deckLevel = engine.getValue(deckGroup, "VuMeter"); const columnBaseIndex = (deckNum - 1) * (deckSegments + 2); const scaledLevel = deckLevel * deckSegments; From 96ef3777a8e49fb8ee916ff4e03d23c716c1e4a6 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Sun, 2 Apr 2023 21:39:30 +0100 Subject: [PATCH 15/19] Kontrol S4 Mk3: PR feedback --- res/controllers/Traktor-Kontrol-S4-MK3.js | 313 +++++++++++----------- 1 file changed, 156 insertions(+), 157 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index f668a246120a..76bf5cce4bbd 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -252,15 +252,14 @@ class HIDInputReport { // 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. - // Note that `field.byteOffset` is an absolute offset from the report data, which includes the report ID on the first byte, however `reportData` omits the report ID, thus the "minus one" offset if (numBytes === 1) { - data = view.getUint8(field.byteOffset - 1); + data = view.getUint8(field.byteOffset); } else if (numBytes === 2) { - data = view.getUint16(field.byteOffset - 1, true); + data = view.getUint16(field.byteOffset, true); } else if (numBytes === 3) { - data = view.getUint32(field.byteOffset - 1, true) >>> 8; + data = view.getUint32(field.byteOffset, true) >>> 8; } else if (numBytes === 4) { - data = view.getUint32(field.byteOffset - 1, true); + data = view.getUint32(field.byteOffset, true); } else { throw Error("field bitLength must be between 1 and 32"); } @@ -1010,63 +1009,63 @@ class Mixer extends ComponentContainer { this.mixerColumnDeck1 = new S4Mk3MixerColumn(1, inReports, outReports[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}, + 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: 3}, - pfl: {inByte: 8, inBit: 3, outByte: 77}, - crossfaderSwitch: {inByte: 18, inBit: 4}, + 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: 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}, + 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}, + eqLow: {inByte: 54}, + quickEffectKnob: {inByte: 66}, + volume: {inByte: 4}, + pfl: {inByte: 7, inBit: 6, outByte: 81}, + crossfaderSwitch: {inByte: 17, inBit: 2}, } ); this.mixerColumnDeck3 = new S4Mk3MixerColumn(3, inReports, outReports[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}, + saveGain: {inByte: 2, inBit: 1, outByte: 88}, + effectUnit1Assign: {inByte: 2, inBit: 0, outByte: 86}, + effectUnit2Assign: {inByte: 2, inBit: 2, outByte: 87}, + gain: {inByte: 14}, + eqHigh: {inByte: 38}, + eqMid: {inByte: 40}, + eqLow: {inByte: 42}, + quickEffectKnob: {inByte: 62}, + volume: {inByte: 6}, + pfl: {inByte: 7, inBit: 2, outByte: 85}, + crossfaderSwitch: {inByte: 17, inBit: 6}, } ); this.mixerColumnDeck4 = new S4Mk3MixerColumn(4, inReports, outReports[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}, + saveGain: {inByte: 11, inBit: 2, outByte: 92}, + effectUnit1Assign: {inByte: 2, inBit: 7, outByte: 90}, + effectUnit2Assign: {inByte: 11, inBit: 7, outByte: 91}, + gain: {inByte: 20}, + eqHigh: {inByte: 56}, + eqMid: {inByte: 58}, + eqLow: {inByte: 60}, + quickEffectKnob: {inByte: 68}, + volume: {inByte: 8}, + pfl: {inByte: 7, inBit: 7, outByte: 89}, + crossfaderSwitch: {inByte: 17, inBit: 0}, } ); @@ -1075,11 +1074,11 @@ class Mixer extends ComponentContainer { 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}, + {inByte: 8, inBit: 5}, + {inByte: 8, inBit: 1}, + {inByte: 8, inBit: 6}, + {inByte: 8, inBit: 0}, + {inByte: 8, inBit: 7}, ]; this.fxSelects = []; // FX SELECT buttons: Filter, 1, 2, 3, 4 @@ -1093,10 +1092,10 @@ class Mixer extends ComponentContainer { } 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}, + {inByte: 7, inBit: 0, outByte: 46}, + {inByte: 7, inBit: 5, outByte: 47}, + {inByte: 7, inBit: 1, outByte: 48}, + {inByte: 7, inBit: 4, outByte: 49}, ]; this.quickEffectButtons = []; // FX SELECT buttons: 1, 2, 3, 4 @@ -1121,7 +1120,7 @@ class Mixer extends ComponentContainer { } }, globalQuantizeOn: false, - inByte: 12, + inByte: 11, inBit: 6, outByte: 93, }); @@ -1129,11 +1128,11 @@ class Mixer extends ComponentContainer { this.crossfader = new Pot({ group: "[Master]", inKey: "crossfader", - inByte: 1, + inByte: 0, inReport: inReports[2], }); this.crossfaderCurveSwitch = new Component({ - inByte: 19, + inByte: 18, inBit: 0, inBitLength: 2, input: function(value) { @@ -2765,33 +2764,33 @@ class S4MK3 { this.effectUnit1 = new S4Mk3EffectUnit(1, this.inReports, this.outReports[128], { - mixKnob: {inByte: 31}, - mainButton: {inByte: 2, inBit: 6, outByte: 62}, + mixKnob: {inByte: 30}, + mainButton: {inByte: 1, inBit: 6, outByte: 62}, knobs: [ - {inByte: 33}, - {inByte: 35}, - {inByte: 37}, + {inByte: 32}, + {inByte: 34}, + {inByte: 36}, ], buttons: [ - {inByte: 2, inBit: 7, outByte: 63}, - {inByte: 2, inBit: 3, outByte: 64}, - {inByte: 2, inBit: 2, outByte: 65}, + {inByte: 1, inBit: 7, outByte: 63}, + {inByte: 1, inBit: 3, outByte: 64}, + {inByte: 1, inBit: 2, outByte: 65}, ], } ); this.effectUnit2 = new S4Mk3EffectUnit(2, this.inReports, this.outReports[128], { - mixKnob: {inByte: 71}, - mainButton: {inByte: 10, inBit: 4, outByte: 73}, + mixKnob: {inByte: 70}, + mainButton: {inByte: 9, inBit: 4, outByte: 73}, knobs: [ - {inByte: 73}, - {inByte: 75}, - {inByte: 77}, + {inByte: 72}, + {inByte: 74}, + {inByte: 76}, ], buttons: [ - {inByte: 10, inBit: 5, outByte: 74}, - {inByte: 10, inBit: 6, outByte: 75}, - {inByte: 10, inBit: 7, outByte: 76}, + {inByte: 9, inBit: 5, outByte: 74}, + {inByte: 9, inBit: 6, outByte: 75}, + {inByte: 9, inBit: 7, outByte: 76}, ], } ); @@ -2808,50 +2807,50 @@ class S4MK3 { [1, 3], [DeckColors[0], DeckColors[2]], this.effectUnit1, this.mixer, this.inReports, this.outReports[128], { - playButton: {inByte: 5, inBit: 0, outByte: 55}, - cueButton: {inByte: 5, inBit: 1, outByte: 8}, - syncButton: {inByte: 6, inBit: 7, outByte: 14}, - syncMasterButton: {inByte: 1, inBit: 0, outByte: 15}, - hotcuePadModeButton: {inByte: 5, inBit: 2, outByte: 9}, - recordPadModeButton: {inByte: 5, inBit: 3, outByte: 56}, - samplesPadModeButton: {inByte: 5, inBit: 4, outByte: 57}, - mutePadModeButton: {inByte: 5, inBit: 5, outByte: 58}, - stemsPadModeButton: {inByte: 6, inBit: 0, outByte: 10}, - deckButtonLeft: {inByte: 6, inBit: 2}, - deckButtonRight: {inByte: 6, inBit: 3}, + playButton: {inByte: 4, inBit: 0, outByte: 55}, + cueButton: {inByte: 4, inBit: 1, outByte: 8}, + syncButton: {inByte: 5, inBit: 7, outByte: 14}, + syncMasterButton: {inByte: 0, inBit: 0, outByte: 15}, + hotcuePadModeButton: {inByte: 4, inBit: 2, outByte: 9}, + recordPadModeButton: {inByte: 4, inBit: 3, outByte: 56}, + samplesPadModeButton: {inByte: 4, inBit: 4, outByte: 57}, + mutePadModeButton: {inByte: 4, inBit: 5, outByte: 58}, + stemsPadModeButton: {inByte: 5, inBit: 0, outByte: 10}, + deckButtonLeft: {inByte: 5, inBit: 2}, + deckButtonRight: {inByte: 5, inBit: 3}, deckButtonOutputByteOffset: 12, tempoFaderLED: {outByte: 11}, - shiftButton: {inByte: 6, inBit: 1, outByte: 59}, - leftEncoder: {inByte: 20, inBit: 0}, - leftEncoderPress: {inByte: 7, inBit: 2}, - rightEncoder: {inByte: 20, inBit: 4}, - rightEncoderPress: {inByte: 7, inBit: 5}, - libraryEncoder: {inByte: 21, inBit: 0}, - libraryEncoderPress: {inByte: 1, inBit: 1}, - turntableButton: {inByte: 6, inBit: 5, outByte: 17}, - jogButton: {inByte: 6, inBit: 4, outByte: 16}, - gridButton: {inByte: 6, inBit: 6, outByte: 18}, - reverseButton: {inByte: 2, inBit: 4, outByte: 60}, - fluxButton: {inByte: 2, inBit: 5, outByte: 61}, - libraryPlayButton: {inByte: 1, inBit: 5, outByte: 22}, - libraryStarButton: {inByte: 1, inBit: 4, outByte: 21}, - libraryPlaylistButton: {inByte: 2, inBit: 1, outByte: 20}, - libraryViewButton: {inByte: 2, inBit: 0, outByte: 19}, + shiftButton: {inByte: 5, inBit: 1, outByte: 59}, + leftEncoder: {inByte: 19, inBit: 0}, + leftEncoderPress: {inByte: 6, inBit: 2}, + rightEncoder: {inByte: 19, inBit: 4}, + rightEncoderPress: {inByte: 6, inBit: 5}, + libraryEncoder: {inByte: 20, inBit: 0}, + libraryEncoderPress: {inByte: 0, inBit: 1}, + turntableButton: {inByte: 5, inBit: 5, outByte: 17}, + jogButton: {inByte: 5, inBit: 4, outByte: 16}, + gridButton: {inByte: 5, inBit: 6, outByte: 18}, + reverseButton: {inByte: 1, inBit: 4, outByte: 60}, + fluxButton: {inByte: 1, inBit: 5, outByte: 61}, + libraryPlayButton: {inByte: 0, inBit: 5, outByte: 22}, + libraryStarButton: {inByte: 0, inBit: 4, outByte: 21}, + libraryPlaylistButton: {inByte: 1, inBit: 1, outByte: 20}, + libraryViewButton: {inByte: 1, inBit: 0, outByte: 19}, 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}, + {inByte: 3, inBit: 5, outByte: 0}, + {inByte: 3, inBit: 4, outByte: 1}, + {inByte: 3, inBit: 7, outByte: 2}, + {inByte: 3, inBit: 6, outByte: 3}, + + {inByte: 3, inBit: 3, outByte: 4}, + {inByte: 3, inBit: 2, outByte: 5}, + {inByte: 3, inBit: 1, outByte: 6}, + {inByte: 3, inBit: 0, outByte: 7}, ], - tempoFader: {inByte: 13, inBit: 0, inBitLength: 16, inReport: this.inReports[2]}, - wheelRelative: {inByte: 12, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, - wheelAbsolute: {inByte: 16, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, - wheelTouch: {inByte: 17, inBit: 4}, + tempoFader: {inByte: 12, inBit: 0, inBitLength: 16, inReport: this.inReports[2]}, + wheelRelative: {inByte: 11, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, + wheelAbsolute: {inByte: 15, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, + wheelTouch: {inByte: 16, inBit: 4}, } ); @@ -2859,50 +2858,50 @@ class S4MK3 { [2, 4], [DeckColors[1], DeckColors[3]], this.effectUnit2, this.mixer, this.inReports, this.outReports[128], { - playButton: {inByte: 13, inBit: 0, outByte: 66}, - cueButton: {inByte: 15, inBit: 5, outByte: 31}, - syncButton: {inByte: 15, inBit: 4, outByte: 37}, - syncMasterButton: {inByte: 11, inBit: 0, outByte: 38}, - hotcuePadModeButton: {inByte: 13, inBit: 2, outByte: 32}, - recordPadModeButton: {inByte: 13, inBit: 3, outByte: 67}, - samplesPadModeButton: {inByte: 13, inBit: 4, outByte: 68}, - mutePadModeButton: {inByte: 13, inBit: 5, outByte: 69}, - stemsPadModeButton: {inByte: 13, inBit: 1, outByte: 33}, - deckButtonLeft: {inByte: 15, inBit: 2}, - deckButtonRight: {inByte: 15, inBit: 3}, + playButton: {inByte: 12, inBit: 0, outByte: 66}, + cueButton: {inByte: 14, inBit: 5, outByte: 31}, + syncButton: {inByte: 14, inBit: 4, outByte: 37}, + syncMasterButton: {inByte: 10, inBit: 0, outByte: 38}, + hotcuePadModeButton: {inByte: 12, inBit: 2, outByte: 32}, + recordPadModeButton: {inByte: 12, inBit: 3, outByte: 67}, + samplesPadModeButton: {inByte: 12, inBit: 4, outByte: 68}, + mutePadModeButton: {inByte: 12, inBit: 5, outByte: 69}, + stemsPadModeButton: {inByte: 12, inBit: 1, outByte: 33}, + deckButtonLeft: {inByte: 14, inBit: 2}, + deckButtonRight: {inByte: 14, inBit: 3}, deckButtonOutputByteOffset: 35, tempoFaderLED: {outByte: 34}, - shiftButton: {inByte: 15, inBit: 1, outByte: 70}, - leftEncoder: {inByte: 21, inBit: 4}, - leftEncoderPress: {inByte: 16, inBit: 5}, - rightEncoder: {inByte: 22, inBit: 0}, - rightEncoderPress: {inByte: 16, inBit: 2}, - libraryEncoder: {inByte: 22, inBit: 4}, - libraryEncoderPress: {inByte: 11, inBit: 1}, - turntableButton: {inByte: 15, inBit: 6, outByte: 40}, - jogButton: {inByte: 15, inBit: 0, outByte: 39}, - gridButton: {inByte: 15, inBit: 7, outByte: 41}, - reverseButton: {inByte: 11, inBit: 4, outByte: 71}, - fluxButton: {inByte: 11, inBit: 5, outByte: 72}, - libraryPlayButton: {inByte: 10, inBit: 2, outByte: 45}, - libraryStarButton: {inByte: 10, inBit: 1, outByte: 44}, - libraryPlaylistButton: {inByte: 10, inBit: 3, outByte: 43}, - libraryViewButton: {inByte: 10, inBit: 0, outByte: 42}, + shiftButton: {inByte: 14, inBit: 1, outByte: 70}, + leftEncoder: {inByte: 20, inBit: 4}, + leftEncoderPress: {inByte: 15, inBit: 5}, + rightEncoder: {inByte: 21, inBit: 0}, + rightEncoderPress: {inByte: 15, inBit: 2}, + libraryEncoder: {inByte: 21, inBit: 4}, + libraryEncoderPress: {inByte: 10, inBit: 1}, + turntableButton: {inByte: 14, inBit: 6, outByte: 40}, + jogButton: {inByte: 14, inBit: 0, outByte: 39}, + gridButton: {inByte: 14, inBit: 7, outByte: 41}, + reverseButton: {inByte: 10, inBit: 4, outByte: 71}, + fluxButton: {inByte: 10, inBit: 5, outByte: 72}, + libraryPlayButton: {inByte: 9, inBit: 2, outByte: 45}, + libraryStarButton: {inByte: 9, inBit: 1, outByte: 44}, + libraryPlaylistButton: {inByte: 9, inBit: 3, outByte: 43}, + libraryViewButton: {inByte: 9, inBit: 0, outByte: 42}, 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}, + {inByte: 13, inBit: 5, outByte: 23}, + {inByte: 13, inBit: 4, outByte: 24}, + {inByte: 13, inBit: 7, outByte: 25}, + {inByte: 13, inBit: 6, outByte: 26}, + + {inByte: 13, inBit: 3, outByte: 27}, + {inByte: 13, inBit: 2, outByte: 28}, + {inByte: 13, inBit: 1, outByte: 29}, + {inByte: 13, inBit: 0, outByte: 30}, ], - tempoFader: {inByte: 11, inBit: 0, inBitLength: 16, inReport: this.inReports[2]}, - wheelRelative: {inByte: 40, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, - wheelAbsolute: {inByte: 44, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, - wheelTouch: {inByte: 17, inBit: 5}, + tempoFader: {inByte: 10, inBit: 0, inBitLength: 16, inReport: this.inReports[2]}, + wheelRelative: {inByte: 39, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, + wheelAbsolute: {inByte: 43, inBit: 0, inBitLength: 16, inReport: this.inReports[3]}, + wheelTouch: {inByte: 16, inBit: 5}, } ); From 1bde71e1811c589841a8917d052e121422092f79 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Mon, 3 Apr 2023 20:19:59 +0100 Subject: [PATCH 16/19] Kontrol S4 Mk3: PR feedback --- res/controllers/Traktor-Kontrol-S4-MK3.js | 209 ++++++++---------- res/controllers/common-controller-scripts.js | 32 +++ .../controller_mapping_validation_test.cpp | 8 + src/test/controller_mapping_validation_test.h | 4 + 4 files changed, 136 insertions(+), 117 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index 76bf5cce4bbd..4bdcc6bd7fb1 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -1,4 +1,5 @@ /// Created by Be and A. Colombier +/* global LibraryColumns*/ const LedColors = { off: 0, @@ -21,28 +22,6 @@ const LedColors = { white: 68, }; -// A full list can be found here: https://manual.mixxx.org/2.4/en/chapters/appendix/mixxx_controls.html#control-[Library]-sort_column -const LibraryColumns = { - Artist: 1, - Title: 2, - Album: 3, - Albumartist: 4, - Year: 5, - Genre: 6, - Composer: 7, - Grouping: 8, - Tracknumber: 9, - Filetype: 10, - NativeLocation: 11, - Comment: 12, - Duration: 13, - Bitrate: 14, - BPM: 15, - ReplayGain: 16, - DatetimeAdded: 17, - TimesPlayed: 18, - Rating: 19, -}; // 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 = [ @@ -149,7 +128,7 @@ const TurnTableSpeedSample = 20; 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 im +// 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 @@ -211,16 +190,13 @@ class HIDInputReport { throw Error("callback must be a function"); } - if (byteOffset === undefined || typeof byteOffset !== "number" || !Number.isInteger(byteOffset)) { + if (!Number.isInteger(byteOffset)) { throw Error("byteOffset must be 0 or a positive integer"); } - - - if (typeof bitOffset !== "number" || bitOffset < 0 || !Number.isInteger(bitOffset)) { + if (!Number.isInteger(bitOffset) || bitOffset < 0) { throw Error("bitOffset must be 0 or a positive integer"); } - - if (typeof bitLength !== "number" || bitLength < 1 || !Number.isInteger(bitOffset) || bitLength > 32) { + if (!Number.isInteger(bitOffset) || bitLength < 1 || bitLength > 32) { throw Error("bitLength must be an integer between 1 and 32"); } @@ -279,10 +255,10 @@ class HIDInputReport { class HIDOutputReport { constructor(reportId, length) { this.reportId = reportId; - this.data = Array(length).fill(0); + this.data = new Uint8Array(length).fill(0); } send() { - controller.send(this.data, null, this.reportId); + controller.sendOutputReport(this.reportId, this.data.buffer); } } @@ -303,12 +279,11 @@ class Component { this.inKey = this.key; this.outKey = this.key; } - if (this.unshift !== undefined && typeof this.unshift === "function") { + if (typeof this.unshift === "function") { this.unshift(); } this.shifted = false; - if (this.input !== undefined && typeof this.input === "function" - && this.inReport !== undefined && this.inReport instanceof HIDInputReport) { + if (typeof this.input === "function" && this.inReport instanceof HIDInputReport) { this.inConnect(); } this.outConnect(); @@ -346,7 +321,7 @@ class Component { 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."); + console.warn(`Unable to connect ${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`); } } } @@ -385,24 +360,24 @@ class ComponentContainer extends Component { } reconnectComponents(callback) { for (const component of this) { - if (component.outDisconnect !== undefined && typeof component.outDisconnect === "function") { + if (typeof component.outDisconnect === "function") { component.outDisconnect(); } - if (callback !== undefined && typeof callback === "function") { + if (typeof callback === "function") { callback.call(this, component); } - if (component.outConnect !== undefined && typeof component.outConnect === "function") { + if (typeof component.outConnect === "function") { component.outConnect(); } component.outTrigger(); - if (component.unshift !== undefined && typeof component.unshift === "function") { + if (typeof component.unshift === "function") { component.unshift(); } } } unshift() { for (const component of this) { - if (component.unshift !== undefined && typeof component.unshift === "function") { + if (typeof component.unshift === "function") { component.unshift(); } component.shifted = false; @@ -411,7 +386,7 @@ class ComponentContainer extends Component { } shift() { for (const component of this) { - if (component.shift !== undefined && typeof component.shift === "function") { + if (typeof component.shift === "function") { component.shift(); } component.shifted = true; @@ -487,9 +462,9 @@ class Deck extends ComponentContainer { || component.group.search(script.channelRegEx) !== -1) { component.group = newGroup; } else if (component.group.search(script.eqRegEx) !== -1) { - component.group = "[EqualizerRack1_" + newGroup + "_Effect1]"; + component.group = `[EqualizerRack1_${newGroup}_Effect1]`; } else if (component.group.search(script.quickEffectRegEx) !== -1) { - component.group = "[QuickEffectRack1_" + newGroup + "]"; + component.group = `[QuickEffectRack1_${newGroup}]`; } component.color = this.groupsToColors[newGroup]; @@ -497,7 +472,7 @@ class Deck extends ComponentContainer { this.secondDeckModes = currentModes; } static groupForNumber(deckNumber) { - return "[Channel" + deckNumber + "]"; + return `[Channel${deckNumber}]`; } } @@ -724,15 +699,15 @@ class HotcueButton extends PushButton { 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.outKey = `hotcue_${this.number}_enabled`; + this.colorKey = `hotcue_${this.number}_color`; this.outConnect(); } unshift() { - this.inKey = "hotcue_" + this.number + "_activate"; + this.inKey = `hotcue_${this.number}_activate`; } shift() { - this.inKey = "hotcue_" + this.number + "_clear"; + this.inKey = `hotcue_${this.number}_clear`; } input(pressed) { engine.setValue(this.group, "scratch2_enable", false); @@ -751,7 +726,7 @@ class HotcueButton extends PushButton { 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."); + 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); @@ -760,7 +735,7 @@ class HotcueButton extends PushButton { 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."); + console.warn(`Unable to connect ${this.group}.${this.colorKey}' to the controller output. The control appears to be unavailable.`); } } } @@ -826,7 +801,7 @@ class KeyboardButton extends PushButton { 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."); + console.warn(`Unable to connect ${this.group}.key' to the controller output. The control appears to be unavailable.`); } } } @@ -882,7 +857,7 @@ class SamplerButton extends Button { 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.group = "[Sampler${this.number}]"; this.outConnect(); } onShortPress() { @@ -925,13 +900,13 @@ class SamplerButton extends Button { 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."); + 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."); + console.warn(`Unable to connect ${this.group}.track_loaded' to the controller output. The control appears to be unavailable.`); } } } @@ -946,14 +921,14 @@ class IntroOutroButton extends PushButton { 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.outKey = `${this.cueBaseName}_enabled`; this.outConnect(); } unshift() { - this.inKey = this.cueBaseName + "_activate"; + this.inKey = `${this.cueBaseName}_activate`; } shift() { - this.inKey = this.cueBaseName + "_clear"; + this.inKey = `${this.cueBaseName}_clear`; } output(value) { if (value) { @@ -1114,7 +1089,7 @@ class Mixer extends ComponentContainer { if (pressed) { this.globalQuantizeOn = !this.globalQuantizeOn; for (let deckIdx = 1; deckIdx <= 4; deckIdx++) { - engine.setValue("[Channel" + deckIdx + "]", "quantize", this.globalQuantizeOn); + engine.setValue(`[Channel${deckIdx}]`, "quantize", this.globalQuantizeOn); } this.send(this.globalQuantizeOn ? 127 : 0); } @@ -1167,7 +1142,7 @@ class Mixer extends ComponentContainer { let lightQuantizeButton = true; for (let deckIdx = 1; deckIdx <= 4; deckIdx++) { - if (!engine.getValue("[Channel" + deckIdx + "]", "quantize")) { + if (!engine.getValue(`[Channel${deckIdx}]`, "quantize")) { lightQuantizeButton = false; } } @@ -1237,7 +1212,7 @@ class FXSelect extends Button { 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); + engine.setValue(`[QuickEffectRack1_[Channel${deck}]]`, "loaded_chain_preset", presetNumber + 1); } } if (this.mixer.firstPressedFxSelector === this.number) { @@ -1262,7 +1237,7 @@ class QuickEffectButton extends Button { 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.group = `[QuickEffectRack1_[Channel${this.number}]]`; this.outConnect(); } onShortPress() { @@ -1301,13 +1276,13 @@ class QuickEffectButton extends Button { if (connection0) { this.outConnections[0] = connection0; } else { - console.warn("Unable to connect '" + this.group + ".loaded_chain_preset' to the controller output. The control appears to be unavailable."); + console.warn(`Unable to connect ${this.group}.loaded_chain_preset' to the controller output. The control appears to be unavailable.`); } const connection1 = engine.makeConnection(this.group, "enabled", this.output.bind(this)); if (connection1) { this.outConnections[1] = connection1; } else { - console.warn("Unable to connect '" + this.group + ".enabled' to the controller output. The control appears to be unavailable."); + console.warn(`Unable to connect ${this.group}.enabled' to the controller output. The control appears to be unavailable.`); } } } @@ -1405,7 +1380,7 @@ let wheelTimerDelta = 0; class S4Mk3EffectUnit extends ComponentContainer { constructor(unitNumber, inReports, outReport, io) { super(); - this.group = "[EffectRack1_EffectUnit" + unitNumber + "]"; + this.group = `[EffectRack1_EffectUnit${unitNumber}]`; this.unitNumber = unitNumber; this.focusedEffect = null; @@ -1438,7 +1413,7 @@ class S4Mk3EffectUnit extends ComponentContainer { input: function(pressed) { if (!this.shifted) { for (const index of [0, 1, 2]) { - const effectGroup = "[EffectRack1_EffectUnit" + unitNumber + "_Effect" + (index + 1) + "]"; + const effectGroup = `[EffectRack1_EffectUnit${unitNumber}_Effect${index + 1}]`; engine.setValue(effectGroup, "enabled", pressed); } this.output(pressed); @@ -1456,7 +1431,7 @@ class S4Mk3EffectUnit extends ComponentContainer { this.knobs = []; this.buttons = []; for (const index of [0, 1, 2]) { - const effectGroup = "[EffectRack1_EffectUnit" + unitNumber + "_Effect" + (index + 1) + "]"; + const effectGroup = `[EffectRack1_EffectUnit${unitNumber}_Effect${index + 1}]`; this.knobs[index] = new Pot({ inKey: "meta", group: effectGroup, @@ -1511,9 +1486,9 @@ class S4Mk3EffectUnit extends ComponentContainer { engine.setValue(this.group, "show_parameters", this.focusedEffect !== null); - const effectGroup = "[EffectRack1_EffectUnit" + this.unitNumber + "_Effect" + (this.focusedEffect + 1) + "]"; + const effectGroup = `[EffectRack1_EffectUnit${this.unitNumber}_Effect${this.focusedEffect + 1}]`; for (const index of [0, 1, 2]) { - const unfocusGroup = "[EffectRack1_EffectUnit" + this.unitNumber + "_Effect" + (index + 1) + "]"; + const unfocusGroup = `[EffectRack1_EffectUnit${this.unitNumber}_Effect${index + 1}]`; this.buttons[index].outDisconnect(); this.buttons[index].group = this.focusedEffect === null ? unfocusGroup : effectGroup; this.buttons[index].inKey = this.focusedEffect === null ? "enabled" : "button_parameter" + (index + 1); @@ -1655,10 +1630,10 @@ class S4Mk3Deck extends Deck { loopModeOff: function(skipRestore) { if (this.previousWheelMode !== null) { this.indicator(false); - const wheelOutput = Array(40).fill(0); + const wheelOutput = new Uint8Array(40).fill(0); wheelOutput[0] = decks[0] - 1; const that = this; - controller.send(wheelOutput, null, 50, true); + controller.sendOutputReport(50, wheelOutput.buffer, true); if (!skipRestore) { that.deck.wheelMode = that.previousWheelMode; } @@ -1693,12 +1668,12 @@ class S4Mk3Deck extends Deck { this.loopModeConnection = engine.makeConnection(this.group, this.outKey, this.onLoopChange.bind(this)); } - const wheelOutput = Array(40).fill(0); + const wheelOutput = new Uint8Array(40).fill(0); wheelOutput[0] = decks[0] - 1; wheelOutput[1] = wheelLEDmodes.ringFlash; wheelOutput[4] = this.color + Button.prototype.brightnessOn; - controller.send(wheelOutput, null, 50, true); + controller.sendOutputReport(50, wheelOutput.buffer, true); this.indicator(true); } else if (this.previousWheelMode !== null) { @@ -1727,7 +1702,7 @@ class S4Mk3Deck extends Deck { 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."); + console.warn(`Unable to connect ${this.group}.${this.outKey}' to the controller output. The control appears to be unavailable.`); } } }, @@ -1741,10 +1716,10 @@ class S4Mk3Deck extends Deck { loopModeOff: function(skipRestore) { if (this.previousWheelMode !== null) { this.indicator(false); - const wheelOutput = Array(40).fill(0); + const wheelOutput = new Uint8Array(40).fill(0); wheelOutput[0] = decks[0] - 1; const that = this; - controller.send(wheelOutput, null, 50, true); + controller.sendOutputReport(wheelOutput.buffer, null, 50, true); if (!skipRestore) { that.deck.wheelMode = that.previousWheelMode; } @@ -1777,12 +1752,12 @@ class S4Mk3Deck extends Deck { this.loopModeConnection = engine.makeConnection(this.group, this.outKey, this.onLoopChange.bind(this)); } - const wheelOutput = Array(40).fill(0); + const wheelOutput = new Uint8Array(40).fill(0); wheelOutput[0] = decks[0] - 1; wheelOutput[1] = wheelLEDmodes.ringFlash; wheelOutput[4] = this.color + Button.prototype.brightnessOn; - controller.send(wheelOutput, null, 50, true); + controller.sendOutputReport(50, wheelOutput.buffer, true); this.indicator(true); } else if (this.previousWheelMode !== null) { @@ -1983,7 +1958,7 @@ class S4Mk3Deck extends Deck { currentSortedColumnIdx: -1, onChange: function(right) { if (this.libraryViewButtonPressed) { - this.currentSortedColumnIdx = (this.currentSortedColumnIdx + (right ? 1 : -1)) % LibrarySortableColumns.length; + this.currentSortedColumnIdx = (LibrarySortableColumns.length + this.currentSortedColumnIdx + (right ? 1 : -1)) % LibrarySortableColumns.length; engine.setValue("[Library]", "sort_column", LibrarySortableColumns[this.currentSortedColumnIdx]); } else if (this.starButtonPressed) { if (this.shifted) { @@ -2075,7 +2050,7 @@ class S4Mk3Deck extends Deck { if (connection) { this.outConnections[0] = connection; } else { - console.warn("Unable to connect '" + this.group + ".focused_widget' to the controller output. The control appears to be unavailable."); + console.warn(`Unable to connect ${this.group}.focused_widget' to the controller output. The control appears to be unavailable.`); } }, onShortRelease: function() { @@ -2179,7 +2154,7 @@ class S4Mk3Deck extends Deck { }); if (SamplerCrossfaderAssign) { engine.setValue( - "[Sampler" + samplerNumber + "]", + `[Sampler${samplerNumber}]`, "orientation", (decks[0] === 1) ? 0 : 2 ); @@ -2513,7 +2488,7 @@ class S4Mk3Deck extends Deck { const fractionalRevolution = revolutions - Math.floor(revolutions); const LEDposition = fractionalRevolution * wheelAbsoluteMax; - const wheelOutput = Array(40).fill(0); + const wheelOutput = new Uint8Array(40).fill(0); wheelOutput[0] = decks[0] - 1; if (engine.getValue(this.group, "end_of_track") && WheelLedBlinkOnTrackEnd) { @@ -2525,7 +2500,7 @@ class S4Mk3Deck extends Deck { } wheelOutput[4] = this.color + Button.prototype.brightnessOn; - controller.send(wheelOutput, null, 50, true); + controller.sendOutputReport(50, wheelOutput.buffer, true); } }); @@ -2551,7 +2526,7 @@ class S4Mk3Deck extends Deck { component.inConnect(); component.outConnect(); component.outTrigger(); - if (this.unshift !== undefined && typeof this.unshift === "function") { + if (typeof this.unshift === "function") { this.unshift(); } } @@ -2599,25 +2574,25 @@ class S4Mk3MixerColumn extends ComponentContainer { super(); this.idx = idx; - this.group = "[Channel" + idx + "]"; + this.group = `[Channel${idx}]`; this.gain = new Pot({ inKey: "pregain", }); this.eqHigh = new Pot({ - group: "[EqualizerRack1_" + this.group + "_Effect1]", + group: `[EqualizerRack1_${this.group}_Effect1]`, inKey: "parameter3", }); this.eqMid = new Pot({ - group: "[EqualizerRack1_" + this.group + "_Effect1]", + group: `[EqualizerRack1_${this.group}_Effect1]`, inKey: "parameter2", }); this.eqLow = new Pot({ - group: "[EqualizerRack1_" + this.group + "_Effect1]", + group: `[EqualizerRack1_${this.group}_Effect1]`, inKey: "parameter1", }); this.quickEffectKnob = new Pot({ - group: "[QuickEffectRack1_" + this.group + "]", + group: `[QuickEffectRack1_${this.group}]`, inKey: "super1", }); this.volume = new Pot({ @@ -2625,7 +2600,7 @@ class S4Mk3MixerColumn extends ComponentContainer { mixer: this, input: MixerControlsMixAnxOnShift ? function(value) { if (this.mixer.shifted) { - const controlKey = (this.group === "[Microphone" + this.mixer.idx + "]" || this.group === "[Microphone]") ? "talkover" : "master"; + const controlKey = (this.group === `[Microphone" + this.mixer.${idx}]` || this.group === "[Microphone]") ? "talkover" : "master"; const isPlaying = engine.getValue(this.group, controlKey); if ((value !== 0) !== isPlaying) { engine.setValue(this.group, controlKey, value !== 0); @@ -2642,12 +2617,12 @@ class S4Mk3MixerColumn extends ComponentContainer { this.effectUnit1Assign = new PowerWindowButton({ group: "[EffectRack1_EffectUnit1]", - key: "group_" + this.group + "_enable", + key: `group_${this.group}_enable`, }); this.effectUnit2Assign = new PowerWindowButton({ group: "[EffectRack1_EffectUnit2]", - key: "group_" + this.group + "_enable", + key: `group_${this.group}_enable`, }); // FIXME: Why is output not working for these? @@ -2706,16 +2681,16 @@ class S4Mk3MixerColumn extends ComponentContainer { updateGroup(shifted) { let alternativeInput = null; - if (engine.getValue("[Auxiliary" + this.idx + "]", "input_configured")) { - alternativeInput = "[Auxiliary" + this.idx + "]"; - } else if (engine.getValue(this.idx !== 1 ? "[Microphone" + this.idx + "]" : "[Microphone]", "input_configured")) { - alternativeInput = this.idx !== 1 ? "[Microphone" + this.idx + "]" : "[Microphone]"; + if (engine.getValue(`[Auxiliary${this.idx}]`, "input_configured")) { + alternativeInput = `[Auxiliary${this.idx}]`; + } else if (engine.getValue(this.idx !== 1 ? `[Microphone${this.idx}]` : "[Microphone]", "input_configured")) { + alternativeInput = this.idx !== 1 ? `[Microphone${this.idx}]` : "[Microphone]"; } if (!alternativeInput) { return; } - this.group = shifted ? alternativeInput : "[Channel" + this.idx + "]"; + this.group = shifted ? alternativeInput : `[Channel${this.idx}]`; for (const property of ["gain", "volume", "pfl", "crossfaderSwitch"]) { const component = this[property]; if (component instanceof Component) { @@ -2732,8 +2707,8 @@ class S4Mk3MixerColumn extends ComponentContainer { if (component instanceof Component) { component.outDisconnect(); component.inDisconnect(); - component.inKey = "group_" + this.group + "_enable"; - component.outKey = "group_" + this.group + "_enable"; + component.inKey = `group_${this.group}_enable`; + component.outKey = `group_${this.group}_enable`; component.inConnect(); component.outConnect(); component.outTrigger(); @@ -2908,16 +2883,16 @@ class S4MK3 { const that = this; /* eslint no-unused-vars: "off" */ const meterConnection = engine.makeConnection("[Master]", "guiTick50ms", function(_value) { - const deckMeters = Array(78).fill(0); + const deckMeters = new Uint8Array(78).fill(0); // Each column has 14 segments, but treat the top one specially for the clip indicator. const deckSegments = 13; for (let deckNum = 1; deckNum <= 4; deckNum++) { - let deckGroup = "[Channel" + deckNum + "]"; + let deckGroup = `[Channel${deckNum}]`; if (that.leftDeck.shifted || that.rightDeck.shifted) { - if (engine.getValue("[Auxiliary" + deckNum + "]", "input_configured")) { - deckGroup = "[Auxiliary" + deckNum + "]"; - } else if (engine.getValue(deckNum !== 1 ? "[Microphone" + deckNum + "]" : "[Microphone]", "input_configured")) { - deckGroup = deckNum !== 1 ? "[Microphone" + deckNum + "]" : "[Microphone]"; + if (engine.getValue(`[Auxiliary${deckNum}]`, "input_configured")) { + deckGroup = `[Auxiliary${deckNum}]`; + } else if (engine.getValue(deckNum !== 1 ? `[Microphone${deckNum}]` : "[Microphone]", "input_configured")) { + deckGroup = deckNum !== 1 ? `[Microphone${deckNum}]` : "[Microphone]"; } } const deckLevel = engine.getValue(deckGroup, "VuMeter"); @@ -2941,7 +2916,7 @@ class S4MK3 { // There are more bytes in the report which seem like they should be for the main // mix meters, but setting those bytes does not do anything, except for lighting // the clip lights on the main mix meters. - controller.send(deckMeters, null, 129); + controller.sendOutputReport(129, deckMeters.buffer); }); if (UseMotors) { engine.beginTimer(20, this.motorCallback.bind(this)); @@ -2961,11 +2936,11 @@ class S4MK3 { } motorCallback() { - const motorData = [ + const motorData = new Uint8Array([ 1, 0x20, 1, 0, 0, 1, 0x20, 1, 0, 0, - ]; + ]); const maxVelocity = 10; let velocityLeft = 0; @@ -3114,7 +3089,7 @@ class S4MK3 { motorData[8] = velocityRight & 0xff; motorData[9] = velocityRight >> 8; - controller.send(motorData, null, 49, true); + controller.sendOutputReport(49, motorData.buffer, true); } incomingData(data) { const reportId = data[0]; @@ -3138,17 +3113,17 @@ class S4MK3 { this.leftDeck.wheelRelative.input(view.getUint16(12, true)); this.rightDeck.wheelRelative.input(view.getUint16(40, true)); } else { - console.warn("Unsupported HID repord with ID "+ reportId + ". Contains: "+data); + console.warn(`Unsupported HID repord with ID ${reportId}. Contains: ${data}`); } } init() { // sending these magic reports is required for the jog wheel LEDs to work - const wheelLEDinitReport = Array(26).fill(0); + const wheelLEDinitReport = new Uint8Array(26).fill(0); wheelLEDinitReport[1] = 1; wheelLEDinitReport[2] = 3; - controller.send(wheelLEDinitReport, null, 48, true); + controller.sendOutputReport(48, wheelLEDinitReport.buffer, true); wheelLEDinitReport[0] = 1; - controller.send(wheelLEDinitReport, null, 48); + controller.sendOutputReport(48, wheelLEDinitReport.buffer); // Init wheel timer data wheelTimer = null; @@ -3161,17 +3136,17 @@ class S4MK3 { } shutdown() { // button LEDs - controller.send(new Array(94).fill(0), null, 128); + controller.sendOutputReport(128, new Uint8Array(94).fill(0).buffer); // meter LEDs - controller.send(new Array(78).fill(0), null, 129); + controller.sendOutputReport(129, new Uint8Array(78).fill(0).buffer); - const wheelOutput = Array(40).fill(0); + const wheelOutput = new Uint8Array(40).fill(0); // left wheel LEDs - controller.send(wheelOutput, null, 50, true); + controller.sendOutputReport(50, wheelOutput.buffer, true); // right wheel LEDs wheelOutput[0] = 1; - controller.send(wheelOutput, null, 50, true); + controller.sendOutputReport(50, wheelOutput.buffer, true); } } diff --git a/res/controllers/common-controller-scripts.js b/res/controllers/common-controller-scripts.js index f0d19253ee7b..2e985768ab84 100644 --- a/res/controllers/common-controller-scripts.js +++ b/res/controllers/common-controller-scripts.js @@ -16,6 +16,38 @@ Control:off Deck:off */ +/* exported LibraryColumns */ +/*eslint no-var:off */ + +// ----------------- Mapping constants --------------------- + +// Library column value, which can be used to interact with the CO for "[Library] sort_column" +var LibraryColumns = { + Artist: 1, + Title: 2, + Album: 3, + Albumartist: 4, + Year: 5, + Genre: 6, + Composer: 7, + Grouping: 8, + Tracknumber: 9, + Filetype: 10, + NativeLocation: 11, + Comment: 12, + Duration: 13, + Bitrate: 14, + BPM: 15, + ReplayGain: 16, + DatetimeAdded: 17, + TimesPlayed: 18, + Rating: 19, + Key: 20, + Preview: 21, + Coverart: 22, + TrackColor: 30, + LastPlayed: 31, +}; // ----------------- Prototype enhancements --------------------- diff --git a/src/test/controller_mapping_validation_test.cpp b/src/test/controller_mapping_validation_test.cpp index 2a91e4715fe5..f0586c98ddb6 100644 --- a/src/test/controller_mapping_validation_test.cpp +++ b/src/test/controller_mapping_validation_test.cpp @@ -13,6 +13,14 @@ void FakeControllerJSProxy::send(const QList& data, unsigned int length) { Q_UNUSED(length); } +void FakeControllerJSProxy::sendOutputReport(quint8 reportID, + const QByteArray& dataArray, + bool resendUnchangedReport) { + Q_UNUSED(reportID); + Q_UNUSED(dataArray); + Q_UNUSED(resendUnchangedReport); +} + void FakeControllerJSProxy::sendSysexMsg(const QList& data, unsigned int length) { Q_UNUSED(data); Q_UNUSED(length); diff --git a/src/test/controller_mapping_validation_test.h b/src/test/controller_mapping_validation_test.h index a9a539fb3236..1e6c9a6eacff 100644 --- a/src/test/controller_mapping_validation_test.h +++ b/src/test/controller_mapping_validation_test.h @@ -18,6 +18,10 @@ class FakeControllerJSProxy : public ControllerJSProxy { Q_INVOKABLE void sendShortMsg(unsigned char status, unsigned char byte1, unsigned char byte2); + + Q_INVOKABLE void sendOutputReport(quint8 reportID, + const QByteArray& dataArray, + bool resendUnchangedReport = false); }; class FakeController : public Controller { From 76b98bdd52f657afa79d5d5cbbc8ebb9ded6f1c9 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Wed, 5 Apr 2023 19:31:16 +0100 Subject: [PATCH 17/19] Kontrol S4 Mk3: PR feedback --- res/controllers/Traktor-Kontrol-S4-MK3.js | 50 ++++++++-------- res/controllers/common-controller-scripts.js | 61 ++++++++++---------- 2 files changed, 57 insertions(+), 54 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index 4bdcc6bd7fb1..f557e6eb1ee6 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -52,11 +52,11 @@ const DeckColors = [ ]; const LibrarySortableColumns = [ - LibraryColumns.Artist, - LibraryColumns.Title, - LibraryColumns.BPM, - LibraryColumns.Key, - LibraryColumns.DatetimeAdded, + script.LIBRARY_COLUMNS.ARTIST, + script.LIBRARY_COLUMNS.TITLE, + script.LIBRARY_COLUMNS.BPM, + script.LIBRARY_COLUMNS.KEY, + script.LIBRARY_COLUMNS.DATETIME_ADDED, ]; const LoopWheelMoveFactor = 50; @@ -279,11 +279,11 @@ class Component { this.inKey = this.key; this.outKey = this.key; } - if (typeof this.unshift === "function") { + if (typeof this.unshift === "function" && this.unshift.length) { this.unshift(); } this.shifted = false; - if (typeof this.input === "function" && this.inReport instanceof HIDInputReport) { + if (typeof this.input === "function" && this.inReport instanceof HIDInputReport && this.inReport.length === 0) { this.inConnect(); } this.outConnect(); @@ -360,24 +360,24 @@ class ComponentContainer extends Component { } reconnectComponents(callback) { for (const component of this) { - if (typeof component.outDisconnect === "function") { + if (typeof component.outDisconnect === "function" && component.outDisconnect.length === 0) { component.outDisconnect(); } - if (typeof callback === "function") { + if (typeof callback === "function" && callback.length === 0) { callback.call(this, component); } - if (typeof component.outConnect === "function") { + if (typeof component.outConnect === "function" && component.outConnect.length === 0) { component.outConnect(); } component.outTrigger(); - if (typeof component.unshift === "function") { + if (typeof component.unshift === "function" && component.unshift.length === 0) { component.unshift(); } } } unshift() { for (const component of this) { - if (typeof component.unshift === "function") { + if (typeof component.unshift === "function" && component.unshift.length === 0) { component.unshift(); } component.shifted = false; @@ -386,7 +386,7 @@ class ComponentContainer extends Component { } shift() { for (const component of this) { - if (typeof component.shift === "function") { + if (typeof component.shift === "function" && component.shift.length === 0) { component.shift(); } component.shifted = true; @@ -485,7 +485,8 @@ class Button extends Component { if (this.input === undefined) { this.input = this.defaultInput; if (typeof this.input === "function" - && this.inReport !== undefined && this.inReport instanceof HIDInputReport) { + && this.inReport instanceof HIDInputReport + && this.input.length === 0) { this.inConnect(); } } @@ -549,8 +550,8 @@ class Button extends Component { defaultInput(pressed) { if (pressed) { this.isLongPress = false; - if (typeof this.onShortPress === "function") { this.onShortPress(); } - if (typeof this.onLongPress === "function" || typeof this.onLongRelease === "function") { + 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; @@ -559,13 +560,13 @@ class Button extends Component { }, true); } } else if (this.isLongPress) { - if (typeof this.onLongRelease === "function") { this.onLongRelease(); } + 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(); } + if (typeof this.onShortRelease === "function" && this.onShortRelease.length === 0) { this.onShortRelease(); } } } } @@ -818,13 +819,16 @@ class BeatLoopRollButton extends TriggerButton { if (options.number <= 5 || !AddLoopHalveAndDoubleOnBeatloopRollTab) { options.key = "beatlooproll_"+BeatLoopRolls[AddLoopHalveAndDoubleOnBeatloopRollTab ? options.number + 1 : options.number]+"_activate"; options.onShortPress = function() { - this.beatloopSize = engine.getValue(this.group, "beatloop_size"); + 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.beatloopSize) { - engine.setValue(this.group, "beatloop_size", this.beatloopSize); + if (this.deck.beatloopSize) { + engine.setValue(this.group, "beatloop_size", this.deck.beatloopSize); + this.deck.beatloopSize = undefined; } }; } else if (options.number === 6) { @@ -1990,7 +1994,7 @@ class S4Mk3Deck extends Deck { }); this.libraryEncoderPress = new Button({ libraryViewButtonPressed: false, - onShortPress: function(pressed) { + onShortPress: function() { if (this.libraryViewButtonPressed) { script.toggleControl("[Library]", "sort_order"); } else { @@ -2526,7 +2530,7 @@ class S4Mk3Deck extends Deck { component.inConnect(); component.outConnect(); component.outTrigger(); - if (typeof this.unshift === "function") { + if (typeof this.unshift === "function" && this.unshift.length === 0) { this.unshift(); } } diff --git a/res/controllers/common-controller-scripts.js b/res/controllers/common-controller-scripts.js index 2e985768ab84..b2530f08270a 100644 --- a/res/controllers/common-controller-scripts.js +++ b/res/controllers/common-controller-scripts.js @@ -16,39 +16,8 @@ Control:off Deck:off */ -/* exported LibraryColumns */ /*eslint no-var:off */ -// ----------------- Mapping constants --------------------- - -// Library column value, which can be used to interact with the CO for "[Library] sort_column" -var LibraryColumns = { - Artist: 1, - Title: 2, - Album: 3, - Albumartist: 4, - Year: 5, - Genre: 6, - Composer: 7, - Grouping: 8, - Tracknumber: 9, - Filetype: 10, - NativeLocation: 11, - Comment: 12, - Duration: 13, - Bitrate: 14, - BPM: 15, - ReplayGain: 16, - DatetimeAdded: 17, - TimesPlayed: 18, - Rating: 19, - Key: 20, - Preview: 21, - Coverart: 22, - TrackColor: 30, - LastPlayed: 31, -}; - // ----------------- Prototype enhancements --------------------- // Returns an ASCII byte array for the string @@ -159,6 +128,36 @@ var colorCodeToObject = function(colorCode) { var script = function() { }; +// ----------------- Mapping constants --------------------- + +// Library column value, which can be used to interact with the CO for "[Library] sort_column" +script.LIBRARY_COLUMNS = Object.freeze({ + ARTIST: 1, + TITLE: 2, + ALBUM: 3, + ALBUM_ARTIST: 4, + YEAR: 5, + GENRE: 6, + COMPOSER: 7, + GROUPING: 8, + TRACK_NUMBER: 9, + FILETYPE: 10, + NATIVE_LOCATION: 11, + COMMENT: 12, + DURATION: 13, + BITRATE: 14, + BPM: 15, + REPLAY_GAIN: 16, + DATETIME_ADDED: 17, + TIMES_PLAYED: 18, + RATING: 19, + KEY: 20, + PREVIEW: 21, + COVERART: 22, + TRACK_COLOR: 30, + LAST_PLAYED: 31, +}); + // DEPRECATED -- use script.midiDebug() instead script.debug = function(channel, control, value, status, group) { print("Warning: script.debug() is deprecated. Use script.midiDebug() instead."); From 61b34515da1ce60ef49a8c3ead4864e565a06d13 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Sun, 30 Apr 2023 20:08:20 +0100 Subject: [PATCH 18/19] Kontrol S4 Mk3: Fix deck change --- res/controllers/Traktor-Kontrol-S4-MK3.js | 19 +++++++++---------- res/controllers/common-controller-scripts.js | 1 - 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index f557e6eb1ee6..258e4911dc64 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -1,5 +1,4 @@ /// Created by Be and A. Colombier -/* global LibraryColumns*/ const LedColors = { off: 0, @@ -88,7 +87,7 @@ 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: true -const MixerControlsMixAnxOnShift = true; +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 @@ -96,7 +95,7 @@ const WheelSpeedSample = 3; // Make the sampler tab a beatlooproll tab instead // Default: false -const UseBeatloopRollInsteadOfSampler = true; +const UseBeatloopRollInsteadOfSampler = false; // Predefined beatlooproll sizes. Note that if you use AddLoopHalveAndDoubleOnBeatloopRollTab, the first and // last size will be ignored @@ -114,7 +113,7 @@ 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 = true; +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. @@ -279,7 +278,7 @@ class Component { this.inKey = this.key; this.outKey = this.key; } - if (typeof this.unshift === "function" && this.unshift.length) { + if (typeof this.unshift === "function" && this.unshift.length === 0) { this.unshift(); } this.shifted = false; @@ -363,7 +362,7 @@ class ComponentContainer extends Component { if (typeof component.outDisconnect === "function" && component.outDisconnect.length === 0) { component.outDisconnect(); } - if (typeof callback === "function" && callback.length === 0) { + if (typeof callback === "function" && callback.length === 1) { callback.call(this, component); } if (typeof component.outConnect === "function" && component.outConnect.length === 0) { @@ -2602,9 +2601,9 @@ class S4Mk3MixerColumn extends ComponentContainer { this.volume = new Pot({ inKey: "volume", mixer: this, - input: MixerControlsMixAnxOnShift ? function(value) { - if (this.mixer.shifted) { - const controlKey = (this.group === `[Microphone" + this.mixer.${idx}]` || this.group === "[Microphone]") ? "talkover" : "master"; + input: MixerControlsMixAuxOnShift ? function(value) { + if (this.mixer.shifted && this.group !== `[Channel${idx}]`) { // FIXME only if group != [ChannelX] + const controlKey = (this.group === `[Microphone${idx}]` || this.group === "[Microphone]") ? "talkover" : "master"; const isPlaying = engine.getValue(this.group, controlKey); if ((value !== 0) !== isPlaying) { engine.setValue(this.group, controlKey, value !== 0); @@ -2670,7 +2669,7 @@ class S4Mk3MixerColumn extends ComponentContainer { } } - if (MixerControlsMixAnxOnShift) { + if (MixerControlsMixAuxOnShift) { this.shift = function() { engine.setValue("[Microphone]", "show_microphone", true); this.updateGroup(true); diff --git a/res/controllers/common-controller-scripts.js b/res/controllers/common-controller-scripts.js index b2530f08270a..bcc8602f6c8d 100644 --- a/res/controllers/common-controller-scripts.js +++ b/res/controllers/common-controller-scripts.js @@ -16,7 +16,6 @@ Control:off Deck:off */ -/*eslint no-var:off */ // ----------------- Prototype enhancements --------------------- From febccc2ba374acfc0d0e71fabb826fc8539b31c8 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Sat, 3 Jun 2023 21:28:29 +0100 Subject: [PATCH 19/19] fix pre-commit failing due to TS new linting rules --- res/controllers/Traktor-Kontrol-S4-MK3.js | 20 ++++++++++---------- res/controllers/common-controller-scripts.js | 5 +++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index 258e4911dc64..b20056345aa7 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -86,7 +86,7 @@ const GridButtonBlinkOverBeat = false; 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: true +// 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 @@ -860,7 +860,7 @@ class SamplerButton extends Button { 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.group = `[Sampler${this.number}]`; this.outConnect(); } onShortPress() { @@ -1635,12 +1635,11 @@ class S4Mk3Deck extends Deck { this.indicator(false); const wheelOutput = new Uint8Array(40).fill(0); wheelOutput[0] = decks[0] - 1; - const that = this; controller.sendOutputReport(50, wheelOutput.buffer, true); if (!skipRestore) { - that.deck.wheelMode = that.previousWheelMode; + this.deck.wheelMode = this.previousWheelMode; } - that.previousWheelMode = null; + this.previousWheelMode = null; if (this.loopModeConnection !== null) { this.loopModeConnection.disconnect(); this.loopModeConnection = null; @@ -1721,12 +1720,11 @@ class S4Mk3Deck extends Deck { this.indicator(false); const wheelOutput = new Uint8Array(40).fill(0); wheelOutput[0] = decks[0] - 1; - const that = this; controller.sendOutputReport(wheelOutput.buffer, null, 50, true); if (!skipRestore) { - that.deck.wheelMode = that.previousWheelMode; + this.deck.wheelMode = this.previousWheelMode; } - that.previousWheelMode = null; + this.previousWheelMode = null; if (this.loopModeConnection !== null) { this.loopModeConnection.disconnect(); this.loopModeConnection = null; @@ -2350,8 +2348,10 @@ class S4Mk3Deck extends Deck { outTrigger: function() { const vinylOn = this.deck.wheelMode === wheelModes.vinyl; this.send(this.color + (vinylOn ? this.brightnessOn : this.brightnessOff)); - const motorOn = this.deck.wheelMode === wheelModes.motor; - this.deck.turntableButton.send(this.color + (motorOn ? this.brightnessOn : this.brightnessOff)); + if (this.deck.turntableButton) { + const motorOn = this.deck.wheelMode === wheelModes.motor; + this.deck.turntableButton.send(this.color + (motorOn ? this.brightnessOn : this.brightnessOff)); + } }, }); diff --git a/res/controllers/common-controller-scripts.js b/res/controllers/common-controller-scripts.js index bcc8602f6c8d..68eed5895412 100644 --- a/res/controllers/common-controller-scripts.js +++ b/res/controllers/common-controller-scripts.js @@ -21,7 +21,7 @@ // Returns an ASCII byte array for the string String.prototype.toInt = function() { - const a = new Array(); + const a = []; for (let i = 0; i < this.length; i++) { a[i] = this.charCodeAt(i); } @@ -124,6 +124,7 @@ var colorCodeToObject = function(colorCode) { }; }; +/* eslint @typescript-eslint/no-empty-function: "off" */ var script = function() { }; @@ -494,7 +495,6 @@ script.softStart = function(channel, control, value, status, group, factor) { }; // bpm - Used for tapping the desired BPM for a deck - var bpm = function() { }; @@ -584,6 +584,7 @@ var Controller = function() { Controller.prototype.addButton = function(buttonName, button, eventHandler) { if (eventHandler) { + /* eslint @typescript-eslint/no-this-alias: "off" */ const executionEnvironment = this; const handler = function(value) { button.state = value;