Today I learned that JavaScript can not perform simple arithmetic on floating point numbers without rounding issues. Turns out it’s a known issue in computing with floating point data types that I was completely oblivious to.
To test this, open up a Console in your browser and type in:
0.1 + 0.2
You would expect the result to be 0.3 and it’s close, but not exactly.
This of course got me thinking that perhaps the so called “rounding errors” that several users have been reporting in the MIDI Remote API could be due to a shortcoming in the nature of floating point arithmetic?
Perhaps this idea has been presented already and I missed it, if so, I apologize. If not, am I on to something here?
Hello my friend, we always have to round (numberFormat) appropriately when dealing with floats and doubles and preparing an output, be it a display or another device, and this is not an issue with javaScript only. It’s just too common. Some languages try to perform such actions internally, other leave it all up to the coder.
Sure, it’s possible that a rounding was missed. However, I did notice that when I’m sending an output inside a script (I know that probably you’re talking about the assistant) even if I don’t round, the midi value is properly rounded in the end. Maybe this is not universal, and I missed something.
Hey buddy! I’m not talking about rounding to a 7bit value for outputing to a MIDI device. I’m talking about floating point number in JavaScript.
Nope, I’m talking about the JavaScript API as in the title.
More specifically, when in your script you manually manipulate a processValue. I’m sure you’re doing that in your scripts too, right?
Try this for fun.
Var sum = 0.1 + 0.2
console.log(sum)
Do you get 0.3?
(I can’t verify in Cubase atm. But both Chrome and Firefox does not perfectly sum the two values.)
Edit:
I changed the title of the topic to “Rounding errors” from “Rounding issue”. Hopefully that makes it a bit more clear.
This is a well known issue as I’ve previously said. But, at which point exactly do you have to do this type of calculations and not perform rounding at the very end? Isn’t it all about sending messages to your device? Maybe you’re talking about something else and I’m totally out of subject here.
Here’s what I really mean, in case I haven’t made my self clear. When we perform such calculations, we always have to take care and round when needed. In your example, here’s how we can deal with it:
And here’s the log inside the midi remote:
Note that I go for the Math.round because toFixed spits out a string. It’s all about the type of representation we want to achieve. In a report, we normally use toFixed and toLocaleString, in setting values (as the 0-127 range) we can live with round.
I did admit that I was not aware of this universal rounding issue with floating point numbers.
When manipulating a Cubase process value directly. These values are always floating point between 0.0 and 1.0. How do you take care of it in these cases since we don’t know how many decimals the process value has? Multiply and divide by a million?
In a scenario similar to this for example.
MyEncoder.knob.mSurfaceValue.mOnProcessValueChange = function (activeDevice, value) {
var newValue = value + 0.01
MyEncoder.PV.setProcessValue(activeDevice, newValue )
//Here, MyEncoder.PV is a "custom value variable"
}
I’m not 100% sure but I believe that these rounding errors are the cause of my jumping parameters.
PS. In my scripts, I’m binding endless encoders, sending relative values, to custom value variables in order to control the resolution the parameter.
You don’t have to do anything special here, since you’re dealing with a customVar. BUT if you want, you can round to 2 digits, since you apply a 0.01 increase. Note that the precision problem usually occurs far away from the point to normally be of any significance.
You mean that the step your parameter changes is unexpected somehow? If that’s the case, you should allow for more digits, for example newVal=val+0.001. In fact you can even apply a trick, by checking the speed of moving your knob and apply different steps accordingly.
sorry to barge into the conversation, but doesn’t the sensible rounding depend entirely on the available range of the parameter that the 0-1 range is being app;ied to?
e.g. if the target of the rounding ends up a CC then, one needs less rounding precision, than if the final target is a Pitchbend.
Jumping parameters can also be caused by latency between receiving a parameter from the MIDI hardrware controller and sending the value from Cubase back out while in “absolute” mode.
I don’t think that’s a good approach. If the process value is 0.12345 I would end up with 0.124. Depending on what parameter you control, that might not be close enough.
Now you’re being ridiculous.
What I think is happening is that a discrepancy occurs when I manipulate the process value and the new value doesn’t really fit in the steps of the process value. Then Cubase corrects the value and a new onProcessValueChange is triggered causing a glitchy jump. I can see in the console that it gets triggered twice.
This is my hypothesis and I have little to back it up with. But there was some discussion on the subject a few months ago but no resolve.
In my case, the final target is a processValue and is basically the raw VST parameter value. For example, in MIDI Remote scripts, I can have one encoder changing a multitude of different parameters using Pages, SubPages or other means. Any and all parameters in Cubase has a 0.0-1.0 process value but internally gets scaled to whatever the end function is. For example, compare the parameter for controlling Channel Volume to one that controls the EQ Type in the channel strip. both of these functions have a processValue and are floating point data types with a range of 0.0-1.0.
maybe my experience has not to do with “rounding errors”, but it could be a reason. In particular i have often a hard time to get to the initial value some parameters have. For example the Send and Cue Send Level. They are at 0.0 as default and if i turn my encoders now, then it is often really hard to get to the 0.0 value again. In theory i should be at 0.0 if i just move a tick back and forth, but i dont end at 0.0 . Same is with Pan settings, it is somehow annoying.
I really wonder why the value changes behave like this. I dont experience this with the MCU protocol.
Hey u-man! Long time no speak.
I haven’t run into that problem I’m afraid.
I don’t know what your script looks like but in my script I change the processValue whenever the encoder sends a value. I then have to check to make sure that I don’t set the process value higher than 1.0 or lower than 0.0. I do that with Math.min() and Math.max(). That way the process value will reach 0.0 and 1.0 but never exceed those numbers no matter how far I turn the encoder. I do this in the onProcessValueChange callback routine.
I dont have the problem to reach the min. and max. values (or to exceed them) with my encoders, this does not happen or is the problem. I have a problem to reach the default values and the default is in between the max. and min. values. Did you really try my examples? If that works for you, then the problem must be inside of my (or Martin Jirsak´s) script and i apologize.
I see now. I misunderstood when I read Send value at 0.0, thinking it was the minimum value.
I think you can overcome that by adjusting the scaling factor.
Just what i wrote before: try to change the default values of the Send and Cue Send levels. Then try to get to the default values again (which are 0.0). You would do me a favor and so i thank you in advance.
@u-man I did some quick testing and I did not have an issue reaching any value when mapping to a Send.
I can go from -∞ to 6.02 and anywhere in between, including 0.0. I only tested with regular Sends as I’m sure it’ll be the same with Cue Sends.
As a matter of fact, there seems to be a “catch range” around 0.0. I noticed I had to turn my encoder a bit extra to get out of 0.0. That makes it extra odd that you can’t seem to reach 0.0 on Sends.
I’ve done a bit more testing and I think I can rule out any rounding errors as the cause of my issue. From what I gathered, the culprit seems to be when I try to capture the value from the Host either by capturing the value (arg3) from mOnValueChange or if I use getProcessValue in the callback mOnProcessValueChange.
I started a new topic where I hope I’m explaining myself a little bit better. Thank you!
I think i have not explained my problem good enough. I can reach the default value of 0.0 too, but it is not easy. I need to turn the encoder back and forth a LOT sometimes, just to reach the default again. Again, in my case it is like this: If i move from the default just one step to the right (or left) and move one step back again, then i am not at the default value again, which should be the normal case. I am slightly away from that default, which is annoying. To my understanding this should not happen, if the steps are equal, but this is not my observation.
FYI i am moving in a 1-127 range (7bit), maybe you are moving in a wider range and therefore it is easier for you to reach the default value again.
I did not experience this at all. I can be at a 0.0 value for a Send Gain, turn the encoder one click at a time until the value changes and then back again and I’m at 0.0 again consistently.
I tried with a number of different resolutions including 7-bits. I could always reach 0.0 as expected.
Again, I noticed that 0.0 seems to have a “catch range”. Meaning I have to turn the encoder a few extra clicks to go from 0.0 to one value lower/higher. Perhaps that feature is not as noticeable with a 7-bit resolution, I’m not sure.
Ok, thank you for testing, mlindeb.
So i guess, there is something wrong on my side and i have another bug to report for Martin .
I am also not experiencing your mentioned “catch range”, which would make sense.