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+
1450var 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
619658TraktorS2MK3 . 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
628686TraktorS2MK3 . 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
678793TraktorS2MK3 . fxHandler = function ( field ) {
0 commit comments