MIDI Remote - Defects & Lesson Learned

The comments at the end of this code describe testing.
Highlights are:

To work around the issue with moving usb ports changing the name use

    .expectOutputNameEndsWith(expectedName)
    .expectInputNameEndsWith(expectedName);

ValueChange → encoder is moving (and by how much).

ProcesValueChange with value = previous value → host changed in some unknown way or direction. (it could still be the same as the encoder)

ProcesValueChange with value != previous value → encoder moved.

DisplayValueChange → value is different than it use to be in the host.

There is no Host Changed Event!


const expectedName = "Launch Control XL"; // The end of the name of the device.
const CC = 0x0D; // CC the encoder is on
const MIDIChannel = 0x0F; // midi channel the encoder is on

var midiremote_api_v1_1 = require("midiremote_api_v1");
var deviceDriver = midiremote_api_v1_1.makeDeviceDriver('DeviceName', expectedName, 'MyName');
var MIDIIn = deviceDriver.mPorts.makeMidiInput();
var MIDIOut = deviceDriver.mPorts.makeMidiOutput();

// We have to use Ends With instead of equals to solve multi port multi device issue.
// Even with one device, if you plug it in to a different USB port on windows
// it will have a different name.
deviceDriver
    .makeDetectionUnit()
    .detectPortPair(MIDIIn, MIDIOut)
    .expectOutputNameEndsWith(expectedName)
    .expectInputNameEndsWith(expectedName);

// Register for on active event
deviceDriver.mOnActivate = function (activeDevice) {
    console.log('Launch Control XL Initialized');
}


var surface = deviceDriver.mSurface;
var knob = surface.makeKnob(0, 0, 2, 2);
knob.mSurfaceValue.mMidiBinding
    .setInputPort(MIDIIn)
    .setOutputPort(MIDIOut)
    .bindToControlChange(MIDIChannel, CC)
var bindable = knob.mSurfaceValue;

// Page 1
var page = deviceDriver.mMapping.makePage("TEST");

var valueBinding = page.makeValueBinding(bindable, page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mVolume)
valueBinding.setValueTakeOverModePickup();

// Page 2
var page2 = deviceDriver.mMapping.makePage("TEST2");

var valueBinding2 = page2.makeValueBinding(bindable, page2.mHostAccess.mTrackSelection.mMixerChannel.mPreFilter.mGain)
valueBinding2.setValueTakeOverModePickup();
//valueBinding2.mapToValue(1);

// testing events

// Notice this is registering with the binding. It is only called when the binding's page is active

// Is NOT register for host event
// This callback is ONLY called every time the encoder is changed.
// It's value will refect the position of the Encoder, NOT the value in the Host.
// Is NOT called when changing tracks etc.
// Processing placed in this event can (and likely will) freez cubase until return!
// Not Useful!!!!!!!!!!!!!!! Don't do this it slows things down.
//
// var savedHostValue = 0;
// valueBinding2.mOnValueChange = 
// function(activeDevice, activeMapping, value, diffValue) {
//     console.log("==== valueBinding.mOnValueChange ====")  
//     console.log("----                 value: " + value)
//     console.log("----            diffValue: " + diffValue)
//     console.log("---- savedHostValue: " + savedHostValue);
//     // churn(50)
//     // console.log("=====================================")
//     savedHostValue = value;
// }

// Notice that these are registering with the encoder "surface" bindable

// Is Not register for host display event
// This callback is called every time the encoder is changed AND the value in the Host's display has changed
// This is the display value from the host, but only called when the Encoder is touched or moved, and only 
// The first time it is moved once the value in the host has changed.
// This means that if the encoder picks up the host, it will be called continuously.
// Is called when changing tracks etc.
// Processing placed in this event can freez cubase until return!
var savedHostDisplayValue = 0;
bindable.mOnDisplayValueChange = 
function(activeDevice, valueString) {
    console.log(":::: bindable.mOnDisplayValueChange ::::")
    console.log("----                    valueString: " + valueString)
    console.log("---- savedHostDisplayValue: " + savedHostDisplayValue);
    // churn(50)
    // console.log("::::::::::::::::::::::::::::::::::::::::")
    savedHostDisplayValue = valueString;
}

// Is NOT register for Encoder Event
// This callback is called every time the Host OR Encoder is changed.
// It's value will refect the position of the Encoder. Even though it is called
// when the host changes it is stil always the last known value of the Encoder.
// Is called when changing tracks, etc.
// Processing placed in this event can freez cubase until return!
var savedEncoderValue = 0;
bindable.mOnProcessValueChange = 
function(activeDevice, value) {
    console.log("[[[[ bindable.mOnProcessValueChange ]]]]")
    console.log("----                       value: " + value)
    console.log("---- savedEncoderValue: " + savedEncoderValue);
    // churn(50);
    // console.log("[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]")
    savedEncoderValue = value;
}

// little primes function to create churn for testing where computation will slow cubase down.
function churn(iterations) {
    var primes = [];
    for (var i = 0; i < iterations; i++) {
      var candidate = i * (1000000000 * Math.random());
      var isPrime = true;
      for (var c = 2; c <= Math.sqrt(candidate); ++c) {
        if (candidate % c === 0) {
            // not prime
            isPrime = false;
            break;
         }
      }
      if (isPrime) {
        primes.push(candidate);
      }
    }
    return primes;
  }


// Value Binding ONLY methods.
// -----------------------------------------------------------------------------------------------
// setTypeDefault () : this
//      Whatever Cubase thinks the default setting should be. (Usualy Jump)
// setTypeToggle () : this
//      Every time a non 0 value is sent to Cubase it results in the value of 0 or 127. (0 or 1)
// setValueTakeOverModeJump () : this
//      Jumps the Host value when the knob is touched to the value of the encoder
// setValueTakeOverModePickup () : this
//     The Host value is only changed when the encoder crosses the Host value.
// setValueTakeOverModeScaled () : this
//     Changes the value SENT to Cubase between scaled to the difference until it picks up.
//     Aguable more intuitive that pickup mode, but the granularity of movement of the knob
//     is dependent on the difference in the Host value and the encoder value.

// Value Binding, Command binding, & Action Binding methods
// -----------------------------------------------------------------------------------------------
// setSubPage (subPage : SubPage) : this
//      Sets the sub page in which the binding is active, like making it active on a different page.
// filterByValue (filterValue : number) : this
//      Seems to do the same as mapToValue, only, it doesn't seem to send the value. IDK?
// filterByValueRange (from : number, to : number) : this
//      Simmilare to MapTovalueRange but differes in the value SENT to Cubase
//      is only effective in the range provided. Pickup mode is disabled.
// mapToValue (mapValue : number) : this
//      Makes only one single value SENT to Cubase.
//      Likely intended to be used with a button.
// mapToValueRange (from : number, to : number) : this
//      Changes the value SENT to Cubase does not change the value in the events.
//      Can really only be used to limit the range.
//      Doing so will cause Cubase UI to stutter.


// ValueChange -> encoder is moving (and by how much).

// ProcesValueChange with value = previous value -> host changed in some unknown way or direction. 
//       (it could still be the same as the encoder)

// ProcesValueChange with value != previous value -> encoder moved.

// DisplayValueChange -> value is different than it use to be in the host.

// There is no Host Changed Event!


1 Like

use this one:

page.makeValueBinding(surfaceElements.knob1.mSurfaceValue, hostSelectedTrackChannel.mPreFilter.mGain).setValueTakeOverModePickup().mOnValueChange = function(activeDevice, activeMapping, value, diffValue) {

it is directly on the hw control and only called by touching the encoder. It includes the diff to the last call.
I use it in the CC121 script to detect left and right turns of the encoders to trigger zoom in and out.

I got this from @Jochen_Trappe in your thread: Midi Remote - Scripting Questions

1 Like

List of Known Defects so far:

(1)
Write and Read through key commands are broken for all track types.
Not technically an MR thing. But hinders the building of MRs
Screenshot 2022-03-15 211949

[page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mAutomationWrite]
does work, but not for midi tracks (see 2).

(2)
Track values do not work for MIDI tracks
page.mHostAccess.mTrackSelection.mMixerChannel.mValue.*
such as:
mMute
mSolo
mRecordEnable

(3)
Command “Exit” - “Solo”, “Mute” etc.
DO work for MIDI tracks but do NOT provide feedback.
i.e. The button works, but you have to look at the screen to see that it worked.

This can be worked around by making the button a toggle button in the device, but it is then a toggle button for all MR pages.

Write and read do not work (see 1)

(4)
There is no feedback for Value bindings. This means motorized faders are impossible to script for. (See code above for testing of this.)

(5)
Track Quick Controls do not work on MIDI tracks.

(6)
Reload Scripts key command not functioning.

(7)
Pages created with previous scripts or no-script settings through the UI. Show up even though the previous script has been disabled.
And there is no way to rearrange the order of pages once created through the UI.

2 Likes

Methods documentation


Value Binding ONLY methods.


setTypeDefault() : this

Whatever Cubase thinks the default setting should be. (Usualy Jump)

setTypeToggle () : this

Every time a non 0 value is sent to Cubase it results in 
the value of 0 or 127. (0 or 1)

setValueTakeOverModeJump () : this

Jumps the Host value when the knob is touched to the value of the encoder

setValueTakeOverModePickup () : this

The Host value is only changed when the encoder crosses the Host value.

setValueTakeOverModeScaled () : this

Changes the value SENT to Cubase between scaled to the difference 
until it picks up. Arguably more intuitive that pickup mode, but the 
granularity of movement of the knob is dependent on the difference 
in the Host value and the encoder value.

Value Binding, Command binding, & Action Binding methods


setSubPage (subPage : SubPage) : this

Sets the sub page in which the binding is active, like making it 
active on a different page.

filterByValue (filterValue : number) : this

Seems to do the same as mapToValue, only, it doesn't seem t
o send the value. IDK?

filterByValueRange (from : number, to : number) : this

Simmilare to MapTovalueRange but differes in the value SENT to Cubase
is only effective in the range provided. Pickup mode is disabled.

mapToValue (mapValue : number) : this

Makes only one single value SENT to Cubase.
Likely intended to be used with a button.

mapToValueRange (from : number, to : number) : this

Changes the value SENT to Cubase does not change the value in the events.
Can really only be used to limit the range.
Doing so imporperly will cause Cubase UI to stutter.
1 Like

@MarcoE

That is covered in comments in the exploratory script above. See the list of known defects listed above. Primarily (4). There is no way to script for continuous encoders with position lights, encoders with colour coded leds ,or motorized faders because there is no way to get the Host value from Cubase.

I’ll send you my full code soon, or post it… It has a workaround that shows “pickup not captured” indication like an older Korg would do. They would light an LED when in “pickup not captured”. This solution dims the LED when “pickup not captured” or “unknown”.

Your earlier clever solution using display value had a few edge conditions. The primary one is, (as indicated above, and also by you I think) that display value event is only provided when the encoder sends information. The only edge condition I have found for the solution is that occasionally when one clicks, rather than moves, the value, it doesn’t immediately update.

I’m not quite done with the code yet, but this Controller is working.

Here are the pertinent methods from the Controller. It works for all three modes.

 
    handleTitleChange(activeDevice, value, units) {
        this.hostChangedWhenEncoderDidnt = 2;
        this.eventHostDifferent(activeDevice, value);
    }

    handleDisplayValueChanged(activeDevice, valueString){
        this.displayValue = valueString;
        if( this.hostChangedWhenEncoderDidnt == 0 ) {
            this.eventHostLikelySame(activeDevice, valueString, this.encoderPosition);
        }
        
    }

    handleProcessValueChanged(activeDevice, value){
        var oldValue = this.encoderPosition;
        this.encoderPosition = value;

        if( oldValue == value ) {

            if( this.hostChangedWhenEncoderDidnt > 2 ) {
                this.eventHostDifferent(activeDevice, value);
            } 
            this.hostChangedWhenEncoderDidnt++;
        } else 
            this.hostChangedWhenEncoderDidnt = 0;

            if( this.registerEventsEncoder ) {

                var encoderChange = value - oldValue;
                if( this.evalEncoderSensitivity(encoderChange) ) {               
                    this.eventEncoderChanged(activeDevice, value, encoderChange);
            }
        }      
    }

    eventHostLikelySame(activeDevice, valueString, value) {
        this.encoder.setColour(activeDevice, this.colour.high)
    }

    eventHostDifferent(activeDevice, value) {
        this.encoder.setColour(activeDevice, this.colour.low);
    }

It makes a lot more sense when you see it all at once like this

var activeDevice

page.mAction.mActivate.trigger(activeDevice)

page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mBypassInserts
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mLevelValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mMetronomeClickActiveValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mMetronomeClickLevelValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mMetronomeClickPanValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mMuteValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mSelectSourceAuxValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mSelectSourceExternalInputValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mSelectSourceMonitorMixValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mTalkbackEnabledValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mTalkbackLevelValue

page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mBypassInserts
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mLevelValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mMetronomeClickActiveValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mMetronomeClickLevelValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mMetronomeClickPanValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mMuteValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mSelectSourceAuxValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mSelectSourceExternalInputValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mSelectSourceMonitorMixValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mTalkbackEnabledValue
page.mHostAccess.mControlRoom.getCueChannelByIndex(0).mTalkbackLevelValue

page.mHostAccess.mControlRoom.getExternalInputChannelByIndex(0).mBypassInserts
page.mHostAccess.mControlRoom.getExternalInputChannelByIndex(0).mLevelValue
page.mHostAccess.mControlRoom.getExternalInputChannelByIndex(0).mMuteValue

page.mHostAccess.mControlRoom.getMaxCueChannels()
page.mHostAccess.mControlRoom.getMaxExternalInputChannels()
page.mHostAccess.mControlRoom.getMaxMonitorChannels()
page.mHostAccess.mControlRoom.getMaxPhonesChannels()
page.mHostAccess.mControlRoom.getMaxTalkbackChannels()

page.mHostAccess.mControlRoom.getMonitorChannelByIndex(0).mBypassInserts
page.mHostAccess.mControlRoom.getMonitorChannelByIndex(0).mLevelValue
page.mHostAccess.mControlRoom.getMonitorChannelByIndex(0).mMuteValue

page.mHostAccess.mControlRoom.getPhonesChannelByIndex(0).getSelectSourceCueValueByIndex(0)

page.mHostAccess.mControlRoom.getSelectSourceExternalInputValueByIndex(0)
page.mHostAccess.mControlRoom.getSelectTargetMonitorValueByIndex(0)

page.mHostAccess.mControlRoom.getTalkbackChannelByIndex(0).mBypassInserts
page.mHostAccess.mControlRoom.getTalkbackChannelByIndex(0).mLevelValue
page.mHostAccess.mControlRoom.getTalkbackChannelByIndex(0).mMuteValue

page.mHostAccess.mFocusedQuickControls.getByIndex(0)
page.mHostAccess.mFocusedQuickControls.mFocusLockedValue
page.mHostAccess.mFocusedQuickControls.getSize()

var makeMixerBankZone = page.mHostAccess.mMixConsole.makeMixerBankZone("name")
makeMixerBankZone.mAction.mNextBank
makeMixerBankZone.mAction.mPrevBank
makeMixerBankZone.mAction.mResetBank
makeMixerBankZone.mAction.mShiftLeft
makeMixerBankZone.mAction.mShiftRight
makeMixerBankZone.excludeAudioChannels()
makeMixerBankZone.excludeFXChannels()
makeMixerBankZone.excludeGroupChannels()
makeMixerBankZone.excludeInputChannels()
makeMixerBankZone.excludeInstrumentChannels()
makeMixerBankZone.excludeMIDIChannels()
makeMixerBankZone.excludeOutputChannels()
makeMixerBankZone.excludeSamplerChannels()
makeMixerBankZone.excludeVCAChannels()
makeMixerBankZone.excludeWindowZoneLeftChannels()
makeMixerBankZone.excludeWindowZoneRightChannels()
makeMixerBankZone.includeAudioChannels() // same as above for all includes
makeMixerBankZone.setFollowVisibility(true)
makeMixerBankZone.makeMixerBankChannel()

var subPageArea = page.makeSubPageArea("name")
subPageArea.mAction.mNext
subPageArea.mAction.mPrev
subPageArea.mAction.mReset

var subPage = subPageArea.makeSubPage("name")
subPage.mAction.mActivate

page.mHostAccess.mMouseCursor.mValueUnderMouse
page.mHostAccess.mMouseCursor.mValueLocked

page.mHostAccess.mTrackSelection.mAction.mNextTrack
page.mHostAccess.mTrackSelection.mAction.mPrevTrack

page.mHostAccess.mTrackSelection.mMixerChannel.mChannelEQ.mBand1 // 2,3,4
page.mHostAccess.mTrackSelection.mMixerChannel.mChannelEQ.mBand1.mFilterType
page.mHostAccess.mTrackSelection.mMixerChannel.mChannelEQ.mBand1.mFreq
page.mHostAccess.mTrackSelection.mMixerChannel.mChannelEQ.mBand1.mGain
page.mHostAccess.mTrackSelection.mMixerChannel.mChannelEQ.mBand1.mOn
page.mHostAccess.mTrackSelection.mMixerChannel.mChannelEQ.mBand1.mQ

page.mHostAccess.mTrackSelection.mMixerChannel.mPreFilter.mBypass
page.mHostAccess.mTrackSelection.mMixerChannel.mPreFilter.mGain
page.mHostAccess.mTrackSelection.mMixerChannel.mPreFilter.mHighCutFreq
page.mHostAccess.mTrackSelection.mMixerChannel.mPreFilter.mHighCutOn
page.mHostAccess.mTrackSelection.mMixerChannel.mPreFilter.mHighCutSlope
page.mHostAccess.mTrackSelection.mMixerChannel.mPreFilter.mLowCutFreq
page.mHostAccess.mTrackSelection.mMixerChannel.mPreFilter.mLowCutOn
page.mHostAccess.mTrackSelection.mMixerChannel.mPreFilter.mLowCutSlope
page.mHostAccess.mTrackSelection.mMixerChannel.mPreFilter.mPhaseSwitch

page.mHostAccess.mTrackSelection.mMixerChannel.mQuickControls.getByIndex(0)

page.mHostAccess.mTrackSelection.mMixerChannel.mSends.getSize()
page.mHostAccess.mTrackSelection.mMixerChannel.mSends.getByIndex(0).mLevel
page.mHostAccess.mTrackSelection.mMixerChannel.mSends.getByIndex(0).mOn
page.mHostAccess.mTrackSelection.mMixerChannel.mSends.getByIndex(0).mPrePost

page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mAutomationRead
page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mAutomationWrite
page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mEditorOpen
page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mInstrumentOpen
page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mMonitorEnable
page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mMute
page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mPan
page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mRecordEnable
page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mSelected
page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mSolo
page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mVUMeter
page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mVUMeterClip

page.mHostAccess.mTransport.mValue.mCycleActive
page.mHostAccess.mTransport.mValue.mForward
page.mHostAccess.mTransport.mValue.mMetronomeActive
page.mHostAccess.mTransport.mValue.mMetronomeClickLevel
page.mHostAccess.mTransport.mValue.mRecord
page.mHostAccess.mTransport.mValue.mRewind
page.mHostAccess.mTransport.mValue.mStart
page.mHostAccess.mTransport.mValue.mStop
page.mHostAccess.mTransport.mValue

var surfaceValue
page.makeCommandBinding(surfaceValue, "commandCategory", "commandName")

var x, y, w, h;

deviceDriver.mSurface.makeBlindPanel(x, y, w, h)
deviceDriver.mSurface.makeButton(x, y, w, h)
deviceDriver.mSurface.makeControlLayerZone("name")
deviceDriver.mSurface.makeCustomValueVariable("name")
deviceDriver.mSurface.makeFader(x, y, w, h)
deviceDriver.mSurface.makeJoyStickXY(x, y, w, h)
deviceDriver.mSurface.makeFader(x, y, w, h)
deviceDriver.mSurface.makeKnob(x, y, w, h)
deviceDriver.mSurface.makeFader(x, y, w, h)
deviceDriver.mSurface.makeLabelField(x, y, w, h)
deviceDriver.mSurface.makeLamp(x, y, w, h)
deviceDriver.mSurface.makeModWheel(x, y, w, h)
deviceDriver.mSurface.makePadXY(x, y, w, h)
deviceDriver.mSurface.makePianoKeys(x, y, w, h, 0, 0)
3 Likes

please do not repetetively use the same “getCueChannelByIndex” with the same index. Use a variable instead. That will perform much better.

It’s just most, if not all of the options laid out on one page so it is easier to find what you are looking for. The index is superfluous.

1 Like

thanks for making that listing - I find it very useful for learning to have it represented this way, because it makes it easier to figure out which object parent tree to look for when adding a child

1 Like

You are welcome. I missed a few, probably because I already knew them by heart at that point.

1 Like

Hi @oqion , understood! :+1:

1 Like

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

This is the biggest issue I’ve had with the API.

From what I can see, There exists no method to disable those callbacks, and initiate new calls either, So i guess the ‘fix’ to have a callbacks that are page aware is to bind global variables that define the current page state?

I’ve not done anything with the API for a few days, as decided to go back to making music! :slight_smile:

1 Like

Look at the Encoder class in the TS. It solves this problem by only binding once, and then looking up the correct functor to call. And it’s not global. The Page class resets it’s Controllers, which inform the bound Encoder what the current page is.

I don’t understand why the MR_ API has to have the “page” everywhere, it takes a different slice from the design possibilities than I would have imagined.

I noticed @pettor has taken a different ES5 style approach, and isn’t bound to a device, but has the same issue with the callbacks.

I prefer TypeScript, as it is a much more strait forward Class definition. I have spent way too much time trying to figure out the import issue though.

I wanted to get a couple of devices done before starting to abstract the Controllers to be generically available, so I tackled the device I actually use. I like the way Pettor is thinking though, and we have some similarities.

@pettor, do you think you could help figure out this import issue, so I don’t have to write everything in one file? And so we could be more descriptive in TypeScript rather than struggling through ES5?

Also, would you like to collaborate on Controllers?

Ah yes, callbacks and imports were overall difficult to work with in many ways. Personally not a fan of ES5 and I work daily with Typescript so I would have much preferred that. I was actually looking into ways of transpiling Typescript into ES5 to go around the issue.

One big issue that I have still is working with imports over multiple files will give an error like “undefined on line 37” not referring to the actual script but in some other external file of my lib. So anytime Cubase didn’t like the code I wrote it was painful to actually locate the issue.

Now @oqion what was the import issue you had? I scrolled quickly but didn’t find which one you referred to. I had many import issues so maybe I can help :smile:

1 Like

One lesson learned or maybe just a tip is to use a symlink from Cubase Device Scripts to the src folder of the Github repo. Basically that’s how I develop my lib.

Since I use Windows for Cubase I used New-Item -ItemType Junction -Path "\Driver Scripts\.lib\some-name" -Target "\github-repo\" from Powershell. This works quite fine actually and also the reason why I made the root the way I did. I wanted to make it into a module like the API is (by defining a package with main etc) but Cubase couldn’t load this. So that would be cooler in the future.

1 Like

Also in the jsconfig.json in the Driver Scripts root the config is defined with:

{
    "paths": {
      ".": [".src"],
      "midiremote_api_v1": [".api/v1"]
    },

I really wanted to use my own lib in a similar way but this config is rewritten everytime scripts are reloaded.

1 Like

Man thanks!

look at this part of the code


// <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
// 62: Object.defineProperty(exports, "__esModule", { value: true }); // <-- Delete this line above /\/\/\/\
// <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>

// ============================================================================================


//-----------------------------------------------------------------------------
// 0. DRIVER SETUP - create driver object and detection information
//-----------------------------------------------------------------------------

// get the api's entry point - This like will show as an error, but still works
import midiremote_api from 'midiremote_api_v1';

const expectedName = "Launch Control XL";

// create the device driver main object

// <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
// The preceding Line needs to be edited:
// 81 : var deviceDriver = midiremote_api_v1_1.default.makeDeviceDriver('Novation', expectedName, 'Oqion'); // <- change this
// 81 : var deviceDriver = midiremote_api_v1_1.makeDeviceDriver('Novation', expectedName, 'Oqion'); // <- to this
// <><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>

const deviceDriver = midiremote_api.makeDeviceDriver(
    'Novation',
     expectedName,
    'Oqion'
  );

//-----------------------------------------------------------------------------
//  Macros

// Does it log? Can't seem to attache the debuger. Since we still have the issue
// with the imports the console here reduces the error to one place in the code.
var doesLog = true;

var LOG = function(logString)
{
    if(doesLog){
        // this line will show as an error. But still works.
        console.log(logString)
    }
}

Do you see what I am doing? Every time I run the transpiler I have to go delete line 62 and edit line 81.

This also cascades though any other files I create. So all of the classes are in the same file. This slowed me down a lot. I really don’t understand JS import system.

I have spent so much time trying to figure this out, this was as close as I could get, but I don’t want to have to do it in multiple files, so I just have one.

It doesn’t look like the export from midiremote_api_v1 is done in the right way, but there is also like, so many ways to do it, I can’t keep it strait.

Hrmmmm that’s a rough one. Honestly never worked with ES5 before Midi Remote but I would start trying out different settings. Using ES5 lib and target in tsconfig does sound like it should be enough but maybe there’s even older targets or other libs to try. I would also try to use Babel to transpile the code into ES5 just to see if that works better (@babel/preset-typescript · Babel).

Some other random thoughts about the issue. Have you tried using var api = require(....) in the ts instead of import? I find it strange that the output is midiremote_api_v1_1.default.makeDeviceDriver because that’s just pure wrong. Default doesn’t have the makeDeviceDriver and why is this even appended. Must be something silly :smile:

In regards to the even weirder Object.defineProperty(...) I at least found this:

Not any solution as such but maybe possible to get something interesting out of it. Personally I think tinkering with transpiler settings should fix this. But all that would be easier if only we knew the details of Cubase JS engine. Or why it doesn’t support anything newer than ES5 :smile: