Skip to content

Traktor S2 Mk3: Change jogwheel to use scratch2#15792

Open
BoredGuy1 wants to merge 5 commits intomixxxdj:2.6from
BoredGuy1:2.5
Open

Traktor S2 Mk3: Change jogwheel to use scratch2#15792
BoredGuy1 wants to merge 5 commits intomixxxdj:2.6from
BoredGuy1:2.5

Conversation

@BoredGuy1
Copy link
Contributor

@BoredGuy1 BoredGuy1 commented Dec 26, 2025

Relevant forum post

This PR makes two minor improvements to the jogwheels of the Traktor S2 Mk3 script.

  1. A single revolution on the physical controller used to result in roughly 2/3 of a revolution on the Mixxx virtual jogwheel. This is because the old script assumed one revolution was 1024 ticks, and passed that to engine.scratchEnable. Based on my personal tests, I have changed it to 648 ticks per revolution. This produces a much closer result, although this value might have to be calibrated from controller to controller to be 100% accurate.

  2. The old script used to discard one of the four bytes sent from the jogwheel, because no one knew what that byte meant. I was able to decode it. The first 2 bits are an extension of the jogwheel position, while the last 6 bits are an extension of the 100 kHz counter/timer. So now, the first 10 bits (instead of the first 8 bits) represent the wheel position and are used to calculate tickDelta, while the last 22 bits (instead of the last 16 bits) represent the counter and are used to calculate timeDeltas. are a number that increments and overflows at 100 kHz. The 2 bits were the MSBs of the wheel position while the 6 bits were the LSBs of the counter. So while the precision of the wheel position measurement didn't change much, the precision of the timer improved drastically. In theory, this means jogwheel bending (turning the jogwheel without touching the top) is now 64 times more precise, although in practice it was already quite precise to begin with.

Edit: This PR now overhauls the entire scratching part of the S2MK3 script by using the high-res timer in the jogwheel to calculate velocity, which is then directly applied with scratch2 instead of scratchTicks. The title has been updated accordingly to reflect this.

@ronso0
Copy link
Member

ronso0 commented Dec 28, 2025

Thanks for this PR.

It seems the Traktor Mk3 models all share the same high-res wheels, so IMO it would make sense to at least test the wheel code of the S4 Mk3 -- that uses scratch2 which has some advantages over scratchTicks().
It also has some spinback / interia code that the S2 may benefit (if it doesn't have something similar already, didn' check)

edit at least they seem to have the same high-res timer.
The wheel turn handler of the S4 is here

@ronso0 ronso0 changed the title Changed jogwheel ticks per revolution Traktor S2 Mk3: Change jogwheel ticks per revolution Dec 29, 2025
@BoredGuy1
Copy link
Contributor Author

Hi ronso0,

I am not sure why, but with the current scratchTicks implementation it seems like inertia is already implemented. In other words, if you tap the jogwheel to activate jogging mode, spin, and let go, it stays in scratching mode until the physical jogwheel comes to a stop, just like a backspin. Does the jog control have special logic when switching between scratching and non-scratching mode?

Also, what is the difference between scratch2 and scratchTicks? I have been looking at scratch2 but am still not sure how to use it based on the documentation in the Mixxx User Manual. Although I'm not sure how accurate the manual is, since it says the jog control is deprecated as well.

@ronso0
Copy link
Member

ronso0 commented Dec 31, 2025

I am not sure why, but with the current scratchTicks implementation it seems like inertia is already implemented. In other words, if you tap the jogwheel to activate jogging mode, spin, and let go, it stays in scratching mode until the physical jogwheel comes to a stop, just like a backspin.

Okay, great. Though it's not scratchTicks itself but the script I reckon.

The difference between scratchTicks and scratch2 is not really documented unfortunatley.
scratchTicks are accumulated in the engine, and they are filtered/smoothed so it's kind of an indirect tempo control.
scratch2 otoh affects the rate (tempo) directly, no smoothing, so it's more precise.

@ronso0
Copy link
Member

ronso0 commented Dec 31, 2025

Although I'm not sure how accurate the manual is, since it says the jog control is deprecated as well.

Jep, that is wrong. Will fix it.
edit mixxxdj/manual#828

IIRC that was a scratch/jog control long ago.
But it's still the only control to map wheel turns to pitch bend.

@BoredGuy1
Copy link
Contributor Author

BoredGuy1 commented Jan 1, 2026

OK, so I think there actually is a difference between the S4's wheels and the S2's wheels. The S4's wheels are motorized, so they are always sending HID packets with updated wheel position data. On the other hand, the S2's wheels are not, so it will stop sending packets when the wheels are stopped. This leads to a strange situation where scratch2 will "remember" the last velocity and keep moving even when the wheel is stopped, since there is no HID packet with 0 velocity coming in.

I have explored workarounds to this, such as creating my own timer loop to decay the velocity without having to receive HID packets. But this introduces other problems, such as decaying the velocity in the middle of a scratch. I haven't been able to get scratch2 to work better than simply using scratchTicks.

In my opinion, we should stick with the scratchEnable/scratchTicks implementation. The inertia is already implemented, and it's reasonably accurate. In fact, this is what the script for the S3 (which also does not have motorized jogwheels) does. It's not 100% accurate, but even the native Traktor software requires jogwheel calibration, so that might just be a hardware issue.

EDIT: Actually, hold that thought. There's one more thing I haven't tried yet.

@BoredGuy1
Copy link
Contributor Author

I suddenly remembered that Date.now() exists, so I was able to put together an implementation using that to check if a velocity is stale.

Anyway, this version uses scratch2 with the high-res timer, so the jogwheels should feel tighter. It works pretty well, except for one thing. When the track is playing and I try to change directions while scratching, sometimes the Mixxx virtual jogwheel will keep going in the old direction for a few ticks before changing directions. This issue only occurs rarely, and never when the track is paused. I have tried printing the scratch2 value when this happens, and scratch2 indeed reflects the direction the virtual jogwheel is supposed to go, yet the virtual jogwheel still goes the other way.

I suspect it might have something to do with the engine itself, which is giving me an extra buffer's worth of audio in the wrong direction. If that's the case, I'm not sure how I would address this script-side, or if that's even possible.

In any case, we should probably get some others with S2Mk3s to test this.

@BoredGuy1
Copy link
Contributor Author

Pre-commit is complaining about != null, but that's intentional because I need to check if the timer is equal to null (set by me) or undefined (expired). Is there a preferred method besides != null?

@BoredGuy1 BoredGuy1 changed the title Traktor S2 Mk3: Change jogwheel ticks per revolution Traktor S2 Mk3: Change jogwheel to use scratch2 Jan 15, 2026
@ronso0
Copy link
Member

ronso0 commented Feb 1, 2026

So how does it work/feel now, with #15845 merged?

@BoredGuy1
Copy link
Contributor Author

Works pretty well! The jogwheel is a lot more responsive compared to the stock script, especially for complicated scratching tricks.

@ronso0
Copy link
Member

ronso0 commented Feb 2, 2026

Nice.
The changes LGTM, though I'd feel better if mapping pro would take a look, too @mixxxdj/developers

Copy link
Member

@ywwg ywwg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for this. I added a few notes and questions

// Otherwise, check again after a while
} else {
engine.scratchDisable(deckNumber);
TraktorS2MK3.jogStopTimerId[deckNumber - 1] = engine.beginTimer(JOGWHEEL_STOP_POLL_TIME, () => TraktorS2MK3.jogStopper(field), true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we check to see if there is already an active timer (for whatever reason) and cancel it before creating the new one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe? The way the jogTouchHandler works is that when the jogwheel is touched, the stop timer is cancelled, then when the jogwheel is released, the stop timer begins.

In order to physically release the jogwheel, it must be touched first, and therefore any existing timers should already be cancelled. So I didn't think it was necessary.

I might have missed some edge cases though.

Comment on lines +687 to +691
if ((TraktorS2MK3.jogDecayTimerId[deckNumber - 1] !== null) &&
(TraktorS2MK3.jogDecayTimerId[deckNumber - 1] !== undefined)) {
engine.stopTimer(TraktorS2MK3.jogDecayTimerId[deckNumber - 1]);
TraktorS2MK3.jogDecayTimerId[deckNumber - 1] = null;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the second time this code appears, better to factor it out to a function that takes the deck number as a parameter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean rewriting the function to take a deckNumber parameter rather than a field parameter?

I think that would work for jogStopper and jogDecayer, since those are only dependent on the deck number (only field.group), but not jogTouchHandler and jogHandler, since those are dependent on both the deck number and the value (that is, field.group and field.value).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair -- refactor however makes the most sense to you! but stopping the timer seems like a useful helper function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, just as a heads-up - I'm currently abroad right now and don't have my equipment. I probably won't be making any big commits until I'm back home in June, unless someone else has a Traktor S2Mk3 and is willing to test my commits.


// Affects how sensitive jogging/nudging (turning the wheel without touching the top) is.
// A constant of 0.5 makes jogging/nudging roughly equivalent to scratching.
const JOG_SENSITIVITY = 0.25;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to expose any of these constants as controller preference values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically they can all be configured as preferences, although TICKS_PER_REV, JOGWHEEL_CLOCK_HZ, and TARGET_RPM probably shouldn't be changed.

Would you like me to move all the config options that make sense (JOG_SENSITIVITY, JOGWHEEL_STOP_POLL_TIME, JOGWHEEL_DECAY_POLL_TIME, JOGWHEEL_ALPHA, and JOGWHEEL_EPSILON) to the top? That might be more user-friendly.

I can also take the time to alter the comments if needed. They make sense to me, but they might not make sense to everyone.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to expose the minimum to the user.... now that I go through them, JOG_SENSITIVITY is the only one that I think people might want to tweak.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JOG_SENSITIVITY is definitely important.

JOGWHEEL_ALPHA and JOGWHEEL_EPSILON are maybe a little important? JOGWHEEL_ALPHA controls the smoothing factor ("slipperiness"), and JOGWHEEL_EPSILON controls the responsiveness when changing directions.

JOGWHEEL_DECAY_POLL_TIME and JOGWHEEL_STOP_POLL_TIME... yeah, probably not so important. Right now they're already set to the fastest (20ms), and you can directly control decay speed or stop sensitivity via JOGWHEEL_ALPHA and JOGWHEEL_EPSILON respectively.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can put these three (JOG_SENSITIVITY, JOGWHEEL_ALPHA, and JOGWHEEL_EPSILON) in the controller settings as long as you write good descriptions of them so users will understand whether they might want to change them and how.

@acolombier acolombier added this to the 2.6.0 milestone Mar 13, 2026
@acolombier acolombier changed the base branch from 2.5 to 2.6 March 13, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants