From e38537e99a3e57c5d02424213037883097ebfc5c Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Fri, 9 Dec 2022 14:25:49 +0100 Subject: [PATCH 01/36] Change default S3 deck colors to match Traktor It's trivial to change this back if you prefer the calmer orange colors for decks 1 and 2, but it seems like a good idea to stay as close to stock as possible. --- res/controllers/Traktor-Kontrol-S3-hid-scripts.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 4e9cf47acdc5..53f9ea7dd3a1 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -50,10 +50,10 @@ TraktorS3.SixteenSamplers = false; // PURPLE, FUCHSIA, MAGENTA, AZALEA, SALMON, WHITE // Some colors may look odd because of how they are encoded inside the controller. TraktorS3.ChannelColors = { - "[Channel1]": "CARROT", - "[Channel2]": "CARROT", - "[Channel3]": "BLUE", - "[Channel4]": "BLUE" + "[Channel1]": "BLUE", + "[Channel2]": "BLUE", + "[Channel3]": "CARROT", + "[Channel4]": "CARROT" }; // Each color has four brightnesses, so these values can be between 0 and 3. From e071976cc7c51ca5f4b97a257e4b796374b886e4 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Fri, 9 Dec 2022 14:51:04 +0100 Subject: [PATCH 02/36] Place S3 jog inertia behavior behind flag I feel like by default it should just behave like everything else and releasing the jog wheel should immediately resume playback. --- .../Traktor-Kontrol-S3-hid-scripts.js | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 53f9ea7dd3a1..32542a99e28d 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -41,6 +41,11 @@ TraktorS3.SamplerModePressAndHold = false; // enables scratch mode. TraktorS3.JogDefaultOn = true; +// When set to true, wait for the track to come to a complete standstill after +// releasing the jog wheel before resuming playback. When this is disabled the +// track continues playing immediately when you release the jog wheel. +TraktorS3.JogInertiaDelay = false; + // If true, the sampler buttons on Deck 1 are samplers 1-8 and the sampler buttons on Deck 2 are // 9-16. If false, both decks are samplers 1-8. TraktorS3.SixteenSamplers = false; @@ -194,6 +199,7 @@ TraktorS3.Deck = function(controller, deckNumber, group) { this.lastTickVal = 0; this.lastTickTime = 0; this.lastTickWallClock = 0; + // Only used when `TraktorS3.JogInertiaDelay` is disabled this.wheelTouchInertiaTimer = 0; // Knob encoder states (hold values between 0x0 and 0xF) @@ -634,29 +640,38 @@ TraktorS3.Deck.prototype.jogTouchHandler = function(field) { if (!this.jogToggled) { return; } - if (this.wheelTouchInertiaTimer !== 0) { + + if (TraktorS3.JogInertiaDelay && this.wheelTouchInertiaTimer !== 0) { // The wheel was touched again, reset the timer. engine.stopTimer(this.wheelTouchInertiaTimer); this.wheelTouchInertiaTimer = 0; } - if (field.value !== 0) { - engine.setValue(this.activeChannel, "scratch2_enable", true); - return; - } + // If shift is pressed, reset right away. - if (this.shiftPressed) { + if (field.value === 0 && this.shiftPressed) { engine.setValue(this.activeChannel, "scratch2", 0.0); engine.setValue(this.activeChannel, "scratch2_enable", false); this.playIndicatorHandler(0, this.activeChannel); return; } - // The wheel keeps moving after the user lifts their finger, so don't release scratch mode - // right away. - this.tickReceived = false; - this.wheelTouchInertiaTimer = engine.beginTimer( - 100, TraktorS3.bind(TraktorS3.Deck.prototype.checkJogInertia, this), false); + + if (field.value !== 0) { + engine.setValue(this.activeChannel, "scratch2_enable", true); + } else if (TraktorS3.JogInertiaDelay) { + // The wheel keeps moving after the user lifts their finger, so don't + // release scratch mode right away + this.tickReceived = false; + this.wheelTouchInertiaTimer = engine.beginTimer( + 100, TraktorS3.bind(TraktorS3.Deck.prototype.checkJogInertia, this), false); + } else { + // If `TraktorS3.JogInertiaDelay` is not enabled then releasing the jog + // wheel should immediately disable scratch mode so the track continues + // playing back normally (with any leftover momentum from scratching) + engine.setValue(this.activeChannel, "scratch2_enable", false); + } }; +// Only used when `TraktorS3.JogInertiaDelay` has been enabled` TraktorS3.Deck.prototype.checkJogInertia = function() { // If we've received no ticks since the last call we are stopped. // In jog mode we always stop right away. From 26067685bd626076e09909aa301870eb89969854 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Fri, 9 Dec 2022 14:17:03 +0100 Subject: [PATCH 03/36] Use the engine scratch methods instead of setvalue For the S3 controller mapping. --- .../Traktor-Kontrol-S3-hid-scripts.js | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 32542a99e28d..3bfa7c081109 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -65,6 +65,10 @@ TraktorS3.ChannelColors = { TraktorS3.LEDDimValue = 0x00; TraktorS3.LEDBrightValue = 0x02; +// Parameters for the scratch smoothing +TraktorS3.Alpha = 1.0 / 8; +TraktorS3.Beta = TraktorS3.Alpha / 32; + // Set to true to output debug messages and debug light outputs. TraktorS3.DebugMode = false; @@ -650,13 +654,13 @@ TraktorS3.Deck.prototype.jogTouchHandler = function(field) { // If shift is pressed, reset right away. if (field.value === 0 && this.shiftPressed) { engine.setValue(this.activeChannel, "scratch2", 0.0); - engine.setValue(this.activeChannel, "scratch2_enable", false); + engine.scratchDisable(this.deckNumber); this.playIndicatorHandler(0, this.activeChannel); return; } if (field.value !== 0) { - engine.setValue(this.activeChannel, "scratch2_enable", true); + engine.scratchEnable(this.deckNumber, 768, 33.33334, TraktorS3.Alpha, TraktorS3.Beta); } else if (TraktorS3.JogInertiaDelay) { // The wheel keeps moving after the user lifts their finger, so don't // release scratch mode right away @@ -667,7 +671,7 @@ TraktorS3.Deck.prototype.jogTouchHandler = function(field) { // If `TraktorS3.JogInertiaDelay` is not enabled then releasing the jog // wheel should immediately disable scratch mode so the track continues // playing back normally (with any leftover momentum from scratching) - engine.setValue(this.activeChannel, "scratch2_enable", false); + engine.scratchDisable(this.deckNumber); } }; @@ -676,8 +680,8 @@ TraktorS3.Deck.prototype.checkJogInertia = function() { // If we've received no ticks since the last call we are stopped. // In jog mode we always stop right away. if (!this.tickReceived) { - engine.setValue(this.activeChannel, "scratch2", 0.0); - engine.setValue(this.activeChannel, "scratch2_enable", false); + engine.scratchTick(this.deckNumber, 0.0); + engine.scratchDisable(this.deckNumber); this.playIndicatorHandler(0, this.activeChannel); engine.stopTimer(this.wheelTouchInertiaTimer); this.wheelTouchInertiaTimer = 0; @@ -716,8 +720,8 @@ TraktorS3.Deck.prototype.jogHandler = function(field) { // The Mixxx scratch code tries to do accumulation and time calculation itself. // This controller is better, so just use its values. - if (engine.getValue(this.activeChannel, "scratch2_enable")) { - engine.setValue(this.activeChannel, "scratch2", velocity); + if (engine.isScratching(this.deckNumber)) { + engine.scratchTick(this.deckNumber, velocity); } else { // If we're playing, just nudge. if (engine.getValue(this.activeChannel, "play")) { @@ -1840,10 +1844,8 @@ TraktorS3.Controller.prototype.deckSwitchHandler = function(field) { const channel = this.Channels[field.group]; const deck = channel.parentDeck; - const isScratching = engine.getValue(deck.activeChannel, "scratch2_enable"); - if (isScratching) { - engine.setValue(deck.activeChannel, "scratch2", 0.0); - engine.setValue(deck.activeChannel, "scratch2_enable", false); + if (engine.isScratching(deck.deckNumber)) { + engine.scratchDisable(deck.deckNumber); } deck.activateChannel(channel); From de02f36e310f270bea70e18d598640f8cc3a70ff Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Fri, 9 Dec 2022 16:06:02 +0100 Subject: [PATCH 04/36] Completely remove jog inertia behavior from S3 I don't see why anyone would want to use this with scratch2 mode. --- .../Traktor-Kontrol-S3-hid-scripts.js | 55 ++----------------- 1 file changed, 5 insertions(+), 50 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 3bfa7c081109..44220ff9cac3 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -41,11 +41,6 @@ TraktorS3.SamplerModePressAndHold = false; // enables scratch mode. TraktorS3.JogDefaultOn = true; -// When set to true, wait for the track to come to a complete standstill after -// releasing the jog wheel before resuming playback. When this is disabled the -// track continues playing immediately when you release the jog wheel. -TraktorS3.JogInertiaDelay = false; - // If true, the sampler buttons on Deck 1 are samplers 1-8 and the sampler buttons on Deck 2 are // 9-16. If false, both decks are samplers 1-8. TraktorS3.SixteenSamplers = false; @@ -198,13 +193,9 @@ TraktorS3.Deck = function(controller, deckNumber, group) { this.padModeState = 0; // Jog wheel state - // tickReceived is used to detect when the platter has stopped moving. - this.tickReceived = false; this.lastTickVal = 0; this.lastTickTime = 0; this.lastTickWallClock = 0; - // Only used when `TraktorS3.JogInertiaDelay` is disabled - this.wheelTouchInertiaTimer = 0; // Knob encoder states (hold values between 0x0 and 0xF) // Rotate to the right is +1 and to the left is means -1 @@ -645,60 +636,24 @@ TraktorS3.Deck.prototype.jogTouchHandler = function(field) { return; } - if (TraktorS3.JogInertiaDelay && this.wheelTouchInertiaTimer !== 0) { - // The wheel was touched again, reset the timer. - engine.stopTimer(this.wheelTouchInertiaTimer); - this.wheelTouchInertiaTimer = 0; - } - - // If shift is pressed, reset right away. - if (field.value === 0 && this.shiftPressed) { - engine.setValue(this.activeChannel, "scratch2", 0.0); - engine.scratchDisable(this.deckNumber); - this.playIndicatorHandler(0, this.activeChannel); - return; - } - if (field.value !== 0) { engine.scratchEnable(this.deckNumber, 768, 33.33334, TraktorS3.Alpha, TraktorS3.Beta); - } else if (TraktorS3.JogInertiaDelay) { - // The wheel keeps moving after the user lifts their finger, so don't - // release scratch mode right away - this.tickReceived = false; - this.wheelTouchInertiaTimer = engine.beginTimer( - 100, TraktorS3.bind(TraktorS3.Deck.prototype.checkJogInertia, this), false); } else { - // If `TraktorS3.JogInertiaDelay` is not enabled then releasing the jog - // wheel should immediately disable scratch mode so the track continues - // playing back normally (with any leftover momentum from scratching) engine.scratchDisable(this.deckNumber); - } -}; -// Only used when `TraktorS3.JogInertiaDelay` has been enabled` -TraktorS3.Deck.prototype.checkJogInertia = function() { - // If we've received no ticks since the last call we are stopped. - // In jog mode we always stop right away. - if (!this.tickReceived) { - engine.scratchTick(this.deckNumber, 0.0); - engine.scratchDisable(this.deckNumber); - this.playIndicatorHandler(0, this.activeChannel); - engine.stopTimer(this.wheelTouchInertiaTimer); - this.wheelTouchInertiaTimer = 0; + // If shift is pressed, reset right away. + if (this.shiftPressed) { + engine.setValue(this.activeChannel, "scratch2", 0.0); + this.playIndicatorHandler(0, this.activeChannel); + } } - this.tickReceived = false; }; TraktorS3.Deck.prototype.jogHandler = function(field) { - this.tickReceived = true; const deltas = this.wheelDeltas(field.value); // If shift button is held, do a simple seek. if (this.shiftPressed) { - // But if we're in the inertial period, ignore any wheel motion. - if (this.wheelTouchInertiaTimer !== 0) { - return; - } let playPosition = engine.getValue(this.activeChannel, "playposition"); playPosition += deltas[0] / 2048.0; playPosition = Math.max(Math.min(playPosition, 1.0), 0.0); From 6fde90e603bec76e46dd7ae52612ee9e4249fd38 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Fri, 9 Dec 2022 17:55:16 +0100 Subject: [PATCH 05/36] Pass ticks to the scratch function for S3 Instead of the velocity from this manual velocity calculation. --- .../Traktor-Kontrol-S3-hid-scripts.js | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 44220ff9cac3..3b5c5a7f8174 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -663,21 +663,19 @@ TraktorS3.Deck.prototype.jogHandler = function(field) { const tickDelta = deltas[0]; const timeDelta = deltas[1]; - // The scratch rate is the ratio of the wheel's speed to "regular" speed, - // which we're going to call 33.33 RPM. It's 768 ticks for a circle, and - // 400000 ticks per second, and 33.33 RPM is 1.8 seconds per rotation, so - // the standard speed is 768 / (400000 * 1.8) - const thirtyThree = 768 / 720000; - - // Our actual speed is tickDelta / timeDelta. Take the ratio of those to get the - // rate ratio. - let velocity = (tickDelta / timeDelta) / thirtyThree; - - // The Mixxx scratch code tries to do accumulation and time calculation itself. - // This controller is better, so just use its values. if (engine.isScratching(this.deckNumber)) { - engine.scratchTick(this.deckNumber, velocity); + engine.scratchTick(this.deckNumber, tickDelta); } else { + // The scratch rate is the ratio of the wheel's speed to "regular" + // speed, which we're going to call 33.33 RPM. It's 768 ticks for a + // circle, and 400000 ticks per second, and 33.33 RPM is 1.8 seconds per + // rotation, so the standard speed is 768 / (400000 * 1.8) + const thirtyThree = 768 / 720000; + + // Our actual speed is tickDelta / timeDelta. Take the ratio of those to + // get the rate ratio. + let velocity = (tickDelta / timeDelta) / thirtyThree; + // If we're playing, just nudge. if (engine.getValue(this.activeChannel, "play")) { velocity /= 4; From 72fbb67ad254b1c345cca3f30f8915dda06c4133 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Fri, 9 Dec 2022 18:50:17 +0100 Subject: [PATCH 06/36] Use the correct channel number for scratching Instead of the deck number. --- .../Traktor-Kontrol-S3-hid-scripts.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 3b5c5a7f8174..fe56e80a2637 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -176,6 +176,7 @@ TraktorS3.Deck = function(controller, deckNumber, group) { this.deckNumber = deckNumber; this.group = group; this.activeChannel = "[Channel" + deckNumber + "]"; + this.activeChannelNumber = deckNumber; // When true, touching the wheel enables scratch mode. When off, touching the wheel // has no special effect this.jogToggled = TraktorS3.JogDefaultOn; @@ -210,6 +211,7 @@ TraktorS3.Deck.prototype.activateChannel = function(channel) { return; } this.activeChannel = channel.group; + this.activeChannelNumber = channel.groupNumber; engine.softTakeoverIgnoreNextValue(this.activeChannel, "rate"); this.controller.lightDeck(this.activeChannel); }; @@ -637,9 +639,9 @@ TraktorS3.Deck.prototype.jogTouchHandler = function(field) { } if (field.value !== 0) { - engine.scratchEnable(this.deckNumber, 768, 33.33334, TraktorS3.Alpha, TraktorS3.Beta); + engine.scratchEnable(this.activeChannelNumber, 768, 33.33334, TraktorS3.Alpha, TraktorS3.Beta); } else { - engine.scratchDisable(this.deckNumber); + engine.scratchDisable(this.activeChannelNumber); // If shift is pressed, reset right away. if (this.shiftPressed) { @@ -663,8 +665,8 @@ TraktorS3.Deck.prototype.jogHandler = function(field) { const tickDelta = deltas[0]; const timeDelta = deltas[1]; - if (engine.isScratching(this.deckNumber)) { - engine.scratchTick(this.deckNumber, tickDelta); + if (engine.isScratching(this.activeChannelNumber)) { + engine.scratchTick(this.activeChannelNumber, tickDelta); } else { // The scratch rate is the ratio of the wheel's speed to "regular" // speed, which we're going to call 33.33 RPM. It's 768 ticks for a @@ -968,6 +970,8 @@ TraktorS3.Channel = function(controller, parentDeck, group) { this.controller = controller; this.parentDeck = parentDeck; this.group = group; + // We need the channel number for the scratch controls + this.groupNumber = Number(group.match(/\[Channel(\d+)\]/)[1]); this.fxEnabledState = false; this.trackDurationSec = 0; @@ -1797,8 +1801,8 @@ TraktorS3.Controller.prototype.deckSwitchHandler = function(field) { const channel = this.Channels[field.group]; const deck = channel.parentDeck; - if (engine.isScratching(deck.deckNumber)) { - engine.scratchDisable(deck.deckNumber); + if (engine.isScratching(channel.groupNumber)) { + engine.scratchDisable(channel.groupNumber); } deck.activateChannel(channel); From e5365a23307790fcbc66d2b116e402f8611be33f Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Fri, 9 Dec 2022 19:41:16 +0100 Subject: [PATCH 07/36] Remove paused jog multiplier in S3 script Mixxx already does this internally. --- res/controllers/Traktor-Kontrol-S3-hid-scripts.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index fe56e80a2637..b3aa4f295ea5 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -676,14 +676,8 @@ TraktorS3.Deck.prototype.jogHandler = function(field) { // Our actual speed is tickDelta / timeDelta. Take the ratio of those to // get the rate ratio. - let velocity = (tickDelta / timeDelta) / thirtyThree; + const velocity = (tickDelta / timeDelta) / thirtyThree; - // If we're playing, just nudge. - if (engine.getValue(this.activeChannel, "play")) { - velocity /= 4; - } else { - velocity *= 2; - } engine.setValue(this.activeChannel, "jog", velocity); } }; From 9260f4f98748ec0a6d627eb9a3b8b07986cd9252 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sun, 11 Dec 2022 16:56:21 +0100 Subject: [PATCH 08/36] Allow initializing beatjump and beatloop sizes --- res/controllers/Traktor-Kontrol-S3-hid-scripts.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index b3aa4f295ea5..26b882598bc5 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -64,6 +64,11 @@ TraktorS3.LEDBrightValue = 0x02; TraktorS3.Alpha = 1.0 / 8; TraktorS3.Beta = TraktorS3.Alpha / 32; +// These options can be set to non-null values to initialize the beat jump and +// loop sizes for all four decks when the controller is connected +TraktorS3.DefaultBeatJumpSize = null; // 32 +TraktorS3.DefaultBeatLoopLength = null; // 32 + // Set to true to output debug messages and debug light outputs. TraktorS3.DebugMode = false; @@ -980,6 +985,16 @@ TraktorS3.Channel = function(controller, parentDeck, group) { this.vuConnection = {}; this.clipConnection = {}; this.hotcueCallbacks = []; + + // The script by default doesn't change any of the deck's settings, but it's + // useful to be able to initialize these settings to your preferences when + // you turn on the controller + if (TraktorS3.DefaultBeatJumpSize !== null) { + engine.setValue(group, "beatjump_size", TraktorS3.DefaultBeatJumpSize); + } + if (TraktorS3.DefaultBeatLoopLength !== null) { + engine.setValue(group, "beatloop_size", TraktorS3.DefaultBeatLoopLength); + } }; // Finds the shortest distance between two angles on the wheel, assuming From 928d4122632c4123565da4fc7fe044a6a45ab806 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sat, 17 Dec 2022 21:43:14 +0100 Subject: [PATCH 09/36] Allow initializing sync, quantize, keylock for S3 --- res/controllers/Traktor-Kontrol-S3-hid-scripts.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 26b882598bc5..c60015743921 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -65,9 +65,13 @@ TraktorS3.Alpha = 1.0 / 8; TraktorS3.Beta = TraktorS3.Alpha / 32; // These options can be set to non-null values to initialize the beat jump and -// loop sizes for all four decks when the controller is connected +// loop sizes and sync and quantize states for all four decks when the +// controller is connected TraktorS3.DefaultBeatJumpSize = null; // 32 TraktorS3.DefaultBeatLoopLength = null; // 32 +TraktorS3.DefaultSyncEnabled = null; // true +TraktorS3.DefaultQuantizeEnabled = null; // true +TraktorS3.DefaultKeylockEnabled = null; // true // Set to true to output debug messages and debug light outputs. TraktorS3.DebugMode = false; @@ -995,6 +999,15 @@ TraktorS3.Channel = function(controller, parentDeck, group) { if (TraktorS3.DefaultBeatLoopLength !== null) { engine.setValue(group, "beatloop_size", TraktorS3.DefaultBeatLoopLength); } + if (TraktorS3.DefaultSyncEnabled !== null) { + engine.setValue(group, "sync_enabled", TraktorS3.DefaultSyncEnabled); + } + if (TraktorS3.DefaultQuantizeEnabled !== null) { + engine.setValue(group, "quantize", TraktorS3.DefaultQuantizeEnabled); + } + if (TraktorS3.DefaultKeylockEnabled !== null) { + engine.setValue(group, "keylock", TraktorS3.DefaultKeylockEnabled); + } }; // Finds the shortest distance between two angles on the wheel, assuming From 28eab2d73303f4979ae5da6a274b686eb7d880dc Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sun, 18 Dec 2022 01:47:31 +0100 Subject: [PATCH 10/36] Query current knob and slider values from the S3 This makes it so Mixxx's initial state matches the S3. --- .../Traktor-Kontrol-S3-hid-scripts.js | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index c60015743921..e3f687bb2393 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -1688,7 +1688,27 @@ TraktorS3.Controller.prototype.registerInputPackets = function() { this.hid.registerInputPacket(messageLong); - // Soft takeovers + for (ch in this.Channels) { + const chanob = this.Channels[ch]; + engine.makeConnection(ch, "playposition", + TraktorS3.bind(TraktorS3.Channel.prototype.playpositionChanged, chanob)); + engine.connectControl(ch, "track_loaded", + TraktorS3.bind(TraktorS3.Channel.prototype.trackLoadedHandler, chanob)); + engine.connectControl(ch, "end_of_track", + TraktorS3.bind(TraktorS3.Channel.prototype.endOfTrackHandler, chanob)); + } + + // Dirty hack to set initial values in the packet parser. The packet parser + // only sends updates to this controller script if they have changed from + // their previous value, and it will ignore the initial value value. + TraktorS3.incomingData([1, ...Array(19).fill(0)]); + TraktorS3.incomingData([2, ...Array(62).fill(0)]); + + // Query the current values from the controller and set them + TraktorS3.incomingData([2, ...Array.from(new Uint8Array(controller.getInputReport(2)))]); + + // NOTE: Soft takeovers must only be enabled after setting the initial + // value, or the above line won't have any effect for (var ch = 1; ch <= 4; ch++) { var group = "[Channel" + ch + "]"; if (!TraktorS3.PitchSliderRelativeMode) { @@ -1738,20 +1758,6 @@ TraktorS3.Controller.prototype.registerInputPackets = function() { for (let i = 1; i <= 16; ++i) { engine.softTakeover("[Sampler" + i + "]", "pregain", true); } - - for (ch in this.Channels) { - const chanob = this.Channels[ch]; - engine.makeConnection(ch, "playposition", - TraktorS3.bind(TraktorS3.Channel.prototype.playpositionChanged, chanob)); - engine.connectControl(ch, "track_loaded", - TraktorS3.bind(TraktorS3.Channel.prototype.trackLoadedHandler, chanob)); - engine.connectControl(ch, "end_of_track", - TraktorS3.bind(TraktorS3.Channel.prototype.endOfTrackHandler, chanob)); - } - - // Dirty hack to set initial values in the packet parser - const data = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - TraktorS3.incomingData(data); }; TraktorS3.Controller.prototype.registerInputJog = function(message, group, name, offset, bitmask, callback) { From e22188584c436be69e03a69d8dd2d6427c9f7964 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sun, 18 Dec 2022 14:08:27 +0100 Subject: [PATCH 11/36] Initialize packet parser with report data for S3 This makes sure no values can be lost. --- .../Traktor-Kontrol-S3-hid-scripts.js | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index e3f687bb2393..82ff116d7146 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -1698,14 +1698,20 @@ TraktorS3.Controller.prototype.registerInputPackets = function() { TraktorS3.bind(TraktorS3.Channel.prototype.endOfTrackHandler, chanob)); } - // Dirty hack to set initial values in the packet parser. The packet parser - // only sends updates to this controller script if they have changed from - // their previous value, and it will ignore the initial value value. - TraktorS3.incomingData([1, ...Array(19).fill(0)]); - TraktorS3.incomingData([2, ...Array(62).fill(0)]); - - // Query the current values from the controller and set them - TraktorS3.incomingData([2, ...Array.from(new Uint8Array(controller.getInputReport(2)))]); + // Query the current values from the controller and set them. The packet + // parser ignores the first time a value is set, so we'll need to set it + // with different values once. Report 2 contains the state of the mixer + // controls. + const report2Values = new Uint8Array(controller.getInputReport(2)); + TraktorS3.incomingData([2, ...Array.from(report2Values.map(x => ~x))]); + TraktorS3.incomingData([2, ...Array.from(report2Values)]); + + // Report 1 is the state of the deck controls. These shouldn't have any + // initial effect, and most of these values will be 0 anyways. We'll just + // tell the packet parser the current values so it won't ignore the next + // input. + const report1Values = new Uint8Array(controller.getInputReport(1)); + TraktorS3.incomingData([1, ...Array.from(report1Values)]); // NOTE: Soft takeovers must only be enabled after setting the initial // value, or the above line won't have any effect From a25de9ec36a90028a7605d550a25c39f3f21ba03 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 19 Dec 2022 21:28:40 +0100 Subject: [PATCH 12/36] Add most of a Traktor-like alternative S3 FX impl This behaves as intended by Native Instruments. Only the input parts have been implemented, the LEDs still need to be done. --- .../Traktor-Kontrol-S3-hid-scripts.js | 376 +++++++++++++++++- 1 file changed, 374 insertions(+), 2 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 82ff116d7146..e002207b902d 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -15,6 +15,38 @@ var TraktorS3 = {}; // ==== Friendly User Configuration ==== +// This controller script has two modes for controlling FX: +// +// - A mode where the Filter and FX 1-4 buttons switch between the first five +// quick effect chain presets found in Settings -> Effects, and the FX Enable +// buttons toggle the enabled/bypass status of those quick effect chains. This +// emulates the intended behavior of the Mixer FX section on the Traktor +// Kontrol S3. The Filter button is bound to the first preset in the list, and +// the FX 1-4 buttons are bound to presets 2-5. For consistency you should +// make sure that the default quick effect chain in Settings -> Equalizers is +// set to the first chain so that pressing the Filter button returns you to +// the default quick effect chain. The super knob's value is preserved when +// changing between quick effect chain presets. +// +// The intended use case is to set quick effect chain preset 0 to either the +// Filter or Moog Filter effect, and presets 1-5 to a chain that contains that +// same filter and an additional effect. +// +// Pressing one of the five FX/Filter buttons switches every channel's quick +// effect chain to the corresponding preset. Holding one of the buttons down +// while pressing one of the four channel's Filter Enable buttons will assign +// the chain to just that channel. When the Filter Enable button is not +// enabled then channel will behave the same as if the first Filter quick +// effect chain preset was used. +// +// - Another mode that exposes all of Mixxx's effect controls at the expense of +// being more complex to use. See the manual for the full key binding scheme. +// +// The first mode is dubbed 'stock mode' as it behaves in the way the mixer FX +// section was originally intended to be used by Native Instruments. Disable +// this option to use the second, Mixxx-specific mode. +TraktorS3.StockFxMode = true; + // The pitch slider can operate either in absolute or relative mode. // In absolute mode: // * Moving the pitch slider works like normal @@ -2296,6 +2328,337 @@ TraktorS3.shutdown = function() { HIDDebug("TraktorS3: Shutdown done!"); }; +/** + * An alternative to `FXControl` that behaves more similarly to how the + * controller works with Traktor. See the description for + * `TraktorS3.StockFxMode` for more information. + * + * TODO: Return to quick effect chain 1 on shutdown, and probably do the same + * thing on init. + * TODO: Link the outputs for when the effect chain is changed from the software. + */ +TraktorS3.StockFxControl = class { + constructor(controller) { + this.controller = controller; + + // This contains the indices of the currently held down Filter and FX + // select buttons, 0 being the filter and 1-4 being the four FX buttons. + // We keep track of whether they're currently held down so we can assign + // an effect chain to a single channel by holding down one of those five + // buttons and then pressing the channel's FX On button. + this.pressedFxSelectButtons = []; + // When one of the FX select buttons is held down we need to keep track + // of whether or not we assigned any quick effect chains. If this + // happened, then we should avoid changing the other channel's quick + // effect chains when the button is released. This is reset to false + // when the last FX Select button is released. + this.individualFxChainAssigned = false; + + // When non-null, the channel's super knob value will be set to this + // value when the quick effect chain has changed. This value is stored + // just before changing the quick effect chain because there's no built + // in way to preserve the value. + this.oldSuperKnobValues = [null, null, null, null]; + } + + registerInputs(messageShort, messageLong) { + // The filter button + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx0", 0x08, 0x80, this.fxSelectHandler.bind(this)); + // The FX 1-4 buttons + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx1", 0x08, 0x08, this.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx2", 0x08, 0x10, this.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx3", 0x08, 0x20, this.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx4", 0x08, 0x40, this.fxSelectHandler.bind(this)); + + this.controller.registerInputScaler(messageLong, "[Channel1]", "!fxKnob", 0x39, 0xFFFF, this.fxKnobHandler.bind(this)); + this.controller.registerInputScaler(messageLong, "[Channel2]", "!fxKnob", 0x3B, 0xFFFF, this.fxKnobHandler.bind(this)); + this.controller.registerInputScaler(messageLong, "[Channel3]", "!fxKnob", 0x37, 0xFFFF, this.fxKnobHandler.bind(this)); + this.controller.registerInputScaler(messageLong, "[Channel4]", "!fxKnob", 0x3D, 0xFFFF, this.fxKnobHandler.bind(this)); + + this.controller.registerInputButton(messageShort, "[Channel3]", "!fxEnabled", 0x07, 0x08, this.fxEnableHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[Channel1]", "!fxEnabled", 0x07, 0x10, this.fxEnableHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[Channel2]", "!fxEnabled", 0x07, 0x20, this.fxEnableHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[Channel4]", "!fxEnabled", 0x07, 0x40, this.fxEnableHandler.bind(this)); + + // We'll restore the old super knob values here when changing quick effect chains + engine.connectControl("[QuickEffectRack1_[Channel1]]", "loaded_chain_preset", this.quickEffectChainLoadHandler.bind(this)); + engine.connectControl("[QuickEffectRack1_[Channel2]]", "loaded_chain_preset", this.quickEffectChainLoadHandler.bind(this)); + engine.connectControl("[QuickEffectRack1_[Channel3]]", "loaded_chain_preset", this.quickEffectChainLoadHandler.bind(this)); + engine.connectControl("[QuickEffectRack1_[Channel4]]", "loaded_chain_preset", this.quickEffectChainLoadHandler.bind(this)); + } + + fxSelectHandler(field) { + // FX Number 0 is the filter, and 1-4 are the FX 1-4 buttons + const fxNumber = parseInt(field.name[field.name.length - 1]); + + // If one of the four FX On buttons is pressed while one of the five FX + // select buttons are held down, then only that channel's quick effect + // chain assignments are changed. `this.individualFxChainAssigned` keeps + // track of whether a quick effects chain has been assigned to an + // individual channel. To avoid weird things from happening, we keep + // track of which buttons are pressed. The global effect chain should + // only change when the last button has been released, and holding down + // one FX button, assigning that effect to a channel, holding down a + // second button, and releasing both shouldn't change the global effect + // assignments. + if (field.value === 1) { + this.pressedFxSelectButtons.push(fxNumber); + if (this.pressedFxSelectButtons.length === 1) { + this.individualFxChainAssigned = false; + } + } else { + this.pressedFxSelectButtons.splice(this.pressedFxSelectButtons.indexOf(fxNumber)); + if (!this.individualFxChainAssigned) { + for (let channel = 1; channel <= 4; channel++) { + this.setQuickEffectChain(channel, fxNumber); + } + } + } + } + + fxKnobHandler(field) { + // If the external input toggle is enabled then we should not change + // channel four's filters as the other mixer controls now also affect + // the microphone rather than channel four + if (field.group === "[Channel4]" && this.controller.channel4InputMode) { + return; + } + + const value = field.value / 4095; + engine.setParameter(`[QuickEffectRack1_${field.group}]`, "super1", value); + } + + fxEnableHandler(field) { + if (field.value === 0) { + return; + } + + // Depending on whether or not one of the five FX+Filter buttons are + // pressed we'll either assign a quick effects chain to this channel or + // we'll toggle the quick effect chain's status + if (this.pressedFxSelectButtons.length > 0) { + // We'll use the last button pressed + const fxNumber = this.pressedFxSelectButtons[this.pressedFxSelectButtons.length - 1]; + const channelNumber = parseInt(field.group[field.group.length - 2]); + + this.setQuickEffectChain(channelNumber, fxNumber); + + // This will prevent releasing the button(s) from affecting the + // other channels + this.individualFxChainAssigned = true; + } else { + const currentStatus = engine.getValue(`[QuickEffectRack1_${field.group}]`, "enabled"); + engine.setValue(`[QuickEffectRack1_${field.group}]`, "enabled", !currentStatus); + } + } + + quickEffectChainLoadHandler(_value, group, _key) { + // There's no built in way to change effect chains while preserving the + // super knob values, so we'll do this ourselves. The old super knob + // value is set in `this.setQuickEffectChain()`. + // FIXME: There needs to be a way in Mixxx to change the quick effect + // chain without changing this value. You can hear the default + // value poking through when changing to the same effect. + const channelNumber = parseInt(group[group.length - 3]); + if (this.oldSuperKnobValues[channelNumber - 1] !== null) { + engine.softTakeover(group, "super1", false); + engine.setValue(group, "super1", this.oldSuperKnobValues[channelNumber - 1]); + engine.softTakeover(group, "super1", true); + + this.oldSuperKnobValues[channelNumber - 1] = null; + } + } + + /** + * Set the quick effect chain preset for a channel. This preserves the current super knob value + * + * @param {number} channel The channel number, in 1-4. + * @param {number} fxButtonIndex The index of the FX button, 0 being the + * filter button. Quick effect chain presets are one-indexed, so + * fxButtonIndex 0-5 will be mapped to quick effect chain presets 1-6. + */ + setQuickEffectChain(channel, fxButtonIndex) { + // We can't immediately restore this value because it may take a buffer + // until the effect chain is actually changed. So instead we'll store + // the old value, and then restore it in + // `this.quickEffectChainLoadHandler()`. + const superValue = engine.getValue(`[QuickEffectRack1_[Channel${channel}]]`, "super1"); + this.oldSuperKnobValues[channel - 1] = superValue; + + engine.setValue(`[QuickEffectRack1_[Channel${channel}]]`, "loaded_chain_preset", fxButtonIndex + 1); + } + + // TODO: Link the LEDs for the FX select and enable buttons + + // getFXSelectLEDValue(fxNumber, status) { + // const ledValue = this.controller.fxLEDValue[fxNumber]; + // switch (status) { + // case this.LIGHT_OFF: + // return 0x00; + // case this.LIGHT_DIM: + // return ledValue; + // case this.LIGHT_BRIGHT: + // return ledValue + 0x02; + // } + // } + // getChannelColor(group, status) { + // const ledValue = this.controller.hid.LEDColors[TraktorS3.ChannelColors[group]]; + // switch (status) { + // case this.LIGHT_OFF: + // return 0x00; + // case this.LIGHT_DIM: + // return ledValue; + // case this.LIGHT_BRIGHT: + // return ledValue + 0x02; + // } + // } + // lightFX() { + // this.controller.batchingOutputs = true; + + // // Loop through select buttons + // // Idx zero is filter button + // for (let idx = 0; idx < 5; idx++) { + // this.lightSelect(idx); + // } + // for (let ch = 1; ch <= 4; ch++) { + // const channel = "[Channel" + ch + "]"; + // this.lightEnable(channel); + // } + + // this.controller.batchingOutputs = false; + // for (const packetName in this.controller.hid.OutputPackets) { + // this.controller.hid.OutputPackets[packetName].send(); + // } + // } + // lightSelect(idx) { + // let status = this.LIGHT_OFF; + // let ledValue = 0x00; + // switch (this.currentState) { + // case this.STATE_FILTER: + // // Always light when pressed + // if (this.selectPressed[idx]) { + // status = this.LIGHT_BRIGHT; + // } else { + // // select buttons on if fx unit enabled for the pressed channel, + // // otherwise disabled. + // status = this.LIGHT_DIM; + // const pressed = this.firstPressedEnable(); + // if (pressed) { + // if (idx === 0) { + // var fxGroup = "[QuickEffectRack1_" + pressed + "_Effect1]"; + // var fxKey = "enabled"; + // } else { + // fxGroup = "[EffectRack1_EffectUnit" + idx + "]"; + // fxKey = "group_" + pressed + "_enable"; + // } + // if (engine.getParameter(fxGroup, fxKey)) { + // status = this.LIGHT_BRIGHT; + // } else { + // status = this.LIGHT_OFF; + // } + // } + // ledValue = this.getFXSelectLEDValue(idx, status); + // } + // break; + // case this.STATE_EFFECT_INIT: + // // Fallthrough intended + // case this.STATE_EFFECT: + // // Highlight if pressed, disable if active effect. + // // Otherwise off. + // if (this.selectPressed[idx]) { + // status = this.LIGHT_BRIGHT; + // } else if (idx === this.activeFX) { + // status = this.LIGHT_BRIGHT; + // } + // break; + // case this.STATE_FOCUS: + // // if blink state is false, only like active fx bright + // // if blink state is true, active fx is bright and selected effect + // // is dim. if those are the same, active fx is dim + // if (this.selectPressed[idx]) { + // status = this.LIGHT_BRIGHT; + // } else { + // if (idx === this.activeFX) { + // status = this.LIGHT_BRIGHT; + // } + // if (this.focusBlinkState) { + // const fxGroupPrefix = "[EffectRack1_EffectUnit" + this.activeFX; + // const focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); + // if (idx === focusedEffect) { + // status = this.LIGHT_DIM; + // } + // } + // } + // break; + // } + // ledValue = this.getFXSelectLEDValue(idx, status); + // this.controller.hid.setOutput("[ChannelX]", "!fxButton" + idx, ledValue, false); + // } + // lightEnable(channel) { + // let status = this.LIGHT_OFF; + // let ledValue = 0x00; + // const buttonNumber = this.channelToIndex(channel); + // switch (this.currentState) { + // case this.STATE_FILTER: + // // enable buttons highlighted if pressed or if any fx unit enabled for channel. + // // Highlight if pressed. + // status = this.LIGHT_DIM; + // if (this.enablePressed[channel]) { + // status = this.LIGHT_BRIGHT; + // } else { + // for (let idx = 1; idx <= 4 && status === this.LIGHT_OFF; idx++) { + // var group = "[EffectRack1_EffectUnit" + idx + "]"; + // var key = "group_" + channel + "_enable"; + // if (engine.getParameter(group, key)) { + // status = this.LIGHT_DIM; + // } + // } + // } + // // Enable buttons have regular deck colors + // ledValue = this.getChannelColor(channel, status); + // break; + // case this.STATE_EFFECT_INIT: + // // Fallthrough intended + // case this.STATE_EFFECT: + // if (this.enablePressed[channel]) { + // status = this.LIGHT_BRIGHT; + // } else { + // // off if nothing loaded, dim if loaded, bright if enabled. + // group = "[EffectRack1_EffectUnit" + this.activeFX + "_Effect" + buttonNumber + "]"; + // if (engine.getParameter(group, "loaded")) { + // status = this.LIGHT_DIM; + // } + // if (engine.getParameter(group, "enabled")) { + // status = this.LIGHT_BRIGHT; + // } + // } + // // Colors match effect colors so it's obvious we're in a different mode + // ledValue = this.getFXSelectLEDValue(this.activeFX, status); + // break; + // case this.STATE_FOCUS: + // if (this.enablePressed[channel]) { + // status = this.LIGHT_BRIGHT; + // } else { + // const fxGroupPrefix = "[EffectRack1_EffectUnit" + this.activeFX; + // const focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); + // group = fxGroupPrefix + "_Effect" + focusedEffect + "]"; + // key = "button_parameter" + buttonNumber; + // // Off if not loaded, dim if loaded, bright if enabled. + // if (engine.getParameter(group, key + "_loaded")) { + // status = this.LIGHT_DIM; + // } + // if (engine.getParameter(group, key)) { + // status = this.LIGHT_BRIGHT; + // } + // } + // // Colors match effect colors so it's obvious we're in a different mode + // ledValue = this.getFXSelectLEDValue(this.activeFX, status); + // break; + // } + // this.controller.hid.setOutput(channel, "!fxEnabled", ledValue, false); + // } +}; + TraktorS3.init = function(_id) { this.kontrol = new TraktorS3.Controller(); this.kontrol.Decks = { @@ -2310,7 +2673,11 @@ TraktorS3.init = function(_id) { "[Channel2]": new TraktorS3.Channel(this.kontrol, this.kontrol.Decks.deck2, "[Channel2]"), }; - this.kontrol.fxController = new TraktorS3.FXControl(this.kontrol); + if (TraktorS3.StockFxMode) { + this.kontrol.fxController = new TraktorS3.StockFxControl(this.kontrol); + } else { + this.kontrol.fxController = new TraktorS3.FXControl(this.kontrol); + } this.kontrol.registerInputPackets(); this.kontrol.registerOutputPackets(); @@ -2325,7 +2692,12 @@ TraktorS3.init = function(_id) { this.kontrol.lightDeck("[Channel4]", false); this.kontrol.lightDeck("[Channel1]", false); this.kontrol.lightDeck("[Channel2]", true); - this.kontrol.fxController.lightFX(); + + if (TraktorS3.StockFxMode) { + // FIXME: Either manually light things or have output links figure it out + } else { + this.kontrol.fxController.lightFX(); + } } this.kontrol.setInputLineMode(TraktorS3.inputModeLine); From 6571c7591d131f2a35069bdbbe531f091841cc6a Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 19 Dec 2022 21:34:08 +0100 Subject: [PATCH 13/36] Add an option to initialize S3 FX chains to filter This makes more sense to me than keeping whatever was set last time. --- res/controllers/Traktor-Kontrol-S3-hid-scripts.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index e002207b902d..2e35d5c6c4d4 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -46,6 +46,9 @@ var TraktorS3 = {}; // section was originally intended to be used by Native Instruments. Disable // this option to use the second, Mixxx-specific mode. TraktorS3.StockFxMode = true; +// When enabled, set all channels to the first FX chain on startup. Otherwise +// the quick FX chain assignments from the last Mixxx run are preserved. +TraktorS3.StockFxModeDefaultToFilter = true; // The pitch slider can operate either in absolute or relative mode. // In absolute mode: @@ -2359,6 +2362,12 @@ TraktorS3.StockFxControl = class { // just before changing the quick effect chain because there's no built // in way to preserve the value. this.oldSuperKnobValues = [null, null, null, null]; + + if (TraktorS3.StockFxModeDefaultToFilter) { + for (let channel = 1; channel <= 4; channel++) { + this.setQuickEffectChain(channel, 0); + } + } } registerInputs(messageShort, messageLong) { From d7c4f29e7e42f0c51faf040744c9bde1e384060b Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 19 Dec 2022 23:39:50 +0100 Subject: [PATCH 14/36] Add LED output for the new S3 FX behavior This should now be pretty much done. --- .../Traktor-Kontrol-S3-hid-scripts.js | 265 ++++++------------ 1 file changed, 90 insertions(+), 175 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 2e35d5c6c4d4..51a23dca899d 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -2335,10 +2335,6 @@ TraktorS3.shutdown = function() { * An alternative to `FXControl` that behaves more similarly to how the * controller works with Traktor. See the description for * `TraktorS3.StockFxMode` for more information. - * - * TODO: Return to quick effect chain 1 on shutdown, and probably do the same - * thing on init. - * TODO: Link the outputs for when the effect chain is changed from the software. */ TraktorS3.StockFxControl = class { constructor(controller) { @@ -2363,6 +2359,16 @@ TraktorS3.StockFxControl = class { // in way to preserve the value. this.oldSuperKnobValues = [null, null, null, null]; + // These are the colors for each FX button, with 0 being the filter + // button + this.fxColors = { + 0: this.controller.hid.LEDColors.ORANGE, + 1: this.controller.hid.LEDColors.RED, + 2: this.controller.hid.LEDColors.GREEN, + 3: this.controller.hid.LEDColors.CELESTE, + 4: this.controller.hid.LEDColors.YELLOW, + }; + if (TraktorS3.StockFxModeDefaultToFilter) { for (let channel = 1; channel <= 4; channel++) { this.setQuickEffectChain(channel, 0); @@ -2389,13 +2395,24 @@ TraktorS3.StockFxControl = class { this.controller.registerInputButton(messageShort, "[Channel2]", "!fxEnabled", 0x07, 0x20, this.fxEnableHandler.bind(this)); this.controller.registerInputButton(messageShort, "[Channel4]", "!fxEnabled", 0x07, 0x40, this.fxEnableHandler.bind(this)); - // We'll restore the old super knob values here when changing quick effect chains + // We'll restore the old super knob values here when changing quick + // effect chains. This also changes the lighting of the five FX Select + // buttons and maybe also the FX Enable buttons. engine.connectControl("[QuickEffectRack1_[Channel1]]", "loaded_chain_preset", this.quickEffectChainLoadHandler.bind(this)); engine.connectControl("[QuickEffectRack1_[Channel2]]", "loaded_chain_preset", this.quickEffectChainLoadHandler.bind(this)); engine.connectControl("[QuickEffectRack1_[Channel3]]", "loaded_chain_preset", this.quickEffectChainLoadHandler.bind(this)); engine.connectControl("[QuickEffectRack1_[Channel4]]", "loaded_chain_preset", this.quickEffectChainLoadHandler.bind(this)); + + // The FX enable buttons can directly be bound to the quick effect chain + // enabled status as their lighting doesn't depend on other factors + engine.connectControl("[QuickEffectRack1_[Channel1]]", "enabled", value => this.lightFxEnable(1, value === 1)); + engine.connectControl("[QuickEffectRack1_[Channel2]]", "enabled", value => this.lightFxEnable(2, value === 1)); + engine.connectControl("[QuickEffectRack1_[Channel3]]", "enabled", value => this.lightFxEnable(3, value === 1)); + engine.connectControl("[QuickEffectRack1_[Channel4]]", "enabled", value => this.lightFxEnable(4, value === 1)); } + // Input handling + fxSelectHandler(field) { // FX Number 0 is the filter, and 1-4 are the FX 1-4 buttons const fxNumber = parseInt(field.name[field.name.length - 1]); @@ -2476,6 +2493,9 @@ TraktorS3.StockFxControl = class { this.oldSuperKnobValues[channelNumber - 1] = null; } + + // All of the lights should be updated at this point + this.lightFx(); } /** @@ -2497,175 +2517,69 @@ TraktorS3.StockFxControl = class { engine.setValue(`[QuickEffectRack1_[Channel${channel}]]`, "loaded_chain_preset", fxButtonIndex + 1); } - // TODO: Link the LEDs for the FX select and enable buttons - - // getFXSelectLEDValue(fxNumber, status) { - // const ledValue = this.controller.fxLEDValue[fxNumber]; - // switch (status) { - // case this.LIGHT_OFF: - // return 0x00; - // case this.LIGHT_DIM: - // return ledValue; - // case this.LIGHT_BRIGHT: - // return ledValue + 0x02; - // } - // } - // getChannelColor(group, status) { - // const ledValue = this.controller.hid.LEDColors[TraktorS3.ChannelColors[group]]; - // switch (status) { - // case this.LIGHT_OFF: - // return 0x00; - // case this.LIGHT_DIM: - // return ledValue; - // case this.LIGHT_BRIGHT: - // return ledValue + 0x02; - // } - // } - // lightFX() { - // this.controller.batchingOutputs = true; - - // // Loop through select buttons - // // Idx zero is filter button - // for (let idx = 0; idx < 5; idx++) { - // this.lightSelect(idx); - // } - // for (let ch = 1; ch <= 4; ch++) { - // const channel = "[Channel" + ch + "]"; - // this.lightEnable(channel); - // } - - // this.controller.batchingOutputs = false; - // for (const packetName in this.controller.hid.OutputPackets) { - // this.controller.hid.OutputPackets[packetName].send(); - // } - // } - // lightSelect(idx) { - // let status = this.LIGHT_OFF; - // let ledValue = 0x00; - // switch (this.currentState) { - // case this.STATE_FILTER: - // // Always light when pressed - // if (this.selectPressed[idx]) { - // status = this.LIGHT_BRIGHT; - // } else { - // // select buttons on if fx unit enabled for the pressed channel, - // // otherwise disabled. - // status = this.LIGHT_DIM; - // const pressed = this.firstPressedEnable(); - // if (pressed) { - // if (idx === 0) { - // var fxGroup = "[QuickEffectRack1_" + pressed + "_Effect1]"; - // var fxKey = "enabled"; - // } else { - // fxGroup = "[EffectRack1_EffectUnit" + idx + "]"; - // fxKey = "group_" + pressed + "_enable"; - // } - // if (engine.getParameter(fxGroup, fxKey)) { - // status = this.LIGHT_BRIGHT; - // } else { - // status = this.LIGHT_OFF; - // } - // } - // ledValue = this.getFXSelectLEDValue(idx, status); - // } - // break; - // case this.STATE_EFFECT_INIT: - // // Fallthrough intended - // case this.STATE_EFFECT: - // // Highlight if pressed, disable if active effect. - // // Otherwise off. - // if (this.selectPressed[idx]) { - // status = this.LIGHT_BRIGHT; - // } else if (idx === this.activeFX) { - // status = this.LIGHT_BRIGHT; - // } - // break; - // case this.STATE_FOCUS: - // // if blink state is false, only like active fx bright - // // if blink state is true, active fx is bright and selected effect - // // is dim. if those are the same, active fx is dim - // if (this.selectPressed[idx]) { - // status = this.LIGHT_BRIGHT; - // } else { - // if (idx === this.activeFX) { - // status = this.LIGHT_BRIGHT; - // } - // if (this.focusBlinkState) { - // const fxGroupPrefix = "[EffectRack1_EffectUnit" + this.activeFX; - // const focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); - // if (idx === focusedEffect) { - // status = this.LIGHT_DIM; - // } - // } - // } - // break; - // } - // ledValue = this.getFXSelectLEDValue(idx, status); - // this.controller.hid.setOutput("[ChannelX]", "!fxButton" + idx, ledValue, false); - // } - // lightEnable(channel) { - // let status = this.LIGHT_OFF; - // let ledValue = 0x00; - // const buttonNumber = this.channelToIndex(channel); - // switch (this.currentState) { - // case this.STATE_FILTER: - // // enable buttons highlighted if pressed or if any fx unit enabled for channel. - // // Highlight if pressed. - // status = this.LIGHT_DIM; - // if (this.enablePressed[channel]) { - // status = this.LIGHT_BRIGHT; - // } else { - // for (let idx = 1; idx <= 4 && status === this.LIGHT_OFF; idx++) { - // var group = "[EffectRack1_EffectUnit" + idx + "]"; - // var key = "group_" + channel + "_enable"; - // if (engine.getParameter(group, key)) { - // status = this.LIGHT_DIM; - // } - // } - // } - // // Enable buttons have regular deck colors - // ledValue = this.getChannelColor(channel, status); - // break; - // case this.STATE_EFFECT_INIT: - // // Fallthrough intended - // case this.STATE_EFFECT: - // if (this.enablePressed[channel]) { - // status = this.LIGHT_BRIGHT; - // } else { - // // off if nothing loaded, dim if loaded, bright if enabled. - // group = "[EffectRack1_EffectUnit" + this.activeFX + "_Effect" + buttonNumber + "]"; - // if (engine.getParameter(group, "loaded")) { - // status = this.LIGHT_DIM; - // } - // if (engine.getParameter(group, "enabled")) { - // status = this.LIGHT_BRIGHT; - // } - // } - // // Colors match effect colors so it's obvious we're in a different mode - // ledValue = this.getFXSelectLEDValue(this.activeFX, status); - // break; - // case this.STATE_FOCUS: - // if (this.enablePressed[channel]) { - // status = this.LIGHT_BRIGHT; - // } else { - // const fxGroupPrefix = "[EffectRack1_EffectUnit" + this.activeFX; - // const focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); - // group = fxGroupPrefix + "_Effect" + focusedEffect + "]"; - // key = "button_parameter" + buttonNumber; - // // Off if not loaded, dim if loaded, bright if enabled. - // if (engine.getParameter(group, key + "_loaded")) { - // status = this.LIGHT_DIM; - // } - // if (engine.getParameter(group, key)) { - // status = this.LIGHT_BRIGHT; - // } - // } - // // Colors match effect colors so it's obvious we're in a different mode - // ledValue = this.getFXSelectLEDValue(this.activeFX, status); - // break; - // } - // this.controller.hid.setOutput(channel, "!fxEnabled", ledValue, false); - // } + // Output handling + + lightFx() { + this.controller.batchingOutputs = true; + + this.lightFxSelectButtons(); + for (let channel = 1; channel <= 4; channel++) { + this.lightFxEnable(channel); + } + + this.controller.batchingOutputs = false; + for (const packetName in this.controller.hid.OutputPackets) { + this.controller.hid.OutputPackets[packetName].send(); + } + } + + lightFxSelectButtons() { + // We'll light up the currently active FX chains. This means only a + // single button is lit when all channels use the same quick effect + // chain. + const activeFxSelectButtons = new Set(); + for (let channel = 1; channel <= 4; channel++) { + const fxNumber = engine.getValue(`[QuickEffectRack1_[Channel${channel}]]`, "loaded_chain_preset") - 1; + activeFxSelectButtons.add(fxNumber); + } + + for (let fxNumber = 0; fxNumber <= 4; fxNumber++) { + // By default the LED is off + const ledColor = this.fxColors[fxNumber] + + ((activeFxSelectButtons.has(fxNumber) || this.pressedFxSelectButtons.indexOf(fxNumber) !== -1) + ? TraktorS3.LEDBrightValue + : TraktorS3.LEDDimValue); + + this.controller.hid.setOutput("[ChannelX]", `!fxButton${fxNumber}`, ledColor, !this.controller.batchingOutputs); + } + } + + lightFxEnable(channelNumber, enabled = null) { + const channelGroup = `[Channel${channelNumber}]`; + const quickEffectChainGroup = `[QuickEffectRack1_${channelGroup}]`; + + const fxNumber = engine.getValue(quickEffectChainGroup, "loaded_chain_preset") - 1; + // We don't need to query this from the engine when this is called as + // part of a connection + const fxEnabled = (enabled !== undefined && enabled !== null) + ? enabled + : engine.getValue(quickEffectChainGroup, "enabled") === 1; + + // With the filter/FX number 0 we'll use the channel's color for the FX + // On LED. Otherwise we'll show the color of the effect. This deviates + // from the behavior in Traktor, but I think using the deck-colors makes + // it much easier to remember which channel is active when switching + // between channels. We'll also fall back to the channel colors if the + // user manually selected an out of bounds chain + let ledColor = fxEnabled ? TraktorS3.LEDBrightValue : TraktorS3.LEDDimValue; + if (fxNumber >= 1 && fxNumber <= 5) { + ledColor += this.fxColors[fxNumber]; + } else { + ledColor += this.controller.hid.LEDColors[TraktorS3.ChannelColors[channelGroup]]; + } + + this.controller.hid.setOutput(channelGroup, "!fxEnabled", ledColor, !this.controller.batchingOutputs); + } }; TraktorS3.init = function(_id) { @@ -2702,8 +2616,9 @@ TraktorS3.init = function(_id) { this.kontrol.lightDeck("[Channel1]", false); this.kontrol.lightDeck("[Channel2]", true); + // TODO: Fix capitalization for the old mode if (TraktorS3.StockFxMode) { - // FIXME: Either manually light things or have output links figure it out + this.kontrol.fxController.lightFx(); } else { this.kontrol.fxController.lightFX(); } From 21eca82a4e5228cf46d7504caf38370c312d6047 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 19 Dec 2022 23:46:45 +0100 Subject: [PATCH 15/36] Fix handling of multiple pressed FX select buttons --- res/controllers/Traktor-Kontrol-S3-hid-scripts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 51a23dca899d..cf3e85da4e13 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -2434,7 +2434,7 @@ TraktorS3.StockFxControl = class { } } else { this.pressedFxSelectButtons.splice(this.pressedFxSelectButtons.indexOf(fxNumber)); - if (!this.individualFxChainAssigned) { + if (this.pressedFxSelectButtons.length === 0 && !this.individualFxChainAssigned) { for (let channel = 1; channel <= 4; channel++) { this.setQuickEffectChain(channel, fxNumber); } From d1994f6c1eb2b7267c0d9b4c8c505b9acf278226 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 19 Dec 2022 23:53:58 +0100 Subject: [PATCH 16/36] Dimly light pressed FX Select buttons And don't highlight the unpressed ones. This doesn't look as cool but it makes it more obvious which ones are active. --- .../Traktor-Kontrol-S3-hid-scripts.js | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index cf3e85da4e13..fd4385e193ba 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -2432,6 +2432,10 @@ TraktorS3.StockFxControl = class { if (this.pressedFxSelectButtons.length === 1) { this.individualFxChainAssigned = false; } + + // The button will be dimly lit while pressed if it has not yet been + // assigned to any channels + this.lightFxSelectButtons(fxNumber); } else { this.pressedFxSelectButtons.splice(this.pressedFxSelectButtons.indexOf(fxNumber)); if (this.pressedFxSelectButtons.length === 0 && !this.individualFxChainAssigned) { @@ -2533,7 +2537,7 @@ TraktorS3.StockFxControl = class { } } - lightFxSelectButtons() { + lightFxSelectButtons(singleFxNumber = null) { // We'll light up the currently active FX chains. This means only a // single button is lit when all channels use the same quick effect // chain. @@ -2543,14 +2547,24 @@ TraktorS3.StockFxControl = class { activeFxSelectButtons.add(fxNumber); } - for (let fxNumber = 0; fxNumber <= 4; fxNumber++) { + const lightButton = function(fxNumber) { // By default the LED is off - const ledColor = this.fxColors[fxNumber] + - ((activeFxSelectButtons.has(fxNumber) || this.pressedFxSelectButtons.indexOf(fxNumber) !== -1) - ? TraktorS3.LEDBrightValue - : TraktorS3.LEDDimValue); + let ledColor = 0; + if (activeFxSelectButtons.has(fxNumber)) { + ledColor = this.fxColors[fxNumber] + TraktorS3.LEDBrightValue; + } else if (this.pressedFxSelectButtons.indexOf(fxNumber) !== -1) { + ledColor = this.fxColors[fxNumber] + TraktorS3.LEDDimValue; + } this.controller.hid.setOutput("[ChannelX]", `!fxButton${fxNumber}`, ledColor, !this.controller.batchingOutputs); + }.bind(this); + + if (singleFxNumber !== null) { + lightButton(singleFxNumber); + } else { + for (let fxNumber = 0; fxNumber <= 4; fxNumber++) { + lightButton(fxNumber); + } } } From 26400d854c9130e4165c53c5eb786d3bf7c5bf20 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 20 Dec 2022 00:56:23 +0100 Subject: [PATCH 17/36] Rename StockFx to VanillaFx --- .../Traktor-Kontrol-S3-hid-scripts.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index fd4385e193ba..0db0db2ea9d7 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -42,13 +42,13 @@ var TraktorS3 = {}; // - Another mode that exposes all of Mixxx's effect controls at the expense of // being more complex to use. See the manual for the full key binding scheme. // -// The first mode is dubbed 'stock mode' as it behaves in the way the mixer FX +// The first mode is dubbed 'vanilla mode' as it behaves in the way the mixer FX // section was originally intended to be used by Native Instruments. Disable // this option to use the second, Mixxx-specific mode. -TraktorS3.StockFxMode = true; +TraktorS3.VanillaFxMode = true; // When enabled, set all channels to the first FX chain on startup. Otherwise // the quick FX chain assignments from the last Mixxx run are preserved. -TraktorS3.StockFxModeDefaultToFilter = true; +TraktorS3.VanillaFxModeDefaultToFilter = true; // The pitch slider can operate either in absolute or relative mode. // In absolute mode: @@ -2334,9 +2334,9 @@ TraktorS3.shutdown = function() { /** * An alternative to `FXControl` that behaves more similarly to how the * controller works with Traktor. See the description for - * `TraktorS3.StockFxMode` for more information. + * `TraktorS3.VanillaFxMode` for more information. */ -TraktorS3.StockFxControl = class { +TraktorS3.VanillaFxControl = class { constructor(controller) { this.controller = controller; @@ -2369,7 +2369,7 @@ TraktorS3.StockFxControl = class { 4: this.controller.hid.LEDColors.YELLOW, }; - if (TraktorS3.StockFxModeDefaultToFilter) { + if (TraktorS3.VanillaFxModeDefaultToFilter) { for (let channel = 1; channel <= 4; channel++) { this.setQuickEffectChain(channel, 0); } @@ -2610,8 +2610,8 @@ TraktorS3.init = function(_id) { "[Channel2]": new TraktorS3.Channel(this.kontrol, this.kontrol.Decks.deck2, "[Channel2]"), }; - if (TraktorS3.StockFxMode) { - this.kontrol.fxController = new TraktorS3.StockFxControl(this.kontrol); + if (TraktorS3.VanillaFxMode) { + this.kontrol.fxController = new TraktorS3.VanillaFxControl(this.kontrol); } else { this.kontrol.fxController = new TraktorS3.FXControl(this.kontrol); } @@ -2631,7 +2631,7 @@ TraktorS3.init = function(_id) { this.kontrol.lightDeck("[Channel2]", true); // TODO: Fix capitalization for the old mode - if (TraktorS3.StockFxMode) { + if (TraktorS3.VanillaFxMode) { this.kontrol.fxController.lightFx(); } else { this.kontrol.fxController.lightFX(); From 3a520f449b8a376d7b0a2fc599e57daab711caf7 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 20 Dec 2022 00:57:02 +0100 Subject: [PATCH 18/36] Rename lightFX() to lightFx()0 --- .../Traktor-Kontrol-S3-hid-scripts.js | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 0db0db2ea9d7..f4e49b353e6f 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -1353,7 +1353,7 @@ TraktorS3.FXControl.prototype.changeState = function(newState) { case this.STATE_FOCUS: this.focusBlinkTimer = engine.beginTimer(150, function() { TraktorS3.kontrol.fxController.focusBlinkState = !TraktorS3.kontrol.fxController.focusBlinkState; - TraktorS3.kontrol.fxController.lightFX(); + TraktorS3.kontrol.fxController.lightFx(); }, false); } }; @@ -1371,7 +1371,7 @@ TraktorS3.FXControl.prototype.fxSelectHandler = function(field) { this.changeState(this.STATE_EFFECT); } } - this.lightFX(); + this.lightFx(); return; } @@ -1419,7 +1419,7 @@ TraktorS3.FXControl.prototype.fxSelectHandler = function(field) { this.activeFX = fxNumber; break; } - this.lightFX(); + this.lightFx(); }; TraktorS3.FXControl.prototype.fxEnableHandler = function(field) { @@ -1427,7 +1427,7 @@ TraktorS3.FXControl.prototype.fxEnableHandler = function(field) { this.enablePressed[field.group] = !!field.value; if (!field.value) { - this.lightFX(); + this.lightFx(); return; } @@ -1456,7 +1456,7 @@ TraktorS3.FXControl.prototype.fxEnableHandler = function(field) { script.toggleControl(group, key); break; } - this.lightFX(); + this.lightFx(); }; TraktorS3.FXControl.prototype.fxKnobHandler = function(field) { @@ -1515,7 +1515,7 @@ TraktorS3.FXControl.prototype.getChannelColor = function(group, status) { } }; -TraktorS3.FXControl.prototype.lightFX = function() { +TraktorS3.FXControl.prototype.lightFx = function() { this.controller.batchingOutputs = true; // Loop through select buttons @@ -2178,7 +2178,7 @@ TraktorS3.Controller.prototype.lightDeck = function(group, sendPackets) { this.basicOutput(0, "[Master]", "!extButton"); } } - // this.lightFX(); + // this.lightFx(); // Selected deck lights if (group === "[Channel1]") { @@ -2630,12 +2630,7 @@ TraktorS3.init = function(_id) { this.kontrol.lightDeck("[Channel1]", false); this.kontrol.lightDeck("[Channel2]", true); - // TODO: Fix capitalization for the old mode - if (TraktorS3.VanillaFxMode) { - this.kontrol.fxController.lightFx(); - } else { - this.kontrol.fxController.lightFX(); - } + this.kontrol.fxController.lightFx(); } this.kontrol.setInputLineMode(TraktorS3.inputModeLine); From 11af93b895feea674297980127eb472217b71cac Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 20 Dec 2022 01:00:18 +0100 Subject: [PATCH 19/36] Allow disabling the channel colors with vanilla FX --- .../Traktor-Kontrol-S3-hid-scripts.js | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index f4e49b353e6f..27588b32694d 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -49,6 +49,10 @@ TraktorS3.VanillaFxMode = true; // When enabled, set all channels to the first FX chain on startup. Otherwise // the quick FX chain assignments from the last Mixxx run are preserved. TraktorS3.VanillaFxModeDefaultToFilter = true; +// When enabled, the FX Enable buttons will use the colors set in +// `TraktorS3.ChannelColors` when the filter effect is selected. Disabling this +// will use the Filter button's orange color instead. +TraktorS3.VanillaFxModeChannelColors = true; // The pitch slider can operate either in absolute or relative mode. // In absolute mode: @@ -2344,7 +2348,7 @@ TraktorS3.VanillaFxControl = class { // select buttons, 0 being the filter and 1-4 being the four FX buttons. // We keep track of whether they're currently held down so we can assign // an effect chain to a single channel by holding down one of those five - // buttons and then pressing the channel's FX On button. + // buttons and then pressing the channel's FX Enable button. this.pressedFxSelectButtons = []; // When one of the FX select buttons is held down we need to keep track // of whether or not we assigned any quick effect chains. If this @@ -2417,17 +2421,17 @@ TraktorS3.VanillaFxControl = class { // FX Number 0 is the filter, and 1-4 are the FX 1-4 buttons const fxNumber = parseInt(field.name[field.name.length - 1]); - // If one of the four FX On buttons is pressed while one of the five FX - // select buttons are held down, then only that channel's quick effect - // chain assignments are changed. `this.individualFxChainAssigned` keeps - // track of whether a quick effects chain has been assigned to an - // individual channel. To avoid weird things from happening, we keep - // track of which buttons are pressed. The global effect chain should - // only change when the last button has been released, and holding down - // one FX button, assigning that effect to a channel, holding down a - // second button, and releasing both shouldn't change the global effect - // assignments. - if (field.value === 1) { + // If one of the four FX Enable buttons is pressed while one of the five + // FX select buttons are held down, then only that channel's quick + // effect chain assignments are changed. + // `this.individualFxChainAssigned` keeps track of whether a quick + // effects chain has been assigned to an individual channel. To avoid + // weird things from happening, we keep track of which buttons are + // pressed. The global effect chain should only change when the last + // button has been released, and holding down one FX button, assigning + // that effect to a channel, holding down a second button, and releasing + // both shouldn't change the global effect assignments. + if (TraktorS3.VanillaFxModeChannelColors && field.value === 1) { this.pressedFxSelectButtons.push(fxNumber); if (this.pressedFxSelectButtons.length === 1) { this.individualFxChainAssigned = false; From cadf7faa72e99cf84bad96cbf9601e248f50235f Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 20 Dec 2022 01:04:15 +0100 Subject: [PATCH 20/36] Update the Traktor Kontrol S3 script authors --- res/controllers/Traktor-Kontrol-S3-hid-scripts.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 27588b32694d..6ac00e2223ce 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -1,8 +1,8 @@ /////////////////////////////////////////////////////////////////////////////////// // -// Traktor Kontrol S3 HID controller script v1.00 -// Last modification: August 2020 -// Author: Owen Williams +// Traktor Kontrol S3 HID controller script v2.00 +// Last modification: December 2022 +// Authors: Owen Williams, Robbert van der Helm // https://www.mixxx.org/wiki/doku.php/native_instruments_traktor_kontrol_s3 // /////////////////////////////////////////////////////////////////////////////////// From ded8331c15701833b3559e32281458b5ca119698 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 20 Dec 2022 01:08:02 +0100 Subject: [PATCH 21/36] Replace uses of TraktorS3.bind() with fn.bind() --- .../Traktor-Kontrol-S3-hid-scripts.js | 113 ++++++++---------- 1 file changed, 53 insertions(+), 60 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 6ac00e2223ce..4a8fd8c2c7ac 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -209,13 +209,6 @@ TraktorS3.Controller = function() { this.samplerCallbacks = []; }; -// Mixxx's javascript doesn't support .bind natively, so here's a simple version. -TraktorS3.bind = function(fn, obj) { - return function() { - return fn.apply(obj, arguments); - }; -}; - //// Deck Objects //// // Decks are the physical controllers on either side of the controller. // Each Deck can control 2 channels. @@ -272,7 +265,7 @@ TraktorS3.Deck.prototype.defineButton = function(msg, name, deckOffset, deckBitm deckOffset = deck2Offset; deckBitmask = deck2Bitmask; } - this.controller.registerInputButton(msg, this.group, name, deckOffset, deckBitmask, TraktorS3.bind(fn, this)); + this.controller.registerInputButton(msg, this.group, name, deckOffset, deckBitmask, fn.bind(this)); }; TraktorS3.Deck.prototype.defineJog = function(message, name, deckOffset, deck2Offset, callback) { @@ -281,7 +274,7 @@ TraktorS3.Deck.prototype.defineJog = function(message, name, deckOffset, deck2Of } // Jog wheels have four byte input: 1 byte for distance ticks, and 3 bytes for a timecode. message.addControl(this.group, name, deckOffset, "I", 0xFFFFFFFF); - message.setCallback(this.group, name, TraktorS3.bind(callback, this)); + message.setCallback(this.group, name, callback.bind(this)); }; // defineScaler configures ranged controls like knobs and sliders. @@ -290,7 +283,7 @@ TraktorS3.Deck.prototype.defineScaler = function(msg, name, deckOffset, deckBitm deckOffset = deck2Offset; deckBitmask = deck2Bitmask; } - this.controller.registerInputScaler(msg, this.group, name, deckOffset, deckBitmask, TraktorS3.bind(fn, this)); + this.controller.registerInputScaler(msg, this.group, name, deckOffset, deckBitmask, fn.bind(this)); }; TraktorS3.Deck.prototype.registerInputs = function(messageShort, messageLong) { @@ -387,13 +380,13 @@ TraktorS3.Deck.prototype.syncHandler = function(field) { if (engine.getValue(this.activeChannel, "sync_enabled") === 0) { script.triggerControl(this.activeChannel, "beatsync"); // Start timer to measure how long button is pressed - this.syncPressedTimer = engine.beginTimer(300, TraktorS3.bind(function() { + this.syncPressedTimer = engine.beginTimer(300, function() { engine.setValue(this.activeChannel, "sync_enabled", 1); // Reset sync button timer state if active if (this.syncPressedTimer !== 0) { this.syncPressedTimer = 0; } - }, this), true); + }.bind(this), this, true); // Light corresponding LED when button is pressed this.colorOutput(1, "sync_enabled"); @@ -886,14 +879,14 @@ TraktorS3.Deck.prototype.linkOutputs = function() { this.basicOutput(value, key); }; - this.defineLink("play_indicator", TraktorS3.bind(TraktorS3.Deck.prototype.playIndicatorHandler, this)); - this.defineLink("cue_indicator", TraktorS3.bind(colorOutput, this)); - this.defineLink("sync_enabled", TraktorS3.bind(colorOutput, this)); - this.defineLink("keylock", TraktorS3.bind(colorOutput, this)); - this.defineLink("slip_enabled", TraktorS3.bind(colorOutput, this)); - this.defineLink("quantize", TraktorS3.bind(colorOutput, this)); - this.defineLink("reverse", TraktorS3.bind(basicOutput, this)); - this.defineLink("scratch2_enable", TraktorS3.bind(colorOutput, this)); + this.defineLink("play_indicator", TraktorS3.Deck.prototype.playIndicatorHandler.bind(this)); + this.defineLink("cue_indicator", colorOutput.bind(this)); + this.defineLink("sync_enabled", colorOutput.bind(this)); + this.defineLink("keylock", colorOutput.bind(this)); + this.defineLink("slip_enabled", colorOutput.bind(this)); + this.defineLink("quantize", colorOutput.bind(this)); + this.defineLink("reverse", basicOutput.bind(this)); + this.defineLink("scratch2_enable", colorOutput.bind(this)); }; TraktorS3.Deck.prototype.deckBaseColor = function() { @@ -1115,16 +1108,16 @@ TraktorS3.Channel.prototype.vuMeterHandler = function(value) { }; TraktorS3.Channel.prototype.linkOutputs = function() { - this.vuConnection = engine.makeConnection(this.group, "VuMeter", TraktorS3.bind(TraktorS3.Channel.prototype.vuMeterHandler, this)); - this.clipConnection = engine.makeConnection(this.group, "PeakIndicator", TraktorS3.bind(TraktorS3.Controller.prototype.peakOutput, this.controller)); - this.controller.linkChannelOutput(this.group, "pfl", TraktorS3.bind(TraktorS3.Controller.prototype.pflOutput, this.controller)); + this.vuConnection = engine.makeConnection(this.group, "VuMeter", TraktorS3.Channel.prototype.vuMeterHandler.bind(this)); + this.clipConnection = engine.makeConnection(this.group, "PeakIndicator", TraktorS3.Controller.prototype.peakOutput.bind(this.controller)); + this.controller.linkChannelOutput(this.group, "pfl", TraktorS3.Controller.prototype.pflOutput.bind(this.controller)); for (let j = 1; j <= 8; j++) { this.hotcueCallbacks.push(engine.makeConnection(this.group, "hotcue_" + j + "_enabled", - TraktorS3.bind(TraktorS3.Channel.prototype.hotcuesOutput, this))); + TraktorS3.Channel.prototype.hotcuesOutput.bind(this))); this.hotcueCallbacks.push(engine.makeConnection(this.group, "hotcue_" + j + "_activate", - TraktorS3.bind(TraktorS3.Channel.prototype.hotcuesOutput, this))); + TraktorS3.Channel.prototype.hotcuesOutput.bind(this))); this.hotcueCallbacks.push(engine.makeConnection(this.group, "hotcue_" + j + "_color", - TraktorS3.bind(TraktorS3.Channel.prototype.hotcuesOutput, this))); + TraktorS3.Channel.prototype.hotcuesOutput.bind(this))); } }; @@ -1252,21 +1245,21 @@ TraktorS3.FXControl = function(controller) { TraktorS3.FXControl.prototype.registerInputs = function(messageShort, messageLong) { // FX Buttons const fxFn = TraktorS3.FXControl.prototype; - this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx1", 0x08, 0x08, TraktorS3.bind(fxFn.fxSelectHandler, this)); - this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx2", 0x08, 0x10, TraktorS3.bind(fxFn.fxSelectHandler, this)); - this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx3", 0x08, 0x20, TraktorS3.bind(fxFn.fxSelectHandler, this)); - this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx4", 0x08, 0x40, TraktorS3.bind(fxFn.fxSelectHandler, this)); - this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx0", 0x08, 0x80, TraktorS3.bind(fxFn.fxSelectHandler, this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx1", 0x08, 0x08, fxFn.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx2", 0x08, 0x10, fxFn.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx3", 0x08, 0x20, fxFn.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx4", 0x08, 0x40, fxFn.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx0", 0x08, 0x80, fxFn.fxSelectHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[Channel3]", "!fxEnabled", 0x07, 0x08, TraktorS3.bind(fxFn.fxEnableHandler, this)); - this.controller.registerInputButton(messageShort, "[Channel1]", "!fxEnabled", 0x07, 0x10, TraktorS3.bind(fxFn.fxEnableHandler, this)); - this.controller.registerInputButton(messageShort, "[Channel2]", "!fxEnabled", 0x07, 0x20, TraktorS3.bind(fxFn.fxEnableHandler, this)); - this.controller.registerInputButton(messageShort, "[Channel4]", "!fxEnabled", 0x07, 0x40, TraktorS3.bind(fxFn.fxEnableHandler, this)); + this.controller.registerInputButton(messageShort, "[Channel3]", "!fxEnabled", 0x07, 0x08, fxFn.fxEnableHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[Channel1]", "!fxEnabled", 0x07, 0x10, fxFn.fxEnableHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[Channel2]", "!fxEnabled", 0x07, 0x20, fxFn.fxEnableHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[Channel4]", "!fxEnabled", 0x07, 0x40, fxFn.fxEnableHandler.bind(this)); - this.controller.registerInputScaler(messageLong, "[Channel1]", "!fxKnob", 0x39, 0xFFFF, TraktorS3.bind(fxFn.fxKnobHandler, this)); - this.controller.registerInputScaler(messageLong, "[Channel2]", "!fxKnob", 0x3B, 0xFFFF, TraktorS3.bind(fxFn.fxKnobHandler, this)); - this.controller.registerInputScaler(messageLong, "[Channel3]", "!fxKnob", 0x37, 0xFFFF, TraktorS3.bind(fxFn.fxKnobHandler, this)); - this.controller.registerInputScaler(messageLong, "[Channel4]", "!fxKnob", 0x3D, 0xFFFF, TraktorS3.bind(fxFn.fxKnobHandler, this)); + this.controller.registerInputScaler(messageLong, "[Channel1]", "!fxKnob", 0x39, 0xFFFF, fxFn.fxKnobHandler.bind(this)); + this.controller.registerInputScaler(messageLong, "[Channel2]", "!fxKnob", 0x3B, 0xFFFF, fxFn.fxKnobHandler.bind(this)); + this.controller.registerInputScaler(messageLong, "[Channel3]", "!fxKnob", 0x37, 0xFFFF, fxFn.fxKnobHandler.bind(this)); + this.controller.registerInputScaler(messageLong, "[Channel4]", "!fxKnob", 0x3D, 0xFFFF, fxFn.fxKnobHandler.bind(this)); }; TraktorS3.FXControl.prototype.channelToIndex = function(group) { @@ -1676,19 +1669,19 @@ TraktorS3.Controller.prototype.registerInputPackets = function() { deck.registerInputs(messageShort, messageLong); } - this.registerInputButton(messageShort, "[Channel1]", "!switchDeck", 0x02, 0x02, TraktorS3.bind(TraktorS3.Controller.prototype.deckSwitchHandler, this)); - this.registerInputButton(messageShort, "[Channel2]", "!switchDeck", 0x05, 0x04, TraktorS3.bind(TraktorS3.Controller.prototype.deckSwitchHandler, this)); - this.registerInputButton(messageShort, "[Channel3]", "!switchDeck", 0x02, 0x04, TraktorS3.bind(TraktorS3.Controller.prototype.deckSwitchHandler, this)); - this.registerInputButton(messageShort, "[Channel4]", "!switchDeck", 0x05, 0x08, TraktorS3.bind(TraktorS3.Controller.prototype.deckSwitchHandler, this)); + this.registerInputButton(messageShort, "[Channel1]", "!switchDeck", 0x02, 0x02, TraktorS3.Controller.prototype.deckSwitchHandler.bind(this)); + this.registerInputButton(messageShort, "[Channel2]", "!switchDeck", 0x05, 0x04, TraktorS3.Controller.prototype.deckSwitchHandler.bind(this)); + this.registerInputButton(messageShort, "[Channel3]", "!switchDeck", 0x02, 0x04, TraktorS3.Controller.prototype.deckSwitchHandler.bind(this)); + this.registerInputButton(messageShort, "[Channel4]", "!switchDeck", 0x05, 0x08, TraktorS3.Controller.prototype.deckSwitchHandler.bind(this)); // Headphone buttons - this.registerInputButton(messageShort, "[Channel1]", "pfl", 0x08, 0x01, TraktorS3.bind(TraktorS3.Controller.prototype.headphoneHandler, this)); - this.registerInputButton(messageShort, "[Channel2]", "pfl", 0x08, 0x02, TraktorS3.bind(TraktorS3.Controller.prototype.headphoneHandler, this)); - this.registerInputButton(messageShort, "[Channel3]", "pfl", 0x07, 0x80, TraktorS3.bind(TraktorS3.Controller.prototype.headphoneHandler, this)); - this.registerInputButton(messageShort, "[Channel4]", "pfl", 0x08, 0x04, TraktorS3.bind(TraktorS3.Controller.prototype.headphoneHandler, this)); + this.registerInputButton(messageShort, "[Channel1]", "pfl", 0x08, 0x01, TraktorS3.Controller.prototype.headphoneHandler.bind(this)); + this.registerInputButton(messageShort, "[Channel2]", "pfl", 0x08, 0x02, TraktorS3.Controller.prototype.headphoneHandler.bind(this)); + this.registerInputButton(messageShort, "[Channel3]", "pfl", 0x07, 0x80, TraktorS3.Controller.prototype.headphoneHandler.bind(this)); + this.registerInputButton(messageShort, "[Channel4]", "pfl", 0x08, 0x04, TraktorS3.Controller.prototype.headphoneHandler.bind(this)); // EXT Button - this.registerInputButton(messageShort, "[Master]", "!extButton", 0x07, 0x04, TraktorS3.bind(TraktorS3.Controller.prototype.extModeHandler, this)); + this.registerInputButton(messageShort, "[Master]", "!extButton", 0x07, 0x04, TraktorS3.Controller.prototype.extModeHandler.bind(this)); this.fxController.registerInputs(messageShort, messageLong); @@ -1721,7 +1714,7 @@ TraktorS3.Controller.prototype.registerInputPackets = function() { this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel4]_Effect1]", "parameter1", 0x35, 0xFFFF, this.parameterHandler); this.registerInputScaler(messageLong, "[Master]", "crossfader", 0x0B, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[Master]", "gain", 0x17, 0xFFFF, TraktorS3.bind(TraktorS3.Controller.prototype.masterGainHandler, this)); + this.registerInputScaler(messageLong, "[Master]", "gain", 0x17, 0xFFFF, TraktorS3.Controller.prototype.masterGainHandler.bind(this)); this.registerInputScaler(messageLong, "[Master]", "headMix", 0x1D, 0xFFFF, this.parameterHandler); this.registerInputScaler(messageLong, "[Master]", "headGain", 0x1B, 0xFFFF, this.parameterHandler); @@ -1730,11 +1723,11 @@ TraktorS3.Controller.prototype.registerInputPackets = function() { for (ch in this.Channels) { const chanob = this.Channels[ch]; engine.makeConnection(ch, "playposition", - TraktorS3.bind(TraktorS3.Channel.prototype.playpositionChanged, chanob)); + TraktorS3.Channel.prototype.playpositionChanged.bind(chanob)); engine.connectControl(ch, "track_loaded", - TraktorS3.bind(TraktorS3.Channel.prototype.trackLoadedHandler, chanob)); + TraktorS3.Channel.prototype.trackLoadedHandler.bind(chanob)); engine.connectControl(ch, "end_of_track", - TraktorS3.bind(TraktorS3.Channel.prototype.endOfTrackHandler, chanob)); + TraktorS3.Channel.prototype.endOfTrackHandler.bind(chanob)); } // Query the current values from the controller and set them. The packet @@ -1981,19 +1974,19 @@ TraktorS3.Controller.prototype.registerOutputPackets = function() { engine.connectControl("[Microphone]", "pfl", this.pflOutput); - engine.connectControl("[Master]", "maximize_library", TraktorS3.bind(TraktorS3.Controller.prototype.maximizeLibraryOutput, this)); + engine.connectControl("[Master]", "maximize_library", TraktorS3.Controller.prototype.maximizeLibraryOutput.bind(this)); // Master VuMeters - this.masterVuMeter.VuMeterL.connection = engine.makeConnection("[Master]", "VuMeterL", TraktorS3.bind(TraktorS3.Controller.prototype.masterVuMeterHandler, this)); - this.masterVuMeter.VuMeterR.connection = engine.makeConnection("[Master]", "VuMeterR", TraktorS3.bind(TraktorS3.Controller.prototype.masterVuMeterHandler, this)); - this.linkChannelOutput("[Master]", "PeakIndicatorL", TraktorS3.bind(TraktorS3.Controller.prototype.peakOutput, this)); - this.linkChannelOutput("[Master]", "PeakIndicatorR", TraktorS3.bind(TraktorS3.Controller.prototype.peakOutput, this)); - this.guiTickConnection = engine.makeConnection("[Master]", "guiTick50ms", TraktorS3.bind(TraktorS3.Controller.prototype.guiTickHandler, this)); + this.masterVuMeter.VuMeterL.connection = engine.makeConnection("[Master]", "VuMeterL", TraktorS3.Controller.prototype.masterVuMeterHandler.bind(this)); + this.masterVuMeter.VuMeterR.connection = engine.makeConnection("[Master]", "VuMeterR", TraktorS3.Controller.prototype.masterVuMeterHandler.bind(this)); + this.linkChannelOutput("[Master]", "PeakIndicatorL", TraktorS3.Controller.prototype.peakOutput.bind(this)); + this.linkChannelOutput("[Master]", "PeakIndicatorR", TraktorS3.Controller.prototype.peakOutput.bind(this)); + this.guiTickConnection = engine.makeConnection("[Master]", "guiTick50ms", TraktorS3.Controller.prototype.guiTickHandler.bind(this)); // Sampler callbacks for (i = 1; i <= 8; ++i) { - this.samplerCallbacks.push(engine.makeConnection("[Sampler" + i + "]", "track_loaded", TraktorS3.bind(TraktorS3.Controller.prototype.samplesOutput, this))); - this.samplerCallbacks.push(engine.makeConnection("[Sampler" + i + "]", "play_indicator", TraktorS3.bind(TraktorS3.Controller.prototype.samplesOutput, this))); + this.samplerCallbacks.push(engine.makeConnection("[Sampler" + i + "]", "track_loaded", TraktorS3.Controller.prototype.samplesOutput.bind(this))); + this.samplerCallbacks.push(engine.makeConnection("[Sampler" + i + "]", "play_indicator", TraktorS3.Controller.prototype.samplesOutput.bind(this))); } }; From 6967102bb44f4f52c1c8baec5c3407b03dc90ed1 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 20 Dec 2022 01:17:51 +0100 Subject: [PATCH 22/36] Convert S3 script to use ES6 classes --- .../Traktor-Kontrol-S3-hid-scripts.js | 3649 +++++++++-------- 1 file changed, 1830 insertions(+), 1819 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 4a8fd8c2c7ac..02b3c0d076e1 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -115,2134 +115,2145 @@ TraktorS3.DefaultKeylockEnabled = null; // true // Set to true to output debug messages and debug light outputs. TraktorS3.DebugMode = false; -TraktorS3.Controller = function() { - this.hid = new HIDController(); - - // When true, packets will not be sent to the controller. Good for doing mass updates. - this.batchingOutputs = false; - - // "5" is the "filter" button below the other 4. - this.fxButtonState = {1: false, 2: false, 3: false, 4: false, 5: false}; - - this.masterVuMeter = { - "VuMeterL": { - connection: null, - updated: false, - value: 0 - }, - "VuMeterR": { - connection: null, - updated: false, - value: 0 - } - }; - - this.guiTickConnection = {}; - - // The S3 has a set of predefined colors for many buttons. They are not - // mapped by RGB, but 16 colors, each with 4 levels of brightness, plus white. - this.hid.LEDColors = { - OFF: 0x00, - RED: 0x04, - CARROT: 0x08, - ORANGE: 0x0C, - HONEY: 0x10, - YELLOW: 0x14, - LIME: 0x18, - GREEN: 0x1C, - AQUA: 0x20, - CELESTE: 0x24, - SKY: 0x28, - BLUE: 0x2C, - PURPLE: 0x30, - FUCHSIA: 0x34, - MAGENTA: 0x38, - AZALEA: 0x3C, - SALMON: 0x40, - WHITE: 0x44 - }; +TraktorS3.Controller = class { + constructor() { + this.hid = new HIDController(); + + // When true, packets will not be sent to the controller. Good for doing + // mass updates. + this.batchingOutputs = false; + + // "5" is the "filter" button below the other 4. + this.fxButtonState = {1: false, 2: false, 3: false, 4: false, 5: false}; + + this.masterVuMeter = { + "VuMeterL": { + connection: null, + updated: false, + value: 0 + }, + "VuMeterR": { + connection: null, + updated: false, + value: 0 + } + }; + this.guiTickConnection = {}; + + // The S3 has a set of predefined colors for many buttons. They are not + // mapped by RGB, but 16 colors, each with 4 levels of brightness, plus + // white. + this.hid.LEDColors = { + OFF: 0x00, + RED: 0x04, + CARROT: 0x08, + ORANGE: 0x0C, + HONEY: 0x10, + YELLOW: 0x14, + LIME: 0x18, + GREEN: 0x1C, + AQUA: 0x20, + CELESTE: 0x24, + SKY: 0x28, + BLUE: 0x2C, + PURPLE: 0x30, + FUCHSIA: 0x34, + MAGENTA: 0x38, + AZALEA: 0x3C, + SALMON: 0x40, + WHITE: 0x44 + }; - // FX 5 is the Filter - this.fxLEDValue = { - 0: this.hid.LEDColors.PURPLE, - 1: this.hid.LEDColors.RED, - 2: this.hid.LEDColors.GREEN, - 3: this.hid.LEDColors.CELESTE, - 4: this.hid.LEDColors.YELLOW, - }; - this.colorMap = new ColorMapper({ - 0xCC0000: this.hid.LEDColors.RED, - 0xCC5E00: this.hid.LEDColors.CARROT, - 0xCC7800: this.hid.LEDColors.ORANGE, - 0xCC9200: this.hid.LEDColors.HONEY, + // FX 5 is the Filter + this.fxLEDValue = { + 0: this.hid.LEDColors.PURPLE, + 1: this.hid.LEDColors.RED, + 2: this.hid.LEDColors.GREEN, + 3: this.hid.LEDColors.CELESTE, + 4: this.hid.LEDColors.YELLOW, + }; - 0xCCCC00: this.hid.LEDColors.YELLOW, - 0x81CC00: this.hid.LEDColors.LIME, - 0x00CC00: this.hid.LEDColors.GREEN, - 0x00CC49: this.hid.LEDColors.AQUA, + this.colorMap = new ColorMapper({ + 0xCC0000: this.hid.LEDColors.RED, + 0xCC5E00: this.hid.LEDColors.CARROT, + 0xCC7800: this.hid.LEDColors.ORANGE, + 0xCC9200: this.hid.LEDColors.HONEY, - 0x00CCCC: this.hid.LEDColors.CELESTE, - 0x0091CC: this.hid.LEDColors.SKY, - 0x0000CC: this.hid.LEDColors.BLUE, - 0xCC00CC: this.hid.LEDColors.PURPLE, + 0xCCCC00: this.hid.LEDColors.YELLOW, + 0x81CC00: this.hid.LEDColors.LIME, + 0x00CC00: this.hid.LEDColors.GREEN, + 0x00CC49: this.hid.LEDColors.AQUA, - 0xCC0091: this.hid.LEDColors.FUCHSIA, - 0xCC0079: this.hid.LEDColors.MAGENTA, - 0xCC477E: this.hid.LEDColors.AZALEA, - 0xCC4761: this.hid.LEDColors.SALMON, + 0x00CCCC: this.hid.LEDColors.CELESTE, + 0x0091CC: this.hid.LEDColors.SKY, + 0x0000CC: this.hid.LEDColors.BLUE, + 0xCC00CC: this.hid.LEDColors.PURPLE, - 0xCCCCCC: this.hid.LEDColors.WHITE, - }); + 0xCC0091: this.hid.LEDColors.FUCHSIA, + 0xCC0079: this.hid.LEDColors.MAGENTA, + 0xCC477E: this.hid.LEDColors.AZALEA, + 0xCC4761: this.hid.LEDColors.SALMON, - // State for controller input loudness setting - this.inputModeLine = false; + 0xCCCCCC: this.hid.LEDColors.WHITE, + }); - // If true, channel 4 is in input mode - this.channel4InputMode = false; + // State for controller input loudness setting + this.inputModeLine = false; + + // If true, channel 4 is in input mode + this.channel4InputMode = false; + + // Represents the first-pressed deck switch button, used for tracking + // deck clones. + this.deckSwitchPressed = ""; + + // callbacks + this.samplerCallbacks = []; + } + + registerInputPackets() { + const messageShort = new HIDPacket("shortmessage", 0x01, TraktorS3.messageCallback); + const messageLong = new HIDPacket("longmessage", 0x02, TraktorS3.messageCallback); + + for (const idx in this.Decks) { + const deck = this.Decks[idx]; + deck.registerInputs(messageShort, messageLong); + } + + this.registerInputButton(messageShort, "[Channel1]", "!switchDeck", 0x02, 0x02, TraktorS3.Controller.prototype.deckSwitchHandler.bind(this)); + this.registerInputButton(messageShort, "[Channel2]", "!switchDeck", 0x05, 0x04, TraktorS3.Controller.prototype.deckSwitchHandler.bind(this)); + this.registerInputButton(messageShort, "[Channel3]", "!switchDeck", 0x02, 0x04, TraktorS3.Controller.prototype.deckSwitchHandler.bind(this)); + this.registerInputButton(messageShort, "[Channel4]", "!switchDeck", 0x05, 0x08, TraktorS3.Controller.prototype.deckSwitchHandler.bind(this)); + + // Headphone buttons + this.registerInputButton(messageShort, "[Channel1]", "pfl", 0x08, 0x01, TraktorS3.Controller.prototype.headphoneHandler.bind(this)); + this.registerInputButton(messageShort, "[Channel2]", "pfl", 0x08, 0x02, TraktorS3.Controller.prototype.headphoneHandler.bind(this)); + this.registerInputButton(messageShort, "[Channel3]", "pfl", 0x07, 0x80, TraktorS3.Controller.prototype.headphoneHandler.bind(this)); + this.registerInputButton(messageShort, "[Channel4]", "pfl", 0x08, 0x04, TraktorS3.Controller.prototype.headphoneHandler.bind(this)); + + // EXT Button + this.registerInputButton(messageShort, "[Master]", "!extButton", 0x07, 0x04, TraktorS3.Controller.prototype.extModeHandler.bind(this)); + + this.fxController.registerInputs(messageShort, messageLong); + + this.hid.registerInputPacket(messageShort); + + this.registerInputScaler(messageLong, "[Channel1]", "volume", 0x05, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[Channel2]", "volume", 0x07, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[Channel3]", "volume", 0x03, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[Channel4]", "volume", 0x09, 0xFFFF, this.parameterHandler); + + this.registerInputScaler(messageLong, "[Channel1]", "pregain", 0x11, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[Channel2]", "pregain", 0x13, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[Channel3]", "pregain", 0x0F, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[Channel4]", "pregain", 0x15, 0xFFFF, this.parameterHandler); + + this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel1]_Effect1]", "parameter3", 0x25, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel1]_Effect1]", "parameter2", 0x27, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel1]_Effect1]", "parameter1", 0x29, 0xFFFF, this.parameterHandler); + + this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel2]_Effect1]", "parameter3", 0x2B, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel2]_Effect1]", "parameter2", 0x2D, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel2]_Effect1]", "parameter1", 0x2F, 0xFFFF, this.parameterHandler); + + this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel3]_Effect1]", "parameter3", 0x1F, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel3]_Effect1]", "parameter2", 0x21, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel3]_Effect1]", "parameter1", 0x23, 0xFFFF, this.parameterHandler); + + this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel4]_Effect1]", "parameter3", 0x31, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel4]_Effect1]", "parameter2", 0x33, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel4]_Effect1]", "parameter1", 0x35, 0xFFFF, this.parameterHandler); + + this.registerInputScaler(messageLong, "[Master]", "crossfader", 0x0B, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[Master]", "gain", 0x17, 0xFFFF, TraktorS3.Controller.prototype.masterGainHandler.bind(this)); + this.registerInputScaler(messageLong, "[Master]", "headMix", 0x1D, 0xFFFF, this.parameterHandler); + this.registerInputScaler(messageLong, "[Master]", "headGain", 0x1B, 0xFFFF, this.parameterHandler); + + this.hid.registerInputPacket(messageLong); + + for (ch in this.Channels) { + const chanob = this.Channels[ch]; + engine.makeConnection(ch, "playposition", + TraktorS3.Channel.prototype.playpositionChanged.bind(chanob)); + engine.connectControl(ch, "track_loaded", + TraktorS3.Channel.prototype.trackLoadedHandler.bind(chanob)); + engine.connectControl(ch, "end_of_track", + TraktorS3.Channel.prototype.endOfTrackHandler.bind(chanob)); + } + + // Query the current values from the controller and set them. The packet + // parser ignores the first time a value is set, so we'll need to set it + // with different values once. Report 2 contains the state of the mixer + // controls. + const report2Values = new Uint8Array(controller.getInputReport(2)); + TraktorS3.incomingData([2, ...Array.from(report2Values.map(x => ~x))]); + TraktorS3.incomingData([2, ...Array.from(report2Values)]); - // Represents the first-pressed deck switch button, used for tracking deck clones. - this.deckSwitchPressed = ""; + // Report 1 is the state of the deck controls. These shouldn't have any + // initial effect, and most of these values will be 0 anyways. We'll + // just tell the packet parser the current values so it won't ignore the + // next input. + const report1Values = new Uint8Array(controller.getInputReport(1)); + TraktorS3.incomingData([1, ...Array.from(report1Values)]); - // callbacks - this.samplerCallbacks = []; -}; + // NOTE: Soft takeovers must only be enabled after setting the initial + // value, or the above line won't have any effect + for (var ch = 1; ch <= 4; ch++) { + var group = "[Channel" + ch + "]"; + if (!TraktorS3.PitchSliderRelativeMode) { + engine.softTakeover(group, "rate", true); + } + engine.softTakeover(group, "pitch_adjust", true); + engine.softTakeover(group, "volume", true); + engine.softTakeover(group, "pregain", true); + engine.softTakeover("[QuickEffectRack1_" + group + "]", "super1", true); + } + for (let unit = 1; unit <= 4; unit++) { + group = "[EffectRack1_EffectUnit" + unit + "]"; + let key = "mix"; + engine.softTakeover(group, key, true); + for (let effect = 1; effect <= 4; effect++) { + group = "[EffectRack1_EffectUnit" + unit + "_Effect" + effect + "]"; + key = "meta"; + engine.softTakeover(group, key, true); + for (let param = 1; param <= 4; param++) { + key = "parameter" + param; + engine.softTakeover(group, key, true); + } + } + } -//// Deck Objects //// -// Decks are the physical controllers on either side of the controller. -// Each Deck can control 2 channels. -TraktorS3.Deck = function(controller, deckNumber, group) { - this.controller = controller; - this.deckNumber = deckNumber; - this.group = group; - this.activeChannel = "[Channel" + deckNumber + "]"; - this.activeChannelNumber = deckNumber; - // When true, touching the wheel enables scratch mode. When off, touching the wheel - // has no special effect - this.jogToggled = TraktorS3.JogDefaultOn; - this.shiftPressed = false; - - // State for pitch slider relative mode - this.pitchSliderLastValue = -1; - this.keylockPressed = false; - this.keyAdjusted = false; - - // Various states - this.syncPressedTimer = 0; - this.previewPressed = false; - // padModeState 0 is hotcues, 1 is samplers - this.padModeState = 0; - - // Jog wheel state - this.lastTickVal = 0; - this.lastTickTime = 0; - this.lastTickWallClock = 0; - - // Knob encoder states (hold values between 0x0 and 0xF) - // Rotate to the right is +1 and to the left is means -1 - this.browseKnobEncoderState = 0; - this.loopKnobEncoderState = 0; - this.moveKnobEncoderState = 0; -}; + engine.softTakeover("[Microphone]", "volume", true); + engine.softTakeover("[Microphone]", "pregain", true); -TraktorS3.Deck.prototype.activateChannel = function(channel) { - if (channel.parentDeck !== this) { - HIDDebug("Programming ERROR: tried to activate a channel with a deck that is not its parent"); - return; - } - this.activeChannel = channel.group; - this.activeChannelNumber = channel.groupNumber; - engine.softTakeoverIgnoreNextValue(this.activeChannel, "rate"); - this.controller.lightDeck(this.activeChannel); -}; + engine.softTakeover("[EqualizerRack1_[Channel1]_Effect1]", "parameter1", true); + engine.softTakeover("[EqualizerRack1_[Channel1]_Effect1]", "parameter2", true); + engine.softTakeover("[EqualizerRack1_[Channel1]_Effect1]", "parameter3", true); + engine.softTakeover("[EqualizerRack1_[Channel2]_Effect1]", "parameter1", true); + engine.softTakeover("[EqualizerRack1_[Channel2]_Effect1]", "parameter2", true); + engine.softTakeover("[EqualizerRack1_[Channel2]_Effect1]", "parameter3", true); + engine.softTakeover("[EqualizerRack1_[Channel3]_Effect1]", "parameter1", true); + engine.softTakeover("[EqualizerRack1_[Channel3]_Effect1]", "parameter2", true); + engine.softTakeover("[EqualizerRack1_[Channel3]_Effect1]", "parameter3", true); + engine.softTakeover("[EqualizerRack1_[Channel4]_Effect1]", "parameter1", true); + engine.softTakeover("[EqualizerRack1_[Channel4]_Effect1]", "parameter2", true); + engine.softTakeover("[EqualizerRack1_[Channel4]_Effect1]", "parameter3", true); -// defineButton allows us to configure either the right deck or the left deck, depending on which -// is appropriate. This avoids extra logic in the function where we define all the magic numbers. -// We use a similar approach in the other define funcs. -TraktorS3.Deck.prototype.defineButton = function(msg, name, deckOffset, deckBitmask, deck2Offset, deck2Bitmask, fn) { - if (this.deckNumber === 2) { - deckOffset = deck2Offset; - deckBitmask = deck2Bitmask; + // engine.softTakeover("[Master]", "crossfader", true); + engine.softTakeover("[Master]", "gain", true); + // engine.softTakeover("[Master]", "headMix", true); + // engine.softTakeover("[Master]", "headGain", true); + for (let i = 1; i <= 16; ++i) { + engine.softTakeover("[Sampler" + i + "]", "pregain", true); + } } - this.controller.registerInputButton(msg, this.group, name, deckOffset, deckBitmask, fn.bind(this)); -}; -TraktorS3.Deck.prototype.defineJog = function(message, name, deckOffset, deck2Offset, callback) { - if (this.deckNumber === 2) { - deckOffset = deck2Offset; + registerInputJog(message, group, name, offset, bitmask, callback) { + // Jog wheels have 4 byte input + message.addControl(group, name, offset, "I", bitmask); + message.setCallback(group, name, callback); } - // Jog wheels have four byte input: 1 byte for distance ticks, and 3 bytes for a timecode. - message.addControl(this.group, name, deckOffset, "I", 0xFFFFFFFF); - message.setCallback(this.group, name, callback.bind(this)); -}; -// defineScaler configures ranged controls like knobs and sliders. -TraktorS3.Deck.prototype.defineScaler = function(msg, name, deckOffset, deckBitmask, deck2Offset, deck2Bitmask, fn) { - if (this.deckNumber === 2) { - deckOffset = deck2Offset; - deckBitmask = deck2Bitmask; + registerInputScaler(message, group, name, offset, bitmask, callback) { + message.addControl(group, name, offset, "H", bitmask); + message.setCallback(group, name, callback); } - this.controller.registerInputScaler(msg, this.group, name, deckOffset, deckBitmask, fn.bind(this)); -}; -TraktorS3.Deck.prototype.registerInputs = function(messageShort, messageLong) { - const deckFn = TraktorS3.Deck.prototype; - this.defineButton(messageShort, "!play", 0x03, 0x01, 0x06, 0x02, deckFn.playHandler); - this.defineButton(messageShort, "!cue_default", 0x02, 0x80, 0x06, 0x01, deckFn.cueHandler); - this.defineButton(messageShort, "!shift", 0x01, 0x01, 0x04, 0x02, deckFn.shiftHandler); - this.defineButton(messageShort, "!sync", 0x02, 0x08, 0x05, 0x10, deckFn.syncHandler); - this.defineButton(messageShort, "!keylock", 0x02, 0x10, 0x05, 0x20, deckFn.keylockHandler); - this.defineButton(messageShort, "!hotcues", 0x02, 0x20, 0x05, 0x40, deckFn.padModeHandler); - this.defineButton(messageShort, "!samples", 0x02, 0x40, 0x05, 0x80, deckFn.padModeHandler); - - this.defineButton(messageShort, "!pad_1", 0x03, 0x02, 0x06, 0x04, deckFn.numberButtonHandler); - this.defineButton(messageShort, "!pad_2", 0x03, 0x04, 0x06, 0x08, deckFn.numberButtonHandler); - this.defineButton(messageShort, "!pad_3", 0x03, 0x08, 0x06, 0x10, deckFn.numberButtonHandler); - this.defineButton(messageShort, "!pad_4", 0x03, 0x10, 0x06, 0x20, deckFn.numberButtonHandler); - this.defineButton(messageShort, "!pad_5", 0x03, 0x20, 0x06, 0x40, deckFn.numberButtonHandler); - this.defineButton(messageShort, "!pad_6", 0x03, 0x40, 0x06, 0x80, deckFn.numberButtonHandler); - this.defineButton(messageShort, "!pad_7", 0x03, 0x80, 0x07, 0x01, deckFn.numberButtonHandler); - this.defineButton(messageShort, "!pad_8", 0x04, 0x01, 0x07, 0x02, deckFn.numberButtonHandler); - - // TODO: bind touch: 0x09/0x40, 0x0A/0x02 - this.defineButton(messageShort, "!SelectTrack", 0x0B, 0x0F, 0x0C, 0xF0, deckFn.selectTrackHandler); - this.defineButton(messageShort, "!LoadSelectedTrack", 0x09, 0x01, 0x09, 0x08, deckFn.loadTrackHandler); - this.defineButton(messageShort, "!PreviewTrack", 0x01, 0x08, 0x04, 0x10, deckFn.previewTrackHandler); - // There is no control object to mark / unmark a track as played. - // this.defineButton(messageShort, "!SetPlayed", 0x01, 0x10, 0x04, 0x20, deckFn.SetPlayedHandler); - this.defineButton(messageShort, "!LibraryFocus", 0x01, 0x20, 0x04, 0x40, deckFn.LibraryFocusHandler); - this.defineButton(messageShort, "!MaximizeLibrary", 0x01, 0x40, 0x04, 0x80, deckFn.MaximizeLibraryHandler); - - // Loop control - // TODO: bind touch detections: 0x0A/0x01, 0x0A/0x08 - this.defineButton(messageShort, "!SelectLoop", 0x0C, 0x0F, 0x0D, 0xF0, deckFn.selectLoopHandler); - this.defineButton(messageShort, "!ActivateLoop", 0x09, 0x04, 0x09, 0x20, deckFn.activateLoopHandler); - - // Rev / Flux / Grid / Jog - this.defineButton(messageShort, "!reverse", 0x01, 0x04, 0x04, 0x08, deckFn.reverseHandler); - this.defineButton(messageShort, "!slip_enabled", 0x01, 0x02, 0x04, 0x04, deckFn.fluxHandler); - this.defineButton(messageShort, "quantize", 0x01, 0x80, 0x05, 0x01, deckFn.quantizeHandler); - this.defineButton(messageShort, "!jogButton", 0x02, 0x01, 0x05, 0x02, deckFn.jogButtonHandler); - - // Beatjump - // TODO: bind touch detections: 0x09/0x80, 0x0A/0x04 - this.defineButton(messageShort, "!SelectBeatjump", 0x0B, 0xF0, 0x0D, 0x0F, deckFn.selectBeatjumpHandler); - this.defineButton(messageShort, "!ActivateBeatjump", 0x09, 0x02, 0x09, 0x10, deckFn.activateBeatjumpHandler); - - // Jog wheels - this.defineButton(messageShort, "!jog_touch", 0x0A, 0x10, 0x0A, 0x20, deckFn.jogTouchHandler); - this.defineJog(messageShort, "!jog", 0x0E, 0x12, deckFn.jogHandler); - - this.defineScaler(messageLong, "rate", 0x01, 0xFFFF, 0x0D, 0xFFFF, deckFn.pitchSliderHandler); -}; - -TraktorS3.Deck.prototype.shiftHandler = function(field) { - // Mixxx only knows about one shift value, but this controller has two shift buttons. - // This control object could get confused if both physical buttons are pushed at the same - // time. - engine.setValue("[Controls]", "touch_shift", field.value); - this.shiftPressed = field.value; - if (field.value) { - engine.softTakeoverIgnoreNextValue("[Master]", "gain"); + registerInputButton(message, group, name, offset, bitmask, callback) { + message.addControl(group, name, offset, "B", bitmask); + message.setCallback(group, name, callback); } - this.controller.basicOutput(field.value, field.group, "!shift"); -}; -TraktorS3.Deck.prototype.playHandler = function(field) { - if (this.shiftPressed) { - engine.setValue(this.activeChannel, "start_stop", field.value); - } else if (field.value === 1) { - script.toggleControl(this.activeChannel, "play"); + parameterHandler(field) { + if (field.group === "[Channel4]" && this.channel4InputMode) { + engine.setParameter("[Microphone]", field.name, field.value / 4095); + } else { + engine.setParameter(field.group, field.name, field.value / 4095); + } } -}; -TraktorS3.Deck.prototype.cueHandler = function(field) { - if (this.shiftPressed) { - engine.setValue(this.activeChannel, "cue_gotoandstop", field.value); - } else { - engine.setValue(this.activeChannel, "cue_default", field.value); + anyShiftPressed() { + return this.Decks.deck1.shiftPressed || this.Decks.deck2.shiftPressed; } -}; -TraktorS3.Deck.prototype.syncHandler = function(field) { - if (this.shiftPressed) { - engine.setValue(this.activeChannel, "beatsync_phase", field.value); - // Light LED while pressed - this.colorOutput(field.value, "sync_enabled"); - return; - } - - // Unshifted - if (field.value) { - // We have to reimplement push-to-lock because it's only defined in the midi code - // in Mixxx. - if (engine.getValue(this.activeChannel, "sync_enabled") === 0) { - script.triggerControl(this.activeChannel, "beatsync"); - // Start timer to measure how long button is pressed - this.syncPressedTimer = engine.beginTimer(300, function() { - engine.setValue(this.activeChannel, "sync_enabled", 1); - // Reset sync button timer state if active - if (this.syncPressedTimer !== 0) { - this.syncPressedTimer = 0; - } - }.bind(this), this, true); - - // Light corresponding LED when button is pressed - this.colorOutput(1, "sync_enabled"); - } else { - // Deactivate sync lock - // LED is turned off by the callback handler for sync_enabled - engine.setValue(this.activeChannel, "sync_enabled", 0); + masterGainHandler(field) { + // Only adjust if shift is held. This will still adjust the sound card + // volume but it at least allows for control of Mixxx's master gain. + if (this.anyShiftPressed()) { + engine.setParameter(field.group, field.name, field.value / 4095); } - } else if (this.syncPressedTimer !== 0) { - // Timer still running -> stop it and unlight LED - engine.stopTimer(this.syncPressedTimer); - this.colorOutput(0, "sync_enabled"); } -}; -TraktorS3.Deck.prototype.keylockHandler = function(field) { - // shift + keylock resets pitch (in either mode). - if (this.shiftPressed) { - if (field.value) { - engine.setValue(this.activeChannel, "pitch_adjust_set_default", 1); + headphoneHandler(field) { + if (field.value === 0) { + return; } - } else if (TraktorS3.PitchSliderRelativeMode) { - if (field.value) { - // In relative mode on down-press, reset the values and note that - // the button is pressed. - this.keylockPressed = true; - this.keyAdjusted = false; + if (field.group === "[Channel4]" && this.channel4InputMode) { + script.toggleControl("[Microphone]", "pfl"); } else { - // On release, note that the button is released, and if the key *wasn't* adjusted, - // activate keylock. - this.keylockPressed = false; - if (!this.keyAdjusted) { - script.toggleControl(this.activeChannel, "keylock"); - } + script.toggleControl(field.group, "pfl"); } - } else if (field.value) { - // In absolute mode, do a simple toggle on down-press. - script.toggleControl(this.activeChannel, "keylock"); - } - - // Adjust the light on release depending on keylock status. Down-press is always lit. - if (!field.value) { - const val = engine.getValue(this.activeChannel, "keylock"); - this.colorOutput(val, "keylock"); - } else { - this.colorOutput(1, "keylock"); } -}; -// This handles when the mode buttons for the pads is pressed. -TraktorS3.Deck.prototype.padModeHandler = function(field) { - if (field.value === 0) { - return; - } + deckSwitchHandler(field) { + if (field.value === 0) { + if (this.deckSwitchPressed === field.group) { + this.deckSwitchPressed = ""; + } + return; + } - if (this.padModeState === 0 && field.name === "!samples") { - // If we are in hotcues mode and samples mode is activated - engine.setValue("[Samplers]", "show_samplers", 1); - this.padModeState = 1; - } else if (field.name === "!hotcues") { - // If we are in samples mode and hotcues mode is activated - this.padModeState = 0; - } - this.lightPads(); -}; + if (this.deckSwitchPressed === "") { + this.deckSwitchPressed = field.group; + } else { + // If a different deck switch is already pressed, do an instant + // double and do not select the deck. + const cloneFrom = this.Channels[this.deckSwitchPressed]; + const cloneFromNum = cloneFrom.parentDeck.deckNumber; + engine.setValue(field.group, "CloneFromDeck", cloneFromNum); + return; + } -TraktorS3.Deck.prototype.numberButtonHandler = function(field) { - const padNumber = parseInt(field.name[field.name.length - 1]); + const channel = this.Channels[field.group]; + const deck = channel.parentDeck; - // Hotcues mode - if (this.padModeState === 0) { - var action = this.shiftPressed ? "_clear" : "_activate"; - engine.setValue(this.activeChannel, "hotcue_" + padNumber + action, field.value); - return; - } + if (engine.isScratching(channel.groupNumber)) { + engine.scratchDisable(channel.groupNumber); + } - // Samples mode - let sampler = padNumber; - if (field.group === "deck2" && TraktorS3.SixteenSamplers) { - sampler += 8; + deck.activateChannel(channel); } - const playing = engine.getValue("[Sampler" + sampler + "]", "play"); - if (this.shiftPressed) { - if (playing) { - action = "cue_default"; - } else { - action = "eject"; + extModeHandler(field) { + if (!field.value) { + this.basicOutput(this.channel4InputMode, field.group, field.name); + return; } - engine.setValue("[Sampler" + sampler + "]", action, field.value); - return; - } - const loaded = engine.getValue("[Sampler" + sampler + "]", "track_loaded"); - if (loaded) { - if (TraktorS3.SamplerModePressAndHold) { - if (field.value) { - action = "cue_gotoandplay"; - } else { - action = "stop"; - } - engine.setValue("[Sampler" + sampler + "]", action, 1); + if (this.anyShiftPressed()) { + this.basicOutput(field.value, field.group, field.name); + this.inputModeLine = !this.inputModeLine; + this.setInputLineMode(this.inputModeLine); + return; + } + this.channel4InputMode = !this.channel4InputMode; + if (this.channel4InputMode) { + engine.softTakeoverIgnoreNextValue("[Microphone]", "volume"); + engine.softTakeoverIgnoreNextValue("[Microphone]", "pregain"); } else { - if (field.value) { - if (playing) { - action = "stop"; - } else { - action = "cue_gotoandplay"; - } - engine.setValue("[Sampler" + sampler + "]", action, 1); - } + engine.softTakeoverIgnoreNextValue("[Channel4]", "volume"); + engine.softTakeoverIgnoreNextValue("[Channel4]", "pregain"); } - return; + this.lightDeck("[Channel4]"); + this.basicOutput(this.channel4InputMode, field.group, field.name); } - // Play on an empty sampler loads that track into that sampler - engine.setValue("[Sampler" + sampler + "]", "LoadSelectedTrack", field.value); -}; -TraktorS3.Deck.prototype.selectTrackHandler = function(field) { - let delta = 1; - if ((field.value + 1) % 16 === this.browseKnobEncoderState) { - delta = -1; - } - this.browseKnobEncoderState = field.value; + registerOutputPackets() { + const outputA = new HIDPacket("outputA", 0x80); + const outputB = new HIDPacket("outputB", 0x81); - // When preview is held, rotating the library encoder scrolls through the previewing track. - if (this.previewPressed) { - let playPosition = engine.getValue("[PreviewDeck1]", "playposition"); - if (delta > 0) { - playPosition += 0.0125; - } else { - playPosition -= 0.0125; + for (var idx in this.Decks) { + var deck = this.Decks[idx]; + deck.registerOutputs(outputA, outputB); } - engine.setValue("[PreviewDeck1]", "playposition", playPosition); - return; - } - if (this.shiftPressed) { - engine.setValue("[Library]", "MoveHorizontal", delta); - } else { - engine.setValue("[Library]", "MoveVertical", delta); - } -}; + outputA.addOutput("[Channel1]", "!deck_A", 0x0A, "B"); + outputA.addOutput("[Channel2]", "!deck_B", 0x23, "B"); + outputA.addOutput("[Channel3]", "!deck_C", 0x0B, "B"); + outputA.addOutput("[Channel4]", "!deck_D", 0x24, "B"); -TraktorS3.Deck.prototype.loadTrackHandler = function(field) { - if (this.shiftPressed) { - engine.setValue(this.activeChannel, "eject", field.value); - } else { - engine.setValue(this.activeChannel, "LoadSelectedTrack", field.value); - } -}; + outputA.addOutput("[Channel1]", "pfl", 0x39, "B"); + outputA.addOutput("[Channel2]", "pfl", 0x3A, "B"); + outputA.addOutput("[Channel3]", "pfl", 0x38, "B"); + outputA.addOutput("[Channel4]", "pfl", 0x3B, "B"); -TraktorS3.Deck.prototype.previewTrackHandler = function(field) { - this.colorOutput(field.value, "!PreviewTrack"); - if (field.value === 1) { - this.previewPressed = true; - engine.setValue("[PreviewDeck1]", "LoadSelectedTrackAndPlay", 1); - } else { - this.previewPressed = false; - engine.setValue("[PreviewDeck1]", "play", 0); - } -}; + outputA.addOutput("[ChannelX]", "!fxButton1", 0x3C, "B"); + outputA.addOutput("[ChannelX]", "!fxButton2", 0x3D, "B"); + outputA.addOutput("[ChannelX]", "!fxButton3", 0x3E, "B"); + outputA.addOutput("[ChannelX]", "!fxButton4", 0x3F, "B"); + outputA.addOutput("[ChannelX]", "!fxButton0", 0x40, "B"); -TraktorS3.Deck.prototype.LibraryFocusHandler = function(field) { - this.colorOutput(field.value, "!LibraryFocus"); - if (field.value === 0) { - return; - } + outputA.addOutput("[Channel3]", "!fxEnabled", 0x34, "B"); + outputA.addOutput("[Channel1]", "!fxEnabled", 0x35, "B"); + outputA.addOutput("[Channel2]", "!fxEnabled", 0x36, "B"); + outputA.addOutput("[Channel4]", "!fxEnabled", 0x37, "B"); - engine.setValue("[Library]", "MoveFocus", field.value); -}; + outputA.addOutput("[Master]", "!extButton", 0x33, "B"); -TraktorS3.Deck.prototype.MaximizeLibraryHandler = function(field) { - if (field.value === 0) { - return; - } + this.hid.registerOutputPacket(outputA); - script.toggleControl("[Master]", "maximize_library"); -}; + const VuOffsets = { + "[Channel3]": 0x01, + "[Channel1]": 0x10, + "[Channel2]": 0x1F, + "[Channel4]": 0x2E + }; + for (const ch in VuOffsets) { + for (var i = 0; i < 14; i++) { + outputB.addOutput(ch, "!" + "VuMeter" + i, VuOffsets[ch] + i, "B"); + } + } -TraktorS3.Deck.prototype.selectLoopHandler = function(field) { - let delta = 1; - if ((field.value + 1) % 16 === this.loopKnobEncoderState) { - delta = -1; - } + const MasterVuOffsets = { + "VuMeterL": 0x3D, + "VuMeterR": 0x46 + }; + for (i = 0; i < 8; i++) { + outputB.addOutput("[Master]", "!" + "VuMeterL" + i, MasterVuOffsets.VuMeterL + i, "B"); + outputB.addOutput("[Master]", "!" + "VuMeterR" + i, MasterVuOffsets.VuMeterR + i, "B"); + } - if (this.shiftPressed) { - const beatjumpSize = engine.getValue(this.activeChannel, "beatjump_size"); - if (delta > 0) { - script.triggerControl(this.activeChannel, "loop_move_" + beatjumpSize + "_forward"); - } else { - script.triggerControl(this.activeChannel, "loop_move_" + beatjumpSize + "_backward"); + outputB.addOutput("[Master]", "PeakIndicatorL", 0x45, "B"); + outputB.addOutput("[Master]", "PeakIndicatorR", 0x4E, "B"); + + outputB.addOutput("[Channel3]", "PeakIndicator", 0x0F, "B"); + outputB.addOutput("[Channel1]", "PeakIndicator", 0x1E, "B"); + outputB.addOutput("[Channel2]", "PeakIndicator", 0x2D, "B"); + outputB.addOutput("[Channel4]", "PeakIndicator", 0x3C, "B"); + + this.hid.registerOutputPacket(outputB); + + for (idx in this.Decks) { + deck = this.Decks[idx]; + deck.linkOutputs(); } - } else { - if (delta > 0) { - script.triggerControl(this.activeChannel, "loop_double"); - } else { - script.triggerControl(this.activeChannel, "loop_halve"); + + for (idx in this.Channels) { + const chan = this.Channels[idx]; + chan.linkOutputs(); } - } - this.loopKnobEncoderState = field.value; -}; + engine.connectControl("[Microphone]", "pfl", this.pflOutput); -TraktorS3.Deck.prototype.activateLoopHandler = function(field) { - if (field.value === 0) { - return; - } - const isLoopActive = engine.getValue(this.activeChannel, "loop_enabled"); + engine.connectControl("[Master]", "maximize_library", TraktorS3.Controller.prototype.maximizeLibraryOutput.bind(this)); - if (this.shiftPressed) { - engine.setValue(this.activeChannel, "reloop_toggle", field.value); - } else { - if (isLoopActive) { - engine.setValue(this.activeChannel, "reloop_toggle", field.value); - } else { - engine.setValue(this.activeChannel, "beatloop_activate", field.value); + // Master VuMeters + this.masterVuMeter.VuMeterL.connection = engine.makeConnection("[Master]", "VuMeterL", TraktorS3.Controller.prototype.masterVuMeterHandler.bind(this)); + this.masterVuMeter.VuMeterR.connection = engine.makeConnection("[Master]", "VuMeterR", TraktorS3.Controller.prototype.masterVuMeterHandler.bind(this)); + this.linkChannelOutput("[Master]", "PeakIndicatorL", TraktorS3.Controller.prototype.peakOutput.bind(this)); + this.linkChannelOutput("[Master]", "PeakIndicatorR", TraktorS3.Controller.prototype.peakOutput.bind(this)); + this.guiTickConnection = engine.makeConnection("[Master]", "guiTick50ms", TraktorS3.Controller.prototype.guiTickHandler.bind(this)); + + // Sampler callbacks + for (i = 1; i <= 8; ++i) { + this.samplerCallbacks.push(engine.makeConnection("[Sampler" + i + "]", "track_loaded", TraktorS3.Controller.prototype.samplesOutput.bind(this))); + this.samplerCallbacks.push(engine.makeConnection("[Sampler" + i + "]", "play_indicator", TraktorS3.Controller.prototype.samplesOutput.bind(this))); } } -}; -TraktorS3.Deck.prototype.selectBeatjumpHandler = function(field) { - let delta = 1; - if ((field.value + 1) % 16 === this.moveKnobEncoderState) { - delta = -1; + linkChannelOutput(group, name, callback) { + this.hid.linkOutput(group, name, group, name, callback); } - if (this.shiftPressed) { - const beatjumpSize = engine.getValue(this.activeChannel, "beatjump_size"); - if (delta > 0) { - engine.setValue(this.activeChannel, "beatjump_size", beatjumpSize * 2); - } else { - engine.setValue(this.activeChannel, "beatjump_size", beatjumpSize / 2); + pflOutput(value, group, key) { + if (group === "[Microphone]" && this.channel4InputMode) { + this.basicOutput(value, "[Channel4]", key); + return; } - } else { - if (delta > 0) { - script.triggerControl(this.activeChannel, "beatjump_forward"); - } else { - script.triggerControl(this.activeChannel, "beatjump_backward"); + if (group === "[Channel4]" && !this.channel4InputMode) { + this.basicOutput(value, group, key); + return; + } + if (group.match(/^\[Channel[123]\]$/)) { + this.basicOutput(value, group, key); } + // Unhandled case, ignore. } - this.moveKnobEncoderState = field.value; -}; - -TraktorS3.Deck.prototype.activateBeatjumpHandler = function(field) { - if (this.shiftPressed) { - engine.setValue(this.activeChannel, "reloop_andstop", field.value); - } else { - engine.setValue(this.activeChannel, "beatlooproll_activate", field.value); + maximizeLibraryOutput(value, _group, _key) { + this.Decks.deck1.colorOutput(value, "!MaximizeLibrary"); + this.Decks.deck2.colorOutput(value, "!MaximizeLibrary"); } -}; -TraktorS3.Deck.prototype.reverseHandler = function(field) { - // this.basicOutput(field.value, "reverse"); - if (this.shiftPressed) { - engine.setValue(this.activeChannel, "reverse", field.value); - } else { - engine.setValue(this.activeChannel, "reverseroll", field.value); - } -}; + // Output drives lights that only have one color. + basicOutput(value, group, key) { + let ledValue = value; + if (value === 0 || value === false) { + // Off value + ledValue = 0x04; + } else if (value === 1 || value === true) { + // On value + ledValue = 0xFF; + } -TraktorS3.Deck.prototype.fluxHandler = function(field) { - if (field.value === 0) { - return; + this.hid.setOutput(group, key, ledValue, !this.batchingOutputs); } - script.toggleControl(this.activeChannel, "slip_enabled"); -}; -TraktorS3.Deck.prototype.quantizeHandler = function(field) { - if (field.value === 0) { - return; - } - if (this.shiftPressed) { - engine.setValue(this.activeChannel, "beats_translate_curpos", field.value); - } else { - script.toggleControl(this.activeChannel, "quantize"); - } -}; + peakOutput(value, group, key) { + let ledValue = 0x00; + if (value) { + ledValue = 0x7E; + } -TraktorS3.Deck.prototype.jogButtonHandler = function(field) { - if (field.value === 0) { - return; + this.hid.setOutput(group, key, ledValue, !this.batchingOutputs); } - this.jogToggled = !this.jogToggled; - this.colorOutput(this.jogToggled, "!jogButton"); -}; -TraktorS3.Deck.prototype.jogTouchHandler = function(field) { - if (!this.jogToggled) { - return; + masterVuMeterHandler(value, _group, key) { + this.masterVuMeter[key].updated = true; + this.masterVuMeter[key].value = value; } - if (field.value !== 0) { - engine.scratchEnable(this.activeChannelNumber, 768, 33.33334, TraktorS3.Alpha, TraktorS3.Beta); - } else { - engine.scratchDisable(this.activeChannelNumber); - - // If shift is pressed, reset right away. - if (this.shiftPressed) { - engine.setValue(this.activeChannel, "scratch2", 0.0); - this.playIndicatorHandler(0, this.activeChannel); - } - } -}; + vuMeterOutput(value, group, key, segments) { + // This handler is called a lot so it should be as fast as possible. + const scaledValue = value * segments; + const fullIllumCount = Math.floor(scaledValue); -TraktorS3.Deck.prototype.jogHandler = function(field) { - const deltas = this.wheelDeltas(field.value); + // Figure out how much the partially-illuminated segment is illuminated. + const partialIllum = (scaledValue - fullIllumCount) * 0x7F; - // If shift button is held, do a simple seek. - if (this.shiftPressed) { - let playPosition = engine.getValue(this.activeChannel, "playposition"); - playPosition += deltas[0] / 2048.0; - playPosition = Math.max(Math.min(playPosition, 1.0), 0.0); - engine.setValue(this.activeChannel, "playposition", playPosition); - return; + for (let i = 0; i < segments; i++) { + const segmentKey = "!" + key + i; + if (i < fullIllumCount) { + // Don't update lights until they're all done, so the last term is false. + this.hid.setOutput(group, segmentKey, 0x7F, false); + } else if (i === fullIllumCount) { + this.hid.setOutput(group, segmentKey, partialIllum, false); + } else { + this.hid.setOutput(group, segmentKey, 0x00, false); + } + } + if (!this.batchingOutputs) { + this.hid.OutputPackets.outputB.send(); + } } - const tickDelta = deltas[0]; - const timeDelta = deltas[1]; - if (engine.isScratching(this.activeChannelNumber)) { - engine.scratchTick(this.activeChannelNumber, tickDelta); - } else { - // The scratch rate is the ratio of the wheel's speed to "regular" - // speed, which we're going to call 33.33 RPM. It's 768 ticks for a - // circle, and 400000 ticks per second, and 33.33 RPM is 1.8 seconds per - // rotation, so the standard speed is 768 / (400000 * 1.8) - const thirtyThree = 768 / 720000; + resolveSampler(group) { + if (group === undefined) { + return undefined; + } - // Our actual speed is tickDelta / timeDelta. Take the ratio of those to - // get the rate ratio. - const velocity = (tickDelta / timeDelta) / thirtyThree; + const result = group.match(script.samplerRegEx); - engine.setValue(this.activeChannel, "jog", velocity); - } -}; + if (result === null) { + return undefined; + } -TraktorS3.Deck.prototype.wheelDeltas = function(value) { - // When the wheel is touched, 1 byte measures distance ticks, the other - // three represent a timer value. We can use the amount of time required for - // the number of ticks to elapse to get a velocity. - const tickval = value & 0xFF; - let timeval = value >>> 8; - - const prevTick = this.lastTickVal; - const prevTime = this.lastTickTime; - const prevTickWallClock = this.lastTickWallClock; - this.lastTickVal = tickval; - this.lastTickTime = timeval; - this.lastTickWallClock = Date.now(); - - // The user hasn't touched the jog wheel for a long time, so the internal timer may have looped - // around more than once. We have nothing to go by so return 0, 1 (1 to prevent divide by zero). - if (this.lastTickWallClock - prevTickWallClock > 20000) { - return [0, 1]; - } - - if (prevTime > timeval) { - // We looped around. Adjust current time so that subtraction works. - timeval += 0x1000000; - } - // Clamp Suspiciously low numbers. Even flicking the wheel as fast as I can, I can't get it - // lower than this value. Lower values (esp below zero) cause huge jumps / errors in playback. - const timeDelta = Math.max(timeval - prevTime, 500); - let tickDelta = 0; - - // Very generous 8bit loop-around detection. - if (prevTick >= 200 && tickval <= 100) { - tickDelta = tickval + 256 - prevTick; - } else if (prevTick <= 100 && tickval >= 200) { - tickDelta = tickval - prevTick - 256; - } else { - tickDelta = tickval - prevTick; + // Return sampler as number if we can + const strResult = result[1]; + if (strResult === undefined) { + return undefined; + } + return parseInt(strResult); } - return [tickDelta, timeDelta]; -}; - -TraktorS3.Deck.prototype.pitchSliderHandler = function(field) { - // Adapt HID value to rate control range. - const value = -1.0 + ((field.value / 4095) * 2.0); - if (TraktorS3.PitchSliderRelativeMode) { - if (this.pitchSliderLastValue === -1) { - this.pitchSliderLastValue = value; - } else { - // If shift is pressed, don't update any values. - if (this.shiftPressed) { - this.pitchSliderLastValue = value; + samplesOutput(value, group, key) { + // Sampler 1-8 -> Channel1 + // Samples 9-16 -> Channel2 + const sampler = this.resolveSampler(group); + let deck = this.Decks.deck1; + let num = sampler; + if (sampler === undefined) { + return; + } else if (sampler > 8 && sampler < 17) { + if (!TraktorS3.SixteenSamplers) { + // These samplers are ignored return; } + deck = this.Decks.deck2; + num = sampler - 8; + } - let relVal; - if (this.keylockPressed) { - relVal = 1.0 - engine.getValue(this.activeChannel, "pitch_adjust"); + // If we are in samples modes light corresponding LED + if (deck.padModeState !== 1) { + return; + } + if (key === "play_indicator" && engine.getValue(group, "track_loaded")) { + if (value) { + // Green light on play + this.hid.setOutput("deck1", "!pad_" + num, this.hid.LEDColors.GREEN + TraktorS3.LEDBrightValue, !this.batchingOutputs); + // Also light deck2 samplers in 8-sampler mode. + if (!TraktorS3.SixteenSamplers && this.Decks.deck2.padModeState === 1) { + this.hid.setOutput("deck2", "!pad_" + num, this.hid.LEDColors.GREEN + TraktorS3.LEDBrightValue, !this.batchingOutputs); + } } else { - relVal = engine.getValue(this.activeChannel, "rate"); + // Reset LED to base color + deck.colorOutput(1, "!pad_" + num); + if (!TraktorS3.SixteenSamplers && this.Decks.deck2.padModeState === 1) { + this.Decks.deck2.colorOutput(1, "!pad_" + num); + } } - // This can result in values outside -1 to 1, but that is valid for the - // rate control. This means the entire swing of the rate slider can be - // outside the range of the widget, but that's ok because the slider still - // works. - relVal += value - this.pitchSliderLastValue; - this.pitchSliderLastValue = value; - - if (this.keylockPressed) { - // To match the pitch change from adjusting the rate, flip the pitch - // adjustment. - engine.setValue(this.activeChannel, "pitch_adjust", 1.0 - relVal); - this.keyAdjusted = true; - } else { - engine.setValue(this.activeChannel, "rate", relVal); + } else if (key === "track_loaded") { + deck.colorOutput(value, "!pad_" + num); + if (!TraktorS3.SixteenSamplers && this.Decks.deck2.padModeState === 1) { + this.Decks.deck2.colorOutput(value, "!pad_" + num); } } - return; - } - - if (this.shiftPressed) { - // To match the pitch change from adjusting the rate, flip the pitch - // adjustment. - engine.setValue(this.activeChannel, "pitch_adjust", 1.0 - value); - } else { - engine.setValue(this.activeChannel, "rate", value); - } -}; - -//// Deck Outputs //// - -TraktorS3.Deck.prototype.defineOutput = function(packet, name, offsetA, offsetB) { - switch (this.deckNumber) { - case 1: - packet.addOutput(this.group, name, offsetA, "B"); - break; - case 2: - packet.addOutput(this.group, name, offsetB, "B"); - break; - } -}; - -TraktorS3.Deck.prototype.registerOutputs = function(outputA, _outputB) { - this.defineOutput(outputA, "!shift", 0x01, 0x1A); - this.defineOutput(outputA, "slip_enabled", 0x02, 0x1B); - this.defineOutput(outputA, "reverse", 0x03, 0x1C); - this.defineOutput(outputA, "!PreviewTrack", 0x04, 0x1D); - this.defineOutput(outputA, "!LibraryFocus", 0x06, 0x1F); - this.defineOutput(outputA, "!MaximizeLibrary", 0x07, 0x20); - this.defineOutput(outputA, "quantize", 0x08, 0x21); - this.defineOutput(outputA, "!jogButton", 0x09, 0x22); - this.defineOutput(outputA, "sync_enabled", 0x0C, 0x25); - this.defineOutput(outputA, "keylock", 0x0D, 0x26); - this.defineOutput(outputA, "hotcues", 0x0E, 0x27); - this.defineOutput(outputA, "samples", 0x0F, 0x28); - this.defineOutput(outputA, "cue_indicator", 0x10, 0x29); - this.defineOutput(outputA, "play_indicator", 0x11, 0x2A); - - this.defineOutput(outputA, "!pad_1", 0x12, 0x2B); - this.defineOutput(outputA, "!pad_2", 0x13, 0x2C); - this.defineOutput(outputA, "!pad_3", 0x14, 0x2D); - this.defineOutput(outputA, "!pad_4", 0x15, 0x2E); - this.defineOutput(outputA, "!pad_5", 0x16, 0x2F); - this.defineOutput(outputA, "!pad_6", 0x17, 0x30); - this.defineOutput(outputA, "!pad_7", 0x18, 0x31); - this.defineOutput(outputA, "!pad_8", 0x19, 0x32); - - // this.defineOutput(outputA, "addTrack", 0x03, 0x2A); - - const wheelOffsets = [0x43, 0x4B]; - for (let i = 0; i < 8; i++) { - this.defineOutput(outputA, "!" + "wheel" + i, wheelOffsets[0] + i, wheelOffsets[1] + i); } -}; -TraktorS3.Deck.prototype.defineLink = function(key, callback) { - switch (this.deckNumber) { - case 1: - this.controller.hid.linkOutput("deck1", key, "[Channel1]", key, callback); - engine.connectControl("[Channel3]", key, callback); - break; - case 2: - this.controller.hid.linkOutput("deck2", key, "[Channel2]", key, callback); - engine.connectControl("[Channel4]", key, callback); - break; + lightGroup(packet, outputGroupName, coGroupName) { + const groupOb = packet.groups[outputGroupName]; + for (const fieldName in groupOb) { + const field = groupOb[fieldName]; + if (field.name[0] === "!") { + continue; + } + if (field.mapped_callback !== undefined) { + const value = engine.getValue(coGroupName, field.name); + field.mapped_callback(value, coGroupName, field.name); + } + // No callback, no light! + } } -}; - -TraktorS3.Deck.prototype.linkOutputs = function() { - const colorOutput = function(value, _group, key) { - this.colorOutput(value, key); - }; - const basicOutput = function(value, _group, key) { - this.basicOutput(value, key); - }; - - this.defineLink("play_indicator", TraktorS3.Deck.prototype.playIndicatorHandler.bind(this)); - this.defineLink("cue_indicator", colorOutput.bind(this)); - this.defineLink("sync_enabled", colorOutput.bind(this)); - this.defineLink("keylock", colorOutput.bind(this)); - this.defineLink("slip_enabled", colorOutput.bind(this)); - this.defineLink("quantize", colorOutput.bind(this)); - this.defineLink("reverse", basicOutput.bind(this)); - this.defineLink("scratch2_enable", colorOutput.bind(this)); -}; + lightDeck(group, sendPackets) { + if (sendPackets === undefined) { + sendPackets = true; + } + // Freeze the lights while we do this update so we don't spam HID. + this.batchingOutputs = true; + for (var packetName in this.hid.OutputPackets) { + const packet = this.hid.OutputPackets[packetName]; + let deckGroupName = "deck1"; + if (group === "[Channel2]" || group === "[Channel4]") { + deckGroupName = "deck2"; + } -TraktorS3.Deck.prototype.deckBaseColor = function() { - return this.controller.hid.LEDColors[TraktorS3.ChannelColors[this.activeChannel]]; -}; + const deck = this.Decks[deckGroupName]; -// basicOutput drives lights that only have one color. -TraktorS3.Deck.prototype.basicOutput = function(value, key) { - // incoming value will be a channel, we have to resolve back to - // deck. - let ledValue = 0x20; - if (value === 1 || value === true) { - // On value - ledValue = 0x77; - } - this.controller.hid.setOutput(this.group, key, ledValue, !TraktorS3.batchingOutputs); -}; + this.lightGroup(packet, deckGroupName, group); + this.lightGroup(packet, group, group); -// colorOutput drives lights that have the palettized multicolor lights. -TraktorS3.Deck.prototype.colorOutput = function(value, key) { - let ledValue = this.deckBaseColor(); + deck.lightPads(); - if (value === 1 || value === true) { - ledValue += TraktorS3.LEDBrightValue; - } else { - ledValue += TraktorS3.LEDDimValue; + // These lights are different because either they aren't associated + // with a CO, or there are two buttons that point to the same CO. + deck.basicOutput(0, "!shift"); + deck.colorOutput(0, "!PreviewTrack"); + deck.colorOutput(0, "!LibraryFocus"); + deck.colorOutput(0, "!MaximizeLibrary"); + deck.colorOutput(deck.jogToggled, "!jogButton"); + if (group === "[Channel4]") { + this.basicOutput(0, "[Master]", "!extButton"); + } + } + // this.lightFx(); + // Selected deck lights + if (group === "[Channel1]") { + this.hid.setOutput("[Channel1]", "!deck_A", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel1]"]] + TraktorS3.LEDBrightValue, false); + this.hid.setOutput("[Channel3]", "!deck_C", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel3]"]] + TraktorS3.LEDDimValue, false); + } else if (group === "[Channel2]") { + this.hid.setOutput("[Channel2]", "!deck_B", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel2]"]] + TraktorS3.LEDBrightValue, false); + this.hid.setOutput("[Channel4]", "!deck_D", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel4]"]] + TraktorS3.LEDDimValue, false); + } else if (group === "[Channel3]") { + this.hid.setOutput("[Channel3]", "!deck_C", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel3]"]] + TraktorS3.LEDBrightValue, false); + this.hid.setOutput("[Channel1]", "!deck_A", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel1]"]] + TraktorS3.LEDDimValue, false); + } else if (group === "[Channel4]") { + this.hid.setOutput("[Channel4]", "!deck_D", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel4]"]] + TraktorS3.LEDBrightValue, false); + this.hid.setOutput("[Channel2]", "!deck_B", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel2]"]] + TraktorS3.LEDDimValue, false); + } + + this.batchingOutputs = false; + // And now send them all. + if (sendPackets) { + for (packetName in this.hid.OutputPackets) { + this.hid.OutputPackets[packetName].send(); + } + } } - this.controller.hid.setOutput(this.group, key, ledValue, !this.controller.batchingOutputs); -}; -TraktorS3.Deck.prototype.playIndicatorHandler = function(value, group, _key) { - // Also call regular handler - this.basicOutput(value, "play_indicator"); - this.wheelOutputByValue(group, value); -}; + // Render wheel positions, channel VU meters, and master vu meters + guiTickHandler() { + this.batchingOutputs = true; + let gotUpdate = false; + gotUpdate |= this.Channels[this.Decks.deck1.activeChannel].lightWheelPosition(); + gotUpdate |= this.Channels[this.Decks.deck2.activeChannel].lightWheelPosition(); -TraktorS3.Deck.prototype.colorForHotcue = function(num) { - const colorCode = engine.getValue(this.activeChannel, "hotcue_" + num + "_color"); - return this.controller.colorMap.getValueForNearestColor(colorCode); -}; + for (const vu in this.masterVuMeter) { + if (this.masterVuMeter[vu].updated) { + this.vuMeterOutput(this.masterVuMeter[vu].value, "[Master]", vu, 8); + this.masterVuMeter[vu].updated = false; + gotUpdate = true; + } + } + for (let ch = 1; ch <= 4; ch++) { + const chan = this.Channels["[Channel" + ch + "]"]; + if (chan.vuMeterUpdated) { + this.vuMeterOutput(chan.vuMeterValue, chan.group, "VuMeter", 14); + chan.vuMeterUpdated = false; + gotUpdate = true; + } + } -TraktorS3.Deck.prototype.lightHotcue = function(number) { - const loaded = engine.getValue(this.activeChannel, "hotcue_" + number + "_enabled"); - const active = engine.getValue(this.activeChannel, "hotcue_" + number + "_activate"); - let ledValue = this.controller.hid.LEDColors.WHITE; - if (loaded) { - ledValue = this.colorForHotcue(number); - ledValue += TraktorS3.LEDDimValue; - } - if (active) { - ledValue += TraktorS3.LEDBrightValue; - } else { - ledValue += TraktorS3.LEDDimValue; - } - this.controller.hid.setOutput(this.group, "!pad_" + number, ledValue, !TraktorS3.batchingOutputs); -}; + this.batchingOutputs = false; -TraktorS3.Deck.prototype.lightPads = function() { - // Samplers - if (this.padModeState === 1) { - this.colorOutput(0, "hotcues"); - this.colorOutput(1, "samples"); - for (var i = 1; i <= 8; i++) { - let idx = i; - if (this.group === "deck2" && TraktorS3.SixteenSamplers) { - idx += 8; + if (gotUpdate) { + for (const packetName in this.hid.OutputPackets) { + this.hid.OutputPackets[packetName].send(); } - const loaded = engine.getValue("[Sampler" + idx + "]", "track_loaded"); - this.colorOutput(loaded, "!pad_" + i); } - } else { - this.colorOutput(1, "hotcues"); - this.colorOutput(0, "samples"); - for (i = 1; i <= 8; ++i) { - this.lightHotcue(i); + } + + // A special packet sent to the controller switches between mic and line + // input modes. if lineMode is true, sets input to line. Otherwise, mic. + setInputLineMode(lineMode) { + const packet = Array(); + packet.length = 33; + packet[0] = 0x20; + if (!lineMode) { + packet[1] = 0x08; } + controller.send(packet, packet.length, 0xF4); } }; -TraktorS3.Deck.prototype.wheelOutputByValue = function(group, value) { - if (group !== this.activeChannel) { - return; - } +//// Deck Objects //// +// Decks are the physical controllers on either side of the controller. Each +// Deck can control 2 channels. +TraktorS3.Deck = class { + constructor(controller, deckNumber, group) { + this.controller = controller; + this.deckNumber = deckNumber; + this.group = group; + this.activeChannel = "[Channel" + deckNumber + "]"; + this.activeChannelNumber = deckNumber; + // When true, touching the wheel enables scratch mode. When off, + // touching the wheel has no special effect + this.jogToggled = TraktorS3.JogDefaultOn; + this.shiftPressed = false; + + // State for pitch slider relative mode + this.pitchSliderLastValue = -1; + this.keylockPressed = false; + this.keyAdjusted = false; + + // Various states + this.syncPressedTimer = 0; + this.previewPressed = false; + // padModeState 0 is hotcues, 1 is samplers + this.padModeState = 0; - let ledValue = this.deckBaseColor(); + // Jog wheel state + this.lastTickVal = 0; + this.lastTickTime = 0; + this.lastTickWallClock = 0; - if (value === 1 || value === true) { - ledValue += TraktorS3.LEDBrightValue; - } else { - ledValue = 0x00; + // Knob encoder states (hold values between 0x0 and 0xF) Rotate to the + // right is +1 and to the left is means -1 + this.browseKnobEncoderState = 0; + this.loopKnobEncoderState = 0; + this.moveKnobEncoderState = 0; } - this.wheelOutput(group, - [ledValue, ledValue, ledValue, ledValue, ledValue, ledValue, ledValue, ledValue]); -}; -TraktorS3.Deck.prototype.wheelOutput = function(group, valueArray) { - if (group !== this.activeChannel) { - return; + activateChannel(channel) { + if (channel.parentDeck !== this) { + HIDDebug("Programming ERROR: tried to activate a channel with a deck that is not its parent"); + return; + } + this.activeChannel = channel.group; + this.activeChannelNumber = channel.groupNumber; + engine.softTakeoverIgnoreNextValue(this.activeChannel, "rate"); + this.controller.lightDeck(this.activeChannel); } - for (let i = 0; i < 8; i++) { - this.controller.hid.setOutput(this.group, "!wheel" + i, valueArray[i], false); - } - if (!TraktorS3.batchingOutputs) { - for (const packetName in this.controller.hid.OutputPackets) { - this.controller.hid.OutputPackets[packetName].send(); + // defineButton allows us to configure either the right deck or the left + // deck, depending on which is appropriate. This avoids extra logic in the + // function where we define all the magic numbers. We use a similar approach + // in the other define funcs. + defineButton(msg, name, deckOffset, deckBitmask, deck2Offset, deck2Bitmask, fn) { + if (this.deckNumber === 2) { + deckOffset = deck2Offset; + deckBitmask = deck2Bitmask; } + this.controller.registerInputButton(msg, this.group, name, deckOffset, deckBitmask, fn.bind(this)); } -}; -///////////////////////// -//// Channel Objects //// -//// -//// Channels don't have much state, just the fx button state. -TraktorS3.Channel = function(controller, parentDeck, group) { - this.controller = controller; - this.parentDeck = parentDeck; - this.group = group; - // We need the channel number for the scratch controls - this.groupNumber = Number(group.match(/\[Channel(\d+)\]/)[1]); - this.fxEnabledState = false; - - this.trackDurationSec = 0; - this.positionUpdated = false; - this.curPosition = -1; - this.endOfTrackTimer = 0; - this.endOfTrack = false; - this.endOfTrackBlinkState = 0; - this.vuMeterUpdated = false; - this.vuMeterValue = 0; - - this.vuConnection = {}; - this.clipConnection = {}; - this.hotcueCallbacks = []; - - // The script by default doesn't change any of the deck's settings, but it's - // useful to be able to initialize these settings to your preferences when - // you turn on the controller - if (TraktorS3.DefaultBeatJumpSize !== null) { - engine.setValue(group, "beatjump_size", TraktorS3.DefaultBeatJumpSize); - } - if (TraktorS3.DefaultBeatLoopLength !== null) { - engine.setValue(group, "beatloop_size", TraktorS3.DefaultBeatLoopLength); - } - if (TraktorS3.DefaultSyncEnabled !== null) { - engine.setValue(group, "sync_enabled", TraktorS3.DefaultSyncEnabled); - } - if (TraktorS3.DefaultQuantizeEnabled !== null) { - engine.setValue(group, "quantize", TraktorS3.DefaultQuantizeEnabled); - } - if (TraktorS3.DefaultKeylockEnabled !== null) { - engine.setValue(group, "keylock", TraktorS3.DefaultKeylockEnabled); + defineJog(message, name, deckOffset, deck2Offset, callback) { + if (this.deckNumber === 2) { + deckOffset = deck2Offset; + } + // Jog wheels have four byte input: 1 byte for distance ticks, and 3 bytes for a timecode. + message.addControl(this.group, name, deckOffset, "I", 0xFFFFFFFF); + message.setCallback(this.group, name, callback.bind(this)); } -}; -// Finds the shortest distance between two angles on the wheel, assuming -// 0-8.0 angle value. -TraktorS3.wheelSegmentDistance = function(segNum, angle) { - // Account for wraparound - if (Math.abs(segNum - angle) > 4) { - if (angle > segNum) { - segNum += 8; - } else { - angle += 8; + // defineScaler configures ranged controls like knobs and sliders. + defineScaler(msg, name, deckOffset, deckBitmask, deck2Offset, deck2Bitmask, fn) { + if (this.deckNumber === 2) { + deckOffset = deck2Offset; + deckBitmask = deck2Bitmask; } + this.controller.registerInputScaler(msg, this.group, name, deckOffset, deckBitmask, fn.bind(this)); } - return Math.abs(angle - segNum); -}; -TraktorS3.Channel.prototype.trackLoadedHandler = function() { - const trackSamples = engine.getValue(this.group, "track_samples"); - if (trackSamples === 0) { - this.trackDurationSec = 0; - return; + registerInputs(messageShort, messageLong) { + const deckFn = TraktorS3.Deck.prototype; + this.defineButton(messageShort, "!play", 0x03, 0x01, 0x06, 0x02, deckFn.playHandler); + this.defineButton(messageShort, "!cue_default", 0x02, 0x80, 0x06, 0x01, deckFn.cueHandler); + this.defineButton(messageShort, "!shift", 0x01, 0x01, 0x04, 0x02, deckFn.shiftHandler); + this.defineButton(messageShort, "!sync", 0x02, 0x08, 0x05, 0x10, deckFn.syncHandler); + this.defineButton(messageShort, "!keylock", 0x02, 0x10, 0x05, 0x20, deckFn.keylockHandler); + this.defineButton(messageShort, "!hotcues", 0x02, 0x20, 0x05, 0x40, deckFn.padModeHandler); + this.defineButton(messageShort, "!samples", 0x02, 0x40, 0x05, 0x80, deckFn.padModeHandler); + + this.defineButton(messageShort, "!pad_1", 0x03, 0x02, 0x06, 0x04, deckFn.numberButtonHandler); + this.defineButton(messageShort, "!pad_2", 0x03, 0x04, 0x06, 0x08, deckFn.numberButtonHandler); + this.defineButton(messageShort, "!pad_3", 0x03, 0x08, 0x06, 0x10, deckFn.numberButtonHandler); + this.defineButton(messageShort, "!pad_4", 0x03, 0x10, 0x06, 0x20, deckFn.numberButtonHandler); + this.defineButton(messageShort, "!pad_5", 0x03, 0x20, 0x06, 0x40, deckFn.numberButtonHandler); + this.defineButton(messageShort, "!pad_6", 0x03, 0x40, 0x06, 0x80, deckFn.numberButtonHandler); + this.defineButton(messageShort, "!pad_7", 0x03, 0x80, 0x07, 0x01, deckFn.numberButtonHandler); + this.defineButton(messageShort, "!pad_8", 0x04, 0x01, 0x07, 0x02, deckFn.numberButtonHandler); + + // TODO: bind touch: 0x09/0x40, 0x0A/0x02 + this.defineButton(messageShort, "!SelectTrack", 0x0B, 0x0F, 0x0C, 0xF0, deckFn.selectTrackHandler); + this.defineButton(messageShort, "!LoadSelectedTrack", 0x09, 0x01, 0x09, 0x08, deckFn.loadTrackHandler); + this.defineButton(messageShort, "!PreviewTrack", 0x01, 0x08, 0x04, 0x10, deckFn.previewTrackHandler); + // There is no control object to mark / unmark a track as played. + // this.defineButton(messageShort, "!SetPlayed", 0x01, 0x10, 0x04, 0x20, + // deckFn.SetPlayedHandler); + this.defineButton(messageShort, "!LibraryFocus", 0x01, 0x20, 0x04, 0x40, deckFn.LibraryFocusHandler); + this.defineButton(messageShort, "!MaximizeLibrary", 0x01, 0x40, 0x04, 0x80, deckFn.MaximizeLibraryHandler); + + // Loop control + // TODO: bind touch detections: 0x0A/0x01, 0x0A/0x08 + this.defineButton(messageShort, "!SelectLoop", 0x0C, 0x0F, 0x0D, 0xF0, deckFn.selectLoopHandler); + this.defineButton(messageShort, "!ActivateLoop", 0x09, 0x04, 0x09, 0x20, deckFn.activateLoopHandler); + + // Rev / Flux / Grid / Jog + this.defineButton(messageShort, "!reverse", 0x01, 0x04, 0x04, 0x08, deckFn.reverseHandler); + this.defineButton(messageShort, "!slip_enabled", 0x01, 0x02, 0x04, 0x04, deckFn.fluxHandler); + this.defineButton(messageShort, "quantize", 0x01, 0x80, 0x05, 0x01, deckFn.quantizeHandler); + this.defineButton(messageShort, "!jogButton", 0x02, 0x01, 0x05, 0x02, deckFn.jogButtonHandler); + + // Beatjump + // TODO: bind touch detections: 0x09/0x80, 0x0A/0x04 + this.defineButton(messageShort, "!SelectBeatjump", 0x0B, 0xF0, 0x0D, 0x0F, deckFn.selectBeatjumpHandler); + this.defineButton(messageShort, "!ActivateBeatjump", 0x09, 0x02, 0x09, 0x10, deckFn.activateBeatjumpHandler); + + // Jog wheels + this.defineButton(messageShort, "!jog_touch", 0x0A, 0x10, 0x0A, 0x20, deckFn.jogTouchHandler); + this.defineJog(messageShort, "!jog", 0x0E, 0x12, deckFn.jogHandler); + + this.defineScaler(messageLong, "rate", 0x01, 0xFFFF, 0x0D, 0xFFFF, deckFn.pitchSliderHandler); + } + + shiftHandler(field) { + // Mixxx only knows about one shift value, but this controller has two + // shift buttons. This control object could get confused if both + // physical buttons are pushed at the same time. + engine.setValue("[Controls]", "touch_shift", field.value); + this.shiftPressed = field.value; + if (field.value) { + engine.softTakeoverIgnoreNextValue("[Master]", "gain"); + } + this.controller.basicOutput(field.value, field.group, "!shift"); } - const trackSampleRate = engine.getValue(this.group, "track_samplerate"); - // Assume stereo. - this.trackDurationSec = trackSamples / 2.0 / trackSampleRate; - this.parentDeck.lightPads(); -}; -TraktorS3.Channel.prototype.endOfTrackHandler = function(value) { - this.endOfTrack = value; - if (!value) { - if (this.endOfTrackTimer) { - engine.stopTimer(this.endOfTrackTimer); - this.endOfTrackTimer = 0; + playHandler(field) { + if (this.shiftPressed) { + engine.setValue(this.activeChannel, "start_stop", field.value); + } else if (field.value === 1) { + script.toggleControl(this.activeChannel, "play"); } - return; } - this.endOfTrackTimer = engine.beginTimer(400, function() { - this.endOfTrackBlinkState = !this.endOfTrackBlinkState; - }, false); -}; -TraktorS3.Channel.prototype.playpositionChanged = function(value) { - if (this.parentDeck.activeChannel !== this.group) { - return; + cueHandler(field) { + if (this.shiftPressed) { + engine.setValue(this.activeChannel, "cue_gotoandstop", field.value); + } else { + engine.setValue(this.activeChannel, "cue_default", field.value); + } } - // How many segments away from the actual angle should we light? - // (in both directions, so "2" will light up to four segments) - if (this.trackDurationSec === 0) { - const samples = engine.getValue(this.group, "track_loaded"); - if (samples > 0) { - this.trackLoadedHandler(); - } else { - // No track loaded, abort + syncHandler(field) { + if (this.shiftPressed) { + engine.setValue(this.activeChannel, "beatsync_phase", field.value); + // Light LED while pressed + this.colorOutput(field.value, "sync_enabled"); return; } - } - this.curPosition = value * this.trackDurationSec; - this.positionUpdated = true; -}; -TraktorS3.Channel.prototype.vuMeterHandler = function(value) { - this.vuMeterUpdated = true; - this.vuMeterValue = value; -}; - -TraktorS3.Channel.prototype.linkOutputs = function() { - this.vuConnection = engine.makeConnection(this.group, "VuMeter", TraktorS3.Channel.prototype.vuMeterHandler.bind(this)); - this.clipConnection = engine.makeConnection(this.group, "PeakIndicator", TraktorS3.Controller.prototype.peakOutput.bind(this.controller)); - this.controller.linkChannelOutput(this.group, "pfl", TraktorS3.Controller.prototype.pflOutput.bind(this.controller)); - for (let j = 1; j <= 8; j++) { - this.hotcueCallbacks.push(engine.makeConnection(this.group, "hotcue_" + j + "_enabled", - TraktorS3.Channel.prototype.hotcuesOutput.bind(this))); - this.hotcueCallbacks.push(engine.makeConnection(this.group, "hotcue_" + j + "_activate", - TraktorS3.Channel.prototype.hotcuesOutput.bind(this))); - this.hotcueCallbacks.push(engine.makeConnection(this.group, "hotcue_" + j + "_color", - TraktorS3.Channel.prototype.hotcuesOutput.bind(this))); - } -}; - -TraktorS3.Channel.prototype.channelBaseColor = function() { - if (this.group === "[Channel4]" && this.controller.channel4InputMode) { - return this.controller.hid.LEDColors[this.controller.hid.LEDColors.OFF]; - } - return this.controller.hid.LEDColors[TraktorS3.ChannelColors[this.group]]; -}; - -// colorOutput drives lights that have the palettized multicolor lights. -TraktorS3.Channel.prototype.colorOutput = function(value, key) { - let ledValue = this.channelBaseColor(); - if (value === 1 || value === true) { - ledValue += TraktorS3.LEDBrightValue; - } else { - ledValue += TraktorS3.LEDDimValue; - } - this.controller.hid.setOutput(this.group, key, ledValue, !this.controller.batchingOutputs); -}; + // Unshifted + if (field.value) { + // We have to reimplement push-to-lock because it's only defined in + // the midi code in Mixxx. + if (engine.getValue(this.activeChannel, "sync_enabled") === 0) { + script.triggerControl(this.activeChannel, "beatsync"); + // Start timer to measure how long button is pressed + this.syncPressedTimer = engine.beginTimer(300, function() { + engine.setValue(this.activeChannel, "sync_enabled", 1); + // Reset sync button timer state if active + if (this.syncPressedTimer !== 0) { + this.syncPressedTimer = 0; + } + }.bind(this), this, true); -TraktorS3.Channel.prototype.hotcuesOutput = function(_value, group, key) { - const deck = this.controller.Channels[group].parentDeck; - if (deck.activeChannel !== group) { - // Not active, ignore - return; - } - if (deck.padModeState !== 0) { - return; - } - const matches = key.match(/hotcue_(\d+)_/); - if (matches.length !== 2) { - HIDDebug("Didn't get expected hotcue number from string: " + key); - return; + // Light corresponding LED when button is pressed + this.colorOutput(1, "sync_enabled"); + } else { + // Deactivate sync lock + // LED is turned off by the callback handler for sync_enabled + engine.setValue(this.activeChannel, "sync_enabled", 0); + } + } else if (this.syncPressedTimer !== 0) { + // Timer still running -> stop it and unlight LED + engine.stopTimer(this.syncPressedTimer); + this.colorOutput(0, "sync_enabled"); + } } - const cueNum = matches[1]; - deck.lightHotcue(cueNum); -}; -// Returns true if there was an update. -TraktorS3.Channel.prototype.lightWheelPosition = function() { - if (!this.positionUpdated) { - return false; - } - this.positionUpdated = false; - const rotations = this.curPosition * (1 / 1.8); // 1/1.8 is rotations per second (33 1/3 RPM) - // Calculate angle from 0-1.0 - const angle = rotations - Math.floor(rotations); - // The wheel has 8 segments - const wheelAngle = 8.0 * angle; - const baseLedValue = this.channelBaseColor(); - // Reduce the dimming distance at the end of track. - let dimDistance = this.endOfTrack ? 2.5 : 1.5; - const segValues = [0, 0, 0, 0, 0, 0, 0, 0]; - for (let seg = 0; seg < 8; seg++) { - const distance = TraktorS3.wheelSegmentDistance(seg, wheelAngle); - let brightVal = Math.round(4 * (1.0 - (distance / dimDistance))); - if (this.endOfTrack) { - dimDistance = 1.5; - brightVal = Math.round(4 * (1.0 - (distance / dimDistance))); - if (this.endOfTrackBlinkState) { - brightVal = brightVal > 0x03 ? 0x04 : 0x02; + keylockHandler(field) { + // shift + keylock resets pitch (in either mode). + if (this.shiftPressed) { + if (field.value) { + engine.setValue(this.activeChannel, "pitch_adjust_set_default", 1); + } + } else if (TraktorS3.PitchSliderRelativeMode) { + if (field.value) { + // In relative mode on down-press, reset the values and note + // that the button is pressed. + this.keylockPressed = true; + this.keyAdjusted = false; } else { - brightVal = brightVal > 0x02 ? 0x04 : 0x00; + // On release, note that the button is released, and if the key + // *wasn't* adjusted, activate keylock. + this.keylockPressed = false; + if (!this.keyAdjusted) { + script.toggleControl(this.activeChannel, "keylock"); + } } + } else if (field.value) { + // In absolute mode, do a simple toggle on down-press. + script.toggleControl(this.activeChannel, "keylock"); } - if (brightVal <= 0) { - segValues[seg] = 0x00; + + // Adjust the light on release depending on keylock status. Down-press is always lit. + if (!field.value) { + const val = engine.getValue(this.activeChannel, "keylock"); + this.colorOutput(val, "keylock"); } else { - segValues[seg] = baseLedValue + brightVal - 1; + this.colorOutput(1, "keylock"); } } - this.parentDeck.wheelOutput(this.group, segValues); - return true; -}; - -// FXControl is an object that manages the gray area in the middle of the -// controller: the fx control knobs, fxenable buttons, and fx select buttons. -TraktorS3.FXControl = function(controller) { - // 0 is filter, 1-4 are FX Units 1-4 - this.FILTER_EFFECT = 0; - this.activeFX = this.FILTER_EFFECT; - this.controller = controller; - - this.enablePressed = { - "[Channel1]": false, - "[Channel2]": false, - "[Channel3]": false, - "[Channel4]": false - }; - this.selectPressed = [ - false, - false, - false, - false, - false - ]; - this.selectBlinkState = [ - false, - false, - false, - false, - false - ]; - // States - this.STATE_FILTER = 0; - // State for when an effect select has been pressed, but not released yet. - this.STATE_EFFECT_INIT = 1; - // State for when an effect select has been pressed and released. - this.STATE_EFFECT = 2; - this.STATE_FOCUS = 3; + // This handles when the mode buttons for the pads is pressed. + padModeHandler(field) { + if (field.value === 0) { + return; + } - this.currentState = this.STATE_FILTER; + if (this.padModeState === 0 && field.name === "!samples") { + // If we are in hotcues mode and samples mode is activated + engine.setValue("[Samplers]", "show_samplers", 1); + this.padModeState = 1; + } else if (field.name === "!hotcues") { + // If we are in samples mode and hotcues mode is activated + this.padModeState = 0; + } + this.lightPads(); + } - // Light states - this.LIGHT_OFF = 0; - this.LIGHT_DIM = 1; - this.LIGHT_BRIGHT = 2; + numberButtonHandler(field) { + const padNumber = parseInt(field.name[field.name.length - 1]); - this.focusBlinkState = false; - this.focusBlinkTimer = 0; -}; + // Hotcues mode + if (this.padModeState === 0) { + var action = this.shiftPressed ? "_clear" : "_activate"; + engine.setValue(this.activeChannel, "hotcue_" + padNumber + action, field.value); + return; + } -TraktorS3.FXControl.prototype.registerInputs = function(messageShort, messageLong) { - // FX Buttons - const fxFn = TraktorS3.FXControl.prototype; - this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx1", 0x08, 0x08, fxFn.fxSelectHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx2", 0x08, 0x10, fxFn.fxSelectHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx3", 0x08, 0x20, fxFn.fxSelectHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx4", 0x08, 0x40, fxFn.fxSelectHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx0", 0x08, 0x80, fxFn.fxSelectHandler.bind(this)); - - this.controller.registerInputButton(messageShort, "[Channel3]", "!fxEnabled", 0x07, 0x08, fxFn.fxEnableHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[Channel1]", "!fxEnabled", 0x07, 0x10, fxFn.fxEnableHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[Channel2]", "!fxEnabled", 0x07, 0x20, fxFn.fxEnableHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[Channel4]", "!fxEnabled", 0x07, 0x40, fxFn.fxEnableHandler.bind(this)); - - this.controller.registerInputScaler(messageLong, "[Channel1]", "!fxKnob", 0x39, 0xFFFF, fxFn.fxKnobHandler.bind(this)); - this.controller.registerInputScaler(messageLong, "[Channel2]", "!fxKnob", 0x3B, 0xFFFF, fxFn.fxKnobHandler.bind(this)); - this.controller.registerInputScaler(messageLong, "[Channel3]", "!fxKnob", 0x37, 0xFFFF, fxFn.fxKnobHandler.bind(this)); - this.controller.registerInputScaler(messageLong, "[Channel4]", "!fxKnob", 0x3D, 0xFFFF, fxFn.fxKnobHandler.bind(this)); -}; + // Samples mode + let sampler = padNumber; + if (field.group === "deck2" && TraktorS3.SixteenSamplers) { + sampler += 8; + } -TraktorS3.FXControl.prototype.channelToIndex = function(group) { - const result = group.match(script.channelRegEx); - if (result === null) { - return undefined; + const playing = engine.getValue("[Sampler" + sampler + "]", "play"); + if (this.shiftPressed) { + if (playing) { + action = "cue_default"; + } else { + action = "eject"; + } + engine.setValue("[Sampler" + sampler + "]", action, field.value); + return; + } + const loaded = engine.getValue("[Sampler" + sampler + "]", "track_loaded"); + if (loaded) { + if (TraktorS3.SamplerModePressAndHold) { + if (field.value) { + action = "cue_gotoandplay"; + } else { + action = "stop"; + } + engine.setValue("[Sampler" + sampler + "]", action, 1); + } else { + if (field.value) { + if (playing) { + action = "stop"; + } else { + action = "cue_gotoandplay"; + } + engine.setValue("[Sampler" + sampler + "]", action, 1); + } + } + return; + } + // Play on an empty sampler loads that track into that sampler + engine.setValue("[Sampler" + sampler + "]", "LoadSelectedTrack", field.value); } - // Unmap from channel number to button index. - switch (result[1]) { - case "1": - return 2; - case "2": - return 3; - case "3": - return 1; - case "4": - return 4; - } - return undefined; -}; -TraktorS3.FXControl.prototype.firstPressedSelect = function() { - for (const idx in this.selectPressed) { - if (this.selectPressed[idx]) { - return idx; + selectTrackHandler(field) { + let delta = 1; + if ((field.value + 1) % 16 === this.browseKnobEncoderState) { + delta = -1; + } + this.browseKnobEncoderState = field.value; + + // When preview is held, rotating the library encoder scrolls through the previewing track. + if (this.previewPressed) { + let playPosition = engine.getValue("[PreviewDeck1]", "playposition"); + if (delta > 0) { + playPosition += 0.0125; + } else { + playPosition -= 0.0125; + } + engine.setValue("[PreviewDeck1]", "playposition", playPosition); + return; + } + + if (this.shiftPressed) { + engine.setValue("[Library]", "MoveHorizontal", delta); + } else { + engine.setValue("[Library]", "MoveVertical", delta); } } - return undefined; -}; -TraktorS3.FXControl.prototype.firstPressedEnable = function() { - for (const ch in this.enablePressed) { - if (this.enablePressed[ch]) { - return ch; + loadTrackHandler(field) { + if (this.shiftPressed) { + engine.setValue(this.activeChannel, "eject", field.value); + } else { + engine.setValue(this.activeChannel, "LoadSelectedTrack", field.value); } } - return undefined; -}; -TraktorS3.FXControl.prototype.anyEnablePressed = function() { - for (const key in this.enablePressed) { - if (this.enablePressed[key]) { - return true; + previewTrackHandler(field) { + this.colorOutput(field.value, "!PreviewTrack"); + if (field.value === 1) { + this.previewPressed = true; + engine.setValue("[PreviewDeck1]", "LoadSelectedTrackAndPlay", 1); + } else { + this.previewPressed = false; + engine.setValue("[PreviewDeck1]", "play", 0); } } - return false; -}; -TraktorS3.FXControl.prototype.changeState = function(newState) { - if (newState === this.currentState) { - return; + LibraryFocusHandler(field) { + this.colorOutput(field.value, "!LibraryFocus"); + if (field.value === 0) { + return; + } + + engine.setValue("[Library]", "MoveFocus", field.value); } - // Ignore next values for all knob actions. This is safe to do for all knobs - // even if we're ignoring knobs that aren't active in the new state. - for (let ch = 1; ch <= 4; ch++) { - var group = "[Channel" + ch + "]"; - engine.softTakeoverIgnoreNextValue("[QuickEffectRack1_" + group + "]", "super1"); + MaximizeLibraryHandler(field) { + if (field.value === 0) { + return; + } + + script.toggleControl("[Master]", "maximize_library"); } - for (let unit = 1; unit <= 4; unit++) { - group = "[EffectRack1_EffectUnit" + unit + "]"; - key = "mix"; - engine.softTakeoverIgnoreNextValue(group, key); - for (let effect = 1; effect <= 4; effect++) { - group = "[EffectRack1_EffectUnit" + unit + "_Effect" + effect + "]"; - key = "meta"; - engine.softTakeoverIgnoreNextValue(group, key); - for (let param = 1; param <= 4; param++) { - var key = "parameter" + param; - engine.softTakeoverIgnoreNextValue(group, key); + + selectLoopHandler(field) { + let delta = 1; + if ((field.value + 1) % 16 === this.loopKnobEncoderState) { + delta = -1; + } + + if (this.shiftPressed) { + const beatjumpSize = engine.getValue(this.activeChannel, "beatjump_size"); + if (delta > 0) { + script.triggerControl(this.activeChannel, "loop_move_" + beatjumpSize + "_forward"); + } else { + script.triggerControl(this.activeChannel, "loop_move_" + beatjumpSize + "_backward"); + } + } else { + if (delta > 0) { + script.triggerControl(this.activeChannel, "loop_double"); + } else { + script.triggerControl(this.activeChannel, "loop_halve"); } } - } - const oldState = this.currentState; - this.currentState = newState; - if (oldState === this.STATE_FOCUS) { - engine.stopTimer(this.focusBlinkTimer); - this.focusBlinkTimer = 0; + this.loopKnobEncoderState = field.value; } - switch (newState) { - case this.STATE_FILTER: - break; - case this.STATE_EFFECT_INIT: - break; - case this.STATE_EFFECT: - break; - case this.STATE_FOCUS: - this.focusBlinkTimer = engine.beginTimer(150, function() { - TraktorS3.kontrol.fxController.focusBlinkState = !TraktorS3.kontrol.fxController.focusBlinkState; - TraktorS3.kontrol.fxController.lightFx(); - }, false); - } -}; -TraktorS3.FXControl.prototype.fxSelectHandler = function(field) { - const fxNumber = parseInt(field.name[field.name.length - 1]); - // Coerce to boolean - this.selectPressed[fxNumber] = !!field.value; + activateLoopHandler(field) { + if (field.value === 0) { + return; + } + const isLoopActive = engine.getValue(this.activeChannel, "loop_enabled"); - if (!field.value) { - if (fxNumber === this.activeFX) { - if (this.currentState === this.STATE_EFFECT) { - this.changeState(this.STATE_FILTER); - } else if (this.currentState === this.STATE_EFFECT_INIT) { - this.changeState(this.STATE_EFFECT); + if (this.shiftPressed) { + engine.setValue(this.activeChannel, "reloop_toggle", field.value); + } else { + if (isLoopActive) { + engine.setValue(this.activeChannel, "reloop_toggle", field.value); + } else { + engine.setValue(this.activeChannel, "beatloop_activate", field.value); } } - this.lightFx(); - return; } - switch (this.currentState) { - case this.STATE_FILTER: - // If any fxEnable button is pressed, we are toggling fx unit assignment. - if (this.anyEnablePressed()) { - for (const key in this.enablePressed) { - if (this.enablePressed[key]) { - if (fxNumber === 0) { - var fxGroup = "[QuickEffectRack1_" + key + "_Effect1]"; - var fxKey = "enabled"; - } else { - fxGroup = "[EffectRack1_EffectUnit" + fxNumber + "]"; - fxKey = "group_" + key + "_enable"; - } - script.toggleControl(fxGroup, fxKey); - } + selectBeatjumpHandler(field) { + let delta = 1; + if ((field.value + 1) % 16 === this.moveKnobEncoderState) { + delta = -1; + } + + if (this.shiftPressed) { + const beatjumpSize = engine.getValue(this.activeChannel, "beatjump_size"); + if (delta > 0) { + engine.setValue(this.activeChannel, "beatjump_size", beatjumpSize * 2); + } else { + engine.setValue(this.activeChannel, "beatjump_size", beatjumpSize / 2); } } else { - if (fxNumber === 0) { - this.changeState(this.STATE_FILTER); + if (delta > 0) { + script.triggerControl(this.activeChannel, "beatjump_forward"); } else { - this.changeState(this.STATE_EFFECT_INIT); + script.triggerControl(this.activeChannel, "beatjump_backward"); } - this.activeFX = fxNumber; } - break; - case this.STATE_EFFECT_INIT: - // Fallthrough intended - case this.STATE_EFFECT: - if (fxNumber === 0) { - this.changeState(this.STATE_FILTER); - } else if (fxNumber !== this.activeFX) { - this.changeState(this.STATE_EFFECT_INIT); - } - this.activeFX = fxNumber; - break; - case this.STATE_FOCUS: - if (fxNumber === 0) { - this.changeState(this.STATE_FILTER); + + this.moveKnobEncoderState = field.value; + } + + activateBeatjumpHandler(field) { + if (this.shiftPressed) { + engine.setValue(this.activeChannel, "reloop_andstop", field.value); } else { - this.changeState(this.STATE_EFFECT_INIT); + engine.setValue(this.activeChannel, "beatlooproll_activate", field.value); } - this.activeFX = fxNumber; - break; } - this.lightFx(); -}; - -TraktorS3.FXControl.prototype.fxEnableHandler = function(field) { - // Coerce to boolean - this.enablePressed[field.group] = !!field.value; - if (!field.value) { - this.lightFx(); - return; - } - - const fxGroupPrefix = "[EffectRack1_EffectUnit" + this.activeFX; - const buttonNumber = this.channelToIndex(field.group); - switch (this.currentState) { - case this.STATE_FILTER: - break; - case this.STATE_EFFECT_INIT: - // Fallthrough intended - case this.STATE_EFFECT: - if (this.firstPressedSelect()) { - // Choose the first pressed select button only. - this.changeState(this.STATE_FOCUS); - engine.setValue(fxGroupPrefix + "]", "focused_effect", buttonNumber); + reverseHandler(field) { + // this.basicOutput(field.value, "reverse"); + if (this.shiftPressed) { + engine.setValue(this.activeChannel, "reverse", field.value); } else { - var group = fxGroupPrefix + "_Effect" + buttonNumber + "]"; - var key = "enabled"; - script.toggleControl(group, key); + engine.setValue(this.activeChannel, "reverseroll", field.value); } - break; - case this.STATE_FOCUS: - var focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); - group = fxGroupPrefix + "_Effect" + focusedEffect + "]"; - key = "button_parameter" + buttonNumber; - script.toggleControl(group, key); - break; } - this.lightFx(); -}; -TraktorS3.FXControl.prototype.fxKnobHandler = function(field) { - const value = field.value / 4095.; - const fxGroupPrefix = "[EffectRack1_EffectUnit" + this.activeFX; - const knobIdx = this.channelToIndex(field.group); + fluxHandler(field) { + if (field.value === 0) { + return; + } + script.toggleControl(this.activeChannel, "slip_enabled"); + } - switch (this.currentState) { - case this.STATE_FILTER: - if (field.group === "[Channel4]" && this.controller.channel4InputMode) { - // There is no quickeffect for the microphone, do nothing. + quantizeHandler(field) { + if (field.value === 0) { return; } - engine.setParameter("[QuickEffectRack1_" + field.group + "]", "super1", value); - break; - case this.STATE_EFFECT_INIT: - // Fallthrough intended - case this.STATE_EFFECT: - if (knobIdx === 4) { - engine.setParameter(fxGroupPrefix + "]", "mix", value); + if (this.shiftPressed) { + engine.setValue(this.activeChannel, "beats_translate_curpos", field.value); } else { - var group = fxGroupPrefix + "_Effect" + knobIdx + "]"; - engine.setParameter(group, "meta", value); + script.toggleControl(this.activeChannel, "quantize"); } - break; - case this.STATE_FOCUS: - var focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); - group = fxGroupPrefix + "_Effect" + focusedEffect + "]"; - var key = "parameter" + knobIdx; - engine.setParameter(group, key, value); - break; - } -}; - -TraktorS3.FXControl.prototype.getFXSelectLEDValue = function(fxNumber, status) { - const ledValue = this.controller.fxLEDValue[fxNumber]; - switch (status) { - case this.LIGHT_OFF: - return 0x00; - case this.LIGHT_DIM: - return ledValue; - case this.LIGHT_BRIGHT: - return ledValue + 0x02; - } -}; - -TraktorS3.FXControl.prototype.getChannelColor = function(group, status) { - const ledValue = this.controller.hid.LEDColors[TraktorS3.ChannelColors[group]]; - switch (status) { - case this.LIGHT_OFF: - return 0x00; - case this.LIGHT_DIM: - return ledValue; - case this.LIGHT_BRIGHT: - return ledValue + 0x02; } -}; - -TraktorS3.FXControl.prototype.lightFx = function() { - this.controller.batchingOutputs = true; - // Loop through select buttons - // Idx zero is filter button - for (let idx = 0; idx < 5; idx++) { - this.lightSelect(idx); - } - for (let ch = 1; ch <= 4; ch++) { - const channel = "[Channel" + ch + "]"; - this.lightEnable(channel); + jogButtonHandler(field) { + if (field.value === 0) { + return; + } + this.jogToggled = !this.jogToggled; + this.colorOutput(this.jogToggled, "!jogButton"); } - this.controller.batchingOutputs = false; - for (const packetName in this.controller.hid.OutputPackets) { - this.controller.hid.OutputPackets[packetName].send(); - } -}; + jogTouchHandler(field) { + if (!this.jogToggled) { + return; + } -TraktorS3.FXControl.prototype.lightSelect = function(idx) { - let status = this.LIGHT_OFF; - let ledValue = 0x00; - switch (this.currentState) { - case this.STATE_FILTER: - // Always light when pressed - if (this.selectPressed[idx]) { - status = this.LIGHT_BRIGHT; + if (field.value !== 0) { + engine.scratchEnable(this.activeChannelNumber, 768, 33.33334, TraktorS3.Alpha, TraktorS3.Beta); } else { - // select buttons on if fx unit enabled for the pressed channel, - // otherwise disabled. - status = this.LIGHT_DIM; - const pressed = this.firstPressedEnable(); - if (pressed) { - if (idx === 0) { - var fxGroup = "[QuickEffectRack1_" + pressed + "_Effect1]"; - var fxKey = "enabled"; - } else { - fxGroup = "[EffectRack1_EffectUnit" + idx + "]"; - fxKey = "group_" + pressed + "_enable"; - } - if (engine.getParameter(fxGroup, fxKey)) { - status = this.LIGHT_BRIGHT; - } else { - status = this.LIGHT_OFF; - } - } - ledValue = this.getFXSelectLEDValue(idx, status); - } - break; - case this.STATE_EFFECT_INIT: - // Fallthrough intended - case this.STATE_EFFECT: - // Highlight if pressed, disable if active effect. - // Otherwise off. - if (this.selectPressed[idx]) { - status = this.LIGHT_BRIGHT; - } else if (idx === this.activeFX) { - status = this.LIGHT_BRIGHT; - } - break; - case this.STATE_FOCUS: - // if blink state is false, only like active fx bright - // if blink state is true, active fx is bright and selected effect - // is dim. if those are the same, active fx is dim - if (this.selectPressed[idx]) { - status = this.LIGHT_BRIGHT; - } else { - if (idx === this.activeFX) { - status = this.LIGHT_BRIGHT; - } - if (this.focusBlinkState) { - const fxGroupPrefix = "[EffectRack1_EffectUnit" + this.activeFX; - const focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); - if (idx === focusedEffect) { - status = this.LIGHT_DIM; - } + engine.scratchDisable(this.activeChannelNumber); + + // If shift is pressed, reset right away. + if (this.shiftPressed) { + engine.setValue(this.activeChannel, "scratch2", 0.0); + this.playIndicatorHandler(0, this.activeChannel); } } - break; } - ledValue = this.getFXSelectLEDValue(idx, status); - this.controller.hid.setOutput("[ChannelX]", "!fxButton" + idx, ledValue, false); -}; -TraktorS3.FXControl.prototype.lightEnable = function(channel) { - let status = this.LIGHT_OFF; - let ledValue = 0x00; - const buttonNumber = this.channelToIndex(channel); - switch (this.currentState) { - case this.STATE_FILTER: - // enable buttons highlighted if pressed or if any fx unit enabled for channel. - // Highlight if pressed. - status = this.LIGHT_DIM; - if (this.enablePressed[channel]) { - status = this.LIGHT_BRIGHT; - } else { - for (let idx = 1; idx <= 4 && status === this.LIGHT_OFF; idx++) { - var group = "[EffectRack1_EffectUnit" + idx + "]"; - var key = "group_" + channel + "_enable"; - if (engine.getParameter(group, key)) { - status = this.LIGHT_DIM; - } - } + jogHandler(field) { + const deltas = this.wheelDeltas(field.value); + + // If shift button is held, do a simple seek. + if (this.shiftPressed) { + let playPosition = engine.getValue(this.activeChannel, "playposition"); + playPosition += deltas[0] / 2048.0; + playPosition = Math.max(Math.min(playPosition, 1.0), 0.0); + engine.setValue(this.activeChannel, "playposition", playPosition); + return; } - // Enable buttons have regular deck colors - ledValue = this.getChannelColor(channel, status); - break; - case this.STATE_EFFECT_INIT: - // Fallthrough intended - case this.STATE_EFFECT: - if (this.enablePressed[channel]) { - status = this.LIGHT_BRIGHT; + const tickDelta = deltas[0]; + const timeDelta = deltas[1]; + + if (engine.isScratching(this.activeChannelNumber)) { + engine.scratchTick(this.activeChannelNumber, tickDelta); } else { - // off if nothing loaded, dim if loaded, bright if enabled. - group = "[EffectRack1_EffectUnit" + this.activeFX + "_Effect" + buttonNumber + "]"; - if (engine.getParameter(group, "loaded")) { - status = this.LIGHT_DIM; - } - if (engine.getParameter(group, "enabled")) { - status = this.LIGHT_BRIGHT; - } - } - // Colors match effect colors so it's obvious we're in a different mode - ledValue = this.getFXSelectLEDValue(this.activeFX, status); - break; - case this.STATE_FOCUS: - if (this.enablePressed[channel]) { - status = this.LIGHT_BRIGHT; + // The scratch rate is the ratio of the wheel's speed to "regular" + // speed, which we're going to call 33.33 RPM. It's 768 ticks for a + // circle, and 400000 ticks per second, and 33.33 RPM is 1.8 seconds + // per rotation, so the standard speed is 768 / (400000 * 1.8) + const thirtyThree = 768 / 720000; + + // Our actual speed is tickDelta / timeDelta. Take the ratio of those to + // get the rate ratio. + const velocity = (tickDelta / timeDelta) / thirtyThree; + + engine.setValue(this.activeChannel, "jog", velocity); + } + } + + wheelDeltas(value) { + // When the wheel is touched, 1 byte measures distance ticks, the other + // three represent a timer value. We can use the amount of time required for + // the number of ticks to elapse to get a velocity. + const tickval = value & 0xFF; + let timeval = value >>> 8; + + const prevTick = this.lastTickVal; + const prevTime = this.lastTickTime; + const prevTickWallClock = this.lastTickWallClock; + this.lastTickVal = tickval; + this.lastTickTime = timeval; + this.lastTickWallClock = Date.now(); + + // The user hasn't touched the jog wheel for a long time, so the + // internal timer may have looped around more than once. We have nothing + // to go by so return 0, 1 (1 to prevent divide by zero). + if (this.lastTickWallClock - prevTickWallClock > 20000) { + return [0, 1]; + } + + if (prevTime > timeval) { + // We looped around. Adjust current time so that subtraction works. + timeval += 0x1000000; + } + // Clamp Suspiciously low numbers. Even flicking the wheel as fast as I + // can, I can't get it lower than this value. Lower values (esp below + // zero) cause huge jumps / errors in playback. + const timeDelta = Math.max(timeval - prevTime, 500); + let tickDelta = 0; + + // Very generous 8bit loop-around detection. + if (prevTick >= 200 && tickval <= 100) { + tickDelta = tickval + 256 - prevTick; + } else if (prevTick <= 100 && tickval >= 200) { + tickDelta = tickval - prevTick - 256; } else { - const fxGroupPrefix = "[EffectRack1_EffectUnit" + this.activeFX; - const focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); - group = fxGroupPrefix + "_Effect" + focusedEffect + "]"; - key = "button_parameter" + buttonNumber; - // Off if not loaded, dim if loaded, bright if enabled. - if (engine.getParameter(group, key + "_loaded")) { - status = this.LIGHT_DIM; - } - if (engine.getParameter(group, key)) { - status = this.LIGHT_BRIGHT; - } + tickDelta = tickval - prevTick; } - // Colors match effect colors so it's obvious we're in a different mode - ledValue = this.getFXSelectLEDValue(this.activeFX, status); - break; - } - this.controller.hid.setOutput(channel, "!fxEnabled", ledValue, false); -}; -TraktorS3.Controller.prototype.registerInputPackets = function() { - const messageShort = new HIDPacket("shortmessage", 0x01, TraktorS3.messageCallback); - const messageLong = new HIDPacket("longmessage", 0x02, TraktorS3.messageCallback); - - for (const idx in this.Decks) { - const deck = this.Decks[idx]; - deck.registerInputs(messageShort, messageLong); - } - - this.registerInputButton(messageShort, "[Channel1]", "!switchDeck", 0x02, 0x02, TraktorS3.Controller.prototype.deckSwitchHandler.bind(this)); - this.registerInputButton(messageShort, "[Channel2]", "!switchDeck", 0x05, 0x04, TraktorS3.Controller.prototype.deckSwitchHandler.bind(this)); - this.registerInputButton(messageShort, "[Channel3]", "!switchDeck", 0x02, 0x04, TraktorS3.Controller.prototype.deckSwitchHandler.bind(this)); - this.registerInputButton(messageShort, "[Channel4]", "!switchDeck", 0x05, 0x08, TraktorS3.Controller.prototype.deckSwitchHandler.bind(this)); - - // Headphone buttons - this.registerInputButton(messageShort, "[Channel1]", "pfl", 0x08, 0x01, TraktorS3.Controller.prototype.headphoneHandler.bind(this)); - this.registerInputButton(messageShort, "[Channel2]", "pfl", 0x08, 0x02, TraktorS3.Controller.prototype.headphoneHandler.bind(this)); - this.registerInputButton(messageShort, "[Channel3]", "pfl", 0x07, 0x80, TraktorS3.Controller.prototype.headphoneHandler.bind(this)); - this.registerInputButton(messageShort, "[Channel4]", "pfl", 0x08, 0x04, TraktorS3.Controller.prototype.headphoneHandler.bind(this)); - - // EXT Button - this.registerInputButton(messageShort, "[Master]", "!extButton", 0x07, 0x04, TraktorS3.Controller.prototype.extModeHandler.bind(this)); - - this.fxController.registerInputs(messageShort, messageLong); - - this.hid.registerInputPacket(messageShort); - - this.registerInputScaler(messageLong, "[Channel1]", "volume", 0x05, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[Channel2]", "volume", 0x07, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[Channel3]", "volume", 0x03, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[Channel4]", "volume", 0x09, 0xFFFF, this.parameterHandler); - - this.registerInputScaler(messageLong, "[Channel1]", "pregain", 0x11, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[Channel2]", "pregain", 0x13, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[Channel3]", "pregain", 0x0F, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[Channel4]", "pregain", 0x15, 0xFFFF, this.parameterHandler); - - this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel1]_Effect1]", "parameter3", 0x25, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel1]_Effect1]", "parameter2", 0x27, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel1]_Effect1]", "parameter1", 0x29, 0xFFFF, this.parameterHandler); - - this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel2]_Effect1]", "parameter3", 0x2B, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel2]_Effect1]", "parameter2", 0x2D, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel2]_Effect1]", "parameter1", 0x2F, 0xFFFF, this.parameterHandler); - - this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel3]_Effect1]", "parameter3", 0x1F, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel3]_Effect1]", "parameter2", 0x21, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel3]_Effect1]", "parameter1", 0x23, 0xFFFF, this.parameterHandler); - - this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel4]_Effect1]", "parameter3", 0x31, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel4]_Effect1]", "parameter2", 0x33, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[EqualizerRack1_[Channel4]_Effect1]", "parameter1", 0x35, 0xFFFF, this.parameterHandler); - - this.registerInputScaler(messageLong, "[Master]", "crossfader", 0x0B, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[Master]", "gain", 0x17, 0xFFFF, TraktorS3.Controller.prototype.masterGainHandler.bind(this)); - this.registerInputScaler(messageLong, "[Master]", "headMix", 0x1D, 0xFFFF, this.parameterHandler); - this.registerInputScaler(messageLong, "[Master]", "headGain", 0x1B, 0xFFFF, this.parameterHandler); - - this.hid.registerInputPacket(messageLong); - - for (ch in this.Channels) { - const chanob = this.Channels[ch]; - engine.makeConnection(ch, "playposition", - TraktorS3.Channel.prototype.playpositionChanged.bind(chanob)); - engine.connectControl(ch, "track_loaded", - TraktorS3.Channel.prototype.trackLoadedHandler.bind(chanob)); - engine.connectControl(ch, "end_of_track", - TraktorS3.Channel.prototype.endOfTrackHandler.bind(chanob)); - } - - // Query the current values from the controller and set them. The packet - // parser ignores the first time a value is set, so we'll need to set it - // with different values once. Report 2 contains the state of the mixer - // controls. - const report2Values = new Uint8Array(controller.getInputReport(2)); - TraktorS3.incomingData([2, ...Array.from(report2Values.map(x => ~x))]); - TraktorS3.incomingData([2, ...Array.from(report2Values)]); - - // Report 1 is the state of the deck controls. These shouldn't have any - // initial effect, and most of these values will be 0 anyways. We'll just - // tell the packet parser the current values so it won't ignore the next - // input. - const report1Values = new Uint8Array(controller.getInputReport(1)); - TraktorS3.incomingData([1, ...Array.from(report1Values)]); - - // NOTE: Soft takeovers must only be enabled after setting the initial - // value, or the above line won't have any effect - for (var ch = 1; ch <= 4; ch++) { - var group = "[Channel" + ch + "]"; - if (!TraktorS3.PitchSliderRelativeMode) { - engine.softTakeover(group, "rate", true); - } - engine.softTakeover(group, "pitch_adjust", true); - engine.softTakeover(group, "volume", true); - engine.softTakeover(group, "pregain", true); - engine.softTakeover("[QuickEffectRack1_" + group + "]", "super1", true); - } - for (let unit = 1; unit <= 4; unit++) { - group = "[EffectRack1_EffectUnit" + unit + "]"; - let key = "mix"; - engine.softTakeover(group, key, true); - for (let effect = 1; effect <= 4; effect++) { - group = "[EffectRack1_EffectUnit" + unit + "_Effect" + effect + "]"; - key = "meta"; - engine.softTakeover(group, key, true); - for (let param = 1; param <= 4; param++) { - key = "parameter" + param; - engine.softTakeover(group, key, true); - } - } + return [tickDelta, timeDelta]; } - engine.softTakeover("[Microphone]", "volume", true); - engine.softTakeover("[Microphone]", "pregain", true); + pitchSliderHandler(field) { + // Adapt HID value to rate control range. + const value = -1.0 + ((field.value / 4095) * 2.0); + if (TraktorS3.PitchSliderRelativeMode) { + if (this.pitchSliderLastValue === -1) { + this.pitchSliderLastValue = value; + } else { + // If shift is pressed, don't update any values. + if (this.shiftPressed) { + this.pitchSliderLastValue = value; + return; + } - engine.softTakeover("[EqualizerRack1_[Channel1]_Effect1]", "parameter1", true); - engine.softTakeover("[EqualizerRack1_[Channel1]_Effect1]", "parameter2", true); - engine.softTakeover("[EqualizerRack1_[Channel1]_Effect1]", "parameter3", true); - engine.softTakeover("[EqualizerRack1_[Channel2]_Effect1]", "parameter1", true); - engine.softTakeover("[EqualizerRack1_[Channel2]_Effect1]", "parameter2", true); - engine.softTakeover("[EqualizerRack1_[Channel2]_Effect1]", "parameter3", true); - engine.softTakeover("[EqualizerRack1_[Channel3]_Effect1]", "parameter1", true); - engine.softTakeover("[EqualizerRack1_[Channel3]_Effect1]", "parameter2", true); - engine.softTakeover("[EqualizerRack1_[Channel3]_Effect1]", "parameter3", true); - engine.softTakeover("[EqualizerRack1_[Channel4]_Effect1]", "parameter1", true); - engine.softTakeover("[EqualizerRack1_[Channel4]_Effect1]", "parameter2", true); - engine.softTakeover("[EqualizerRack1_[Channel4]_Effect1]", "parameter3", true); + let relVal; + if (this.keylockPressed) { + relVal = 1.0 - engine.getValue(this.activeChannel, "pitch_adjust"); + } else { + relVal = engine.getValue(this.activeChannel, "rate"); + } + // This can result in values outside -1 to 1, but that is valid + // for the rate control. This means the entire swing of the rate + // slider can be outside the range of the widget, but that's ok + // because the slider still works. + relVal += value - this.pitchSliderLastValue; + this.pitchSliderLastValue = value; - // engine.softTakeover("[Master]", "crossfader", true); - engine.softTakeover("[Master]", "gain", true); - // engine.softTakeover("[Master]", "headMix", true); - // engine.softTakeover("[Master]", "headGain", true); + if (this.keylockPressed) { + // To match the pitch change from adjusting the rate, flip + // the pitch adjustment. + engine.setValue(this.activeChannel, "pitch_adjust", 1.0 - relVal); + this.keyAdjusted = true; + } else { + engine.setValue(this.activeChannel, "rate", relVal); + } + } + return; + } - for (let i = 1; i <= 16; ++i) { - engine.softTakeover("[Sampler" + i + "]", "pregain", true); - } -}; + if (this.shiftPressed) { + // To match the pitch change from adjusting the rate, flip the pitch + // adjustment. + engine.setValue(this.activeChannel, "pitch_adjust", 1.0 - value); + } else { + engine.setValue(this.activeChannel, "rate", value); + } + } + + //// Deck Outputs //// + defineOutput(packet, name, offsetA, offsetB) { + switch (this.deckNumber) { + case 1: + packet.addOutput(this.group, name, offsetA, "B"); + break; + case 2: + packet.addOutput(this.group, name, offsetB, "B"); + break; + } + } + + registerOutputs(outputA, _outputB) { + this.defineOutput(outputA, "!shift", 0x01, 0x1A); + this.defineOutput(outputA, "slip_enabled", 0x02, 0x1B); + this.defineOutput(outputA, "reverse", 0x03, 0x1C); + this.defineOutput(outputA, "!PreviewTrack", 0x04, 0x1D); + this.defineOutput(outputA, "!LibraryFocus", 0x06, 0x1F); + this.defineOutput(outputA, "!MaximizeLibrary", 0x07, 0x20); + this.defineOutput(outputA, "quantize", 0x08, 0x21); + this.defineOutput(outputA, "!jogButton", 0x09, 0x22); + this.defineOutput(outputA, "sync_enabled", 0x0C, 0x25); + this.defineOutput(outputA, "keylock", 0x0D, 0x26); + this.defineOutput(outputA, "hotcues", 0x0E, 0x27); + this.defineOutput(outputA, "samples", 0x0F, 0x28); + this.defineOutput(outputA, "cue_indicator", 0x10, 0x29); + this.defineOutput(outputA, "play_indicator", 0x11, 0x2A); + + this.defineOutput(outputA, "!pad_1", 0x12, 0x2B); + this.defineOutput(outputA, "!pad_2", 0x13, 0x2C); + this.defineOutput(outputA, "!pad_3", 0x14, 0x2D); + this.defineOutput(outputA, "!pad_4", 0x15, 0x2E); + this.defineOutput(outputA, "!pad_5", 0x16, 0x2F); + this.defineOutput(outputA, "!pad_6", 0x17, 0x30); + this.defineOutput(outputA, "!pad_7", 0x18, 0x31); + this.defineOutput(outputA, "!pad_8", 0x19, 0x32); + + // this.defineOutput(outputA, "addTrack", 0x03, 0x2A); + const wheelOffsets = [0x43, 0x4B]; + for (let i = 0; i < 8; i++) { + this.defineOutput(outputA, "!" + "wheel" + i, wheelOffsets[0] + i, wheelOffsets[1] + i); + } + } + + defineLink(key, callback) { + switch (this.deckNumber) { + case 1: + this.controller.hid.linkOutput("deck1", key, "[Channel1]", key, callback); + engine.connectControl("[Channel3]", key, callback); + break; + case 2: + this.controller.hid.linkOutput("deck2", key, "[Channel2]", key, callback); + engine.connectControl("[Channel4]", key, callback); + break; + } + } + + linkOutputs() { + const colorOutput = function(value, _group, key) { + this.colorOutput(value, key); + }; -TraktorS3.Controller.prototype.registerInputJog = function(message, group, name, offset, bitmask, callback) { - // Jog wheels have 4 byte input - message.addControl(group, name, offset, "I", bitmask); - message.setCallback(group, name, callback); -}; + const basicOutput = function(value, _group, key) { + this.basicOutput(value, key); + }; -TraktorS3.Controller.prototype.registerInputScaler = function(message, group, name, offset, bitmask, callback) { - message.addControl(group, name, offset, "H", bitmask); - message.setCallback(group, name, callback); -}; + this.defineLink("play_indicator", TraktorS3.Deck.prototype.playIndicatorHandler.bind(this)); + this.defineLink("cue_indicator", colorOutput.bind(this)); + this.defineLink("sync_enabled", colorOutput.bind(this)); + this.defineLink("keylock", colorOutput.bind(this)); + this.defineLink("slip_enabled", colorOutput.bind(this)); + this.defineLink("quantize", colorOutput.bind(this)); + this.defineLink("reverse", basicOutput.bind(this)); + this.defineLink("scratch2_enable", colorOutput.bind(this)); + } -TraktorS3.Controller.prototype.registerInputButton = function(message, group, name, offset, bitmask, callback) { - message.addControl(group, name, offset, "B", bitmask); - message.setCallback(group, name, callback); -}; + deckBaseColor() { + return this.controller.hid.LEDColors[TraktorS3.ChannelColors[this.activeChannel]]; + } -TraktorS3.Controller.prototype.parameterHandler = function(field) { - if (field.group === "[Channel4]" && this.channel4InputMode) { - engine.setParameter("[Microphone]", field.name, field.value / 4095); - } else { - engine.setParameter(field.group, field.name, field.value / 4095); + // basicOutput drives lights that only have one color. + basicOutput(value, key) { + // incoming value will be a channel, we have to resolve back to deck. + let ledValue = 0x20; + if (value === 1 || value === true) { + // On value + ledValue = 0x77; + } + this.controller.hid.setOutput(this.group, key, ledValue, !TraktorS3.batchingOutputs); } -}; -TraktorS3.Controller.prototype.anyShiftPressed = function() { - return this.Decks.deck1.shiftPressed || this.Decks.deck2.shiftPressed; -}; + // colorOutput drives lights that have the palettized multicolor lights. + colorOutput(value, key) { + let ledValue = this.deckBaseColor(); -TraktorS3.Controller.prototype.masterGainHandler = function(field) { - // Only adjust if shift is held. This will still adjust the sound card - // volume but it at least allows for control of Mixxx's master gain. - if (this.anyShiftPressed()) { - engine.setParameter(field.group, field.name, field.value / 4095); + if (value === 1 || value === true) { + ledValue += TraktorS3.LEDBrightValue; + } else { + ledValue += TraktorS3.LEDDimValue; + } + this.controller.hid.setOutput(this.group, key, ledValue, !this.controller.batchingOutputs); } -}; -TraktorS3.Controller.prototype.headphoneHandler = function(field) { - if (field.value === 0) { - return; + playIndicatorHandler(value, group, _key) { + // Also call regular handler + this.basicOutput(value, "play_indicator"); + this.wheelOutputByValue(group, value); } - if (field.group === "[Channel4]" && this.channel4InputMode) { - script.toggleControl("[Microphone]", "pfl"); - } else { - script.toggleControl(field.group, "pfl"); + + colorForHotcue(num) { + const colorCode = engine.getValue(this.activeChannel, "hotcue_" + num + "_color"); + return this.controller.colorMap.getValueForNearestColor(colorCode); } -}; -TraktorS3.Controller.prototype.deckSwitchHandler = function(field) { - if (field.value === 0) { - if (this.deckSwitchPressed === field.group) { - this.deckSwitchPressed = ""; + lightHotcue(number) { + const loaded = engine.getValue(this.activeChannel, "hotcue_" + number + "_enabled"); + const active = engine.getValue(this.activeChannel, "hotcue_" + number + "_activate"); + let ledValue = this.controller.hid.LEDColors.WHITE; + if (loaded) { + ledValue = this.colorForHotcue(number); + ledValue += TraktorS3.LEDDimValue; + } + if (active) { + ledValue += TraktorS3.LEDBrightValue; + } else { + ledValue += TraktorS3.LEDDimValue; } - return; + this.controller.hid.setOutput(this.group, "!pad_" + number, ledValue, !TraktorS3.batchingOutputs); } - if (this.deckSwitchPressed === "") { - this.deckSwitchPressed = field.group; - } else { - // If a different deck switch is already pressed, do an instant double and do not select the - // deck. - const cloneFrom = this.Channels[this.deckSwitchPressed]; - const cloneFromNum = cloneFrom.parentDeck.deckNumber; - engine.setValue(field.group, "CloneFromDeck", cloneFromNum); - return; + lightPads() { + // Samplers + if (this.padModeState === 1) { + this.colorOutput(0, "hotcues"); + this.colorOutput(1, "samples"); + for (var i = 1; i <= 8; i++) { + let idx = i; + if (this.group === "deck2" && TraktorS3.SixteenSamplers) { + idx += 8; + } + const loaded = engine.getValue("[Sampler" + idx + "]", "track_loaded"); + this.colorOutput(loaded, "!pad_" + i); + } + } else { + this.colorOutput(1, "hotcues"); + this.colorOutput(0, "samples"); + for (i = 1; i <= 8; ++i) { + this.lightHotcue(i); + } + } } - const channel = this.Channels[field.group]; - const deck = channel.parentDeck; - - if (engine.isScratching(channel.groupNumber)) { - engine.scratchDisable(channel.groupNumber); - } + wheelOutputByValue(group, value) { + if (group !== this.activeChannel) { + return; + } - deck.activateChannel(channel); -}; + let ledValue = this.deckBaseColor(); -TraktorS3.Controller.prototype.extModeHandler = function(field) { - if (!field.value) { - this.basicOutput(this.channel4InputMode, field.group, field.name); - return; - } - if (this.anyShiftPressed()) { - this.basicOutput(field.value, field.group, field.name); - this.inputModeLine = !this.inputModeLine; - this.setInputLineMode(this.inputModeLine); - return; - } - this.channel4InputMode = !this.channel4InputMode; - if (this.channel4InputMode) { - engine.softTakeoverIgnoreNextValue("[Microphone]", "volume"); - engine.softTakeoverIgnoreNextValue("[Microphone]", "pregain"); - } else { - engine.softTakeoverIgnoreNextValue("[Channel4]", "volume"); - engine.softTakeoverIgnoreNextValue("[Channel4]", "pregain"); + if (value === 1 || value === true) { + ledValue += TraktorS3.LEDBrightValue; + } else { + ledValue = 0x00; + } + this.wheelOutput(group, + [ledValue, ledValue, ledValue, ledValue, ledValue, ledValue, ledValue, ledValue]); } - this.lightDeck("[Channel4]"); - this.basicOutput(this.channel4InputMode, field.group, field.name); -}; -TraktorS3.Controller.prototype.registerOutputPackets = function() { - const outputA = new HIDPacket("outputA", 0x80); - const outputB = new HIDPacket("outputB", 0x81); + wheelOutput(group, valueArray) { + if (group !== this.activeChannel) { + return; + } - for (var idx in this.Decks) { - var deck = this.Decks[idx]; - deck.registerOutputs(outputA, outputB); + for (let i = 0; i < 8; i++) { + this.controller.hid.setOutput(this.group, "!wheel" + i, valueArray[i], false); + } + if (!TraktorS3.batchingOutputs) { + for (const packetName in this.controller.hid.OutputPackets) { + this.controller.hid.OutputPackets[packetName].send(); + } + } } +}; - outputA.addOutput("[Channel1]", "!deck_A", 0x0A, "B"); - outputA.addOutput("[Channel2]", "!deck_B", 0x23, "B"); - outputA.addOutput("[Channel3]", "!deck_C", 0x0B, "B"); - outputA.addOutput("[Channel4]", "!deck_D", 0x24, "B"); - - outputA.addOutput("[Channel1]", "pfl", 0x39, "B"); - outputA.addOutput("[Channel2]", "pfl", 0x3A, "B"); - outputA.addOutput("[Channel3]", "pfl", 0x38, "B"); - outputA.addOutput("[Channel4]", "pfl", 0x3B, "B"); - - outputA.addOutput("[ChannelX]", "!fxButton1", 0x3C, "B"); - outputA.addOutput("[ChannelX]", "!fxButton2", 0x3D, "B"); - outputA.addOutput("[ChannelX]", "!fxButton3", 0x3E, "B"); - outputA.addOutput("[ChannelX]", "!fxButton4", 0x3F, "B"); - outputA.addOutput("[ChannelX]", "!fxButton0", 0x40, "B"); - - outputA.addOutput("[Channel3]", "!fxEnabled", 0x34, "B"); - outputA.addOutput("[Channel1]", "!fxEnabled", 0x35, "B"); - outputA.addOutput("[Channel2]", "!fxEnabled", 0x36, "B"); - outputA.addOutput("[Channel4]", "!fxEnabled", 0x37, "B"); +///////////////////////// +//// Channel Objects //// +//// +//// Channels don't have much state, just the fx button state. +TraktorS3.Channel = class { + constructor(controller, parentDeck, group) { + this.controller = controller; + this.parentDeck = parentDeck; + this.group = group; + // We need the channel number for the scratch controls + this.groupNumber = Number(group.match(/\[Channel(\d+)\]/)[1]); + this.fxEnabledState = false; - outputA.addOutput("[Master]", "!extButton", 0x33, "B"); + this.trackDurationSec = 0; + this.positionUpdated = false; + this.curPosition = -1; + this.endOfTrackTimer = 0; + this.endOfTrack = false; + this.endOfTrackBlinkState = 0; + this.vuMeterUpdated = false; + this.vuMeterValue = 0; - this.hid.registerOutputPacket(outputA); + this.vuConnection = {}; + this.clipConnection = {}; + this.hotcueCallbacks = []; - const VuOffsets = { - "[Channel3]": 0x01, - "[Channel1]": 0x10, - "[Channel2]": 0x1F, - "[Channel4]": 0x2E - }; - for (const ch in VuOffsets) { - for (var i = 0; i < 14; i++) { - outputB.addOutput(ch, "!" + "VuMeter" + i, VuOffsets[ch] + i, "B"); + // The script by default doesn't change any of the deck's settings, but it's + // useful to be able to initialize these settings to your preferences when + // you turn on the controller + if (TraktorS3.DefaultBeatJumpSize !== null) { + engine.setValue(group, "beatjump_size", TraktorS3.DefaultBeatJumpSize); + } + if (TraktorS3.DefaultBeatLoopLength !== null) { + engine.setValue(group, "beatloop_size", TraktorS3.DefaultBeatLoopLength); + } + if (TraktorS3.DefaultSyncEnabled !== null) { + engine.setValue(group, "sync_enabled", TraktorS3.DefaultSyncEnabled); + } + if (TraktorS3.DefaultQuantizeEnabled !== null) { + engine.setValue(group, "quantize", TraktorS3.DefaultQuantizeEnabled); + } + if (TraktorS3.DefaultKeylockEnabled !== null) { + engine.setValue(group, "keylock", TraktorS3.DefaultKeylockEnabled); } } - const MasterVuOffsets = { - "VuMeterL": 0x3D, - "VuMeterR": 0x46 - }; - for (i = 0; i < 8; i++) { - outputB.addOutput("[Master]", "!" + "VuMeterL" + i, MasterVuOffsets.VuMeterL + i, "B"); - outputB.addOutput("[Master]", "!" + "VuMeterR" + i, MasterVuOffsets.VuMeterR + i, "B"); + trackLoadedHandler() { + const trackSamples = engine.getValue(this.group, "track_samples"); + if (trackSamples === 0) { + this.trackDurationSec = 0; + return; + } + const trackSampleRate = engine.getValue(this.group, "track_samplerate"); + // Assume stereo. + this.trackDurationSec = trackSamples / 2.0 / trackSampleRate; + this.parentDeck.lightPads(); } - outputB.addOutput("[Master]", "PeakIndicatorL", 0x45, "B"); - outputB.addOutput("[Master]", "PeakIndicatorR", 0x4E, "B"); - - outputB.addOutput("[Channel3]", "PeakIndicator", 0x0F, "B"); - outputB.addOutput("[Channel1]", "PeakIndicator", 0x1E, "B"); - outputB.addOutput("[Channel2]", "PeakIndicator", 0x2D, "B"); - outputB.addOutput("[Channel4]", "PeakIndicator", 0x3C, "B"); + endOfTrackHandler(value) { + this.endOfTrack = value; + if (!value) { + if (this.endOfTrackTimer) { + engine.stopTimer(this.endOfTrackTimer); + this.endOfTrackTimer = 0; + } + return; + } + this.endOfTrackTimer = engine.beginTimer(400, function() { + this.endOfTrackBlinkState = !this.endOfTrackBlinkState; + }, false); + } - this.hid.registerOutputPacket(outputB); + playpositionChanged(value) { + if (this.parentDeck.activeChannel !== this.group) { + return; + } - for (idx in this.Decks) { - deck = this.Decks[idx]; - deck.linkOutputs(); + // How many segments away from the actual angle should we light? + // (in both directions, so "2" will light up to four segments) + if (this.trackDurationSec === 0) { + const samples = engine.getValue(this.group, "track_loaded"); + if (samples > 0) { + this.trackLoadedHandler(); + } else { + // No track loaded, abort + return; + } + } + this.curPosition = value * this.trackDurationSec; + this.positionUpdated = true; } - for (idx in this.Channels) { - const chan = this.Channels[idx]; - chan.linkOutputs(); + vuMeterHandler(value) { + this.vuMeterUpdated = true; + this.vuMeterValue = value; } - engine.connectControl("[Microphone]", "pfl", this.pflOutput); - - engine.connectControl("[Master]", "maximize_library", TraktorS3.Controller.prototype.maximizeLibraryOutput.bind(this)); - - // Master VuMeters - this.masterVuMeter.VuMeterL.connection = engine.makeConnection("[Master]", "VuMeterL", TraktorS3.Controller.prototype.masterVuMeterHandler.bind(this)); - this.masterVuMeter.VuMeterR.connection = engine.makeConnection("[Master]", "VuMeterR", TraktorS3.Controller.prototype.masterVuMeterHandler.bind(this)); - this.linkChannelOutput("[Master]", "PeakIndicatorL", TraktorS3.Controller.prototype.peakOutput.bind(this)); - this.linkChannelOutput("[Master]", "PeakIndicatorR", TraktorS3.Controller.prototype.peakOutput.bind(this)); - this.guiTickConnection = engine.makeConnection("[Master]", "guiTick50ms", TraktorS3.Controller.prototype.guiTickHandler.bind(this)); - - // Sampler callbacks - for (i = 1; i <= 8; ++i) { - this.samplerCallbacks.push(engine.makeConnection("[Sampler" + i + "]", "track_loaded", TraktorS3.Controller.prototype.samplesOutput.bind(this))); - this.samplerCallbacks.push(engine.makeConnection("[Sampler" + i + "]", "play_indicator", TraktorS3.Controller.prototype.samplesOutput.bind(this))); + linkOutputs() { + this.vuConnection = engine.makeConnection(this.group, "VuMeter", TraktorS3.Channel.prototype.vuMeterHandler.bind(this)); + this.clipConnection = engine.makeConnection(this.group, "PeakIndicator", TraktorS3.Controller.prototype.peakOutput.bind(this.controller)); + this.controller.linkChannelOutput(this.group, "pfl", TraktorS3.Controller.prototype.pflOutput.bind(this.controller)); + for (let j = 1; j <= 8; j++) { + this.hotcueCallbacks.push(engine.makeConnection(this.group, "hotcue_" + j + "_enabled", + TraktorS3.Channel.prototype.hotcuesOutput.bind(this))); + this.hotcueCallbacks.push(engine.makeConnection(this.group, "hotcue_" + j + "_activate", + TraktorS3.Channel.prototype.hotcuesOutput.bind(this))); + this.hotcueCallbacks.push(engine.makeConnection(this.group, "hotcue_" + j + "_color", + TraktorS3.Channel.prototype.hotcuesOutput.bind(this))); + } } -}; - -TraktorS3.Controller.prototype.linkChannelOutput = function(group, name, callback) { - this.hid.linkOutput(group, name, group, name, callback); -}; -TraktorS3.Controller.prototype.pflOutput = function(value, group, key) { - if (group === "[Microphone]" && this.channel4InputMode) { - this.basicOutput(value, "[Channel4]", key); - return; - } - if (group === "[Channel4]" && !this.channel4InputMode) { - this.basicOutput(value, group, key); - return; - } - if (group.match(/^\[Channel[123]\]$/)) { - this.basicOutput(value, group, key); + channelBaseColor() { + if (this.group === "[Channel4]" && this.controller.channel4InputMode) { + return this.controller.hid.LEDColors[this.controller.hid.LEDColors.OFF]; + } + return this.controller.hid.LEDColors[TraktorS3.ChannelColors[this.group]]; } - // Unhandled case, ignore. -}; - -TraktorS3.Controller.prototype.maximizeLibraryOutput = function(value, _group, _key) { - this.Decks.deck1.colorOutput(value, "!MaximizeLibrary"); - this.Decks.deck2.colorOutput(value, "!MaximizeLibrary"); -}; -// Output drives lights that only have one color. -TraktorS3.Controller.prototype.basicOutput = function(value, group, key) { - let ledValue = value; - if (value === 0 || value === false) { - // Off value - ledValue = 0x04; - } else if (value === 1 || value === true) { - // On value - ledValue = 0xFF; + // colorOutput drives lights that have the palettized multicolor lights. + colorOutput(value, key) { + let ledValue = this.channelBaseColor(); + if (value === 1 || value === true) { + ledValue += TraktorS3.LEDBrightValue; + } else { + ledValue += TraktorS3.LEDDimValue; + } + this.controller.hid.setOutput(this.group, key, ledValue, !this.controller.batchingOutputs); } - this.hid.setOutput(group, key, ledValue, !this.batchingOutputs); -}; - -TraktorS3.Controller.prototype.peakOutput = function(value, group, key) { - let ledValue = 0x00; - if (value) { - ledValue = 0x7E; + hotcuesOutput(_value, group, key) { + const deck = this.controller.Channels[group].parentDeck; + if (deck.activeChannel !== group) { + // Not active, ignore + return; + } + if (deck.padModeState !== 0) { + return; + } + const matches = key.match(/hotcue_(\d+)_/); + if (matches.length !== 2) { + HIDDebug("Didn't get expected hotcue number from string: " + key); + return; + } + const cueNum = matches[1]; + deck.lightHotcue(cueNum); + } + + // Returns true if there was an update. + lightWheelPosition() { + if (!this.positionUpdated) { + return false; + } + this.positionUpdated = false; + const rotations = this.curPosition * (1 / 1.8); // 1/1.8 is rotations per second (33 1/3 RPM) + + // Calculate angle from 0-1.0 + const angle = rotations - Math.floor(rotations); + // The wheel has 8 segments + const wheelAngle = 8.0 * angle; + const baseLedValue = this.channelBaseColor(); + // Reduce the dimming distance at the end of track. + let dimDistance = this.endOfTrack ? 2.5 : 1.5; + const segValues = [0, 0, 0, 0, 0, 0, 0, 0]; + for (let seg = 0; seg < 8; seg++) { + const distance = TraktorS3.wheelSegmentDistance(seg, wheelAngle); + let brightVal = Math.round(4 * (1.0 - (distance / dimDistance))); + if (this.endOfTrack) { + dimDistance = 1.5; + brightVal = Math.round(4 * (1.0 - (distance / dimDistance))); + if (this.endOfTrackBlinkState) { + brightVal = brightVal > 0x03 ? 0x04 : 0x02; + } else { + brightVal = brightVal > 0x02 ? 0x04 : 0x00; + } + } + if (brightVal <= 0) { + segValues[seg] = 0x00; + } else { + segValues[seg] = baseLedValue + brightVal - 1; + } + } + this.parentDeck.wheelOutput(this.group, segValues); + return true; } - - this.hid.setOutput(group, key, ledValue, !this.batchingOutputs); -}; - -TraktorS3.Controller.prototype.masterVuMeterHandler = function(value, _group, key) { - this.masterVuMeter[key].updated = true; - this.masterVuMeter[key].value = value; }; -TraktorS3.Controller.prototype.vuMeterOutput = function(value, group, key, segments) { - // This handler is called a lot so it should be as fast as possible. - const scaledValue = value * segments; - const fullIllumCount = Math.floor(scaledValue); - - // Figure out how much the partially-illuminated segment is illuminated. - const partialIllum = (scaledValue - fullIllumCount) * 0x7F; - - for (let i = 0; i < segments; i++) { - const segmentKey = "!" + key + i; - if (i < fullIllumCount) { - // Don't update lights until they're all done, so the last term is false. - this.hid.setOutput(group, segmentKey, 0x7F, false); - } else if (i === fullIllumCount) { - this.hid.setOutput(group, segmentKey, partialIllum, false); +// Finds the shortest distance between two angles on the wheel, assuming +// 0-8.0 angle value. +TraktorS3.wheelSegmentDistance = function(segNum, angle) { + // Account for wraparound + if (Math.abs(segNum - angle) > 4) { + if (angle > segNum) { + segNum += 8; } else { - this.hid.setOutput(group, segmentKey, 0x00, false); + angle += 8; } } - if (!this.batchingOutputs) { - this.hid.OutputPackets.outputB.send(); - } + return Math.abs(angle - segNum); }; -TraktorS3.Controller.prototype.resolveSampler = function(group) { - if (group === undefined) { - return undefined; - } +// FXControl is an object that manages the gray area in the middle of the +// controller: the fx control knobs, fxenable buttons, and fx select buttons. +TraktorS3.FXControl = class { + constructor(controller) { + // 0 is filter, 1-4 are FX Units 1-4 + this.FILTER_EFFECT = 0; + this.activeFX = this.FILTER_EFFECT; + this.controller = controller; - const result = group.match(script.samplerRegEx); + this.enablePressed = { + "[Channel1]": false, + "[Channel2]": false, + "[Channel3]": false, + "[Channel4]": false + }; + this.selectPressed = [ + false, + false, + false, + false, + false + ]; + this.selectBlinkState = [ + false, + false, + false, + false, + false + ]; + + // States + this.STATE_FILTER = 0; + // State for when an effect select has been pressed, but not released yet. + this.STATE_EFFECT_INIT = 1; + // State for when an effect select has been pressed and released. + this.STATE_EFFECT = 2; + this.STATE_FOCUS = 3; + + this.currentState = this.STATE_FILTER; + + // Light states + this.LIGHT_OFF = 0; + this.LIGHT_DIM = 1; + this.LIGHT_BRIGHT = 2; + + this.focusBlinkState = false; + this.focusBlinkTimer = 0; + } - if (result === null) { + registerInputs(messageShort, messageLong) { + // FX Buttons + const fxFn = TraktorS3.FXControl.prototype; + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx1", 0x08, 0x08, fxFn.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx2", 0x08, 0x10, fxFn.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx3", 0x08, 0x20, fxFn.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx4", 0x08, 0x40, fxFn.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx0", 0x08, 0x80, fxFn.fxSelectHandler.bind(this)); + + this.controller.registerInputButton(messageShort, "[Channel3]", "!fxEnabled", 0x07, 0x08, fxFn.fxEnableHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[Channel1]", "!fxEnabled", 0x07, 0x10, fxFn.fxEnableHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[Channel2]", "!fxEnabled", 0x07, 0x20, fxFn.fxEnableHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[Channel4]", "!fxEnabled", 0x07, 0x40, fxFn.fxEnableHandler.bind(this)); + + this.controller.registerInputScaler(messageLong, "[Channel1]", "!fxKnob", 0x39, 0xFFFF, fxFn.fxKnobHandler.bind(this)); + this.controller.registerInputScaler(messageLong, "[Channel2]", "!fxKnob", 0x3B, 0xFFFF, fxFn.fxKnobHandler.bind(this)); + this.controller.registerInputScaler(messageLong, "[Channel3]", "!fxKnob", 0x37, 0xFFFF, fxFn.fxKnobHandler.bind(this)); + this.controller.registerInputScaler(messageLong, "[Channel4]", "!fxKnob", 0x3D, 0xFFFF, fxFn.fxKnobHandler.bind(this)); + } + + channelToIndex(group) { + const result = group.match(script.channelRegEx); + if (result === null) { + return undefined; + } + // Unmap from channel number to button index. + switch (result[1]) { + case "1": + return 2; + case "2": + return 3; + case "3": + return 1; + case "4": + return 4; + } return undefined; } - // Return sampler as number if we can - const strResult = result[1]; - if (strResult === undefined) { + firstPressedSelect() { + for (const idx in this.selectPressed) { + if (this.selectPressed[idx]) { + return idx; + } + } return undefined; } - return parseInt(strResult); -}; -TraktorS3.Controller.prototype.samplesOutput = function(value, group, key) { - // Sampler 1-8 -> Channel1 - // Samples 9-16 -> Channel2 - const sampler = this.resolveSampler(group); - let deck = this.Decks.deck1; - let num = sampler; - if (sampler === undefined) { - return; - } else if (sampler > 8 && sampler < 17) { - if (!TraktorS3.SixteenSamplers) { - // These samplers are ignored - return; + firstPressedEnable() { + for (const ch in this.enablePressed) { + if (this.enablePressed[ch]) { + return ch; + } } - deck = this.Decks.deck2; - num = sampler - 8; + return undefined; } - // If we are in samples modes light corresponding LED - if (deck.padModeState !== 1) { - return; - } - if (key === "play_indicator" && engine.getValue(group, "track_loaded")) { - if (value) { - // Green light on play - this.hid.setOutput("deck1", "!pad_" + num, this.hid.LEDColors.GREEN + TraktorS3.LEDBrightValue, !this.batchingOutputs); - // Also light deck2 samplers in 8-sampler mode. - if (!TraktorS3.SixteenSamplers && this.Decks.deck2.padModeState === 1) { - this.hid.setOutput("deck2", "!pad_" + num, this.hid.LEDColors.GREEN + TraktorS3.LEDBrightValue, !this.batchingOutputs); + anyEnablePressed() { + for (const key in this.enablePressed) { + if (this.enablePressed[key]) { + return true; } - } else { - // Reset LED to base color - deck.colorOutput(1, "!pad_" + num); - if (!TraktorS3.SixteenSamplers && this.Decks.deck2.padModeState === 1) { - this.Decks.deck2.colorOutput(1, "!pad_" + num); - } - } - } else if (key === "track_loaded") { - deck.colorOutput(value, "!pad_" + num); - if (!TraktorS3.SixteenSamplers && this.Decks.deck2.padModeState === 1) { - this.Decks.deck2.colorOutput(value, "!pad_" + num); } + return false; } -}; -TraktorS3.Controller.prototype.lightGroup = function(packet, outputGroupName, coGroupName) { - const groupOb = packet.groups[outputGroupName]; - for (const fieldName in groupOb) { - const field = groupOb[fieldName]; - if (field.name[0] === "!") { - continue; + changeState(newState) { + if (newState === this.currentState) { + return; } - if (field.mapped_callback !== undefined) { - const value = engine.getValue(coGroupName, field.name); - field.mapped_callback(value, coGroupName, field.name); + + // Ignore next values for all knob actions. This is safe to do for all knobs + // even if we're ignoring knobs that aren't active in the new state. + for (let ch = 1; ch <= 4; ch++) { + var group = "[Channel" + ch + "]"; + engine.softTakeoverIgnoreNextValue("[QuickEffectRack1_" + group + "]", "super1"); + } + for (let unit = 1; unit <= 4; unit++) { + group = "[EffectRack1_EffectUnit" + unit + "]"; + key = "mix"; + engine.softTakeoverIgnoreNextValue(group, key); + for (let effect = 1; effect <= 4; effect++) { + group = "[EffectRack1_EffectUnit" + unit + "_Effect" + effect + "]"; + key = "meta"; + engine.softTakeoverIgnoreNextValue(group, key); + for (let param = 1; param <= 4; param++) { + var key = "parameter" + param; + engine.softTakeoverIgnoreNextValue(group, key); + } + } } - // No callback, no light! - } -}; -TraktorS3.Controller.prototype.lightDeck = function(group, sendPackets) { - if (sendPackets === undefined) { - sendPackets = true; + const oldState = this.currentState; + this.currentState = newState; + if (oldState === this.STATE_FOCUS) { + engine.stopTimer(this.focusBlinkTimer); + this.focusBlinkTimer = 0; + } + switch (newState) { + case this.STATE_FILTER: + break; + case this.STATE_EFFECT_INIT: + break; + case this.STATE_EFFECT: + break; + case this.STATE_FOCUS: + this.focusBlinkTimer = engine.beginTimer(150, function() { + TraktorS3.kontrol.fxController.focusBlinkState = !TraktorS3.kontrol.fxController.focusBlinkState; + TraktorS3.kontrol.fxController.lightFx(); + }, false); + } } - // Freeze the lights while we do this update so we don't spam HID. - this.batchingOutputs = true; - for (var packetName in this.hid.OutputPackets) { - const packet = this.hid.OutputPackets[packetName]; - let deckGroupName = "deck1"; - if (group === "[Channel2]" || group === "[Channel4]") { - deckGroupName = "deck2"; + + fxSelectHandler(field) { + const fxNumber = parseInt(field.name[field.name.length - 1]); + // Coerce to boolean + this.selectPressed[fxNumber] = !!field.value; + + if (!field.value) { + if (fxNumber === this.activeFX) { + if (this.currentState === this.STATE_EFFECT) { + this.changeState(this.STATE_FILTER); + } else if (this.currentState === this.STATE_EFFECT_INIT) { + this.changeState(this.STATE_EFFECT); + } + } + this.lightFx(); + return; } - const deck = this.Decks[deckGroupName]; + switch (this.currentState) { + case this.STATE_FILTER: + // If any fxEnable button is pressed, we are toggling fx unit assignment. + if (this.anyEnablePressed()) { + for (const key in this.enablePressed) { + if (this.enablePressed[key]) { + if (fxNumber === 0) { + var fxGroup = "[QuickEffectRack1_" + key + "_Effect1]"; + var fxKey = "enabled"; + } else { + fxGroup = "[EffectRack1_EffectUnit" + fxNumber + "]"; + fxKey = "group_" + key + "_enable"; + } + script.toggleControl(fxGroup, fxKey); + } + } + } else { + if (fxNumber === 0) { + this.changeState(this.STATE_FILTER); + } else { + this.changeState(this.STATE_EFFECT_INIT); + } + this.activeFX = fxNumber; + } + break; + case this.STATE_EFFECT_INIT: + // Fallthrough intended + case this.STATE_EFFECT: + if (fxNumber === 0) { + this.changeState(this.STATE_FILTER); + } else if (fxNumber !== this.activeFX) { + this.changeState(this.STATE_EFFECT_INIT); + } + this.activeFX = fxNumber; + break; + case this.STATE_FOCUS: + if (fxNumber === 0) { + this.changeState(this.STATE_FILTER); + } else { + this.changeState(this.STATE_EFFECT_INIT); + } + this.activeFX = fxNumber; + break; + } + this.lightFx(); + } - this.lightGroup(packet, deckGroupName, group); - this.lightGroup(packet, group, group); + fxEnableHandler(field) { + // Coerce to boolean + this.enablePressed[field.group] = !!field.value; - deck.lightPads(); + if (!field.value) { + this.lightFx(); + return; + } - // These lights are different because either they aren't associated with a CO, or - // there are two buttons that point to the same CO. - deck.basicOutput(0, "!shift"); - deck.colorOutput(0, "!PreviewTrack"); - deck.colorOutput(0, "!LibraryFocus"); - deck.colorOutput(0, "!MaximizeLibrary"); - deck.colorOutput(deck.jogToggled, "!jogButton"); - if (group === "[Channel4]") { - this.basicOutput(0, "[Master]", "!extButton"); + const fxGroupPrefix = "[EffectRack1_EffectUnit" + this.activeFX; + const buttonNumber = this.channelToIndex(field.group); + switch (this.currentState) { + case this.STATE_FILTER: + break; + case this.STATE_EFFECT_INIT: + // Fallthrough intended + case this.STATE_EFFECT: + if (this.firstPressedSelect()) { + // Choose the first pressed select button only. + this.changeState(this.STATE_FOCUS); + engine.setValue(fxGroupPrefix + "]", "focused_effect", buttonNumber); + } else { + var group = fxGroupPrefix + "_Effect" + buttonNumber + "]"; + var key = "enabled"; + script.toggleControl(group, key); + } + break; + case this.STATE_FOCUS: + var focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); + group = fxGroupPrefix + "_Effect" + focusedEffect + "]"; + key = "button_parameter" + buttonNumber; + script.toggleControl(group, key); + break; } + this.lightFx(); } - // this.lightFx(); - // Selected deck lights - if (group === "[Channel1]") { - this.hid.setOutput("[Channel1]", "!deck_A", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel1]"]] + TraktorS3.LEDBrightValue, false); - this.hid.setOutput("[Channel3]", "!deck_C", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel3]"]] + TraktorS3.LEDDimValue, false); - } else if (group === "[Channel2]") { - this.hid.setOutput("[Channel2]", "!deck_B", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel2]"]] + TraktorS3.LEDBrightValue, false); - this.hid.setOutput("[Channel4]", "!deck_D", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel4]"]] + TraktorS3.LEDDimValue, false); - } else if (group === "[Channel3]") { - this.hid.setOutput("[Channel3]", "!deck_C", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel3]"]] + TraktorS3.LEDBrightValue, false); - this.hid.setOutput("[Channel1]", "!deck_A", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel1]"]] + TraktorS3.LEDDimValue, false); - } else if (group === "[Channel4]") { - this.hid.setOutput("[Channel4]", "!deck_D", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel4]"]] + TraktorS3.LEDBrightValue, false); - this.hid.setOutput("[Channel2]", "!deck_B", this.hid.LEDColors[TraktorS3.ChannelColors["[Channel2]"]] + TraktorS3.LEDDimValue, false); + fxKnobHandler(field) { + const value = field.value / 4095.; + const fxGroupPrefix = "[EffectRack1_EffectUnit" + this.activeFX; + const knobIdx = this.channelToIndex(field.group); + + switch (this.currentState) { + case this.STATE_FILTER: + if (field.group === "[Channel4]" && this.controller.channel4InputMode) { + // There is no quickeffect for the microphone, do nothing. + return; + } + engine.setParameter("[QuickEffectRack1_" + field.group + "]", "super1", value); + break; + case this.STATE_EFFECT_INIT: + // Fallthrough intended + case this.STATE_EFFECT: + if (knobIdx === 4) { + engine.setParameter(fxGroupPrefix + "]", "mix", value); + } else { + var group = fxGroupPrefix + "_Effect" + knobIdx + "]"; + engine.setParameter(group, "meta", value); + } + break; + case this.STATE_FOCUS: + var focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); + group = fxGroupPrefix + "_Effect" + focusedEffect + "]"; + var key = "parameter" + knobIdx; + engine.setParameter(group, key, value); + break; + } } - this.batchingOutputs = false; - // And now send them all. - if (sendPackets) { - for (packetName in this.hid.OutputPackets) { - this.hid.OutputPackets[packetName].send(); + getFXSelectLEDValue(fxNumber, status) { + const ledValue = this.controller.fxLEDValue[fxNumber]; + switch (status) { + case this.LIGHT_OFF: + return 0x00; + case this.LIGHT_DIM: + return ledValue; + case this.LIGHT_BRIGHT: + return ledValue + 0x02; } } -}; -// Render wheel positions, channel VU meters, and master vu meters -TraktorS3.Controller.prototype.guiTickHandler = function() { - this.batchingOutputs = true; - let gotUpdate = false; - gotUpdate |= this.Channels[this.Decks.deck1.activeChannel].lightWheelPosition(); - gotUpdate |= this.Channels[this.Decks.deck2.activeChannel].lightWheelPosition(); - - for (const vu in this.masterVuMeter) { - if (this.masterVuMeter[vu].updated) { - this.vuMeterOutput(this.masterVuMeter[vu].value, "[Master]", vu, 8); - this.masterVuMeter[vu].updated = false; - gotUpdate = true; + getChannelColor(group, status) { + const ledValue = this.controller.hid.LEDColors[TraktorS3.ChannelColors[group]]; + switch (status) { + case this.LIGHT_OFF: + return 0x00; + case this.LIGHT_DIM: + return ledValue; + case this.LIGHT_BRIGHT: + return ledValue + 0x02; } } - for (let ch = 1; ch <= 4; ch++) { - const chan = this.Channels["[Channel" + ch + "]"]; - if (chan.vuMeterUpdated) { - this.vuMeterOutput(chan.vuMeterValue, chan.group, "VuMeter", 14); - chan.vuMeterUpdated = false; - gotUpdate = true; + + lightFx() { + this.controller.batchingOutputs = true; + + // Loop through select buttons + // Idx zero is filter button + for (let idx = 0; idx < 5; idx++) { + this.lightSelect(idx); + } + for (let ch = 1; ch <= 4; ch++) { + const channel = "[Channel" + ch + "]"; + this.lightEnable(channel); } - } - this.batchingOutputs = false; + this.controller.batchingOutputs = false; + for (const packetName in this.controller.hid.OutputPackets) { + this.controller.hid.OutputPackets[packetName].send(); + } + } - if (gotUpdate) { - for (const packetName in this.hid.OutputPackets) { - this.hid.OutputPackets[packetName].send(); + lightSelect(idx) { + let status = this.LIGHT_OFF; + let ledValue = 0x00; + switch (this.currentState) { + case this.STATE_FILTER: + // Always light when pressed + if (this.selectPressed[idx]) { + status = this.LIGHT_BRIGHT; + } else { + // select buttons on if fx unit enabled for the pressed channel, + // otherwise disabled. + status = this.LIGHT_DIM; + const pressed = this.firstPressedEnable(); + if (pressed) { + if (idx === 0) { + var fxGroup = "[QuickEffectRack1_" + pressed + "_Effect1]"; + var fxKey = "enabled"; + } else { + fxGroup = "[EffectRack1_EffectUnit" + idx + "]"; + fxKey = "group_" + pressed + "_enable"; + } + if (engine.getParameter(fxGroup, fxKey)) { + status = this.LIGHT_BRIGHT; + } else { + status = this.LIGHT_OFF; + } + } + ledValue = this.getFXSelectLEDValue(idx, status); + } + break; + case this.STATE_EFFECT_INIT: + // Fallthrough intended + case this.STATE_EFFECT: + // Highlight if pressed, disable if active effect. + // Otherwise off. + if (this.selectPressed[idx]) { + status = this.LIGHT_BRIGHT; + } else if (idx === this.activeFX) { + status = this.LIGHT_BRIGHT; + } + break; + case this.STATE_FOCUS: + // if blink state is false, only like active fx bright + // if blink state is true, active fx is bright and selected effect + // is dim. if those are the same, active fx is dim + if (this.selectPressed[idx]) { + status = this.LIGHT_BRIGHT; + } else { + if (idx === this.activeFX) { + status = this.LIGHT_BRIGHT; + } + if (this.focusBlinkState) { + const fxGroupPrefix = "[EffectRack1_EffectUnit" + this.activeFX; + const focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); + if (idx === focusedEffect) { + status = this.LIGHT_DIM; + } + } + } + break; } + ledValue = this.getFXSelectLEDValue(idx, status); + this.controller.hid.setOutput("[ChannelX]", "!fxButton" + idx, ledValue, false); } -}; -// A special packet sent to the controller switches between mic and line -// input modes. if lineMode is true, sets input to line. Otherwise, mic. -TraktorS3.Controller.prototype.setInputLineMode = function(lineMode) { - const packet = Array(); - packet.length = 33; - packet[0] = 0x20; - if (!lineMode) { - packet[1] = 0x08; + lightEnable(channel) { + let status = this.LIGHT_OFF; + let ledValue = 0x00; + const buttonNumber = this.channelToIndex(channel); + switch (this.currentState) { + case this.STATE_FILTER: + // enable buttons highlighted if pressed or if any fx unit enabled for channel. + // Highlight if pressed. + status = this.LIGHT_DIM; + if (this.enablePressed[channel]) { + status = this.LIGHT_BRIGHT; + } else { + for (let idx = 1; idx <= 4 && status === this.LIGHT_OFF; idx++) { + var group = "[EffectRack1_EffectUnit" + idx + "]"; + var key = "group_" + channel + "_enable"; + if (engine.getParameter(group, key)) { + status = this.LIGHT_DIM; + } + } + } + // Enable buttons have regular deck colors + ledValue = this.getChannelColor(channel, status); + break; + case this.STATE_EFFECT_INIT: + // Fallthrough intended + case this.STATE_EFFECT: + if (this.enablePressed[channel]) { + status = this.LIGHT_BRIGHT; + } else { + // off if nothing loaded, dim if loaded, bright if enabled. + group = "[EffectRack1_EffectUnit" + this.activeFX + "_Effect" + buttonNumber + "]"; + if (engine.getParameter(group, "loaded")) { + status = this.LIGHT_DIM; + } + if (engine.getParameter(group, "enabled")) { + status = this.LIGHT_BRIGHT; + } + } + // Colors match effect colors so it's obvious we're in a different mode + ledValue = this.getFXSelectLEDValue(this.activeFX, status); + break; + case this.STATE_FOCUS: + if (this.enablePressed[channel]) { + status = this.LIGHT_BRIGHT; + } else { + const fxGroupPrefix = "[EffectRack1_EffectUnit" + this.activeFX; + const focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); + group = fxGroupPrefix + "_Effect" + focusedEffect + "]"; + key = "button_parameter" + buttonNumber; + // Off if not loaded, dim if loaded, bright if enabled. + if (engine.getParameter(group, key + "_loaded")) { + status = this.LIGHT_DIM; + } + if (engine.getParameter(group, key)) { + status = this.LIGHT_BRIGHT; + } + } + // Colors match effect colors so it's obvious we're in a different mode + ledValue = this.getFXSelectLEDValue(this.activeFX, status); + break; + } + this.controller.hid.setOutput(channel, "!fxEnabled", ledValue, false); } - controller.send(packet, packet.length, 0xF4); }; TraktorS3.messageCallback = function(_packet, data) { From e2924e793b7522ec8249e33e499c3a26e3e5463f Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sun, 25 Dec 2022 02:42:17 +0100 Subject: [PATCH 23/36] Allow setting crossfader assignment from S3 script --- res/controllers/Traktor-Kontrol-S3-hid-scripts.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 02b3c0d076e1..5aec2d36c487 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -111,6 +111,8 @@ TraktorS3.DefaultBeatLoopLength = null; // 32 TraktorS3.DefaultSyncEnabled = null; // true TraktorS3.DefaultQuantizeEnabled = null; // true TraktorS3.DefaultKeylockEnabled = null; // true +// -1 for left, 0 for center/not assigned, 1 for right +TraktorS3.DefaultCrossfaderAssignments = [null, null, null, null]; // [0, 0, 0, 0] // Set to true to output debug messages and debug light outputs. TraktorS3.DebugMode = false; @@ -1632,6 +1634,14 @@ TraktorS3.Channel = class { if (TraktorS3.DefaultKeylockEnabled !== null) { engine.setValue(group, "keylock", TraktorS3.DefaultKeylockEnabled); } + // The visual order of the channels in Mixxx is 4, 2, 1, 3, but we want + // the crossfader assignments array to match the visual layout + const visualChannelIndex = {3: 0, 1: 1, 2: 2, 4: 3}[this.groupNumber]; + if (TraktorS3.DefaultCrossfaderAssignments[visualChannelIndex] !== null) { + // This goes 0-2 for left, right, and center, but having the values + // in this script's config be -1, 0, and 1 makes much more sense + engine.setValue(group, "orientation", TraktorS3.DefaultCrossfaderAssignments[visualChannelIndex] + 1); + } } trackLoadedHandler() { From 837962e02db0625626223b966275c22d7df43cc0 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Thu, 29 Dec 2022 22:32:05 +0100 Subject: [PATCH 24/36] Replace S3 super knob hack with new control object This is the same as `loaded_chain_preset`, except that it preserves the value when changing presets. Bikeshedding over the name is very welcome. --- .../Traktor-Kontrol-S3-hid-scripts.js | 42 ++++--------------- src/effects/effectchain.cpp | 16 +++++++ src/effects/effectchain.h | 2 + 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 5aec2d36c487..ba88fcd3a1b5 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -2371,12 +2371,6 @@ TraktorS3.VanillaFxControl = class { // when the last FX Select button is released. this.individualFxChainAssigned = false; - // When non-null, the channel's super knob value will be set to this - // value when the quick effect chain has changed. This value is stored - // just before changing the quick effect chain because there's no built - // in way to preserve the value. - this.oldSuperKnobValues = [null, null, null, null]; - // These are the colors for each FX button, with 0 being the filter // button this.fxColors = { @@ -2413,9 +2407,8 @@ TraktorS3.VanillaFxControl = class { this.controller.registerInputButton(messageShort, "[Channel2]", "!fxEnabled", 0x07, 0x20, this.fxEnableHandler.bind(this)); this.controller.registerInputButton(messageShort, "[Channel4]", "!fxEnabled", 0x07, 0x40, this.fxEnableHandler.bind(this)); - // We'll restore the old super knob values here when changing quick - // effect chains. This also changes the lighting of the five FX Select - // buttons and maybe also the FX Enable buttons. + // This changes the lighting of the five FX Select buttons and maybe + // also the FX Enable buttons engine.connectControl("[QuickEffectRack1_[Channel1]]", "loaded_chain_preset", this.quickEffectChainLoadHandler.bind(this)); engine.connectControl("[QuickEffectRack1_[Channel2]]", "loaded_chain_preset", this.quickEffectChainLoadHandler.bind(this)); engine.connectControl("[QuickEffectRack1_[Channel3]]", "loaded_chain_preset", this.quickEffectChainLoadHandler.bind(this)); @@ -2500,22 +2493,7 @@ TraktorS3.VanillaFxControl = class { } } - quickEffectChainLoadHandler(_value, group, _key) { - // There's no built in way to change effect chains while preserving the - // super knob values, so we'll do this ourselves. The old super knob - // value is set in `this.setQuickEffectChain()`. - // FIXME: There needs to be a way in Mixxx to change the quick effect - // chain without changing this value. You can hear the default - // value poking through when changing to the same effect. - const channelNumber = parseInt(group[group.length - 3]); - if (this.oldSuperKnobValues[channelNumber - 1] !== null) { - engine.softTakeover(group, "super1", false); - engine.setValue(group, "super1", this.oldSuperKnobValues[channelNumber - 1]); - engine.softTakeover(group, "super1", true); - - this.oldSuperKnobValues[channelNumber - 1] = null; - } - + quickEffectChainLoadHandler(_value, _group, _key) { // All of the lights should be updated at this point this.lightFx(); } @@ -2529,14 +2507,12 @@ TraktorS3.VanillaFxControl = class { * fxButtonIndex 0-5 will be mapped to quick effect chain presets 1-6. */ setQuickEffectChain(channel, fxButtonIndex) { - // We can't immediately restore this value because it may take a buffer - // until the effect chain is actually changed. So instead we'll store - // the old value, and then restore it in - // `this.quickEffectChainLoadHandler()`. - const superValue = engine.getValue(`[QuickEffectRack1_[Channel${channel}]]`, "super1"); - this.oldSuperKnobValues[channel - 1] = superValue; - - engine.setValue(`[QuickEffectRack1_[Channel${channel}]]`, "loaded_chain_preset", fxButtonIndex + 1); + // The normal version of this resets the super knobs to the value it had + // when the chain preset was saved. We need the value to stay consistent + // with the knob on the controller. + engine.setValue(`[QuickEffectRack1_[Channel${channel}]]`, + "loaded_chain_preset_preserving_super_knob_value", + fxButtonIndex + 1); } // Output handling diff --git a/src/effects/effectchain.cpp b/src/effects/effectchain.cpp index 69124a40c66c..5f0e6faaed4a 100644 --- a/src/effects/effectchain.cpp +++ b/src/effects/effectchain.cpp @@ -104,6 +104,12 @@ EffectChain::EffectChain(const QString& group, this, &EffectChain::slotControlLoadedChainPresetRequest); + m_pControlLoadedChainPresetPreservingSuperKnobValue = std::make_unique( + ConfigKey(m_group, "loaded_chain_preset_preserving_super_knob_value"), false); + m_pControlLoadedChainPresetPreservingSuperKnobValue->connectValueChangeRequest( + this, + &EffectChain::slotControlLoadedChainPresetRequestPreservingSuperKnobValue); + m_pControlNextChainPreset = std::make_unique( ConfigKey(m_group, "next_chain_preset")); connect(m_pControlNextChainPreset.get(), @@ -349,6 +355,16 @@ void EffectChain::slotControlLoadedChainPresetRequest(double value) { loadChainPreset(presetAtIndex(index)); } +void EffectChain::slotControlLoadedChainPresetRequestPreservingSuperKnobValue(double value) { + // Loading a chain preset sets the super knob's value to the value it was at + // when the chain preset was saved. It may be desirable to keep the knob's + // value as is, for instance when changing between multiple presets that + // contain a filter and an additional additional effect. + const double old_value = m_pControlChainSuperParameter->get(); + slotControlLoadedChainPresetRequest(value); + slotControlChainSuperParameter(old_value, true); +} + void EffectChain::setControlLoadedPresetIndex(uint index) { // add 1 to make the ControlObject 1-indexed like other ControlObjects m_pControlLoadedChainPreset->setAndConfirm(index + 1); diff --git a/src/effects/effectchain.h b/src/effects/effectchain.h index 42e5dd33218c..72ae7c2c4545 100644 --- a/src/effects/effectchain.h +++ b/src/effects/effectchain.h @@ -117,6 +117,7 @@ class EffectChain : public QObject { private slots: void slotControlLoadedChainPresetRequest(double value); + void slotControlLoadedChainPresetRequestPreservingSuperKnobValue(double value); void slotControlChainPresetSelector(double value); void slotControlNextChainPreset(double value); void slotControlPrevChainPreset(double value); @@ -137,6 +138,7 @@ class EffectChain : public QObject { std::unique_ptr m_pControlChainEnabled; std::unique_ptr m_pControlChainMixMode; std::unique_ptr m_pControlLoadedChainPreset; + std::unique_ptr m_pControlLoadedChainPresetPreservingSuperKnobValue; std::unique_ptr m_pControlChainPresetSelector; std::unique_ptr m_pControlNextChainPreset; std::unique_ptr m_pControlPrevChainPreset; From 39212b6ee488677ead1f1ffcdb2da9329a888378 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 2 Jan 2023 13:40:30 +0100 Subject: [PATCH 25/36] Replace var and global assignment in S3 script With lexically scoped const/let bindings. The `var TraktorS3 = ...` needs to stay a var by design. --- .../Traktor-Kontrol-S3-hid-scripts.js | 176 +++++++----------- 1 file changed, 70 insertions(+), 106 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index ba88fcd3a1b5..a8fd43f18f0f 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -1,3 +1,5 @@ +"use strict"; + /////////////////////////////////////////////////////////////////////////////////// // // Traktor Kontrol S3 HID controller script v2.00 @@ -167,7 +169,6 @@ TraktorS3.Controller = class { WHITE: 0x44 }; - // FX 5 is the Filter this.fxLEDValue = { 0: this.hid.LEDColors.PURPLE, @@ -275,7 +276,7 @@ TraktorS3.Controller = class { this.hid.registerInputPacket(messageLong); - for (ch in this.Channels) { + for (const ch in this.Channels) { const chanob = this.Channels[ch]; engine.makeConnection(ch, "playposition", TraktorS3.Channel.prototype.playpositionChanged.bind(chanob)); @@ -302,8 +303,9 @@ TraktorS3.Controller = class { // NOTE: Soft takeovers must only be enabled after setting the initial // value, or the above line won't have any effect - for (var ch = 1; ch <= 4; ch++) { - var group = "[Channel" + ch + "]"; + for (let ch = 1; ch <= 4; ch++) { + const group = "[Channel" + ch + "]"; + if (!TraktorS3.PitchSliderRelativeMode) { engine.softTakeover(group, "rate", true); } @@ -312,17 +314,16 @@ TraktorS3.Controller = class { engine.softTakeover(group, "pregain", true); engine.softTakeover("[QuickEffectRack1_" + group + "]", "super1", true); } + for (let unit = 1; unit <= 4; unit++) { - group = "[EffectRack1_EffectUnit" + unit + "]"; - let key = "mix"; - engine.softTakeover(group, key, true); + engine.softTakeover("[EffectRack1_EffectUnit" + unit + "]", "mix", true); + for (let effect = 1; effect <= 4; effect++) { - group = "[EffectRack1_EffectUnit" + unit + "_Effect" + effect + "]"; - key = "meta"; - engine.softTakeover(group, key, true); + const group = "[EffectRack1_EffectUnit" + unit + "_Effect" + effect + "]"; + + engine.softTakeover(group, "meta", true); for (let param = 1; param <= 4; param++) { - key = "parameter" + param; - engine.softTakeover(group, key, true); + engine.softTakeover(group, "parameter" + param, true); } } } @@ -455,9 +456,8 @@ TraktorS3.Controller = class { const outputA = new HIDPacket("outputA", 0x80); const outputB = new HIDPacket("outputB", 0x81); - for (var idx in this.Decks) { - var deck = this.Decks[idx]; - deck.registerOutputs(outputA, outputB); + for (const idx in this.Decks) { + this.Decks[idx].registerOutputs(outputA, outputB); } outputA.addOutput("[Channel1]", "!deck_A", 0x0A, "B"); @@ -492,7 +492,7 @@ TraktorS3.Controller = class { "[Channel4]": 0x2E }; for (const ch in VuOffsets) { - for (var i = 0; i < 14; i++) { + for (let i = 0; i < 14; i++) { outputB.addOutput(ch, "!" + "VuMeter" + i, VuOffsets[ch] + i, "B"); } } @@ -501,7 +501,7 @@ TraktorS3.Controller = class { "VuMeterL": 0x3D, "VuMeterR": 0x46 }; - for (i = 0; i < 8; i++) { + for (let i = 0; i < 8; i++) { outputB.addOutput("[Master]", "!" + "VuMeterL" + i, MasterVuOffsets.VuMeterL + i, "B"); outputB.addOutput("[Master]", "!" + "VuMeterR" + i, MasterVuOffsets.VuMeterR + i, "B"); } @@ -516,14 +516,12 @@ TraktorS3.Controller = class { this.hid.registerOutputPacket(outputB); - for (idx in this.Decks) { - deck = this.Decks[idx]; - deck.linkOutputs(); + for (const idx in this.Decks) { + this.Decks[idx].linkOutputs(); } - for (idx in this.Channels) { - const chan = this.Channels[idx]; - chan.linkOutputs(); + for (const idx in this.Channels) { + this.Channels[idx].linkOutputs(); } engine.connectControl("[Microphone]", "pfl", this.pflOutput); @@ -538,7 +536,7 @@ TraktorS3.Controller = class { this.guiTickConnection = engine.makeConnection("[Master]", "guiTick50ms", TraktorS3.Controller.prototype.guiTickHandler.bind(this)); // Sampler callbacks - for (i = 1; i <= 8; ++i) { + for (let i = 1; i <= 8; ++i) { this.samplerCallbacks.push(engine.makeConnection("[Sampler" + i + "]", "track_loaded", TraktorS3.Controller.prototype.samplesOutput.bind(this))); this.samplerCallbacks.push(engine.makeConnection("[Sampler" + i + "]", "play_indicator", TraktorS3.Controller.prototype.samplesOutput.bind(this))); } @@ -704,7 +702,7 @@ TraktorS3.Controller = class { } // Freeze the lights while we do this update so we don't spam HID. this.batchingOutputs = true; - for (var packetName in this.hid.OutputPackets) { + for (const packetName in this.hid.OutputPackets) { const packet = this.hid.OutputPackets[packetName]; let deckGroupName = "deck1"; if (group === "[Channel2]" || group === "[Channel4]") { @@ -748,7 +746,7 @@ TraktorS3.Controller = class { this.batchingOutputs = false; // And now send them all. if (sendPackets) { - for (packetName in this.hid.OutputPackets) { + for (const packetName in this.hid.OutputPackets) { this.hid.OutputPackets[packetName].send(); } } @@ -1051,7 +1049,7 @@ TraktorS3.Deck = class { // Hotcues mode if (this.padModeState === 0) { - var action = this.shiftPressed ? "_clear" : "_activate"; + const action = this.shiftPressed ? "_clear" : "_activate"; engine.setValue(this.activeChannel, "hotcue_" + padNumber + action, field.value); return; } @@ -1064,30 +1062,18 @@ TraktorS3.Deck = class { const playing = engine.getValue("[Sampler" + sampler + "]", "play"); if (this.shiftPressed) { - if (playing) { - action = "cue_default"; - } else { - action = "eject"; - } + const action = playing ? "cue_default" : "eject"; engine.setValue("[Sampler" + sampler + "]", action, field.value); return; } const loaded = engine.getValue("[Sampler" + sampler + "]", "track_loaded"); if (loaded) { if (TraktorS3.SamplerModePressAndHold) { - if (field.value) { - action = "cue_gotoandplay"; - } else { - action = "stop"; - } + const action = field.value ? "cue_gotoandplay" : "stop"; engine.setValue("[Sampler" + sampler + "]", action, 1); } else { if (field.value) { - if (playing) { - action = "stop"; - } else { - action = "cue_gotoandplay"; - } + const action = playing ? "stop" : "cue_gotoandplay"; engine.setValue("[Sampler" + sampler + "]", action, 1); } } @@ -1541,7 +1527,7 @@ TraktorS3.Deck = class { if (this.padModeState === 1) { this.colorOutput(0, "hotcues"); this.colorOutput(1, "samples"); - for (var i = 1; i <= 8; i++) { + for (let i = 1; i <= 8; i++) { let idx = i; if (this.group === "deck2" && TraktorS3.SixteenSamplers) { idx += 8; @@ -1552,7 +1538,7 @@ TraktorS3.Deck = class { } else { this.colorOutput(1, "hotcues"); this.colorOutput(0, "samples"); - for (i = 1; i <= 8; ++i) { + for (let i = 1; i <= 8; ++i) { this.lightHotcue(i); } } @@ -1848,23 +1834,21 @@ TraktorS3.FXControl = class { } registerInputs(messageShort, messageLong) { - // FX Buttons - const fxFn = TraktorS3.FXControl.prototype; - this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx1", 0x08, 0x08, fxFn.fxSelectHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx2", 0x08, 0x10, fxFn.fxSelectHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx3", 0x08, 0x20, fxFn.fxSelectHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx4", 0x08, 0x40, fxFn.fxSelectHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx0", 0x08, 0x80, fxFn.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx1", 0x08, 0x08, this.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx2", 0x08, 0x10, this.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx3", 0x08, 0x20, this.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx4", 0x08, 0x40, this.fxSelectHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[ChannelX]", "!fx0", 0x08, 0x80, this.fxSelectHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[Channel3]", "!fxEnabled", 0x07, 0x08, fxFn.fxEnableHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[Channel1]", "!fxEnabled", 0x07, 0x10, fxFn.fxEnableHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[Channel2]", "!fxEnabled", 0x07, 0x20, fxFn.fxEnableHandler.bind(this)); - this.controller.registerInputButton(messageShort, "[Channel4]", "!fxEnabled", 0x07, 0x40, fxFn.fxEnableHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[Channel3]", "!fxEnabled", 0x07, 0x08, this.fxEnableHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[Channel1]", "!fxEnabled", 0x07, 0x10, this.fxEnableHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[Channel2]", "!fxEnabled", 0x07, 0x20, this.fxEnableHandler.bind(this)); + this.controller.registerInputButton(messageShort, "[Channel4]", "!fxEnabled", 0x07, 0x40, this.fxEnableHandler.bind(this)); - this.controller.registerInputScaler(messageLong, "[Channel1]", "!fxKnob", 0x39, 0xFFFF, fxFn.fxKnobHandler.bind(this)); - this.controller.registerInputScaler(messageLong, "[Channel2]", "!fxKnob", 0x3B, 0xFFFF, fxFn.fxKnobHandler.bind(this)); - this.controller.registerInputScaler(messageLong, "[Channel3]", "!fxKnob", 0x37, 0xFFFF, fxFn.fxKnobHandler.bind(this)); - this.controller.registerInputScaler(messageLong, "[Channel4]", "!fxKnob", 0x3D, 0xFFFF, fxFn.fxKnobHandler.bind(this)); + this.controller.registerInputScaler(messageLong, "[Channel1]", "!fxKnob", 0x39, 0xFFFF, this.fxKnobHandler.bind(this)); + this.controller.registerInputScaler(messageLong, "[Channel2]", "!fxKnob", 0x3B, 0xFFFF, this.fxKnobHandler.bind(this)); + this.controller.registerInputScaler(messageLong, "[Channel3]", "!fxKnob", 0x37, 0xFFFF, this.fxKnobHandler.bind(this)); + this.controller.registerInputScaler(messageLong, "[Channel4]", "!fxKnob", 0x3D, 0xFFFF, this.fxKnobHandler.bind(this)); } channelToIndex(group) { @@ -1921,20 +1905,17 @@ TraktorS3.FXControl = class { // Ignore next values for all knob actions. This is safe to do for all knobs // even if we're ignoring knobs that aren't active in the new state. for (let ch = 1; ch <= 4; ch++) { - var group = "[Channel" + ch + "]"; - engine.softTakeoverIgnoreNextValue("[QuickEffectRack1_" + group + "]", "super1"); + engine.softTakeoverIgnoreNextValue(`[QuickEffectRack1_[Channel${ch}]]`, "super1"); } + for (let unit = 1; unit <= 4; unit++) { - group = "[EffectRack1_EffectUnit" + unit + "]"; - key = "mix"; - engine.softTakeoverIgnoreNextValue(group, key); + engine.softTakeoverIgnoreNextValue(`[EffectRack1_EffectUnit${unit}]`, "mix"); + for (let effect = 1; effect <= 4; effect++) { - group = "[EffectRack1_EffectUnit" + unit + "_Effect" + effect + "]"; - key = "meta"; - engine.softTakeoverIgnoreNextValue(group, key); + const group = `[EffectRack1_EffectUnit${unit}_Effect${effect}]`; + engine.softTakeoverIgnoreNextValue(group, "meta"); for (let param = 1; param <= 4; param++) { - var key = "parameter" + param; - engine.softTakeoverIgnoreNextValue(group, key); + engine.softTakeoverIgnoreNextValue(group, "parameter" + param); } } } @@ -1984,13 +1965,10 @@ TraktorS3.FXControl = class { for (const key in this.enablePressed) { if (this.enablePressed[key]) { if (fxNumber === 0) { - var fxGroup = "[QuickEffectRack1_" + key + "_Effect1]"; - var fxKey = "enabled"; + script.toggleControl(`[QuickEffectRack1_${key}_Effect1]`, "enabled"); } else { - fxGroup = "[EffectRack1_EffectUnit" + fxNumber + "]"; - fxKey = "group_" + key + "_enable"; + script.toggleControl(`[EffectRack1_EffectUnit${fxNumber}]`, `group_${key}_enable`); } - script.toggleControl(fxGroup, fxKey); } } } else { @@ -2046,17 +2024,13 @@ TraktorS3.FXControl = class { this.changeState(this.STATE_FOCUS); engine.setValue(fxGroupPrefix + "]", "focused_effect", buttonNumber); } else { - var group = fxGroupPrefix + "_Effect" + buttonNumber + "]"; - var key = "enabled"; - script.toggleControl(group, key); + script.toggleControl(`${fxGroupPrefix}_Effect${buttonNumber}]`, "enabled"); } break; - case this.STATE_FOCUS: - var focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); - group = fxGroupPrefix + "_Effect" + focusedEffect + "]"; - key = "button_parameter" + buttonNumber; - script.toggleControl(group, key); - break; + case this.STATE_FOCUS: { + const focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); + script.toggleControl(`${fxGroupPrefix}_Effect${focusedEffect}]`, "button_parameter" + buttonNumber); + } break; } this.lightFx(); } @@ -2080,16 +2054,13 @@ TraktorS3.FXControl = class { if (knobIdx === 4) { engine.setParameter(fxGroupPrefix + "]", "mix", value); } else { - var group = fxGroupPrefix + "_Effect" + knobIdx + "]"; - engine.setParameter(group, "meta", value); + engine.setParameter(`${fxGroupPrefix}_Effect${knobIdx}]`, "meta", value); } break; - case this.STATE_FOCUS: - var focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); - group = fxGroupPrefix + "_Effect" + focusedEffect + "]"; - var key = "parameter" + knobIdx; - engine.setParameter(group, key, value); - break; + case this.STATE_FOCUS: { + const focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); + engine.setParameter(`${fxGroupPrefix}_Effect${focusedEffect}]`, "parameter" + knobIdx, value); + } break; } } @@ -2150,13 +2121,8 @@ TraktorS3.FXControl = class { status = this.LIGHT_DIM; const pressed = this.firstPressedEnable(); if (pressed) { - if (idx === 0) { - var fxGroup = "[QuickEffectRack1_" + pressed + "_Effect1]"; - var fxKey = "enabled"; - } else { - fxGroup = "[EffectRack1_EffectUnit" + idx + "]"; - fxKey = "group_" + pressed + "_enable"; - } + const fxGroup = idx === 0 ? `[QuickEffectRack1_${pressed}_Effect1]` : `[EffectRack1_EffectUnit${idx}]`; + const fxKey = idx === 0 ? "enabled" : `group_${pressed}_enable`; if (engine.getParameter(fxGroup, fxKey)) { status = this.LIGHT_BRIGHT; } else { @@ -2214,9 +2180,7 @@ TraktorS3.FXControl = class { status = this.LIGHT_BRIGHT; } else { for (let idx = 1; idx <= 4 && status === this.LIGHT_OFF; idx++) { - var group = "[EffectRack1_EffectUnit" + idx + "]"; - var key = "group_" + channel + "_enable"; - if (engine.getParameter(group, key)) { + if (engine.getParameter(`[EffectRack1_EffectUnit${idx}]`, `group_${channel}_enable`)) { status = this.LIGHT_DIM; } } @@ -2231,7 +2195,7 @@ TraktorS3.FXControl = class { status = this.LIGHT_BRIGHT; } else { // off if nothing loaded, dim if loaded, bright if enabled. - group = "[EffectRack1_EffectUnit" + this.activeFX + "_Effect" + buttonNumber + "]"; + const group = "[EffectRack1_EffectUnit" + this.activeFX + "_Effect" + buttonNumber + "]"; if (engine.getParameter(group, "loaded")) { status = this.LIGHT_DIM; } @@ -2248,8 +2212,8 @@ TraktorS3.FXControl = class { } else { const fxGroupPrefix = "[EffectRack1_EffectUnit" + this.activeFX; const focusedEffect = engine.getValue(fxGroupPrefix + "]", "focused_effect"); - group = fxGroupPrefix + "_Effect" + focusedEffect + "]"; - key = "button_parameter" + buttonNumber; + const group = `${fxGroupPrefix}_Effect${focusedEffect}]`; + const key = "button_parameter" + buttonNumber; // Off if not loaded, dim if loaded, bright if enabled. if (engine.getParameter(group, key + "_loaded")) { status = this.LIGHT_DIM; @@ -2336,12 +2300,12 @@ TraktorS3.debugLights = function() { TraktorS3.shutdown = function() { // Deactivate all LEDs let packet = Array(267); - for (var i = 0; i < packet.length; i++) { + for (let i = 0; i < packet.length; i++) { packet[i] = 0; } controller.send(packet, packet.length, 0x80); packet = Array(251); - for (i = 0; i < packet.length; i++) { + for (let i = 0; i < packet.length; i++) { packet[i] = 0; } controller.send(packet, packet.length, 0x81); From 2feb4ab26c7903cb05f547d3a07bcac71990c360 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 2 Jan 2023 15:22:29 +0100 Subject: [PATCH 26/36] Fix 0.5 case in normalization of S3 values This would make it impossible to set the quick effect super knob to exactly 0.5. This is important for some effects like the Moog filter. --- .../Traktor-Kontrol-S3-hid-scripts.js | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index a8fd43f18f0f..e4684f46e017 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -370,10 +370,11 @@ TraktorS3.Controller = class { } parameterHandler(field) { + const value = TraktorS3.normalize12BitValue(field.value); if (field.group === "[Channel4]" && this.channel4InputMode) { - engine.setParameter("[Microphone]", field.name, field.value / 4095); + engine.setParameter("[Microphone]", field.name, value); } else { - engine.setParameter(field.group, field.name, field.value / 4095); + engine.setParameter(field.group, field.name, value); } } @@ -385,7 +386,8 @@ TraktorS3.Controller = class { // Only adjust if shift is held. This will still adjust the sound card // volume but it at least allows for control of Mixxx's master gain. if (this.anyShiftPressed()) { - engine.setParameter(field.group, field.name, field.value / 4095); + const value = TraktorS3.normalize12BitValue(field.value); + engine.setParameter(field.group, field.name, value); } } @@ -1347,7 +1349,7 @@ TraktorS3.Deck = class { pitchSliderHandler(field) { // Adapt HID value to rate control range. - const value = -1.0 + ((field.value / 4095) * 2.0); + const value = -1.0 + (TraktorS3.normalize12BitValue(field.value) * 2.0); if (TraktorS3.PitchSliderRelativeMode) { if (this.pitchSliderLastValue === -1) { this.pitchSliderLastValue = value; @@ -1770,6 +1772,19 @@ TraktorS3.Channel = class { } }; +/** + * Normalize a 12-bit 0-4095 input value to a `[0, 1]` value, with a special + * mapping from 2047 to 0.5 as that would become 0.499878. This is important for + * FX and anything else where 0.5 has a special meaning (and where that number + * is checked for without an epsilon). + * + * @param {number} value An integer value in the range `[0 .. 4095]`. + * @returns {number} `value` normalized to `[0, 1]`. + */ +TraktorS3.normalize12BitValue = function(value) { + return value === 2047 ? 0.5 : value / 4095; +}; + // Finds the shortest distance between two angles on the wheel, assuming // 0-8.0 angle value. TraktorS3.wheelSegmentDistance = function(segNum, angle) { @@ -2036,7 +2051,7 @@ TraktorS3.FXControl = class { } fxKnobHandler(field) { - const value = field.value / 4095.; + const value = TraktorS3.normalize12BitValue(field.value); const fxGroupPrefix = "[EffectRack1_EffectUnit" + this.activeFX; const knobIdx = this.channelToIndex(field.group); @@ -2429,7 +2444,7 @@ TraktorS3.VanillaFxControl = class { return; } - const value = field.value / 4095; + const value = TraktorS3.normalize12BitValue(field.value); engine.setParameter(`[QuickEffectRack1_${field.group}]`, "super1", value); } From ef8ba9f784d7cfbfe31d468fe6c7222e8ea99fde Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 2 Jan 2023 23:46:54 +0100 Subject: [PATCH 27/36] Fix TraktorS3.VanillaFxModeChannelColors check --- res/controllers/Traktor-Kontrol-S3-hid-scripts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index e4684f46e017..d7e998c7e79d 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -2417,7 +2417,7 @@ TraktorS3.VanillaFxControl = class { // button has been released, and holding down one FX button, assigning // that effect to a channel, holding down a second button, and releasing // both shouldn't change the global effect assignments. - if (TraktorS3.VanillaFxModeChannelColors && field.value === 1) { + if (field.value === 1) { this.pressedFxSelectButtons.push(fxNumber); if (this.pressedFxSelectButtons.length === 1) { this.individualFxChainAssigned = false; @@ -2559,7 +2559,7 @@ TraktorS3.VanillaFxControl = class { // between channels. We'll also fall back to the channel colors if the // user manually selected an out of bounds chain let ledColor = fxEnabled ? TraktorS3.LEDBrightValue : TraktorS3.LEDDimValue; - if (fxNumber >= 1 && fxNumber <= 5) { + if (!TraktorS3.VanillaFxModeChannelColors || (fxNumber >= 1 && fxNumber <= 5)) { ledColor += this.fxColors[fxNumber]; } else { ledColor += this.controller.hid.LEDColors[TraktorS3.ChannelColors[channelGroup]]; From 89bdab334c82b58df5ec9e28f77788d37faacf40 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 2 Jan 2023 23:47:18 +0100 Subject: [PATCH 28/36] Disable VanillaFxModeChannelColors by default I liked the idea, but it quickly becomes confusing once you start assigning effects to channels. --- res/controllers/Traktor-Kontrol-S3-hid-scripts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index d7e998c7e79d..586ac5ee76fe 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -54,7 +54,7 @@ TraktorS3.VanillaFxModeDefaultToFilter = true; // When enabled, the FX Enable buttons will use the colors set in // `TraktorS3.ChannelColors` when the filter effect is selected. Disabling this // will use the Filter button's orange color instead. -TraktorS3.VanillaFxModeChannelColors = true; +TraktorS3.VanillaFxModeChannelColors = false; // The pitch slider can operate either in absolute or relative mode. // In absolute mode: From 68682215e58671ff1d596d7334398415758b26d7 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 2 Jan 2023 23:49:09 +0100 Subject: [PATCH 29/36] Dimly light inactive FX select buttons This is the original S3 behavior and it's probably less confusing. --- res/controllers/Traktor-Kontrol-S3-hid-scripts.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 586ac5ee76fe..15b776af139b 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -2521,12 +2521,12 @@ TraktorS3.VanillaFxControl = class { } const lightButton = function(fxNumber) { - // By default the LED is off - let ledColor = 0; - if (activeFxSelectButtons.has(fxNumber)) { - ledColor = this.fxColors[fxNumber] + TraktorS3.LEDBrightValue; - } else if (this.pressedFxSelectButtons.indexOf(fxNumber) !== -1) { - ledColor = this.fxColors[fxNumber] + TraktorS3.LEDDimValue; + // By default the LED is dimly lit + let ledColor = this.fxColors[fxNumber]; + if (activeFxSelectButtons.has(fxNumber) || this.pressedFxSelectButtons.indexOf(fxNumber) !== -1) { + ledColor += TraktorS3.LEDBrightValue; + } else { + ledColor += TraktorS3.LEDDimValue; } this.controller.hid.setOutput("[ChannelX]", `!fxButton${fxNumber}`, ledColor, !this.controller.batchingOutputs); From b44c972120a294e7a88b518503f616b28e99e16f Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 9 Jan 2023 00:34:45 +0100 Subject: [PATCH 30/36] Adjust quick effect chain mappings in S3 script As the result of https://github.com/mixxxdj/mixxx/pull/10859 being merged. Everything shifted up one spot. --- res/controllers/Traktor-Kontrol-S3-hid-scripts.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 15b776af139b..60af5933350f 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -2489,9 +2489,11 @@ TraktorS3.VanillaFxControl = class { // The normal version of this resets the super knobs to the value it had // when the chain preset was saved. We need the value to stay consistent // with the knob on the controller. + // NOTE: The first preset is now the ---/no preset option, and this is 1-indexed + // https://github.com/mixxxdj/mixxx/pull/10859 engine.setValue(`[QuickEffectRack1_[Channel${channel}]]`, "loaded_chain_preset_preserving_super_knob_value", - fxButtonIndex + 1); + fxButtonIndex + 2); } // Output handling @@ -2516,7 +2518,8 @@ TraktorS3.VanillaFxControl = class { // chain. const activeFxSelectButtons = new Set(); for (let channel = 1; channel <= 4; channel++) { - const fxNumber = engine.getValue(`[QuickEffectRack1_[Channel${channel}]]`, "loaded_chain_preset") - 1; + // NOTE: The first preset is now the ---/no preset option, and this is 1-indexed + const fxNumber = engine.getValue(`[QuickEffectRack1_[Channel${channel}]]`, "loaded_chain_preset") - 2; activeFxSelectButtons.add(fxNumber); } @@ -2545,7 +2548,8 @@ TraktorS3.VanillaFxControl = class { const channelGroup = `[Channel${channelNumber}]`; const quickEffectChainGroup = `[QuickEffectRack1_${channelGroup}]`; - const fxNumber = engine.getValue(quickEffectChainGroup, "loaded_chain_preset") - 1; + // NOTE: The first preset is now the ---/no preset option, and this is 1-indexed + const fxNumber = engine.getValue(quickEffectChainGroup, "loaded_chain_preset") - 2; // We don't need to query this from the engine when this is called as // part of a connection const fxEnabled = (enabled !== undefined && enabled !== null) From 77326ad3e1b55b6fb85b8e13f1af794ee9c15fcd Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 9 Jan 2023 14:42:14 +0100 Subject: [PATCH 31/36] Move effect chain preset offset to a constant So this can be changed easier if this behavior ever changes again. --- .../Traktor-Kontrol-S3-hid-scripts.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 60af5933350f..e08142c2083c 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -2337,6 +2337,13 @@ TraktorS3.VanillaFxControl = class { constructor(controller) { this.controller = controller; + // The difference between the indices for the filter and FX buttons + // (`[0..4]`) and the quick effect chain presets they should be mapped + // to (`[2..6]`). These presets are 1-indexed, and the first entry is a + // blank entry automatically added by Mixxx for disabling the quick + // effects altogether. This is a constant. + this.effectChainOffset = 2; + // This contains the indices of the currently held down Filter and FX // select buttons, 0 being the filter and 1-4 being the four FX buttons. // We keep track of whether they're currently held down so we can assign @@ -2489,11 +2496,9 @@ TraktorS3.VanillaFxControl = class { // The normal version of this resets the super knobs to the value it had // when the chain preset was saved. We need the value to stay consistent // with the knob on the controller. - // NOTE: The first preset is now the ---/no preset option, and this is 1-indexed - // https://github.com/mixxxdj/mixxx/pull/10859 engine.setValue(`[QuickEffectRack1_[Channel${channel}]]`, "loaded_chain_preset_preserving_super_knob_value", - fxButtonIndex + 2); + fxButtonIndex + this.effectChainOffset); } // Output handling @@ -2518,8 +2523,7 @@ TraktorS3.VanillaFxControl = class { // chain. const activeFxSelectButtons = new Set(); for (let channel = 1; channel <= 4; channel++) { - // NOTE: The first preset is now the ---/no preset option, and this is 1-indexed - const fxNumber = engine.getValue(`[QuickEffectRack1_[Channel${channel}]]`, "loaded_chain_preset") - 2; + const fxNumber = engine.getValue(`[QuickEffectRack1_[Channel${channel}]]`, "loaded_chain_preset") - this.effectChainOffset; activeFxSelectButtons.add(fxNumber); } @@ -2548,8 +2552,7 @@ TraktorS3.VanillaFxControl = class { const channelGroup = `[Channel${channelNumber}]`; const quickEffectChainGroup = `[QuickEffectRack1_${channelGroup}]`; - // NOTE: The first preset is now the ---/no preset option, and this is 1-indexed - const fxNumber = engine.getValue(quickEffectChainGroup, "loaded_chain_preset") - 2; + const fxNumber = engine.getValue(quickEffectChainGroup, "loaded_chain_preset") - this.effectChainOffset; // We don't need to query this from the engine when this is called as // part of a connection const fxEnabled = (enabled !== undefined && enabled !== null) From 550531e219d4028e2e9c61b62c086be45dc4b0d1 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Fri, 13 Jan 2023 01:04:01 +0100 Subject: [PATCH 32/36] Replace super knob preserving loaded_chain_preset In the S3 script. There's now a preference that gives you the same behavior. --- .../Traktor-Kontrol-S3-hid-scripts.js | 30 ++++++++++--------- src/effects/effectchain.cpp | 16 ---------- src/effects/effectchain.h | 2 -- 3 files changed, 16 insertions(+), 32 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index e08142c2083c..127afac3c542 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -24,15 +24,7 @@ var TraktorS3 = {}; // buttons toggle the enabled/bypass status of those quick effect chains. This // emulates the intended behavior of the Mixer FX section on the Traktor // Kontrol S3. The Filter button is bound to the first preset in the list, and -// the FX 1-4 buttons are bound to presets 2-5. For consistency you should -// make sure that the default quick effect chain in Settings -> Equalizers is -// set to the first chain so that pressing the Filter button returns you to -// the default quick effect chain. The super knob's value is preserved when -// changing between quick effect chain presets. -// -// The intended use case is to set quick effect chain preset 0 to either the -// Filter or Moog Filter effect, and presets 1-5 to a chain that contains that -// same filter and an additional effect. +// the FX 1-4 buttons are bound to presets 2-5. // // Pressing one of the five FX/Filter buttons switches every channel's quick // effect chain to the corresponding preset. Holding one of the buttons down @@ -41,6 +33,16 @@ var TraktorS3 = {}; // enabled then channel will behave the same as if the first Filter quick // effect chain preset was used. // +// The mode works best with the following configuration: +// +// - The 'Keep superknob position' option in the Effects preferences page +// should be enabled. +// - The very first quick effect preset in the quick effect presets list +// should be set to the Moog Filter preset or another filter preset. +// - The next four quick effect presets should contain that exact same filter +// effect, plus another effect. Delays, reverbs, flangers, trance gates, and +// white noise are some examples of effects that would work well here. +// // - Another mode that exposes all of Mixxx's effect controls at the expense of // being more complex to use. See the manual for the full key binding scheme. // @@ -2485,7 +2487,8 @@ TraktorS3.VanillaFxControl = class { } /** - * Set the quick effect chain preset for a channel. This preserves the current super knob value + * Set the quick effect chain preset for a channel in accordance to one of + * the five effect buttons. * * @param {number} channel The channel number, in 1-4. * @param {number} fxButtonIndex The index of the FX button, 0 being the @@ -2493,11 +2496,10 @@ TraktorS3.VanillaFxControl = class { * fxButtonIndex 0-5 will be mapped to quick effect chain presets 1-6. */ setQuickEffectChain(channel, fxButtonIndex) { - // The normal version of this resets the super knobs to the value it had - // when the chain preset was saved. We need the value to stay consistent - // with the knob on the controller. + // The 'Keep superknob position' option should be enabled for this to + // work as intended engine.setValue(`[QuickEffectRack1_[Channel${channel}]]`, - "loaded_chain_preset_preserving_super_knob_value", + "loaded_chain_preset", fxButtonIndex + this.effectChainOffset); } diff --git a/src/effects/effectchain.cpp b/src/effects/effectchain.cpp index 5f0e6faaed4a..69124a40c66c 100644 --- a/src/effects/effectchain.cpp +++ b/src/effects/effectchain.cpp @@ -104,12 +104,6 @@ EffectChain::EffectChain(const QString& group, this, &EffectChain::slotControlLoadedChainPresetRequest); - m_pControlLoadedChainPresetPreservingSuperKnobValue = std::make_unique( - ConfigKey(m_group, "loaded_chain_preset_preserving_super_knob_value"), false); - m_pControlLoadedChainPresetPreservingSuperKnobValue->connectValueChangeRequest( - this, - &EffectChain::slotControlLoadedChainPresetRequestPreservingSuperKnobValue); - m_pControlNextChainPreset = std::make_unique( ConfigKey(m_group, "next_chain_preset")); connect(m_pControlNextChainPreset.get(), @@ -355,16 +349,6 @@ void EffectChain::slotControlLoadedChainPresetRequest(double value) { loadChainPreset(presetAtIndex(index)); } -void EffectChain::slotControlLoadedChainPresetRequestPreservingSuperKnobValue(double value) { - // Loading a chain preset sets the super knob's value to the value it was at - // when the chain preset was saved. It may be desirable to keep the knob's - // value as is, for instance when changing between multiple presets that - // contain a filter and an additional additional effect. - const double old_value = m_pControlChainSuperParameter->get(); - slotControlLoadedChainPresetRequest(value); - slotControlChainSuperParameter(old_value, true); -} - void EffectChain::setControlLoadedPresetIndex(uint index) { // add 1 to make the ControlObject 1-indexed like other ControlObjects m_pControlLoadedChainPreset->setAndConfirm(index + 1); diff --git a/src/effects/effectchain.h b/src/effects/effectchain.h index 72ae7c2c4545..42e5dd33218c 100644 --- a/src/effects/effectchain.h +++ b/src/effects/effectchain.h @@ -117,7 +117,6 @@ class EffectChain : public QObject { private slots: void slotControlLoadedChainPresetRequest(double value); - void slotControlLoadedChainPresetRequestPreservingSuperKnobValue(double value); void slotControlChainPresetSelector(double value); void slotControlNextChainPreset(double value); void slotControlPrevChainPreset(double value); @@ -138,7 +137,6 @@ class EffectChain : public QObject { std::unique_ptr m_pControlChainEnabled; std::unique_ptr m_pControlChainMixMode; std::unique_ptr m_pControlLoadedChainPreset; - std::unique_ptr m_pControlLoadedChainPresetPreservingSuperKnobValue; std::unique_ptr m_pControlChainPresetSelector; std::unique_ptr m_pControlNextChainPreset; std::unique_ptr m_pControlPrevChainPreset; From 1203cd0c01185f6676f5f2bf604a347c43cd7fea Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Wed, 25 Jan 2023 01:06:12 +0100 Subject: [PATCH 33/36] Revert "Change default S3 deck colors to match Traktor" This reverts commit e38537e99a3e57c5d02424213037883097ebfc5c. --- res/controllers/Traktor-Kontrol-S3-hid-scripts.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 127afac3c542..75d44cc10473 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -93,10 +93,10 @@ TraktorS3.SixteenSamplers = false; // PURPLE, FUCHSIA, MAGENTA, AZALEA, SALMON, WHITE // Some colors may look odd because of how they are encoded inside the controller. TraktorS3.ChannelColors = { - "[Channel1]": "BLUE", - "[Channel2]": "BLUE", - "[Channel3]": "CARROT", - "[Channel4]": "CARROT" + "[Channel1]": "CARROT", + "[Channel2]": "CARROT", + "[Channel3]": "BLUE", + "[Channel4]": "BLUE" }; // Each color has four brightnesses, so these values can be between 0 and 3. From ee52ffe1e62cddd0a4097d4fb75f3bd9f6935d1e Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 30 Jan 2023 17:23:42 +0100 Subject: [PATCH 34/36] Add a jog wheel multiplier constant for the S3 --- .../Traktor-Kontrol-S3-hid-scripts.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 75d44cc10473..3b7bedddb076 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -103,7 +103,13 @@ TraktorS3.ChannelColors = { TraktorS3.LEDDimValue = 0x00; TraktorS3.LEDBrightValue = 0x02; -// Parameters for the scratch smoothing +// By default the jog wheel's behavior when rotating it matches 33 1/3 rpm +// vinyl. Changing this value to 2.0 causes a single rotation of the platter to +// result in twice as much movement, and a value of 0.5 causes the amount of +// movement to be halved. +TraktorS3.JogSpeedMultiplier = 1.0; + +// Parameters for the jog wheel smoothing while scratching TraktorS3.Alpha = 1.0 / 8; TraktorS3.Beta = TraktorS3.Alpha / 32; @@ -1263,7 +1269,13 @@ TraktorS3.Deck = class { } if (field.value !== 0) { - engine.scratchEnable(this.activeChannelNumber, 768, 33.33334, TraktorS3.Alpha, TraktorS3.Beta); + engine.scratchEnable( + this.activeChannelNumber, + 768, + 33.33334 / TraktorS3.JogSpeedMultiplier, + TraktorS3.Alpha, + TraktorS3.Beta + ); } else { engine.scratchDisable(this.activeChannelNumber); @@ -1302,7 +1314,7 @@ TraktorS3.Deck = class { // get the rate ratio. const velocity = (tickDelta / timeDelta) / thirtyThree; - engine.setValue(this.activeChannel, "jog", velocity); + engine.setValue(this.activeChannel, "jog", velocity * TraktorS3.JogSpeedMultiplier); } } From ce70fa0b2d9eb2bd688bbc78d9d68966bab6ebc1 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Wed, 1 Feb 2023 00:23:26 +0100 Subject: [PATCH 35/36] Change 'Vanilla Mode' to 'Quick Effect Mode' --- .../Traktor-Kontrol-S3-hid-scripts.js | 57 ++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 3b7bedddb076..68b3ae99b705 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -3,7 +3,7 @@ /////////////////////////////////////////////////////////////////////////////////// // // Traktor Kontrol S3 HID controller script v2.00 -// Last modification: December 2022 +// Last modification: January 2023 // Authors: Owen Williams, Robbert van der Helm // https://www.mixxx.org/wiki/doku.php/native_instruments_traktor_kontrol_s3 // @@ -19,44 +19,37 @@ var TraktorS3 = {}; // ==== Friendly User Configuration ==== // This controller script has two modes for controlling FX: // -// - A mode where the Filter and FX 1-4 buttons switch between the first five -// quick effect chain presets found in Settings -> Effects, and the FX Enable -// buttons toggle the enabled/bypass status of those quick effect chains. This -// emulates the intended behavior of the Mixer FX section on the Traktor -// Kontrol S3. The Filter button is bound to the first preset in the list, and -// the FX 1-4 buttons are bound to presets 2-5. -// -// Pressing one of the five FX/Filter buttons switches every channel's quick -// effect chain to the corresponding preset. Holding one of the buttons down -// while pressing one of the four channel's Filter Enable buttons will assign -// the chain to just that channel. When the Filter Enable button is not -// enabled then channel will behave the same as if the first Filter quick -// effect chain preset was used. -// -// The mode works best with the following configuration: +// - A mode focussing on quick effect chain presets, emulating the intended +// behavior of the Mixer FX section on the Traktor Kontrol S3. This is the +// default behavior. To make best use of this mode, you will need to make the +// following configuration changes: // // - The 'Keep superknob position' option in the Effects preferences page // should be enabled. -// - The very first quick effect preset in the quick effect presets list -// should be set to the Moog Filter preset or another filter preset. +// - The very first quick effect preset in the quick effect presets list on +// the same preferences page should be set to the Moog Filter preset or +// another filter preset. // - The next four quick effect presets should contain that exact same filter // effect, plus another effect. Delays, reverbs, flangers, trance gates, and // white noise are some examples of effects that would work well here. // -// - Another mode that exposes all of Mixxx's effect controls at the expense of -// being more complex to use. See the manual for the full key binding scheme. +// - Another mode that gives detailed control over Mixxx's individual effect +// sections instead of focusing on the quick effects. This mode is more +// complex to use as the result of the S3's limited number of buttons and +// knobs dedicated to effects. // -// The first mode is dubbed 'vanilla mode' as it behaves in the way the mixer FX -// section was originally intended to be used by Native Instruments. Disable -// this option to use the second, Mixxx-specific mode. -TraktorS3.VanillaFxMode = true; +// The first mode is dubbed 'quick effect mode' and it is enabled by default. +// Disable this option to use the second, Mixxx-specific mode. See the readme at +// https://manual.mixxx.org/latest/en/hardware/controllers/native_instruments_traktor_kontrol_s3.html +// for more information on how to use these modes. +TraktorS3.QuickEffectMode = true; // When enabled, set all channels to the first FX chain on startup. Otherwise // the quick FX chain assignments from the last Mixxx run are preserved. -TraktorS3.VanillaFxModeDefaultToFilter = true; +TraktorS3.QuickEffectModeDefaultToFilter = true; // When enabled, the FX Enable buttons will use the colors set in // `TraktorS3.ChannelColors` when the filter effect is selected. Disabling this // will use the Filter button's orange color instead. -TraktorS3.VanillaFxModeChannelColors = false; +TraktorS3.QuickEffectModeChannelColors = false; // The pitch slider can operate either in absolute or relative mode. // In absolute mode: @@ -2345,9 +2338,9 @@ TraktorS3.shutdown = function() { /** * An alternative to `FXControl` that behaves more similarly to how the * controller works with Traktor. See the description for - * `TraktorS3.VanillaFxMode` for more information. + * `TraktorS3.QuickEffectMode` for more information. */ -TraktorS3.VanillaFxControl = class { +TraktorS3.QuickFxControl = class { constructor(controller) { this.controller = controller; @@ -2381,7 +2374,7 @@ TraktorS3.VanillaFxControl = class { 4: this.controller.hid.LEDColors.YELLOW, }; - if (TraktorS3.VanillaFxModeDefaultToFilter) { + if (TraktorS3.QuickEffectModeDefaultToFilter) { for (let channel = 1; channel <= 4; channel++) { this.setQuickEffectChain(channel, 0); } @@ -2580,7 +2573,7 @@ TraktorS3.VanillaFxControl = class { // between channels. We'll also fall back to the channel colors if the // user manually selected an out of bounds chain let ledColor = fxEnabled ? TraktorS3.LEDBrightValue : TraktorS3.LEDDimValue; - if (!TraktorS3.VanillaFxModeChannelColors || (fxNumber >= 1 && fxNumber <= 5)) { + if (!TraktorS3.QuickEffectModeChannelColors || (fxNumber >= 1 && fxNumber <= 5)) { ledColor += this.fxColors[fxNumber]; } else { ledColor += this.controller.hid.LEDColors[TraktorS3.ChannelColors[channelGroup]]; @@ -2604,8 +2597,8 @@ TraktorS3.init = function(_id) { "[Channel2]": new TraktorS3.Channel(this.kontrol, this.kontrol.Decks.deck2, "[Channel2]"), }; - if (TraktorS3.VanillaFxMode) { - this.kontrol.fxController = new TraktorS3.VanillaFxControl(this.kontrol); + if (TraktorS3.QuickEffectMode) { + this.kontrol.fxController = new TraktorS3.QuickFxControl(this.kontrol); } else { this.kontrol.fxController = new TraktorS3.FXControl(this.kontrol); } From 29bfd1d26c1e2d98c54d3d974c3095289709b88c Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 14 Mar 2023 14:00:21 +0100 Subject: [PATCH 36/36] Pass Uint8Arrays to TraktorS3.incomingData As suggested in https://github.com/mixxxdj/mixxx/pull/11199#discussion_r1133314678. The function does accept both typed arrays and plain arrays. --- res/controllers/Traktor-Kontrol-S3-hid-scripts.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js index 68b3ae99b705..867e76081a38 100644 --- a/res/controllers/Traktor-Kontrol-S3-hid-scripts.js +++ b/res/controllers/Traktor-Kontrol-S3-hid-scripts.js @@ -292,15 +292,15 @@ TraktorS3.Controller = class { // with different values once. Report 2 contains the state of the mixer // controls. const report2Values = new Uint8Array(controller.getInputReport(2)); - TraktorS3.incomingData([2, ...Array.from(report2Values.map(x => ~x))]); - TraktorS3.incomingData([2, ...Array.from(report2Values)]); + TraktorS3.incomingData(new Uint8Array([2, ...Uint8Array.from(report2Values.map(x => ~x))])); + TraktorS3.incomingData(new Uint8Array([2, ...Uint8Array.from(report2Values)])); // Report 1 is the state of the deck controls. These shouldn't have any // initial effect, and most of these values will be 0 anyways. We'll // just tell the packet parser the current values so it won't ignore the // next input. const report1Values = new Uint8Array(controller.getInputReport(1)); - TraktorS3.incomingData([1, ...Array.from(report1Values)]); + TraktorS3.incomingData(new Uint8Array([1, ...Uint8Array.from(report1Values)])); // NOTE: Soft takeovers must only be enabled after setting the initial // value, or the above line won't have any effect