From f79a1e5c1f7e6b6b95b5a311d98b507177c34382 Mon Sep 17 00:00:00 2001 From: Be Date: Mon, 30 Aug 2021 00:43:00 -0500 Subject: [PATCH] Kontrol S4 Mk3: map LED output for left deck play, cue, & deck switch and expand foundation of Components library for HID --- res/controllers/Traktor-Kontrol-S4-MK3.js | 351 +++++++++++++++++++--- 1 file changed, 317 insertions(+), 34 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index 6029f3174eb8..f9d68bfbc84c 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -1,14 +1,182 @@ /* eslint no-redeclare: "off", no-unused-vars: "off" */ +// valid range 0 - 3 +const buttonBrightnessOff = 0; +const buttonBrightnessOn = 2; + +// add 0 - 3 to each color to control brightness +const LEDColors = { + off: 0, + red: 4, + carrot: 8, + orange: 12, + honey: 16, + yellow: 20, + lime: 24, + green: 28, + aqua: 32, + celeste: 36, + sky: 40, + blue: 44, + purple: 48, + fuscia: 52, + magenta: 56, + azalea: 60, + salmon: 64, + white: 68 +}; + +const 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, +}); + +class HIDInputPacket { + constructor(reportId) { + this.reportId = reportId; + this.fields = []; + } + + registerCallback(callback, byteOffset, bitOffset, bitLength) { + if (typeof callback !== "function") { + throw Error("callback must be a function"); + } + + if (byteOffset === undefined || typeof byteOffset !== "number" || !Number.isInteger(byteOffset)) { + throw Error("byteOffset must be 0 or a positive integer"); + } + + if (bitOffset === undefined) { + bitOffset = 0; + } + if (typeof bitOffset !== "number" || bitOffset < 0 || !Number.isInteger(bitOffset)) { + throw Error("bitOffset must be 0 or a positive integer"); + } + + if (bitLength === undefined) { + bitLength = 1; + } + if (typeof bitLength !== "number" || bitLength < 1 || !Number.isInteger(bitOffset) || bitLength > 32) { + throw Error("bitLength must be an integer between 1 and 32"); + } + + this.fields.push({ + callback: callback, + byteOffset: byteOffset, + bitOffset: bitOffset, + bitLength: bitLength, + oldData: 0 + }); + } + + handleInput(byteArray) { + for (let field of this.fields) { + const view = new DataView(byteArray); + const numBytes = Math.ceil(field.bitLength / 8); + let data; + + if (numBytes === 1) { + data = view.getUint8(field.byteOffset); + } else if (numBytes === 2) { + data = view.getUint16(field.byteOffset); + } else if (numBytes === 3) { + data = view.getUint32(field.byteOffset) >>> 8; + } else if (numBytes === 4) { + data = view.getUint32(field.byteOffset); + } else { + throw Error("field bitLength must be between 1 and 32"); + } + + data = (data >>> field.bitOffset) & (2 ** field.bitLength - 1); + + if (field.oldData !== data) { + field.callback(data); + field.oldData = data; + } + } + } +} + +class HIDOutputPacket { + constructor(reportId, length) { + this.reportId = reportId; + this.data = Array(length).fill(0); + this.paused = false; + } + + send() { + if (!this.paused) { + controller.send(this.data, null, this.reportId); + } + } +} + class Component { - constructor(group, inKey) { - this.group = group; - this.inKey = inKey; + constructor(options) { + Object.assign(this, options); + if (this.input !== undefined && typeof this.input === "function" + && this.inPacket !== undefined && this.inPacket instanceof HIDInputPacket) { + this.registerInput(); + } + this.outConnections = []; + this.outConnect(); + } + registerInput(callback) { + if (this.inPacket === undefined || ! (this.inPacket instanceof HIDInputPacket)) { + throw Error("inPacket must be an HIDInputPacket"); + } + if (typeof callback === "function") { + this.input = callback; + } + this.inPacket.registerCallback(this.input.bind(this), this.inByteOffset, this.inBitOffset, this.inBitLength); + } + send(value) { + if (this.outPacket !== undefined && this.outByteOffset !== undefined) { + this.outPacket.data[this.outByteOffset] = value; + this.outPacket.send(); + } + } + output(value) { + this.send(value); + } + outConnect() { + if (this.outKey !== undefined && this.group !== undefined) { + this.outConnections[0] = engine.makeConnection(this.group, this.outKey, this.output.bind(this)); + } + } + outDisconnect() { + for (let connection of this.outConnections) { + connection.disconnect(); + } + } + outTrigger() { + for (let connection of this.outConnections) { + connection.trigger(); + } } } class Deck { - constructor(decks) { + constructor(decks, colors) { if (typeof decks === "number") { this.group = Deck.groupForNumber(decks); } else if (Array.isArray(decks)) { @@ -16,6 +184,14 @@ class Deck { 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++; + } + } } toggleDeck() { if (this.decks === undefined) { @@ -33,14 +209,17 @@ class Deck { switchDeck(newGroup) { for (let property in this) { if (Object.prototype.hasOwnProperty.call(this, property)) { - if (this[property] instanceof Component) { - if (this[property].disconnect !== undefined && typeof this[property].disconnect === "function") { - this[property].disconnect(); + const obj = this[property]; + if (obj instanceof Component) { + if (obj.outDisconnect !== undefined && typeof obj.outDisconnect === "function") { + obj.outDisconnect(); } - this[property].group = newGroup; - if (this[property].connect !== undefined && typeof this[property].connect === "function") { - this[property].connect(); + obj.group = newGroup; + obj.color = this.groupsToColors[newGroup]; + if (obj.outConnect !== undefined && typeof obj.outConnect === "function") { + obj.outConnect(); } + obj.outTrigger(); } } } @@ -51,23 +230,30 @@ class Deck { } class Button extends Component { - constructor(group, inKey) { - super(group, inKey); + constructor(options) { + super(options); + } + output(value) { + const brightness = (value > 0) ? buttonBrightnessOn : buttonBrightnessOff; + this.send(this.color + brightness); } } class PushButton extends Button { - constructor(group, inKey) { - super(group, inKey); + constructor(options) { + super(options); } input(pressed) { - engine.setValue(this.group, this.inKey, pressed); + if (pressed !== this.oldValue) { + engine.setValue(this.group, this.inKey, pressed); + this.oldValue = pressed; + } } } class ToggleButton extends Button { - constructor(group, inKey) { - super(group, inKey); + constructor(options) { + super(options); } input(pressed) { if (pressed) { @@ -77,9 +263,8 @@ class ToggleButton extends Button { } class Pot extends Component { - constructor(group, inKey, max) { - super(group, inKey); - this.max = max; + constructor(options) { + super(options); this.firstValueReceived = false; } input(value) { @@ -103,30 +288,116 @@ const getBit = (byte, index) => { class S4MK3 { constructor() { - this.potMax = 2**12; - this.leftDeck = new Deck([1, 3]); - this.leftDeck.playButton = new ToggleButton(this.leftDeck.group, "play"); - this.leftDeck.cueButton = new PushButton(this.leftDeck.group, "cue_default"); - this.leftDeck.tempoFader = new Pot(this.leftDeck.group, "rate", this.potMax); + this.potMax = 2**12 - 1; + + this.inputPackets = []; + this.inputPackets[1] = new HIDInputPacket(1); + + this.outPackets = []; + this.outPackets[128] = new HIDOutputPacket(128, 94); + + this.leftDeck = new Deck([1, 3], [LEDColors.red, LEDColors.blue]); + this.leftDeck.color = LEDColors.red; + this.leftDeck.playButton = new ToggleButton({ + group: this.leftDeck.group, + inKey: "play", + inPacket: this.inputPackets[1], + inByteOffset: 5, + inBitOffset: 0, + inBitLength: 1, + outKey: "play_indicator", + outPacket: this.outPackets[128], + outByteOffset: 55, + output: function(value) { + // Unfortunately the hardware only supports green but not other colors + if (value) { + this.send(127); + } else { + // Turn the LED off completely because the dim green state + // is too easy to confuse with the bright green state. Light from + // the cue button bleeds through in the off state so the play button + // is not completely dark. + this.send(0); + } + }, + }); + this.leftDeck.cueButton = new PushButton({ + group: this.leftDeck.group, + inKey: "cue_default", + inPacket: this.inputPackets[1], + inByteOffset: 5, + inBitOffset: 1, + inBitLength: 1, + outKey: "cue_indicator", + outPacket: this.outPackets[128], + outByteOffset: 8, + color: this.leftDeck.color + }); + this.leftDeck.deckButton1 = new Button({ + deck: this.leftDeck, + input: function(value) { + if (value) { + this.deck.switchDeck(Deck.groupForNumber(1)); + this.outPacket.data[12] = LEDColors.red + buttonBrightnessOn; + // turn off the other deck selection button's LED + this.outPacket.data[13] = 0; + this.outPacket.send(); + } + }, + inPacket: this.inputPackets[1], + inByteOffset: 6, + inBitOffset: 2, + inBitLength: 1, + outPacket: this.outPackets[128] + }); + this.leftDeck.deckButton3 = new Button({ + deck: this.leftDeck, + input: function(value) { + if (value) { + this.deck.switchDeck(Deck.groupForNumber(3)); + // turn off the other deck selection button's LED + this.outPacket.data[12] = 0; + this.outPacket.data[13] = LEDColors.blue + buttonBrightnessOn; + this.outPacket.send(); + } + }, + inPacket: this.inputPackets[1], + inByteOffset: 6, + inBitOffset: 3, + inBitLength: 1, + outPacket: this.outPackets[128] + }); + + this.leftDeck.tempoFader = new Pot({ + group: this.leftDeck.group, + inKey: "rate", + max: this.potMax + }); + + for (let property in this.leftDeck) { + if (Object.prototype.hasOwnProperty.call(this.leftDeck, property)) { + const obj = this.leftDeck[property]; + if (obj instanceof Component) { + obj.outTrigger(); + } + } + } + // set deck selection button LEDs + this.outPackets[128].data[12] = LEDColors.red + buttonBrightnessOn; + this.outPackets[128].data[13] = 0; + this.outPackets[128].send(); } incomingData(data) { const reportId = data[0]; // slice off the reportId if (reportId === 1) { - let string; + let string = ""; for (let byte of data) { string = string + byte.toString(2) + ","; } print(string); - this.leftDeck.playButton.input(getBit(data[5], 8)); - this.leftDeck.cueButton.input(getBit(data[5], 7)); - - if (getBit(data[6], 6)) { - this.leftDeck.switchDeck(Deck.groupForNumber(1)); - } else if (getBit(data[6], 5)) { - this.leftDeck.switchDeck(Deck.groupForNumber(3)); - } + this.inputPackets[1].handleInput(data.buffer); } else if (reportId === 2) { const buffer = data.buffer.slice(1); const view = new Uint16Array(buffer, 0, buffer.byteLength/2); @@ -185,6 +456,18 @@ class S4MK3 { } } init() { + // controller.send([ + // 0, 0, 0, 0, 0, 0, 0, 0, LEDColors.red + this.buttonBrightnessOn, 0, + // 0, 0, 127, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 127, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // 0, 0, 0, 0 + // ], null, 128); } shutdown() { }