Problems with Cubase Midi Remote and endless Encoders

Hello, I’ve been the proud owner of an Electra mini controller for a week now. Finally, a controller with a large display, multiple pages, and endless encoders!

I intend to use it exclusively for controlling my VST instruments in Cubase. This works fine so far; I’ve created interfaces on the Electra controller, created a MIDI remote interface in Cubase 14 Pro that is controlled by the Electra controller, and assigned the parameters to be controlled to this MIDI remote in the Remote Control Editor of each respective VST. I believe this is how it’s intended to work with external controllers in Cubase.

Of course, this only makes sense if the communication between the MIDI remote and the Electra is bidirectional, so that the parameters on the Electra mini are always updated. This can also be achieved by enabling the checkbox “Send back to controller” in Cubase.

Now to the actual problem: It’s been a known issue since Cubase 14, and thus since the introduction of the MIDI remote, that when the endlessencoders are turned quickly, a delayed echo of individual parameters is sent from the MIDI remote back to the controller with outdated values. This leads to “ghosting/jittering” of the parameters. This is discussed extensively in the Steinberg forum, so it’s not an Electra-specific problem.

Unfortunately, Steinberg doesn’t seem to have solved it even after all these years. Would it be possible to create a simple Lua script in the Electra controller that prevents a parameter from being synchronized externally while the encoder is being touched, but allows it otherwise? That would be the simplest solution I can think of. Or has anyone found a simpler solution for this with Cubase?

I would be grateful for any ideas and suggestions, as this whole situation is quite frustrating (as I said, it’s not an Electra-specific problem, but perhaps Electra with Lua is the solution).

Thank you!

1 Like

No Cubase expert. But if its something you want to happen or not happen when you are touching a pot perhaps these Lua functions would be a useful starting point?

function preset.onReady()
events.subscribe(POTS)
end

function events.onPotTouchChange(potId, controlId, touched)
-- print ("potId: " .. potId .. ", controlId: " .. controlId .. ", touched: " .. (touched and "yes" or "no"))
-- doStuff(potId, controlId, touched)
end

THX, I used this with chatGPT and this works:

-- Anti-jitter retrofit for standard NRPN/CC based presets

-- v4: restore robust per-step echo protection for continuous controls,

-- but with much shorter timing than v2 so encoders feel smoother.

--

-- Requirement: firmware 4.1.0 or newer (parameterMap.updateValue)

assert(

controller.isRequired(controller.getNumModel(), "4.1.0"),

“Anti-jitter Lua requires firmware 4.1.0 or newer”

)

local touchedParams = {}

local lastInternalValues = {}

local suppressUntil = {}

local paramModes = {}

local MODE_CONTINUOUS = 1

local MODE_DISCRETE = 2

-- Continuous controls:

-- Short rolling guard refreshed on each INTERNAL move.

-- This keeps live encoder motion protected from immediate DAW echo,

-- without the heavy/sticky feel of the old 120 ms window.

local CONTINUOUS_STEP_GUARD_MS = 24

local CONTINUOUS_RELEASE_GUARD_MS = 32

-- Discrete controls:

-- Keep these stricter because feel is less important than stability.

local DISCRETE_STEP_GUARD_MS = 120

local DISCRETE_RELEASE_GUARD_MS = 120

-- Bass Station preset specific discrete controls

local DISCRETE_CONTROL_REFS = {

[11] = true, – OSC1 Waveform

[19] = true, – Sub Waveform

[25] = true, – Range

[26] = true, – OSC2 Waveform

[41] = true, – OSC1 PWM

[42] = true, – OSC2 PWM

[17] = true, – LFO Retrigger

[18] = true, – OSC1 LFO Sync

[29] = true, – OSC1 Oct

[30] = true, – OSC2 Sync

[52] = true, – 12/24db

[77] = true, – MONO

[78] = true, – POLY

[79] = true, – LEGATO

[80] = true, – UNISON

[89] = true, – DIST ON/OFF

[90] = true, – CHORUS ON/OFF

[91] = true, – DELAY ON/OFF

[92] = true – REVERB ON/OFF

}

local function nowMs()

return controller.uptime()

end

local function paramKey(deviceId, paramType, parameterNumber)

return string.format(“%d:%d:%d”, deviceId, paramType, parameterNumber)

end

local function getMessageKey(message)

return paramKey(

    message:getDeviceId(),

    message:getType(),

    message:getParameterNumber()

)

end

local function getControlMode(controlId)

if DISCRETE_CONTROL_REFS[controlId] then

return MODE_DISCRETE

end

return MODE_CONTINUOUS

end

local function getStepGuardMs(mode)

if mode == MODE_DISCRETE then

return DISCRETE_STEP_GUARD_MS

end

return CONTINUOUS_STEP_GUARD_MS

end

local function getReleaseGuardMs(mode)

if mode == MODE_DISCRETE then

return DISCRETE_RELEASE_GUARD_MS

end

return CONTINUOUS_RELEASE_GUARD_MS

end

local function rememberCurrentValue(message, key)

local currentValue = parameterMap.get(

    message:getDeviceId(),

    message:getType(),

    message:getParameterNumber()

)

if currentValue ~= nil then

    lastInternalValues\[key\] = currentValue

end

end

local function markControlParameters(controlId, isTouched)

if controlId == nil or controlId == 0 then

return

end

local control = controls.get(controlId)

if control == nil then

return

end

local values = control:getValues()

if values == nil then

return

end

local mode = getControlMode(controlId)

local stepGuardMs = getStepGuardMs(mode)

local releaseGuardMs = getReleaseGuardMs(mode)

for _, valueObject in ipairs(values) do

local message = valueObject:getMessage()

if message ~= nil then

local key = getMessageKey(message)

        paramModes\[key\] = mode

if isTouched then

            touchedParams\[key\] = true

            suppressUntil\[key\] = nowMs() + stepGuardMs

            rememberCurrentValue(message, key)

else

            touchedParams\[key\] = nil

            suppressUntil\[key\] = nowMs() + releaseGuardMs

end

end

end

end

events.subscribe(POTS)

function events.onPotTouch(potId, controlId, touched)

markControlParameters(controlId, touched)

end

function parameterMap.onChange(valueObjects, origin, midiValue)

if valueObjects == nil or #valueObjects == 0 then

return

end

local valueObject = valueObjects[1]

local message = valueObject:getMessage()

if message == nil then

return

end

local deviceId = message:getDeviceId()

local paramType = message:getType()

local parameterNumber = message:getParameterNumber()

local key = paramKey(deviceId, paramType, parameterNumber)

local mode = paramModes[key] or MODE_CONTINUOUS

if origin == INTERNAL then

    lastInternalValues\[key\] = midiValue

    suppressUntil\[key\] = nowMs() + getStepGuardMs(mode)

return

end

if origin == MIDI then

local stillProtected = touchedParams[key] or ((suppressUntil[key] or 0) >= nowMs())

if stillProtected then

local restoreValue = lastInternalValues[key]

if restoreValue ~= nil and parameterMap.updateValue ~= nil then

            parameterMap.updateValue(deviceId, paramType, parameterNumber, restoreValue)

end

end

return

end

end

That’s really interesting! I’m a Cubase user (right now I’m on Cubase 15 on Windows 11), could you please post the Electra Preset on the Preset Library Page? (the formatting seems wrong on this post, so I don’t think I could copy and paste it)

Also, since you are referring to the Bass Station, how do you use the preset? Do you manually recall it when you open the Bass Station VSTi or do you have another automatic way to recall it?

I’ll try to share it when I’ve made some progress. Unfortunately, I’m not entirely sure how to properly share the work, as the settings operate on at least three levels: 1) A JSON and a Lua script for Electra, which could easily be published here, but I only want to do that once I can also add proper instructions for the user. 2) The MIDI remote; this file is relatively easy to find and can be shared here in the forum, for example. 3) The assignments in the RCE (Remote Control Editor); I haven’t yet been able to figure out where Cubase even stores these. So it’s all still quite a patchwork. But this approach seems to be the only one that enables bidirectional communication between Cubase and Electra; unfortunately, it prevents posting “ready to use” presets. Perhaps someone has a better idea. And sorry for posting the code above in such a chaotic way, I’ll probably have to look into the chat functions again… so I have a lot to do.

Here is a possible solution. At least that’s how I’d start looking at it.

If you can’t trust incoming MIDI signals, i.e. they might be jumbling up the actual values you are setting, I’d decouple incoming MIDI from my controls.
The way to do this?

Step 1:
Instead of making controls of type CC or NRPN, I’d create them all as virtual controls.
However that way you loose distinction between controls that were supposed to be virtual ones, versus those supposed to be CC7, CC14 or NRPN.
So make a convention for yourself and write this as a note at the beginning of your lua code.

For instance, distinguish based on ranges. All parameters within the range of:
- 0 to 127 are considered as CC7
- 200 to 264 will be used as CC14 (but they of course are then sent and received as 1 or 2 CC7 messages)
- 300 to 999 will be used as NRPN
- anything above 1000 is considered true virtual and not related directly to MIDI

Step 2
Next step: make a function with parameterMap.onchange parameterMap.onChange(valueObjects, origin, midiValue), with which you convert generically all parameter changes below 1000 into MIDI functions, sending out CC7 (or pairs in case of CC14) or NRPN (4 messages).
Now your preset should be able to control Cubase remotely again, but it won’t listen to any incoming data from Cubase

Step 3
This time we are building in a MIDI listener using midi.onControlChange(midiInput, channel, controllerNumber, value) so you can set again the appropiate parameters by first recognizing if the received data is CC7, NRPN or LSB/MSB or a CC14. I suggest you make 2 temporary toggles too:

  • toggle ‘Listen to CC Y/N’
  • toggle ‘Send out CC Y/N’
    In the midi.onControlChange you then only set the parameters if the first toggle = Yes
    In the parameterMap.onchange you only send the CC back if the second toggle = Yes

Step 4
Time for testing:

  • both toggles off ? Nothing will happen on E1 when MIDI is received nor or in Cubase when E1 controls are changed
  • Send = Yes, Listen = Off ? You should be able to control Cubase, but the preset does not respond to incoming MIDI
  • Send = Off , Listen = On ? You should not be able to control Cubase, but the preset should respond to incoming MIDI and show the right values
  • Both on ? You should get the same bad behaviour as before

If you got to this point, then you have effectively decoupled the parameters from MIDI and now you can play around and see how and when incoming MIDI should apply to the parameters.
Time to build an automated filtering!

Step 5
Now it’s time to build a filter in lua that decides if received MIDI must be applied or ignored. I would sep up a global array in which I store for each CC7 or NRPN number the last timestamp when a parameter is changed manually (caught within function parameterMap.onchange by filtering on origin). Then in the midi.onControlChange I’d build in a test that compares the timestamp of a corresponding received MIDI message against the timestamp stored for its parameter in the array. If both timestamps are too close, you ignore the incoming MIDI (because it is an echo from Cubase), otherwise you apply it to the parameter. You’ll have to experiment with the time difference to figure out the good value.

Step 6
Clean up time: document in the first lines of lua how you eventually did it, set the desired time difference as a constant in the first lines of lua as well (a change in your setup might make the jitter reappear and then you might have to change that value once again) and remove the 2 temporary toggles when no longer desired.

Hopefully this brings some inspiration, but I’m sure you can pull off someting that makes you circumvent those issues and enjoy Cubase remotely controlled.

1 Like

Thanks a lot for your reply, I’m mainly interested in how you are using (or planning to use it). For example, are you creating different pages for differente VST instruments and FX and manually switch to the relative page when you open that specific instruments or insert FX? Are you able to get parameter values back from cubase to the Electra display? I really would like to have an Electra integrations for Cubase similar to what’s available for Ableton and Reaper but right now I’m still limited to a few General Cubase parameters I’ve mapped with the Generic Remote

As I mentioned, I’ve only had the Electra for a few days and am still experimenting. Since I’ve experimented with various controllers with endless encoders in the past, I do have some experience. 1) I create a separate Electra preset for each VST instrument, tailored to the instrument’s specific capabilities. I then load these presets (3 clicks on the Electra) for the respective instrument. In the future, I’d like to automate the preset loading process, but I don’t yet have a way to do that. 2) These presets all address the same MIDI remote in Cubase, which I’ve configured once (96 rotary encoders that receive NRPN and 32 buttons that receive CC). These are then permanently assigned to the “Instrument Parameters of the Selected Tracks 1-128” in the Remote Control Editor. The MIDI input and output must each be assigned to Electra 1 In/out or both to Electra 2 In/out. 3) For each instrument, you then have to open the RCE (you open it by clicking the small black arrow in the upper right corner of the VST end and selecting the Remote Control Editor from the dropdown menu). Important: save the changes at the end, and the changes will only take effect when you reload the synth. This is the normal procedure, a bit clunky, but it works. Jeff Gibbons, for example, made a good YouTube video where he demonstrates such a mapping with a Native Instruments S-Series MK3. The main problem here is that the feedback to the controller creates the jitter problem that prompted me to start this discussion, if you control them in absolute mode instead of relative mode (which is what I want to do because then they feel more like normal potentiometers on a real synth). Fortunately, thanks to the tips above, I managed to create a Lua script that largely prevents this wobbling and jumping of the parameters. You simply have to copy it into the Lua section of the web editor. I’m posting it here in my current version and hope that the format is better this time (the one above is an older, worse version):

-- Electra Mini / Cubase Universal Anti-Jitter Script v6
--
-- Geschützte Bereiche:
--   NRPN  1-64   = Hauptencoder
--   NRPN 65-96   = Reserve / Subpages
--   CC7  97-127  = Buttons / Schalter
--   CC7  0       = letzter Button-Slot (logisch 128)
--
-- Idee:
--   - behält das direkte NRPN/CC-Mapping bei
--   - merkt sich pro Parameter den letzten stabilen Wert
--   - merkt sich pro Parameter den Zeitpunkt der letzten internen Änderung
--   - blockiert eingehendes MIDI, wenn es kurz danach als Echo zurückkommt
--   - schützt zusätzlich Touch- und Release-Phasen
--
-- Voraussetzung:
--   - Firmware 4.1.0+ empfohlen/erforderlich (parameterMap.updateValue)

local DEVICE_ID = 1

-- Feintuning
local ECHO_GUARD_MS = 180
local RELEASE_GUARD_MS = 120
local LOG_EVENTS = false

local protectedParameters = {}

-- Zustände pro Parameter-Key
local touchedParameters = {}
local guardUntil = {}
local lastStableValue = {}
local lastInternalAt = {}
local lastAcceptedMidiAt = {}
local lastBlockedMidiAt = {}

local function ensureTypeTable(paramType)
    if protectedParameters[paramType] == nil then
        protectedParameters[paramType] = {}
    end
    return protectedParameters[paramType]
end

local function addProtectedRange(paramType, firstParam, lastParam)
    local t = ensureTypeTable(paramType)
    for p = firstParam, lastParam do
        t[p] = true
    end
end

local function addProtectedNumber(paramType, paramNumber)
    local t = ensureTypeTable(paramType)
    t[paramNumber] = true
end

-- NRPN: Hauptencoder + Reserve/Subpages
addProtectedRange(PT_NRPN, 1, 64)
addProtectedRange(PT_NRPN, 65, 96)

-- CC7: Buttons / Schalter / Listen-Slots
addProtectedRange(PT_CC7, 97, 127)
addProtectedNumber(PT_CC7, 0)

local function makeKey(deviceId, paramType, paramNumber)
    return tostring(deviceId) .. ":" .. tostring(paramType) .. ":" .. tostring(paramNumber)
end

local function messageIsProtected(message)
    if message == nil then
        return false
    end

    if message:getDeviceId() ~= DEVICE_ID then
        return false
    end

    local paramType = message:getType()
    local paramNumber = message:getParameterNumber()
    local typeTable = protectedParameters[paramType]

    return (typeTable ~= nil and typeTable[paramNumber] == true)
end

local function setGuard(key, expiresAt)
    local current = guardUntil[key]
    if current == nil or expiresAt > current then
        guardUntil[key] = expiresAt
    end
end

local function rememberStableValue(key, midiValue)
    lastStableValue[key] = midiValue
end

local function rememberInternalChange(key, midiValue, now, holdMs)
    lastStableValue[key] = midiValue
    lastInternalAt[key] = now
    setGuard(key, now + holdMs)
end

local function rememberAcceptedMidi(key, midiValue, now)
    lastStableValue[key] = midiValue
    lastAcceptedMidiAt[key] = now
end

local function getCurrentOrMessageValue(message)
    local value = parameterMap.get(
        message:getDeviceId(),
        message:getType(),
        message:getParameterNumber()
    )

    if value == nil then
        value = message:getValue()
    end

    return value
end

local function shouldBlockIncomingMidi(key, incomingValue, now)
    local stableValue = lastStableValue[key]
    if stableValue == nil then
        return false
    end

    local isTouched = (touchedParameters[key] == true)
    local inGuardWindow = ((guardUntil[key] or 0) > now)

    local internalAt = lastInternalAt[key]
    local recentlyInternal = false
    if internalAt ~= nil then
        recentlyInternal = ((now - internalAt) <= ECHO_GUARD_MS)
    end

    -- Nur dann blocken, wenn wir guten Grund haben zu glauben,
    -- dass das gerade ein Echo unserer letzten lokalen Änderung ist.
    if (isTouched or inGuardWindow or recentlyInternal) and incomingValue ~= stableValue then
        return true
    end

    return false
end

local function protectControl(controlId, isTouched)
    if controlId == nil or controlId <= 0 then
        return
    end

    local control = controls.get(controlId)
    if control == nil then
        return
    end

    local values = control:getValues()
    local now = controller.uptime()
    local seen = {}

    for _, valueObject in ipairs(values) do
        local message = valueObject:getMessage()

        if messageIsProtected(message) then
            local deviceId = message:getDeviceId()
            local paramType = message:getType()
            local paramNumber = message:getParameterNumber()
            local key = makeKey(deviceId, paramType, paramNumber)

            if seen[key] ~= true then
                seen[key] = true

                if isTouched then
                    touchedParameters[key] = true

                    local currentValue = getCurrentOrMessageValue(message)
                    rememberStableValue(key, currentValue)
                    setGuard(key, now + ECHO_GUARD_MS)

                    if LOG_EVENTS then
                        print("touch start type=" .. paramType .. " param=" .. paramNumber .. " value=" .. currentValue)
                    end
                else
                    touchedParameters[key] = nil
                    setGuard(key, now + RELEASE_GUARD_MS)

                    if LOG_EVENTS then
                        print("touch end type=" .. paramType .. " param=" .. paramNumber)
                    end
                end
            end
        end
    end
end

function preset.onReady()
    events.subscribe(POTS)

    if parameterMap.updateValue == nil then
        print("Universal Anti-Jitter v6: Firmware 4.1.0+ erforderlich (parameterMap.updateValue fehlt)")
        return
    end

    print("Universal Anti-Jitter v6 aktiv")
    print("Geschuetzte Typen/Bereiche:")
    print("NRPN: 1-96")
    print("CC7 : 97-127, 0")
end

function events.onPotTouchChange(potId, controlId, touched)
    protectControl(controlId, touched)
end

function parameterMap.onChange(valueObjects, origin, midiValue)
    if parameterMap.updateValue == nil then
        return
    end

    local now = controller.uptime()
    local seen = {}

    for _, valueObject in ipairs(valueObjects) do
        local message = valueObject:getMessage()

        if messageIsProtected(message) then
            local deviceId = message:getDeviceId()
            local paramType = message:getType()
            local paramNumber = message:getParameterNumber()
            local key = makeKey(deviceId, paramType, paramNumber)

            -- verhindert Doppelverarbeitung desselben Parameters,
            -- wenn mehrere Controls daran hängen
            if seen[key] ~= true then
                seen[key] = true

                if origin == INTERNAL then
                    rememberInternalChange(key, midiValue, now, ECHO_GUARD_MS)

                    if LOG_EVENTS then
                        print("internal type=" .. paramType .. " param=" .. paramNumber .. " value=" .. midiValue)
                    end

                elseif origin == MIDI then
                    if shouldBlockIncomingMidi(key, midiValue, now) then
                        lastBlockedMidiAt[key] = now

                        if LOG_EVENTS then
                            print(
                                "blocked MIDI echo type=" .. paramType ..
                                " param=" .. paramNumber ..
                                " incoming=" .. midiValue ..
                                " keep=" .. tostring(lastStableValue[key])
                            )
                        end

                        parameterMap.updateValue(
                            deviceId,
                            paramType,
                            paramNumber,
                            lastStableValue[key]
                        )
                    else
                        rememberAcceptedMidi(key, midiValue, now)

                        if LOG_EVENTS then
                            print("accepted MIDI type=" .. paramType .. " param=" .. paramNumber .. " value=" .. midiValue)
                        end
                    end

                elseif origin == LUA then
                    -- Falls später Lua-seitig Werte gesetzt werden,
                    -- bleibt unser interner Referenzwert konsistent.
                    rememberStableValue(key, midiValue)

                    if LOG_EVENTS then
                        print("lua type=" .. paramType .. " param=" .. paramNumber .. " value=" .. midiValue)
                    end
                end
            end
        end
    end
end

If I read correctly, the algorithm will still receive the incoming MIDI and update the values on the E1, but prevents the E1 from sending them out, effectively stopping the loop of MIDI signals. So you will still see jitter appear on the screen, but it won’t affect the control anymore, correct?
That’s quite nice, as you can keep the original CC and NRPN controls.

What I do find strange is assigning the range 1-96 for NRPN. I’d reserve that area exclusively for CC. NRPN can easily be set in higher ranges (above 128) allowing more CC parameters to be controlled.
Also be aware to reserve certain CC numbers for their predestined goals, such as CC0, CC1, CC6, CC7, CC38, CC64, CC96-99 and CC120-CC127. If you reuse them for other purposes you might get unexpected results giving you headaches. You may find that those are often hardcoded.

1 Like

The jittering isn’t completely gone, but it now only occurs rarely and very slightly. It’s perfectly usable now, whereas it was unusable before with Cubase (which is Cubase’s fault, not the Electra Mini’s). The strange way the NRPNs are assigned is due to my lack of experience with them. I chose them to achieve higher resolution and hoped this would solve the original problem. However, since the Electra always communicates with the MIDI remote as an intermediary in this configuration and never directly with the VSTi, there shouldn’t be any conflicts with the hardcoded CCs. At least, I hope so…

1 Like

Thanks, so you’re using as I imagined, with the Remote Control Editor. One way of automating the preset loading process that I was thinking of implementing is with the iOS app Metagrid, that i use to control Cubase. With this app it’s possibile to recall a specific page whenever a track with a given name is selected and/or whenever a window with a specific set of characters in its name is focused. The grid can in turn send a program change messages that could be used to recall a specific preset inside Electra.

You have to consider that the MIDI messages you assign in the MIDI Remote editor are swallowed by Cubase, i.e. they are not anymore received ore recorded in the Cubase MIDI tracks; Cubase consider not only the message but also the channel on which the message is sent, so I choose to use channel 16 for my MIDI Remote messages.

Cubase MIDI Remote workaround: Trackselection automatically changes Electra preset

I got this working reliably in Cubase with the second MIDI port on my Electra minicontroller.

What it does

When the selected track changes, a small MIDI Remote JS script in the Cubase Midiremote checks the track name and sends a Program Change out of a second MIDI port, so the controller can switch presets automatically.

I chose track names, not plugin names, because that gives more flexibility. You can always rename a track if you want to prevent the automatic preset switch.

Important workaround

The key was: do not just drop the JS file into the MIDI Remote script folder first.

That gave me unstable behavior like:

  • No Input Port found

  • No Output Port found

  • remote disappears after reloading scripts

What worked was this:

Step-by-step

  1. In Cubase MIDI Remote Manager, create a new MIDI Remote surface first.

  2. Use the exact same:

    • Manufacturer

    • Model

    • Script Creator
      values that you will use in the JS.

  3. Assign the intended MIDI ports.

  4. Add one dummy control and save the remote.

  5. Close Cubase.

  6. Go to the generated script folder in:
    Documents/Steinberg/Cubase/MIDI Remote/Driver Scripts/Local/<Manufacturer>/<Model>/

  7. Copy your .js file into the same folder as the generated .json.

  8. Reopen Cubase.

  9. Use Script Tools → Reload Scripts.

After that, Cubase linked the JSON surface and the JS correctly, and the remote survived restarts.

Notes

  • Matching Manufacturer / Model / Script Creator values mattered.

  • Filename casing did not seem to matter in my case.

  • On my setup, I used port 1 for the normal remote and port 2 for the track-follow Program Change script.

Example JS

This is the working concept. Adjust port names and track/preset mappings to your setup.

var midiremote_api = require('midiremote_api_v1')

// Device
var deviceDriver = midiremote_api.makeDeviceDriver('Arik', 'TrackToPC', 'ChatGPT')

// MIDI Ports
var midiIn = deviceDriver.mPorts.makeMidiInput('TrackToPC In')
var midiOut = deviceDriver.mPorts.makeMidiOutput('TrackToPC Out')

// Detection
var detectionUnit = deviceDriver.makeDetectionUnit()
var portPair = detectionUnit.detectPortPair(midiIn, midiOut)

portPair.expectInputNameEquals('Electra Controller Electra Port 2')
portPair.expectOutputNameEquals('Electra Controller Electra Port 2')

// Surface / Custom Variable
var surface = deviceDriver.mSurface
var trackNameFollower = surface.makeCustomValueVariable('trackNameFollower')

// Mapping Page
var page = deviceDriver.mMapping.makePage('Main')

page.makeValueBinding(
    trackNameFollower,
    page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mSelected
)

function log(msg) {
    console.log('[TrackToPC] ' + msg)
}

function sendProgramChange(activeDevice, rawValue) {
    midiOut.sendMidi(activeDevice, [0xC0, rawValue])
    log('Sent Program Change raw=' + rawValue)
}

function findProgramForTrackName(trackName) {
    var name = (trackName || '').toLowerCase()

    if (name.indexOf('komplete') !== -1) return 1
    if (name.indexOf('abl3') !== -1) return 2
    if (name.indexOf('axxess') !== -1) return 3
    if (name.indexOf('bass station') !== -1) return 4
    if (name.indexOf('diva') !== -1) return 5
    if (name.indexOf('ob-ez') !== -1) return 6
    if (name.indexOf('ps-20') !== -1) return 7
    if (name.indexOf('retrologue') !== -1) return 8
    if (name.indexOf('sem') !== -1) return 9
    if (name.indexOf('yellowjackets') !== -1) return 10
    if (name.indexOf('yellowjacket') !== -1) return 10
    if (name.indexOf('fm8') !== -1) return 11
    if (name.indexOf('padshop') !== -1) return 12
    if (name.indexOf('massive x') !== -1) return 13
    if (name.indexOf('vcv') !== -1) return 14

    return -1
}

trackNameFollower.mOnTitleChange = function (activeDevice, objectTitle, valueTitle) {
    var trackName = objectTitle || valueTitle || ''

    if (!trackName) {
        return
    }

    log('Selected track: ' + trackName)

    var programNumber = findProgramForTrackName(trackName)

    if (programNumber > 0) {
        sendProgramChange(activeDevice, programNumber)
    }
}

Final note

In my setup, preset 1 = raw PC value 1, preset 2 = raw PC value 2, etc.
So I simply ignored PC0. That may differ depending on the hardware, so test it on your device.

Thanks a lot it works!!! With the Electra MKII I had to change the ports to “Electra Controller Electra CTRL”, because this is the port where Program Change messages to switch presets are received by the Electra AFAIK.

Also I’ve noticed that it’s possibile to update the script while Cubase is running, the only thing that’s needed is to reload the script inside Cubase.
Another thing I’ve noticed is that with PC value 1 I get Electra preset 2, but if I add PC Value 0 in another line it doesn’t work so I can’t find a way to select preset 1 on the Electra, but this is not a big deal