MIDI Remote - Defects & Lesson Learned

The way to think about the architecture:

MIDI goes DIRECTLY to Cubase. Think of your script as a configuration to tell Cubase what to do with those messages. I have no way of knowing for sure this is how it works, but it IS how it behaves.

You also get to insert a callback to receive events from Cubase.

These are one per element!!!

Element = (knob, button, fader).

You can put whatever code you want in the callback, but you only get one, no matter what page you are on.

The 3 functors (i.e. callbacks) you can provide respond to those events.

mOnDisplayValueChange: When the text that is displayed in Cubase changes this functor is called.

mOnTitleChange: When the title of whatever the element is bound to changes this functor is called.

mOnProcessValueChange: When the actual value (0 to 1) of the bound value changes this functor is called.

  • HOWEVER → it is called whether you move the encoder on the device or the bound value in Cubase changes. What is more if the value in changed in Cubase, the “value” field of this functor is returned with the last SENT value from the encoder. If the encoder actually moved the values are different.

I have a suspicion that this behavior with mOnProcessValueChange was not intentional. It absolutely would simplify things if it were only called when Cubase is receiving data from the device.

It would also be beneficial if it were split into the following.

mOnDisplayValueChange: When the text that is displayed in Cubase changes this functor is called.

mOnTitleChange: When the title of whatever the element is bound to changes this functor is called.

[ mOnHostValueChange ]: When the actual value (0 to 1) of the bound value changes and it did not come from the device, this functor is called.

[ mOnEncoderValueChange ]: When the value (0 to 1) of the encoder value sent to Cubase changes and it did not come from Cubase, this functor is called.

  1. The expectation is:
    Device → Script → Host
    Host → Script → Device

  2. But the reality is:
    Device → Host → Script
    Host → Script → Device

Of course (1) would always actually be like (2) but putting it this way represents the fact that you don’t get to touch the messages coming from the device before the host acts on those messages. It’s not like a MIDI Transformer that you would get in other systems. You only get notified.

However, that notification seems to be in-line, in the thread prior to the change of ground-truth. Meaning that if you move a fader in the Cubase UI, the script functor is called before the value in Cubase of the fader actually changes. So if you put, say, a prime number computation in the functor it will cause Cubase tohang waiting for the computation in your script to complete.

The model in (1) is a much better choice than the model in (2) for anything real-time. It could be far superior to that in other systems iff the functor was called on a separate thread! i.e. If Cubase never had to wait on the script computation at all.

Ok back to the mOnEncoderValueChange idea:

You can simulate this fictionality of [ mOnEncoderValueChange ] to some extent by saving the last value from mOnProcessValueChange and checking if it is the same, and doing nothing if it is. This speeds things up a lot!!! But it has an edge condition in that sometimes, when you send data from an encoder it happens to also be the same value as the last time, especially at the end of the movement. So you need to identify when this has happened more than once, and linking this to the mOnDisplayValueChange.

Another thing to be aware of, as mentioned, is that these callbacks are not per-page. They are per-element only!!! So if you set mOn * more than once you are overwriting the callback globally! If you want to do something different depending on which page you are on, then you need to only provide this functor once, and to check in that functor to see which page you are on before you do the thing.

I think it that could look something procedurally like this, but I never tried it.

element.mSurfaceValue.mOnProcessValueChange = function(activeDevice, value) {
    if(currentPage == "page1") {
        // do something for page1
    } else if (currentPage == "page2") {
        // do something for page2
    }...
}.bind(currentPage)

page1.mOnActivate = function (activeDevice) {
           currentPage = "page1" ;
 }.bind(currentPage);

I found this really all VERY difficult to keep track of, so I used TypeScript and a wee bit of OOD to manage it. Ironically I was prototyping, and haven’t used JS in years, have had no experience with ES5, and had no idea what I was doing, so I used "Type"Script with no “types”. I am not sure whether or not to be embarrassed about this, but the code is here.

5 Likes