Description
Currently when using Pick Up or Catch Up modes, working with absolute physical controls (faders, knobs) fast movements can fail to register, and parameter jumps can happen too often.
The trouble is:
- When Jump Max is set to a very low value (say 1%), the fader/knob must be moved very slowly, or else the software target will stop responding due to the distance between two consecutive CC messages exceeding Jump Max; but
- When Jump Max is set to a value high enough to allow for faster fader/knob movements (say 20%), when the physical control is out of sync from the target value, approaching the target value from either direction will cause an unpleasant (in this case 20%) jump in the opposite direction as it comes close.
After thinking this through at length, I believe I have a good solution. Unfortunately I'm completely unfamiliar with Rust as of this writing; otherwise I would make a real pull request.
My suggested approach to resolve this can probably be best explained in pseudocode (pardon the sloppy half-C, half-JavaScript, half-who-knows-what syntax. It probably has some bugs as I haven't actually implemented this yet). Hopefully this makes sense:
Edit: rewrote this completely, still in a C-like pseudocode, after much additional thought and looking at what the Mixxx project does in a similar situation. This would be a helper function, called as each controller data value is received.
bool isInSync(
float jumpMax,
float jumpMin,
unsigned int prevTimestamp,
float prevValue,
unsigned int currTimestamp,
float currValue,
float targetValue,
bool inSync) {
unsigned int elapsedTime = currTimestamp - prevTimestamp;
float currDistance = currValue - targetValue;
float prevDistance = prevValue - targetValue;
bool sameSide = (currDistance < 0 && prevDistance < 0) || (currDistance > 0 && prevDistance > 0);
bool approachingTarget = sameSide && fabs(currDistance) < fabs(prevDistance);
// No matter what, if we're less than JumpMin, don't sync
if fabs(currDistance) < jumpMin return False;
// If we were in sync less than TIMEOUT ms ago, stay in sync
if inSync && elapsedTime <= TIMEOUT return True;
// New takeover logic. Goals:
//
// 1) Avoid awkward "backwards" jumps when approaching the target.
// 2) Ensure that we pick up fast movements that *pass through* the target, even if
// the individual controller values never actually fall inside the Jump Max range.
// 3) Enable a typical "set and forget" Jump Max of 3% or less without sync loss.
//
// Approaching is detected by either a quick movement from outside Jump Max to
// inside it (both on the same side of the target), OR a slow movement (whose step
// timings may exceed TIMEOUT due to coarse 0-127 resolution) within Jump Max.
//
// Inspired by controllers/softtakeover.cpp in the Mixxx DJ project.
if abs(currDistance) <= jumpMax {
if approachingTarget && (elapsedTime < TIMEOUT || fabs(prevDistance) < jumpMax) {
return False;
} else {
return True;
}
} else if !sameSide && elapsedTime < TIMEOUT {
return True;
}
}
return False;
}