Jitter when updating value through mOnValueChange

I’m having an issue with my MIDI Remote script for a rotary encoder. This encoder sends relative CC values and I use a custom value variable that I map to the host function.
I’m experiencing “jitter” or unstable values when I try to capture the value from the callback function mOnValueChange. To illustrate, this is what recorded automation looks like:

If I do not capture the updated values in mOnValueChange I get a none of this jitter.

I suspect I am doing something backwards in my script, but can’t figure out what and I’m hoping someone more experienced with the API could shed some light in where I went wrong.

At one point I suspected rounding errors in the floating point value to be the culprit and added Math.round() in several places. It did not make a difference.

I’m also curious as to why the mOnValueChange gets called twice every time setProcessValue is being called. I believe this is only true if the value binding uses the setValueTakeOverModeJump() method. Other value takeover methods does not seem to work in my case.
If the value is changed from the host, mOnValueChange only gets called once.

My script for one encoder

(The one line that seems to cause this jitter is toward the end of the script.)

//-----------------------------------------------------------------------------
// 1. DRIVER SETUP - create driver object, midi ports and detection information
//-----------------------------------------------------------------------------

var midiremote_api = require('midiremote_api_v1')
var expectedName = "Midi Fighter Twister Pro"
var deviceDriver = midiremote_api.makeDeviceDriver('DJ TECHTOOLS', expectedName, 'mlindeb')

var midiInput = deviceDriver.mPorts.makeMidiInput()
var midiOutput = deviceDriver.mPorts.makeMidiOutput()

deviceDriver.makeDetectionUnit().detectPortPair(midiInput, midiOutput)
    .expectInputNameEquals('Midi Fighter Twister')
    .expectOutputNameEquals('Midi Fighter Twister')

var surface = deviceDriver.mSurface

//----------------------------------------------------------------------------------------------------------------------
// 2. SURFACE LAYOUT - create control elements and midi bindings
//----------------------------------------------------------------------------------------------------------------------

function makeEncoder(x, y, CC, channel) {    
    var knob = {}
    knob.res = 0.005
    knob.encoder = surface.makeKnob(x, y, 1, 1)
    knob.channel = 0xb0 + channel
    knob.encoder.mSurfaceValue.mMidiBinding.setInputPort(midiInput).bindToControlChange(channel, CC)
    knob.cc = CC
    knob.pv = deviceDriver.mSurface.makeCustomValueVariable("Knob" + CC)
    knob.realValue = 0
    knob.encoder.mSurfaceValue.mOnProcessValueChange = function (device, value, value2) {
        var incSign = (value < 0.5) ? -1 : 1
        var absValue = Math.abs(value - 0.5)
        var incAdj = (absValue>0.02) ? absValue * 100 : 1
        var Adj = incSign * incAdj * knob.res
        knob.realValue = knob.realValue + Adj
        knob.realValue = Math.min(Math.max(knob.realValue, 0.0), 1.0)
        knob.pv.setProcessValue(device, knob.realValue)
    }
    knob.encoder.mSurfaceValue.mOnDisplayValueChange = function(device, val1, val2) {
        //This function does not seem to get called ever
        console.log('val1: ' + val1 + ', val2: ' + val2)
    }
    return knob
}

function makeSurfaceElements() {
    var surfaceElements = {}
    surfaceElements.knob = {}
    surfaceElements.knob[0] = makeEncoder(0, 0, 0, 0)
    return surfaceElements
}
var SE = makeSurfaceElements()

//----------------------------------------------------------------------------------------------------------------------
// 3. HOST MAPPING - create mapping pages and host bindings
//----------------------------------------------------------------------------------------------------------------------

function makePages() {
    var page = deviceDriver.mMapping.makePage("Main")

    //BINDINGS **********************
    var selCh = page.mHostAccess.mTrackSelection.mMixerChannel
    //Knob 0
    page.makeValueBinding(SE.knob[0].pv, selCh.mChannelEQ.mBand1.mFreq).setValueTakeOverModeJump()
    .mOnValueChange = function (activeDevice, activeMapping, value, diffValue) {
        midiOutput.sendMidi(activeDevice, [SE.knob[0].channel, SE.knob[0].cc, Math.round(value * 127)])
        var absDiff = Math.abs(diffValue)
        //This callback function gets called twice every time the encoder sends a new value
        //The second time it gets called the 'value' parameter is of a very small value
        if (absDiff > 0.005) {
            // Removing this next line removes the jitter
            SE.knob[0].realValue = value    //<------------- My problem line
        }
    }
    return page
}
var pages = makePages()

I’d like to add that I have tried a different approach to capturing the host value.
Instead of capturing it in the mOnValueChange callback routine as in the above post, I also tried to capture it in the mOnProcessValueChange callback as such:

    knob.encoder.mSurfaceValue.mOnProcessValueChange = function (device, value, value2) {
        var incSign = (value < 0.5) ? -1 : 1
        var absValue = Math.abs(value - 0.5)
        var incAdj = (absValue>0.02) ? absValue * 100 : 1
        var Adj = incSign * incAdj * knob.res
        knob.realValue = knob.pv.getProcessValue(device)  //This produces the same issue
        knob.realValue += Adj
        knob.realValue = Math.min(Math.max(knob.realValue, 0.0), 1.0)
        knob.pv.setProcessValue(device, knob.realValue)
    }

Unfortunately this produces the same unstable results as the above post.

OK, so let’s start from your question about the mOnDisplayValueChange, which is the easy one:
You can’t have it triggered since your encoder is not bound to a hostValue. There’s simply nothing there to display.

Now on to the question about the double triggering of the customVar event (I KNOW you’re talking about the mOnValueChange of the hostValue but let’s start from the customVar):

This is to be expected as well: Whenever you turn your encoder, you change the customVar. Trigger number 1. But when this happens, the API since it has a binding, will re-trigger the mOnProcessValueChange based on the “real” value your hostValue now has.

Let’s think about it for a while. We are not responsible for the real value of whichever parameter, this is left to the API to handle. So, when we’re sending a value, the API “translates” this one to whichever value seems to be suitable. Can there be rounding errors here? Sure, why not? Can it be that a value is simply more suited to the param handled? Again, sure!

Now, to your question about the double triggering of the mOnValueChange. Since we saw we have a double trigger of the customVar for the reason described above, you can always have a double trigger of the bound hostValue mOnValueChange.

From what I see in your code, you’re trying to first grab whichever value is set to the hostValue, so you can let your encoder act from there, instead of a reset position. This is good, however because of the above, you’re going to have trouble with mixing all these together.
What you can do is to create a pseudo encoder, bind it to the very same parameter you want, and let it perform the initialisation of the custom var, i.e. a one-time process.

Here’s your code altered a bit to better explain the above. You can give it a try and see if it helps. Note that in order to test it, I’ve set a Bome script to send just 1 and 127 to CC 0.

//-----------------------------------------------------------------------------
// 1. DRIVER SETUP - create driver object, midi ports and detection information
//-----------------------------------------------------------------------------

var midiremote_api = require('midiremote_api_v1')
var expectedName = "Midi Fighter Twister Pro"
var deviceDriver = midiremote_api.makeDeviceDriver('DJ TECHTOOLS', expectedName, 'mlindeb')

var midiInput = deviceDriver.mPorts.makeMidiInput()
var midiOutput = deviceDriver.mPorts.makeMidiOutput()

deviceDriver.makeDetectionUnit().detectPortPair(midiInput, midiOutput)
    .expectInputNameEquals('Midi Fighter Twister')
    .expectOutputNameEquals('Midi Fighter Twister')

var surface = deviceDriver.mSurface

var encoderTriggered=false 
//----------------------------------------------------------------------------------------------------------------------
// 2. SURFACE LAYOUT - create control elements and midi bindings
//----------------------------------------------------------------------------------------------------------------------

function makeEncoder(x, y, CC, channel) {    

    var knob = {}
    knob.res = 0.005
    knob.encoder = surface.makeKnob(x, y, 1, 1)
    knob.channel = 0xb0 + channel
    knob.encoder.mSurfaceValue.mMidiBinding.setInputPort(midiInput).bindToControlChange(channel, CC)
    knob.cc = CC
    knob.pv = deviceDriver.mSurface.makeCustomValueVariable("Knob" + CC)
    
    //altered
    //knob.realValue = 0
    knob.pseudoEncoder=surface.makeKnob(0,0,0,0)
    knob.initialised=false 
    //------------------------------------------

    knob.encoder.mSurfaceValue.mOnProcessValueChange = function (device, value, value2) {

        console.log("encoder value="+value)
        console.log("initialised="+knob.initialised)

        if(knob.initialised==false){

            return 

        }

        var incSign = (value < 0.5) ? -1 : 1
       
        var Adj = incSign * knob.res
        
        var previousValue=knob.pv.getProcessValue(device)
        
        var newValue=Math.min(Math.max(previousValue+Adj,0),1)

        console.log("new custom var val="+newValue)

        encoderTriggered=true 
        knob.pv.setProcessValue(device,newValue)
        
    }

    knob.pv.mOnProcessValueChange=function(activeDevice,value,diff){
        
        var triggeredFrom="Binding"
        if(encoderTriggered==true){

            triggeredFrom="Encoder"
            encoderTriggered=false 
        
        }
        
        console.log("pv triggered from "+triggeredFrom+" with val="+value)

    }

    knob.pseudoEncoder.mSurfaceValue.mOnProcessValueChange=function(activeDevice,value,diff){

        if (knob.initialised==false){
           
            console.log("initialising with value="+value)
            knob.initialised=true 
            knob.pv.setProcessValue(activeDevice,value)

        }
    
    }

    knob.pseudoEncoder.mSurfaceValue.mOnDisplayValueChange = function(device, val1, val2) {

        //since we have a binding now, we get this triggered
        console.log('val1: ' + val1 + ', val2: ' + val2)
    
    }

    return knob

}

function makeSurfaceElements() {
    var surfaceElements = {}
    surfaceElements.knob = {}
    surfaceElements.knob[0] = makeEncoder(0, 0, 0, 0)
    return surfaceElements
}
var SE = makeSurfaceElements()

//----------------------------------------------------------------------------------------------------------------------
// 3. HOST MAPPING - create mapping pages and host bindings
//----------------------------------------------------------------------------------------------------------------------

function makePages() {

    var page = deviceDriver.mMapping.makePage("Main")

    //BINDINGS **********************
    var selCh = page.mHostAccess.mTrackSelection.mMixerChannel

    //Knob 0
    page.makeValueBinding(SE.knob[0].pseudoEncoder.mSurfaceValue, selCh.mChannelEQ.mBand1.mFreq)

    page.makeValueBinding(SE.knob[0].pv, selCh.mChannelEQ.mBand1.mFreq)
    
    return page

}

var pages = makePages()

A final note: You can see that the frequency is not changing in a linear way. This has nothing to do with us, it’s handled internally. IF you want something more linear, you have to go into more analytical approaches, for example increasing in a smaller step at big values. You can use logarithms for such case or other custom functions as well.

2 Likes

Hi @m.c and thank you for your detailed response.
I tried your version of my script and at first I was quite hopeful until I realized you had removed a few crucial lines of code in the mOnProcessValueChange callback function. These to be exact:

        var absValue = Math.abs(value - 0.5)
        var incAdj = (absValue>0.02) ? absValue * 100 : 1
        var Adj = incSign * incAdj * knob.res

I’m guessing you did it to simplify testing and the fact that you don’t own the same controller as I do.

My encoders send CC values 65-81 on clockwise turn. Higher value means higher velocity. In other words, if I turn the know slowly, it will send a stream of CC value 65 and if I turn it really quick, I might get 81. On a counter clockwise turn the encoder sends 63-47. Similarly, the lower the value, the higher the velocity. For the sake of simplicity, I will call them plus values and minus values since that’s basically what they are with 64 representing zero.
(The encoder never send the value 64.)
In the host/Cubase these values will be represented as 0.0-1.0. So values between 0.5 and 1.0 is a clockwise turn, or a positive value and between 0.5 and 0.0 is a negative value

var absValue = Math.abs(value - 0.5)
This line just give me the absolute value with out any plus/minus signs (or imagined ones!).

var incAdj = (absValue>0.02) ? absValue * 100 : 1
This next line checks to see if the value sent by the encoder is “faster” than 0.02. If it is, I’ll use the absValue * 100 to dynamically change the perceived speed.

var Adj = incSign * incAdj * knob.res
I include that dynamic value in the final adjustment.


I honestly don’t know what the purpose of the pseudoEncoder is. I couldn’t notice any real difference with it as without it.

If you look at my second post in this thread, I mentioned that I also tried to grab the host value from knob.pv.getProcessValue(device). This is basically how you did it also in your version. Once the above lines for changing the velocity dynamically is added back in, I get the exact same issue with jumping, jittery values.

If I change your script:

function makeEncoder(x, y, CC, channel) {    
    var knob = {}
    knob.res = 0.005
    knob.encoder = surface.makeKnob(x, y, 1, 1)
    knob.channel = 0xb0 + channel
    knob.encoder.mSurfaceValue.mMidiBinding.setInputPort(midiInput).bindToControlChange(channel, CC)
    knob.cc = CC
    knob.pv = deviceDriver.mSurface.makeCustomValueVariable("Knob" + CC)
    //altered
    //knob.realValue = 0
    knob.pseudoEncoder=surface.makeKnob(0,0,0,0)
    knob.initialised=false 
    //------------------------------------------
    knob.encoder.mSurfaceValue.mOnProcessValueChange = function (device, value, value2) {
        console.log("encoder value="+value)
        console.log("initialised="+knob.initialised)
        if(knob.initialised==false){
            return 
        }
        var incSign = (value < 0.5) ? -1 : 1
        var Adj = incSign * knob.res
        var previousValue=knob.pv.getProcessValue(device)
        var newValue=Math.min(Math.max(previousValue+Adj,0),1)
        console.log("new custom var val="+newValue)

        encoderTriggered=true 
        knob.pv.setProcessValue(device,newValue)
    }

to:

function makeEncoder(x, y, CC, channel) {
    var knob = {}
    knob.res = 0.005
    knob.encoder = surface.makeKnob(x, y, 1, 1)
    knob.channel = 0xb0 + channel
    knob.encoder.mSurfaceValue.mMidiBinding.setInputPort(midiInput).bindToControlChange(channel, CC)
    knob.cc = CC
    knob.pv = deviceDriver.mSurface.makeCustomValueVariable("Knob" + CC)
    //altered
    knob.realValue = 0 //putting it back in
    knob.pseudoEncoder=surface.makeKnob(0,0,0,0)
    knob.initialised=false 
    //------------------------------------------
    knob.encoder.mSurfaceValue.mOnProcessValueChange = function (device, value, value2) {
        console.log("encoder value="+value)
        console.log("initialised="+knob.initialised)
        if(knob.initialised==false){
            return 
        }
        var incSign = (value < 0.5) ? -1 : 1
        //These next three lines are needed
        var absValue = Math.abs(value - 0.5)
        var incAdj = (absValue>0.02) ? absValue * 100 : 1
        var Adj = incSign * incAdj * knob.res

        //If I remove this, the response is butter smooth
        //var previousValue=knob.pv.getProcessValue(device)
        var newValue=Math.min(Math.max(knob.realValue+Adj,0),1)
        knob.realValue = newValue
        console.log("new custom var val="+newValue)

        encoderTriggered=true 
        knob.pv.setProcessValue(device,newValue)
    }

…I do not get any jitter. The response is smooth. Of course I loose any feedback from the host if the value changes from there, but I know I can’t use this method and I feel like I’m back on square one.

If it is of any help, I have recorded the raw output of my encoder into a MIDI file available from this Dropbox link.

Of course, the aim was to check the jittering. This is why I noted which values I used for testing.

Isn’t this a bit too much? I mean, when you hit for example 0.60, you’ll have an increase of 60 compared to the 1 when in lower speed? What is the higher value you get from the real encoder?

There are controllers that upon activation of a page are sending their current values. Using the pseudoEncoder prevents these values from updating our custom vars. NOT all controllers do that, and since you don’t see a difference, sure, you can omit this.

No matter if you get a smooth response, the fact is that previous value should be what pv holds. Having another var to do this is a workaround and should be used only if we lose hope of correct result using the original.

Of course it is! Now I know what the controller really sends.

Could you attach a screenshot of logging Adj and getProcessValue without the introduction of knob.realValue?

What happens if you use the original custom var and set Adj to just incSign*knob.res? Do you still get nasty returns?

True and this is not helping me to debug the situation :frowning: I’ve created a script for a controller I would swear I was doing the right thing, only to see I was completely wrong when I actually got it in my hands.

Although there has been some great updates to MIDI Remote in v14.0.10, this issue still plagues me.

I have created a minimal script for testing one of my relative encoders and the discrepancy between the values I set using setProcessValue and the values MIDI Remote returns makes using my encoders very difficult.

Here is my test script
//-----------------------------------------------------------------------------
// 1. DRIVER SETUP - create driver object, midi ports and detection information
//-----------------------------------------------------------------------------
var midiremote_api = require('midiremote_api_v1')
var driver = midiremote_api.makeDeviceDriver('DJ TECHTOOLS', 'Midi Fighter Twister Test', 'mlib')
var midiInMF = driver.mPorts.makeMidiInput('MF')
var midiOutMF = driver.mPorts.makeMidiOutput('MF')

var detUnit = driver.makeDetectionUnit()
var detectPortPairMF = detUnit.detectPortPair(midiInMF, midiOutMF)
detectPortPairMF
    .expectInputNameEquals('Midi Fighter Twister')
    .expectOutputNameEquals('Midi Fighter Twister')

var surface = driver.mSurface

//----------------------------------------------------------------------------------------------------------------------
// 2. SURFACE LAYOUT - create control elements and midi bindings
//----------------------------------------------------------------------------------------------------------------------

function makeEncoder(row, column, cc, channel) {
    var encoder = {}
    encoder.controller = surface.makeKnob(column, row, 1, 1)
    encoder.controller.mSurfaceValue.mMidiBinding.setInputPort(midiInMF).bindToControlChange(channel, cc)
    encoder.boundValue = driver.mSurface.makeCustomValueVariable("Encoder_" + cc)

    encoder.channel = 0xb0 + channel
    encoder.cc = cc
    encoder.realValue = 0
    encoder.controller.mSurfaceValue.mOnProcessValueChange = function (activeDevice, value, value2) {
        //Is encoder turning clockwise or counter clockwise?
        var value7bit = value * 127             //7-bit value
        var sign = (value7bit < 64) ? -1 : 1    //Plus or minus
        
        var encoderResolution = 485
        var valueAbs = Math.abs(value7bit - 64)
        var changeValue = valueAbs * (1 / encoderResolution) * sign
        encoder.realValue += changeValue                                        //Update stored value
        encoder.realValue = (encoder.realValue < 0) ? 0 : encoder.realValue     //Prevent going below 0
        encoder.realValue = (encoder.realValue > 1) ? 1 : encoder.realValue     //Prevent going above 1
        console.log("Set Process value to:  " + encoder.realValue)
        encoder.boundValue.setProcessValue(activeDevice, encoder.realValue)     //Update ProcessValue
    }
    return encoder
}

function makeSurfaceElements() {
    //log('start makeSurfaceElements')
    var surfaceElements = {}
    surfaceElements.encoder = {}

    surfaceElements.encoder = makeEncoder(0, 0, 0, 0)
    return surfaceElements
}
var SE = makeSurfaceElements()

//----------------------------------------------------------------------------------------------------------------------
// 3. HOST MAPPING - create mapping pages and host bindings
//----------------------------------------------------------------------------------------------------------------------

function makePage() {
    var p = driver.mMapping.makePage("DefaultPage")
    var selCh = p.mHostAccess.mTrackSelection.mMixerChannel
    
    p.makeValueBinding(SE.encoder.boundValue, selCh.mChannelEQ.mBand1.mFreq).setValueTakeOverModeJump()
        .mOnValueChange = function (activeDevice, activeMapping, value, diffValue) {
            console.log("Value received:            " + value)
            SE.encoder.realValue = value    //Update real value
            midiOutMF.sendMidi(activeDevice, [SE.encoder.channel, SE.encoder.cc, Math.round(value * 127)])            
        }
    return p
}
var page = makePage()

These are the results when trying to write automation data:
MR Jumping

Here is an excerpt of the console log messages:

Hi, could you please adapt the two logs to include the timestamp? It would be nice to see the exact timings, even though your video is pretty well demonstrating the issue :slight_smile:

Logging example:

var timeStamp=new Date().getTime()
console.log(""+timeStamp+" Value received:            " + value)

No problem!

Here are a few screenshots. (Still wish you could copy the text from the console.)

1 Like

I see, very interesting! Looks like the “abusing” of the process variable is part of the issue. At the same time, the delayed reply. It’s a mixture, I think. I will try here too, with a thought I have, using your snippet, to replicate (it surely will) and then alter it a bit and recheck.

This is actually a good feature request. I came up with building a small external app, and then passing the logs using a sysex, so that I can have the logs in my app, just a textArea and a midi class, from which I can copy.

1 Like

I’m pretty sure I posted it as a FR some time ago. Feel free to vote! :wink:

1 Like