Midi Remote - Scripting Questions

It would be awesome to get a dedicated area to ask questions and share knowledge regarding Midi Remote scripting development! Until then, I thought it would be a good idea to have a dedicated thread for those kinds of questions, unless there is any objection to that.

So, feel free to post your questions or insights regarding the scripting language here!

2 Likes

In case someone else might stumble over the setup of Visual Studio Code, here is what is meant by this sentence in the very beginning (2.):

  1. Make sure the folder of the Visual Studio Code executable is added to the PATH environment variable.

Just refer to this link and it should be more understandable:

1 Like

Iā€™m tryng to create custom scripts for my devices. I already created some of them with wizard creator and it worked very well fore some programmable devices (f.i. Behringer BCR2000) , but now Iā€™m stuck with real js scriptingā€¦

Following the MIDI Remote API guide I created a .js script inside /local folder but Cubase refuses to see it so the device is not detected. What am I doing wrong?

I see that script import button works only with .midiremote (I think it relates to scripts created via wizard creator).

The folder structure must be:
vendor/model/vendor_model.js

2 Likes

Yeah! It worked!

2 Likes

New Question.

Say I have a controller with a single knob, and an LED that can be Amber, Red, or Green.

I want to use that knob to control a given setting. for this example we can choose selected track Pre Gain.

The knob is configured to be in pickup mode.

When the state of the knob is more than the state of the selected Tracks Pre Gain, I want the LED to be Red, when it is less, Amber, and when the same, Green. That way the user can see whether they would need to turn the knob left or right to puck up, or if the change would be immediate.

My hardware is capable of changing the colour of the LED if I pass it a specific note with a given velocity. I can send this message to the controller and the light changes.

How would I write this One-Knob script?

How do I save the current value of the knob when there is no device facing event or callback?

All callbacks seem to come from the host.

Is there any way to get the value of the knob so that I can compare it to the newValue?

The Page might have changed.
The value might have been changed with the mouse, (or automation.)
The Track might have change.
The Knob might have been moved.

this.knob.mSurfaceValue.mOnProcessValueChange = function (context, newValue, oldValue) 
{
   if (?????????????????????????????????????) 
   {
       this.midiOutput.sendMidi(context, this.Color['Red'])
   } 
   else if (?????????????????????????????????????) 
   {
       this.midiOutput.sendMidi(context, this.Color['Amber'])
   }   
   else 
   {
       this.midiOutput.sendMidi(context, this.Color['Green'])
   }
}

The name mSurfaceValue sounds like it is referring to the knobā€™s position, but it is actually referring to the value within the host.

What is the oldValue? It isnā€™t the value of the knob itā€™self as I thought it might be.

It seems to be the old value that the currently assigned parameter had in the host before it changed. Why do I need this? Why would I ever need that information?

A callback like

this.knob.mDeviceValue.mOnProcessValueChange = function(context, value)
{
    this.setSavedKnobValue(value)
}

Would allow the conditional above to be used.
But the ā€œSurfaceā€ (which seems to refer to the device. at least it does in the readme) should be storing this value shouldnā€™t it? So maybe just adding the parameter like this:

this.knob.mSurfaceValue.mOnProcessValueChange = function (context, newValue, oldValue, lastSentValue) ...

You can store the old value in a map yourself. Like a key/value pair.

var map = {}; // global

map[ā€˜Gainā€™] = value

If (map[ā€˜Gainā€™] < value) {}

Update the map only when the user has changed the value. See my CC121 script. There I detect this to enable e.g. the low cut only when the user has changed the low cut frequency (not when e.g. switching channels or mapping pages). Does also work when the user changes the frequency with the mouseā€¦

Of course, this works reliable only when you have turned each knob once, as the current controller state is unknown at the init and therefor the map is empty, or the default values do not match the physical state.

Make sure that the element ā€˜Gainā€™ exists in the map when you try to access it (fetch in the if statement above), otherwise the script terminatesā€¦

Good thing is that the values from mOnProcessValueChange are normalized from 0ā€¦1. So you donā€™t need to know which parameter is assigned to the knob.

Hope this helps.

3 Likes

I am very grateful for your help @MarcoE

But that doesnā€™t work. I donā€™t think I explained it will enough the first time, but I have done some extensive testing on this.

I have looked extensively at your code, and Iā€™m not sure it is doing what you think it is.

The callback is called whenever the value changes in the host. So if you move the knob or fader then it calls the function.

If you didnā€™t move the knob on your controller, everything is fine, but when you move the control on the host, at first it will do as you say, but when you stop, the callback just before you stopped will be very close, and then equivalent. So if you are basing the change on the old value, it will be the same as the new value.

The GUI in the lower pain shows the changes just fine. Whatever event is triggering that could be sent to the controller if they have some reason to not just provide an event when the midi comes in.

Do you get it?

And why do I have these funky Amber outlines on the UI?
I was really annoyed at first, but now I kind of like it. Still wish I could make them go away though.

image

I get your point somehowā€¦I will give it a try with my NI F1. I let you know.
You can enable/disable the Amber outlines at the scripting toolbox in the MIDI Remote lower pane.

Hi @MarcoE , DONā€™T DO THAT!
The global scope of the script has no relation to the ā€œActiveDeviceā€. But you can use the ā€œactiveDevice.setState/getStateā€ methods from within the callbacks or just use the JavaScript feature ā€œFunction.bindā€ which goes like this:

var knob0 = surface.makeKnob(0, 0, 1, 1)
var knob1 = surface.makeKnob(0, 0, 1, 1)

var globalState = {
	whatIsValidAcrossAllKnobs: "what ever",
	lastTouchedKnobIndex: -1
}

var knobState0 = {
	lastValue: 0,
	index: 0
}

var knobState1 = {
	lastValue: 0,
	index: 1
}

knob0.mSurfaceValue.mOnProcessValueChange = function(activeDevice, value) {
	var valueDiff = value - this.knobState.lastValue
	var isRisingAboveZero = valueDiff === value
	if(isRisingAboveZero) {
		// do something
	}
	var isNewTouch = this.knobState.index !== this.globalState.lastTouchedKnobIndex
	if (isNewTouch) {
		// do something
	}
	this.knobState.lastValue = value
	this.globalState.lastTouchedKnobIndex = this.knobState.index
}.bind({
	knobState: knobState0,
	globalState
})

knob1.mSurfaceValue.mOnProcessValueChange = function(activeDevice, value) {
	var valueDiff = value - this.knobState.lastValue
	var isRisingAboveZero = valueDiff === value
	if(isRisingAboveZero) {
		// trigger something
	}
	var isNewTouch = this.knobState.index !== this.globalState.lastTouchedKnobIndex
	if (isNewTouch) {
		// do something
	}
	this.knobState.lastValue = value
	this.globalState.lastTouchedKnobIndex = this.knobState.index
}.bind({
	knobState: knobState1,
	globalState
})

2 Likes

Thank you for that. It is unfortunate that one must employ such techniques in JavaScript.

But that isnā€™t what I want to talk about.

The issue I am having is with capturing information from the API. I have an example with screenshots.

After doing some research and writing a test within my existing code I have found that the oldValue doesnā€™t actually exist. Iā€™ve seen examples of this, and clearly was confused. The callback has no oldValue.

I have multiplied the newValue in the callback by 100 to make it easier to read, and logged it to the console every time the callback is made. I have bound a knob to a fader to make it easier to discuss. I have also fibbed and made myself the ā€œmanufacturerā€ to avoid conflicts and having the code erased.


In this example the ā€œfaderā€ refers to moving the bound value by the computerā€™s mouse. The ā€œknobā€ refers to the actually hardware knob. So Fader is through the UI, Knob is through hardware. Contrary to the expectation in code that I have seen, and discussions that I have been in, this will show that the callback is made whenever the knob is moved, or the fader is moved, but that the actual value is only ever change by the knob on the hardware.

Here is the initial state as the script was loaded. The fader on the left, the UI in the middle and the log of the callback on the right. It starts off with newValue being 0. Note that the UI is aware of the difference between this initial state of the captured value that is presented in the callback, and the state of the fader.


Here I have moved the fader. But not the knob. The callback is called, but the value of newValue stays the same. The UI is aware of the difference and is able to display that difference, but the value sent to the callback has not changed.


Now I have moved the knob down to 0 (underneath the fader, never matching itā€™s position). I then moved it back up again. The callback receives the values of the knob position.


Now I have moved the fader again. The newValue remains the same. The callback is being called, but the value is the same as the last time the knob was moved.


Now I have moved the knob up to ā€œcaptureā€ the fader, and then moved it about. The UI in the middle has responded to this, and is now showing that the values are in synch.


Again I have moved the fader away from the value of the knob. The UI has updated to show the difference between that of the knob and the fader, but the value provided to the callback is still the last known position of the knob.


And again moving the knob in the direction of the fader, but not enough for the pickup to capture.

This means the following.

  1. The newValue is only ever changed by the knob.
  2. Knob movements generate an event for which the callback is executed.
  3. Fader movements generate an even for which the callback is executed.
  4. The UI is capturing different events, than those being provided to the MR callback.

Without a more in-depth discussion of the API, I am not sure whether I have uncovered a defect, or if there is some misunderstanding as to how this is suppose to be used. Either way, I would like to know what the intent is, or how the API might evolve.

Thank you.

Thank you for your detailed descriptions.
I believe you are just thinking a bit too ā€œlow levelā€ here.

The API is really only there to create an abstraction of the hardware it is a midi controller ā€œDevice Driverā€ for Cubase.

The coding ā€œsportsā€ here is to try to use only the ā€œhigh levelā€ features (makeValueBinding, makeActionBinding, SubPageArea etc). And if something really doesnā€™t work that way, then use just a few of the ā€œlow levelā€ features (callbacks, setProcessValue method etc) to reach the goal.

I have the impression you are calling for a ā€œcontrol everything in Cubase APIā€ which is a very reasonable request, but not the purpose of MIDI Remote.

I read and clicked that button so many times, and never understood what it was intended to do. I assumed since my script wasnā€™t running it had no effect. Now I know.

I hope the above example illuminates things a bit. I had originally thought that I was only getting values from the host, when in fact, the values are only ever coming from the device. The exact opposite. The ā€œmissing eventā€ if itā€™s fair for me to call it that, is the one that would come from the host.

Also the fact that the callback is made when the host changes, suggests that there was an intent to do something there. I would have designed the event to provide all 4 values. This is done with distributed systems where the event typically has.

  1. Event - The value has changed from node A (new value, offset from last value sent by node A, ground truth)
  2. Event - The value has changed from node B( new value, offset from last value sent by node A, ground truth)
    ā€¦and so on for all nodes.

In this case there is only the Host and the Device, and the Host value is allways ground truth.
You just need to know the new host value that isnt equivelent to the last value sent by device.

In fact that isnā€™t even needed. All that is needed is the offset.

Event - value device sent, devices offset from Host
Event - value host sent, devices offset from Host

Engage Taskmaster theme earworm.

Iā€™m just in the habit of finding solutions to desired workflows. That often means attempting to understand something at a lower level. Itā€™s also the nature of the neurotype if you know what I mean. If the abstraction is clear then finding solutions does not require a deeper understanding. If the abstraction is not clear, the best way forward is to try and understand the abstraction better. Iā€™m the kid that took all of their toys apparat, but I did it because I didnā€™t understand my toys. Iā€™m not powering up, Iā€™m feeling lost.

I tend to build my own abstraction and standards so that I can create solutions more easily, without brut forcing Anti-patterns, or resorting to code generation. Though I never take code generation completely off the table.

I think this may be giving the wrong impression.

It may not be obvious, but Iā€™m afraid I do not understand this abstraction.
Value, Command, Action what is the intent of these different bindings? I ended up in a coding cul-de-sac with that one earlier. I found myself wishing for a Binding type that had the same interface no matter the flavor.

What is the purpose of having the 3 separate interfaces? What is the difference in the use cases that drives them apart? Maybe just an explanation of this would help.

What is the use case for a SubPageArea that can not be accomplished with a Page? This one has me really confused.

At the moment all I am trying to do is to have visual feedback on the device for when the position on the device is different from ground truth. Itā€™s true that I am struggling with the API. To the point that I feel insecure about it, and feel the need to point out that itā€™s not that I donā€™t have a lot of experience doing this sort of thing. So I am trying to pick it apart to figure out how it works so I can use it correctly.

So with the full intent of humility, how would you implement visual feedback to the LED on the device that tells the user that the value in the host, and the value on the device are different? Or with a device that has motorized faders, how would you move those faders as the value in the host changed through the GUI or automation?

You can register a callback in the valuebinding itself (again, very low level):

page.makeValueBinding(knob.mSurfaceValue, page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mVolume)
		.mOnValueChange = function(activeDevice, activeMapping, value, diffValue) {
			
		}

the ā€œactiveMappingā€ context object is a second context handle related to the currently active mapping page. It can be used to switch between sub pages.

Thanks, Ill give that a try.

Attached find my POC of your idea.

I implemented it on my Traktor Kontrol F1 which also allows to set the colors of the buttons. This does exactly what you want. The POC supports 1 knob only and did not use a map to hold the displayed value (see above). My binding is the Pre Gain of the selected track.

How does it work?

There are 2 events per control/binding: mOnProcessValueChange and mOnDisplayValueChange

mOnProcessValueChange is called whenever the control has changed and mOnDisplayValueChange is called whenever the value on the UI changes. In pickup mode mOnDisplayValueChange is called only when the control has really picked up the value.

When the state of the control doesnā€™t match with the UI value, it is not called until the UI value changes. Which happens when the control reached the corresponding position.

These events are also called when the bound host values change. E.g. by switching mapping pages or the selected channel changes. I use this to hold the target UI value in a global variable. This I compare in the controller event to set the colors accordingly. Unfortunately, the UI value is the exact UI value. Meaning it is a string and in the case of the Pre Gain has the unit as well. E.g. -2.5dB. I convert this to 0.0ā€¦1.0 as the controller event value is always normalized to 0.0ā€¦1.0. This prevents a pure generic implementationā€¦. It would be great to get the normalized value.

@Jochen_Trappe How can we find out what GetState accepts for the keys? What is the key for GetState(ā€˜key) to read the current pre grain value?

Unfortunately, the mOnProcessValueChange is also called when the user changes the UI value. This prevents to update the colors on the controller when the user changes a host value by e.g. the mouse. There is no way to reliably distinguish between a controller event and a event trigged by the user in the UI.

Hope this helps.
Native Instruments_F1.zip (1.9 KB)

I looked into this again. Helpful function.

I noticed that the event onDisplayValueChange is not called when the user makes a change on the UI by the mouse. But onProcessValueChange is called with the value of the state of the control. Which of course keeps the same as the user does not change controller, but the UI element.

I donā€™t get this. For me it would be more logical to call the onDisplayValueChange event with the updated display value and not calling the onProcessValueChange at all. Hence there the value keeps the same anyway. @Jochen_Trappe : is this intentional?
This prevents to update the controller surface with changes the user made directly in the UI.

Here with a similar intent and finding this screed I want to say thank you for scribbling down so succinctly, along your path to understanding, an unexpected, validating description of me.