diff --git a/res/controllers/Traktor Kontrol S4 MK3.hid.xml b/res/controllers/Traktor Kontrol S4 MK3.hid.xml index 51a34b13de8b..a254772e6c50 100644 --- a/res/controllers/Traktor Kontrol S4 MK3.hid.xml +++ b/res/controllers/Traktor Kontrol S4 MK3.hid.xml @@ -589,19 +589,6 @@ - - + diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js index 3f3fb65fbc29..9bdf9acf161b 100644 --- a/res/controllers/Traktor-Kontrol-S4-MK3.js +++ b/res/controllers/Traktor-Kontrol-S4-MK3.js @@ -95,10 +95,6 @@ const WheelLedBlinkOnTrackEnd = !!engine.getSetting("wheelLedBlinkOnTrackEnd"); // Default: false const MixerControlsMixAuxOnShift = !!engine.getSetting("mixerControlsMicAuxOnShift"); -// Define how many wheel moves are sampled to compute the speed. The more you have, the more the speed is accurate, but the -// less responsive it gets in Mixxx. Default: 5 -const WheelSpeedSample = engine.getSetting("wheelSpeedSample") || 5; - // Make the sampler tab a beatlooproll tab instead // Default: false const UseBeatloopRollInsteadOfSampler = !!engine.getSetting("useBeatloopRollInsteadOfSampler"); @@ -127,11 +123,9 @@ const BaseRevolutionsPerMinute = engine.getSetting("baseRevolutionsPerMinute") | // Default: false const UseMotors = !!engine.getSetting("useMotors"); -// Define how many wheel moves are sampled to compute the speed when using the motor. This is helpful to mitigate delay that -// occurs in communication as well as Mixxx limitation to 20ms latency. -// The more you have, the more the speed is accurate. -// less responsive it gets in Mixxx. Default: 20 -const TurnTableSpeedSample = engine.getSetting("turnTableSpeedSample") || 20; +// Define whether or not the jog wheel plater provide a haptic feedback when going over the cue point. +// Default: true +const CueHapticFeedback = UseMotors && !!engine.getSetting("cueHapticFeedback"); // Define how much the wheel will resist. It is a similar setting that the Grid+Wheel in Tracktor // Value must defined between 0 to 1. 0 is very tight, 1 is very loose. @@ -469,13 +463,6 @@ class Deck extends ComponentContainer { }, true); } } - - if (currentModes.wheelMode === wheelModes.motor) { - this.wheelTouch.touched = true; - engine.beginTimer(MotorWindDownMilliseconds, () => { - this.wheelTouch.touched = false; - }, true); - } this.reconnectComponents(function(component) { if (component.group === undefined || component.group.search(script.channelRegEx) !== -1) { @@ -1395,7 +1382,7 @@ Button.prototype.colorMap = new ColorMapper({ 0xCCCCCC: LedColors.white, }); -const wheelRelativeMax = 2 ** 16 - 1; +const wheelRelativeMax = 2 ** 32 - 1; const wheelAbsoluteMax = 2879; const wheelTimerMax = 2 ** 32 - 1; @@ -2371,11 +2358,6 @@ class S4Mk3Deck extends Deck { }); this.wheelMode = wheelModes.vinyl; - let motorWindDownTimer = 0; - const motorWindDownTimerCallback = () => { - engine.stopTimer(motorWindDownTimer); - motorWindDownTimer = 0; - }; this.turntableButton = UseMotors ? new Button({ deck: this, input: function(press) { @@ -2384,14 +2366,10 @@ class S4Mk3Deck extends Deck { this.deck.fluxButton.loopModeOff(true); if (this.deck.wheelMode === wheelModes.motor) { this.deck.wheelMode = wheelModes.vinyl; - motorWindDownTimer = engine.beginTimer(MotorWindDownMilliseconds, motorWindDownTimerCallback, true); engine.setValue(this.group, "scratch2_enable", false); } else { this.deck.wheelMode = wheelModes.motor; const group = this.group; - engine.beginTimer(MotorWindUpMilliseconds, () => { - engine.setValue(group, "scratch2_enable", true); - }, true); } this.outTrigger(); } @@ -2412,9 +2390,6 @@ class S4Mk3Deck extends Deck { if (this.deck.wheelMode === wheelModes.vinyl) { this.deck.wheelMode = wheelModes.jog; } else { - if (this.deck.wheelMode === wheelModes.motor) { - motorWindDownTimer = engine.beginTimer(MotorWindDownMilliseconds, motorWindDownTimerCallback, true); - } this.deck.wheelMode = wheelModes.vinyl; } engine.setValue(this.group, "scratch2_enable", false); @@ -2445,7 +2420,7 @@ class S4Mk3Deck extends Deck { } }, stopScratchWhenOver: function() { - if (this.touched || this.deck.wheelMode === wheelModes.motor) { + if (this.touched) { return; } @@ -2469,45 +2444,36 @@ class S4Mk3Deck extends Deck { this.wheelRelative = new Component({ oldValue: null, deck: this, - // We use a rolling average on a sample of speed received. An alternative could - // be to compute precise speed as soon as two points have been received. While the - // alternative is likely going to reduce the delay. it may introduce imprefection due - // to delays that could occurred at various level, so we stick with the naive average for now - stack: [], - stackIdx: 0, - avgSpeed: 0, - // There is a second sampling group, larger, that improve precision but increase delay, which - // is used in TT mode - stackAvg: [], - stackAvgIdx: 0, - ttAvgSpeed: 0, - input: function(value) { - const oldValue = this.oldValue; - this.oldValue = value; - if (oldValue === null) { + speed: 0, + input: function(value, timestamp) { + if (this.oldValue === null) { // This is to avoid the issue where the first time, we diff with 0, leading to the absolute value + this.oldValue = [value, timestamp, 0]; return; } + let [oldValue, oldTimestamp, speed] = this.oldValue; - let diff = value - oldValue; + if (timestamp < oldTimestamp) { + oldTimestamp -= wheelRelativeMax; + } + let diff = value - oldValue; if (diff > wheelRelativeMax / 2) { - diff = (wheelRelativeMax - value + oldValue) * -1; - } else if (diff < -1 * (wheelRelativeMax / 2)) { - diff = wheelRelativeMax - oldValue + value; + oldValue += wheelRelativeMax; + } else if (diff < -wheelRelativeMax / 2) { + oldValue -= wheelRelativeMax; } - this.stack[this.stackIdx] = diff / wheelTimerDelta; - this.stackIdx = (this.stackIdx + 1) % WheelSpeedSample; - - this.avgSpeed = (this.stack.reduce((ps, v) => ps + v, 0) / this.stack.length) * wheelTicksPerTimerTicksToRevolutionsPerSecond; - - this.stackAvg[this.stackAvgIdx] = this.avgSpeed; - this.stackAvgIdx = (this.stackAvgIdx + 1) % TurnTableSpeedSample; - - this.ttAvgSpeed = this.stackAvg.reduce((ps, v) => ps + v, 0) / this.stackAvg.length; + const currentSpeed = (value - oldValue)/(timestamp - oldTimestamp); + if ((currentSpeed <= 0) === (speed <= 0)) { + speed = (speed + currentSpeed)/2; + } else { + speed = currentSpeed; + } + this.oldValue = [value, timestamp, speed]; + this.speed = wheelAbsoluteMax*speed*10; - if (this.avgSpeed === 0 && + if (this.speed === 0 && engine.getValue(this.group, "scratch2") === 0 && engine.getValue(this.group, "jog") === 0 && this.deck.wheelMode !== wheelModes.motor) { @@ -2516,13 +2482,13 @@ class S4Mk3Deck extends Deck { switch (this.deck.wheelMode) { case wheelModes.motor: - engine.setValue(this.group, "scratch2", this.ttAvgSpeed / baseRevolutionsPerSecond); + engine.setValue(this.group, "scratch2", this.speed); break; case wheelModes.loopIn: { const loopStartPosition = engine.getValue(this.group, "loop_start_position"); const loopEndPosition = engine.getValue(this.group, "loop_end_position"); - const value = Math.min(loopStartPosition + (this.avgSpeed * LoopWheelMoveFactor), loopEndPosition - LoopWheelMoveFactor); + const value = Math.min(loopStartPosition + (this.speed * LoopWheelMoveFactor), loopEndPosition - LoopWheelMoveFactor); engine.setValue( this.group, "loop_start_position", @@ -2533,7 +2499,7 @@ class S4Mk3Deck extends Deck { case wheelModes.loopOut: { const loopEndPosition = engine.getValue(this.group, "loop_end_position"); - const value = loopEndPosition + (this.avgSpeed * LoopWheelMoveFactor); + const value = loopEndPosition + (this.speed * LoopWheelMoveFactor); engine.setValue( this.group, "loop_end_position", @@ -2543,24 +2509,56 @@ class S4Mk3Deck extends Deck { break; case wheelModes.vinyl: if (this.deck.wheelTouch.touched || engine.getValue(this.group, "scratch2") !== 0) { - engine.setValue(this.group, "scratch2", this.avgSpeed); + engine.setValue(this.group, "scratch2", this.speed); } else { - engine.setValue(this.group, "jog", this.avgSpeed); + engine.setValue(this.group, "jog", this.speed); } break; default: - engine.setValue(this.group, "jog", this.avgSpeed); + engine.setValue(this.group, "jog", this.speed); } }, }); this.wheelLED = new Component({ deck: this, + lastPos: 0, outKey: "playposition", output: function(fractionOfTrack) { if (this.deck.wheelMode > wheelModes.motor) { return; } + // Emit cue haptic feedback if enabled + const samplePos = Math.round(fractionOfTrack * engine.getValue(this.group, "track_samples")); + if (this.deck.wheelTouch.touched && CueHapticFeedback) { + const cuePos = engine.getValue(this.group, "cue_point"); + const forward = this.lastPos <= samplePos; + let fired = false; + const motorDeckData = new Uint8Array([ + 1, 0x20, 1, MaxWheelForce & 0xff, MaxWheelForce >> 8, + ]); + if (forward && this.lastPos < cuePos && cuePos < samplePos) { + fired = true; + } else if (!forward && cuePos < this.lastPos && samplePos <= cuePos) { + motorDeckData[1] = 0xe0; + motorDeckData[2] = 0xfe; + fired = true; + } + if (fired) { + const motorData = new Uint8Array([ + 1, 0x20, 1, 0, 0, + 1, 0x20, 1, 0, 0, + ]); + if (this.deck === TraktorS4MK3.leftDeck) { + motorData.set(motorDeckData); + } else { + motorData.set(motorDeckData, 5); + } + controller.sendOutputReport(49, motorData.buffer, true); + } + } + this.lastPos = samplePos; + const durationSeconds = engine.getValue(this.group, "duration"); const positionSeconds = fractionOfTrack * durationSeconds; const revolutions = positionSeconds * baseRevolutionsPerSecond; @@ -2648,6 +2646,87 @@ class S4Mk3Deck extends Deck { } } +class S4Mk3MotorManager { + constructor(deck) { + this.deck = deck; + this.userHold = 0; + this.oldValue = [0, 0]; + this.baseFactor = parseInt(110 * BaseRevolutionsPerMinute); + this.zeroSpeedForce = 1650; + this.currentMaxWheelForce = MaxWheelForce; + } + tick() { + const motorData = new Uint8Array([ + 1, 0x20, 1, 0, 0, + ]); + const maxVelocity = 10; + let velocity = 0; + + let expectedSpeed = 0; + + const currentSpeed = this.deck.wheelRelative.speed / baseRevolutionsPerSecond; + + if (this.deck.wheelMode === wheelModes.motor + && engine.getValue(this.deck.group, "play")) { + expectedSpeed = engine.getValue(this.deck.group, "rate_ratio"); + const normalisationFactor = 1/expectedSpeed/5; + velocity = expectedSpeed + Math.pow(-5 * (expectedSpeed / 1) * (currentSpeed - expectedSpeed), 3); + } else if (this.deck.wheelMode !== wheelModes.motor) { + if (TightnessFactor > 0.5) { + // Super loose + const reduceFactor = (Math.min(0.5, TightnessFactor - 0.5) / 0.5) * 0.7; + velocity = currentSpeed * reduceFactor; + } else if (TightnessFactor < 0.5) { + // Super tight + const reduceFactor = (2 - Math.max(0, TightnessFactor) * 4); + velocity = expectedSpeed + Math.min( + maxVelocity, + Math.max( + -maxVelocity, + (expectedSpeed - currentSpeed) * reduceFactor + ) + ); + } + } + + velocity *= this.baseFactor; + + if (velocity < 0) { + motorData[1] = 0xe0; + motorData[2] = 0xfe; + velocity = -velocity; + } else if (this.deck.wheelMode === wheelModes.motor && engine.getValue(this.deck.group, "play")) { + velocity += this.zeroSpeedForce; + } + + if (!this.isBlockedByUser() && velocity > MaxWheelForce) { + this.userHold++; + } else if (velocity < MaxWheelForce / 2 && this.userHold > 0) { + this.userHold--; + } + + if (this.isBlockedByUser()) { + engine.setValue(this.deck.group, "scratch2_enable", true); + this.currentMaxWheelForce = this.zeroSpeedForce + parseInt(this.baseFactor * expectedSpeed); + } else if (expectedSpeed && this.userHold === 0 && !this.deck.wheelTouch.touched) { + engine.setValue(this.deck.group, "scratch2_enable", false); + this.currentMaxWheelForce = MaxWheelForce; + } + + velocity = Math.min( + this.currentMaxWheelForce, + Math.floor(velocity) + ); + + motorData[3] = velocity & 0xff; + motorData[4] = velocity >> 8; + return motorData; + } + isBlockedByUser() { + return this.userHold >= 10; + } +} + class S4Mk3MixerColumn extends ComponentContainer { constructor(idx, inReports, outReport, io) { super(); @@ -2998,176 +3077,15 @@ class S4MK3 { controller.sendOutputReport(129, deckMeters.buffer); }); if (UseMotors) { - engine.beginTimer(20, this.motorCallback.bind(this)); - this.leftVelocityFactor = wheelAbsoluteMax * baseRevolutionsPerSecond * 2; - this.rightVelocityFactor = wheelAbsoluteMax * baseRevolutionsPerSecond * 2; - - this.leftFactor = [this.leftVelocityFactor]; - this.leftFactorIdx = 1; - this.rightFactor = [this.rightVelocityFactor]; - this.rightFactorIdx = 1; - - this.averageLeftCorrectness = []; - this.averageLeftCorrectnessIdx = 0; - this.averageRightCorrectness = []; - this.averageRightCorrectnessIdx = 0; + this.leftMotor = new S4Mk3MotorManager(this.leftDeck); + this.rightMotor = new S4Mk3MotorManager(this.rightDeck); + engine.beginTimer(1, this.motorCallback.bind(this)); } - } motorCallback() { - const motorData = new Uint8Array([ - 1, 0x20, 1, 0, 0, - 1, 0x20, 1, 0, 0, - - ]); - const maxVelocity = 10; - - let velocityLeft = 0; - let velocityRight = 0; - - let expectedLeftSpeed = 0; - let expectedRightSpeed = 0; - - if (this.leftDeck.wheelMode === wheelModes.motor - && engine.getValue(this.leftDeck.group, "play")) { - expectedLeftSpeed = engine.getValue(this.leftDeck.group, "rate_ratio"); - } - - if (this.rightDeck.wheelMode === wheelModes.motor - && engine.getValue(this.rightDeck.group, "play")) { - expectedRightSpeed = engine.getValue(this.rightDeck.group, "rate_ratio"); - } - - const currentLeftSpeed = this.leftDeck.wheelRelative.avgSpeed / baseRevolutionsPerSecond; - const currentRightSpeed = this.rightDeck.wheelRelative.avgSpeed / baseRevolutionsPerSecond; - - if (expectedLeftSpeed) { - velocityLeft = expectedLeftSpeed + Math.min( - maxVelocity, - Math.max( - -maxVelocity, - (expectedLeftSpeed - currentLeftSpeed) - ) - ); - } else { - if (TightnessFactor > 0.5) { - // Super loose - const reduceFactor = (Math.min(0.5, TightnessFactor - 0.5) / 0.5) * 0.7; - velocityLeft = currentLeftSpeed * reduceFactor; - } else if (TightnessFactor < 0.5) { - // Super tight - const reduceFactor = (2 - Math.max(0, TightnessFactor) * 4); - velocityLeft = expectedLeftSpeed + Math.min( - maxVelocity, - Math.max( - -maxVelocity, - (expectedLeftSpeed - currentLeftSpeed) * reduceFactor - ) - ); - - } - } - - if (expectedRightSpeed) { - velocityRight = expectedRightSpeed + Math.min( - maxVelocity, - Math.max( - -maxVelocity, - (expectedRightSpeed - currentRightSpeed) - ) - ); - } else { - if (TightnessFactor > 0.5) { - // Super loose - const reduceFactor = (Math.min(0.5, TightnessFactor - 0.5) / 0.5) * 0.7; - velocityRight = currentRightSpeed * reduceFactor; - } else if (TightnessFactor < 0.5) { - // Super tight - const reduceFactor = (2 - Math.max(0, TightnessFactor) * 4); - console.log(reduceFactor); - velocityRight = expectedRightSpeed + Math.min( - maxVelocity, - Math.max( - -maxVelocity, - (expectedRightSpeed - currentRightSpeed) * reduceFactor - ) - ); - - } - } - - if (velocityLeft < 0) { - motorData[1] = 0xe0; - motorData[2] = 0xfe; - velocityLeft = -velocityLeft; - } - - if (velocityRight < 0) { - motorData[6] = 0xe0; - motorData[7] = 0xfe; - velocityRight = -velocityRight; - } - - const roundedCurrentLeftSpeed = Math.round(currentLeftSpeed * 100); - const roundedCurrentRightSpeed = Math.round(currentRightSpeed * 100); - - velocityLeft = velocityLeft * this.leftVelocityFactor; - velocityRight = velocityRight * this.rightVelocityFactor; - - const minNormalFactor = 0.8 * wheelAbsoluteMax * baseRevolutionsPerSecond * 2; - const maxNormalFactor = 1.2 * wheelAbsoluteMax * baseRevolutionsPerSecond * 2; - - if (velocityLeft > minNormalFactor && velocityLeft < maxNormalFactor) { - this.averageLeftCorrectness[this.averageLeftCorrectnessIdx] = roundedCurrentLeftSpeed; - this.averageLeftCorrectnessIdx = (this.averageLeftCorrectnessIdx + 1) % 10; - const averageCorrectness = Math.round(this.averageLeftCorrectness.reduce((a, b) => a+b, 0) / this.averageLeftCorrectness.length); - this.leftFactor[this.leftFactorIdx] = velocityLeft; - this.leftFactorIdx = (this.leftFactorIdx + 1) % 10; - const averageFactor = Math.round(this.leftFactor.reduce((a, b) => a+b, 0) / this.leftFactor.length); - - - if ((averageCorrectness < 100 && velocityLeft > this.leftVelocityFactor) || (averageCorrectness > 100 && velocityLeft < this.leftVelocityFactor)) { - this.leftVelocityFactor = averageFactor; - } - } - - if (velocityRight > minNormalFactor && velocityRight < maxNormalFactor) { - this.averageRightCorrectness[this.averageRightCorrectnessIdx] = roundedCurrentRightSpeed / (expectedRightSpeed || 0.001); - this.averageRightCorrectnessIdx = (this.averageRightCorrectnessIdx + 1) % 20; - const averageCorrectness = Math.round(this.averageRightCorrectness.reduce((a, b) => a+b, 0) / this.averageRightCorrectness.length); - this.rightFactor[this.rightFactorIdx] = velocityRight; - this.rightFactorIdx = (this.rightFactorIdx + 1) % 20; - const averageFactor = Math.round(this.rightFactor.reduce((a, b) => a+b, 0) / this.rightFactor.length); - - - if ((averageCorrectness < 100 && velocityRight > this.rightVelocityFactor) || (averageCorrectness > 100 && velocityRight < this.rightVelocityFactor)) { - this.rightVelocityFactor = averageFactor; - } - } - - if (velocityLeft) { - velocityLeft += wheelAbsoluteMax / 2; - } - - if (velocityRight) { - velocityRight += wheelAbsoluteMax / 2; - } - - velocityLeft = Math.min( - MaxWheelForce, - Math.floor(velocityLeft) - ); - - velocityRight = Math.min( - MaxWheelForce, - Math.floor(velocityRight) - ); - - motorData[3] = velocityLeft & 0xff; - motorData[4] = velocityLeft >> 8; - - motorData[8] = velocityRight & 0xff; - motorData[9] = velocityRight >> 8; + var motorData = new Uint8Array(10); + motorData.set(this.leftMotor.tick()); + motorData.set(this.rightMotor.tick(), 5); controller.sendOutputReport(49, motorData.buffer, true); } incomingData(data) { @@ -3188,9 +3106,8 @@ class S4MK3 { if (wheelTimerDelta < 0) { wheelTimerDelta += wheelTimerMax; } - - this.leftDeck.wheelRelative.input(view.getUint16(12, true)); - this.rightDeck.wheelRelative.input(view.getUint16(40, true)); + this.leftDeck.wheelRelative.input(view.getUint32(12, true), view.getUint32(8, true)); + this.rightDeck.wheelRelative.input(view.getUint32(40, true), view.getUint32(36, true)); } else { console.warn(`Unsupported HID repord with ID ${reportId}. Contains: ${data}`); }