Skip to content

Commit 869300c

Browse files
authored
Merge pull request mixxxdj#15792 from BoredGuy1/2.5
Traktor S2 Mk3: Change jogwheel to use scratch2
2 parents 3252553 + d9c1e47 commit 869300c

1 file changed

Lines changed: 150 additions & 35 deletions

File tree

res/controllers/Traktor-Kontrol-S2-MK3-hid-scripts.js

Lines changed: 150 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,49 @@
44
/* jshint -W016 */
55
///////////////////////////////////////////////////////////////////////////////////
66
/* */
7-
/* Traktor Kontrol S2 MK3 HID controller script v1.01 */
8-
/* Last modification: February 2021 */
7+
/* Traktor Kontrol S2 MK3 HID controller script v1.02 */
8+
/* Last modification: January 2026 */
99
/* Author: Michael Schmidt */
1010
/* https://github.com/mixxxdj/mixxx/wiki/Native%20Instruments%20Traktor%20Kontrol%20S2%20MK3 */
1111
/* */
1212
///////////////////////////////////////////////////////////////////////////////////
1313

14+
/* ============================================================================
15+
* User Settings
16+
* These values are intended to be adjusted by end users to tune jog wheel feel.
17+
* ========================================================================== */
18+
19+
// Affects how sensitive jogging/nudging (turning the wheel without touching the top) is.
20+
// A constant of 0.5 makes jogging/nudging roughly as fast as scratching.
21+
const JOG_SENSITIVITY = 0.25;
22+
23+
// Coefficient for the jogwheel input's low pass filter.
24+
// Range: 0-1. Lower = more smoothing. A value of 1 results in no smoothing at all.
25+
const JOGWHEEL_ALPHA = 0.5;
26+
27+
// Threshold for the jogwheel input's dead zone. When the raw velocity (ticks/clock Hz) is lower than this,
28+
// the jogwheel is considered to be stopped, allowing it to change directions or exit scratching mode instantly.
29+
const JOGWHEEL_EPSILON = 0.001;
30+
31+
32+
/* ============================================================================
33+
* Internal Tuning Constants
34+
* Do not change these unless you know what you're doing.
35+
* ========================================================================== */
36+
37+
// Interval (ms) at which jog velocity is polled after release to determine whether scratching should stop. Minimum is 20ms.
38+
const JOGWHEEL_STOP_POLL_TIME = 20;
39+
40+
// Interval (ms) at which jog velocity is reduced stepwise after release. Minimum is 20ms.
41+
const JOGWHEEL_DECAY_POLL_TIME = 20;
42+
43+
// Constants used to scale raw velocity (tick delta / time delta) to the appropriate scratch2 value.
44+
const TICKS_PER_REV = 600;
45+
const JOGWHEEL_CLOCK_HZ = 100000;
46+
const TARGET_RPM = 33 + 1/3;
47+
const VELOCITY_TO_SCRATCH = JOGWHEEL_CLOCK_HZ / (TICKS_PER_REV * TARGET_RPM / 60);
48+
const VELOCITY_TO_JOG = VELOCITY_TO_SCRATCH * JOG_SENSITIVITY;
49+
1450
var TraktorS2MK3 = new function() {
1551
this.controller = new HIDController();
1652
this.shiftPressed = {"[Channel1]": false, "[Channel2]": false};
@@ -30,9 +66,12 @@ var TraktorS2MK3 = new function() {
3066
this.syncPressedTimer = {"[Channel1]": 0, "[Channel2]": 0}; // Timer to distinguish between short and long press
3167

3268
// Jog wheels
33-
this.pitchBendMultiplier = 1.1;
3469
this.lastTickVal = [0, 0];
35-
this.lastTickTime = [0.0, 0.0];
70+
this.lastTimestamp = [0, 0];
71+
this.lastVelocity = [0.0, 0.0];
72+
this.lastWallClock = [0, 0];
73+
this.jogStopTimerId = [null, null];
74+
this.jogDecayTimerId = [null, null];
3675

3776
// VuMeter
3877
this.vuLeftConnection = {};
@@ -617,62 +656,138 @@ TraktorS2MK3.samplerPregainHandler = function(field) {
617656
};
618657

619658
TraktorS2MK3.jogTouchHandler = function(field) {
620-
const deckNumber = TraktorS2MK3.controller.resolveDeck(field.group);
659+
const deckIndex = TraktorS2MK3.controller.resolveDeck(field.group) - 1;
660+
621661
if (field.value > 0) {
622-
engine.scratchEnable(deckNumber, 1024, 33 + 1 / 3, 0.125, 0.125 / 8, true);
662+
// Cancel any existing stop timers
663+
TraktorS2MK3.stopTimer(TraktorS2MK3.jogStopTimerId, deckIndex);
664+
engine.setValue(field.group, "scratch2_enable", true);
665+
} else {
666+
TraktorS2MK3.jogStopper(field);
667+
}
668+
};
669+
670+
// Called after the wheel is released. Stops scratching when the wheel is slow enough, allowing for inertia.
671+
TraktorS2MK3.jogStopper = function(field) {
672+
const deckIndex = TraktorS2MK3.controller.resolveDeck(field.group) - 1;
673+
674+
// If the wheel is stopped, exit scratching mode
675+
if (Math.abs(engine.getValue(field.group, "scratch2")) <= JOGWHEEL_EPSILON * VELOCITY_TO_SCRATCH) {
676+
engine.setValue(field.group, "scratch2", 0);
677+
engine.setValue(field.group, "scratch2_enable", false);
678+
TraktorS2MK3.lastVelocity[deckIndex] = 0;
679+
TraktorS2MK3.jogStopTimerId[deckIndex] = null;
680+
// Otherwise, check again after a while
623681
} else {
624-
engine.scratchDisable(deckNumber);
682+
TraktorS2MK3.jogStopTimerId[deckIndex] = engine.beginTimer(JOGWHEEL_STOP_POLL_TIME, () => TraktorS2MK3.jogStopper(field), true);
625683
}
626684
};
627685

628686
TraktorS2MK3.jogHandler = function(field) {
629-
const deckNumber = TraktorS2MK3.controller.resolveDeck(field.group);
630-
const deltas = TraktorS2MK3.wheelDeltas(deckNumber, field.value);
631-
const tickDelta = deltas[0];
632-
const timeDelta = deltas[1];
687+
const deckIndex = TraktorS2MK3.controller.resolveDeck(field.group) - 1;
688+
const velocity = TraktorS2MK3.wheelVelocity(deckIndex, field.value);
633689

634-
if (engine.isScratching(deckNumber)) {
635-
engine.scratchTick(deckNumber, tickDelta);
690+
if (engine.getValue(field.group, "scratch2_enable")) {
691+
engine.setValue(field.group, "scratch2", velocity * VELOCITY_TO_SCRATCH);
692+
693+
// Cancel any existing decay timers
694+
TraktorS2MK3.stopTimer(TraktorS2MK3.jogDecayTimerId, deckIndex);
695+
// Start timer to manually decay the velocity after a while
696+
TraktorS2MK3.jogDecayTimerId[deckIndex] = engine.beginTimer(JOGWHEEL_DECAY_POLL_TIME, () => {
697+
TraktorS2MK3.jogDecayer(field);
698+
}, true);
699+
700+
} else {
701+
engine.setValue(field.group, "jog", velocity * VELOCITY_TO_JOG);
702+
}
703+
};
704+
705+
// Called continuously after jogwheel stops sending packets. Gradually slows the jogwheel.
706+
TraktorS2MK3.jogDecayer = function(field) {
707+
const deckIndex = TraktorS2MK3.controller.resolveDeck(field.group) - 1;
708+
709+
// If wheel is slow enough, immediately set scratch2 to 0
710+
if (Math.abs(engine.getValue(field.group, "scratch2")) <= JOGWHEEL_EPSILON * VELOCITY_TO_SCRATCH) {
711+
TraktorS2MK3.lastVelocity[deckIndex] = 0;
712+
engine.setValue(field.group, "scratch2", 0);
713+
TraktorS2MK3.jogDecayTimerId[deckIndex] = null;
714+
// Otherwise, decay the velocity and call itself again after a while
636715
} else {
637-
const velocity = (tickDelta / timeDelta) * TraktorS2MK3.pitchBendMultiplier;
638-
engine.setValue(field.group, "jog", velocity);
716+
const decayedVelocity = TraktorS2MK3.lastVelocity[deckIndex] * (1 - JOGWHEEL_ALPHA);
717+
TraktorS2MK3.lastVelocity[deckIndex] = decayedVelocity;
718+
engine.setValue(field.group, "scratch2", decayedVelocity * VELOCITY_TO_SCRATCH);
719+
TraktorS2MK3.jogDecayTimerId[deckIndex] = engine.beginTimer(JOGWHEEL_DECAY_POLL_TIME, () => TraktorS2MK3.jogDecayer(field), true);
639720
}
640721
};
641722

642-
TraktorS2MK3.wheelDeltas = function(deckNumber, value) {
643-
// When the wheel is touched, four bytes change, but only the first behaves predictably.
644-
// It looks like the wheel is 1024 ticks per revolution.
645-
const tickval = value & 0xFF;
646-
let timeval = value >>> 16;
647-
let prevTick = 0;
648-
let prevTime = 0;
723+
// Helper function that checks if a timer is running and stop it if it is
724+
TraktorS2MK3.stopTimer = function(timerArray, deckIndex) {
725+
const id = timerArray[deckIndex];
726+
727+
if (id !== null && id !== undefined) {
728+
engine.stopTimer(id);
729+
timerArray[deckIndex] = null;
730+
}
731+
};
732+
733+
TraktorS2MK3.wheelVelocity = function(deckIndex, value) {
734+
// When the wheel is touched, four bytes change.
735+
// The first 10 bits change when the wheel is turned.
736+
// The last 22 bits are a counter, which constantly increments and overflows at 100kHz.
737+
const tickval = value & 0x3FF;
738+
let timeval = value >>> 10;
649739

650740
// Group 1 and 2 -> Array index 0 and 1
651-
prevTick = this.lastTickVal[deckNumber - 1];
652-
prevTime = this.lastTickTime[deckNumber - 1];
653-
this.lastTickVal[deckNumber - 1] = tickval;
654-
this.lastTickTime[deckNumber - 1] = timeval;
741+
const prevTick = this.lastTickVal[deckIndex];
742+
const prevTime = this.lastTimestamp[deckIndex];
743+
const prevWallClock = this.lastWallClock[deckIndex];
744+
this.lastTickVal[deckIndex] = tickval;
745+
this.lastTimestamp[deckIndex] = timeval;
746+
this.lastWallClock[deckIndex] = Date.now();
747+
748+
// If the user hasn't touched the jog wheel for a long time, the
749+
// internal timer may have looped around more than once. We have nothing
750+
// to go by so return 0
751+
if (this.lastWallClock[deckIndex] - prevWallClock > 40000) {
752+
this.lastVelocity[deckIndex] = 0;
753+
return 0;
754+
}
655755

656756
if (prevTime > timeval) {
657757
// We looped around. Adjust current time so that subtraction works.
658-
timeval += 0x10000;
758+
timeval += 0x400000;
659759
}
660760
let timeDelta = timeval - prevTime;
661761
if (timeDelta === 0) {
662-
// Spinning too fast to detect speed! By not dividing we are guessing it took 1ms.
762+
// Spinning too fast to detect speed! By not dividing we are guessing it took 10us.
663763
timeDelta = 1;
664764
}
665765

666-
let tickDelta = 0;
667-
if (prevTick >= 200 && tickval <= 100) {
668-
tickDelta = tickval + 256 - prevTick;
669-
} else if (prevTick <= 100 && tickval >= 200) {
670-
tickDelta = tickval - prevTick - 256;
766+
let tickDelta = tickval - prevTick;
767+
// Check if we looped around
768+
if (tickDelta > 512) {
769+
// Looped around from 0 to max
770+
tickDelta -= 1024;
771+
} else if (tickDelta < -512) {
772+
// Looped around from max to 0
773+
tickDelta += 1024;
774+
}
775+
776+
// Velocity smoothing
777+
const velocity = tickDelta / timeDelta;
778+
const prevVelocity = this.lastVelocity[deckIndex];
779+
let nextVelocity;
780+
// Check if the jogwheel is currently stopped or changing directions.
781+
// If so, set the velocity to the new value instantly.
782+
if ((Math.abs(prevVelocity) < JOGWHEEL_EPSILON) || (velocity * prevVelocity < 0)) {
783+
nextVelocity = velocity;
784+
// Otherwise, smooth the velocity.
671785
} else {
672-
tickDelta = tickval - prevTick;
786+
nextVelocity = JOGWHEEL_ALPHA * velocity + (1 - JOGWHEEL_ALPHA) * prevVelocity;
673787
}
788+
this.lastVelocity[deckIndex] = nextVelocity;
674789

675-
return [tickDelta, timeDelta];
790+
return nextVelocity;
676791
};
677792

678793
TraktorS2MK3.fxHandler = function(field) {

0 commit comments

Comments
 (0)