Sending selected Track Name to an external custom application via MIDI Remote API?

Hi everyone,

I am a simple enthusiast using Cubase 15 Pro and running a very complex custom hardware setup: an external touchscreen control surface running a custom C# WPF application (called Sherlock 2 in homage to the superb work done by Karol Obara of 14BitMIDI with his Sherlock Plugin ) and with 20 pages and 240 buttons.

My workflow relies heavily on keeping tracks deactivated by default in a large template to save system resources. To make my touchscreen dynamic, I absolutely need bidirectional feedback: when I select an instrument track in Cubase (using the mouse or keyboard arrows), I want Cubase to automatically send the Selected Track Name back to my C# application so my touchscreen layout can adapt instantly.

We previously tried using Windows UI Automation in C# to “spy” on the Cubase window focus, but it is way too resource-intensive and causes major UI lags in our template.

We want to move to a clean, native solution using the Cubase MIDI Remote API (JavaScript).

My questions for the script experts here:

Is it possible via the MIDI Remote JS API to listen to the “Selected Track Change” event and extract the string of the track name?

Can a MIDI Remote script send this text string over a virtual MIDI port (like loopMIDI) or via a specific SysEx/MIDI message that my C# application could listen to?

If anyone has a snippet of JS code or an example of a callback function (mOnTitleChange or similar) that achieves this, I would be extremely grateful!

Here is the draft of the MIDI Remote JS callback we want to use to get the track title:

var hostSelectedTrackTitle = page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mTitle;

hostSelectedTrackTitle.mOnTitleChange = function(activeDeviceInstance, activeMapping, trackTitle) { if (!trackTitle) return; var charCodes = []; for (var i = 0; i < trackTitle.length; i++) { charCodes.push(trackTitle.charCodeAt(i)); } var fullMessage = [0xF0, 0x00, 0x21, 0x1A].concat(charCodes, [0xF7]); console.log("Track changed to: " + trackTitle); };

My questions for the script experts here:

  1. Is it possible via the MIDI Remote JS API to listen to the “Selected Track Change” event and extract the string of the track name?

  2. Can a MIDI Remote script send this text string over a virtual MIDI port (like loopMIDI) or via a specific SysEx/MIDI message that my C# application could listen to?

  3. If anyone has a snippet of JS code or an example of a callback function (mOnTitleChange or similar) that achieves this, I would be extremely grateful!

Thanks a lot for your help and insights!

A small addition: my WPF Sherlock 2 window was created in Cubase with only the remote MIDI assignment, and therefore there is only one JSON file. All attempts to create a JS file have been unsuccessful.

Yes. You already provided the necessary snippet right above :slight_smile:

The thing is that this is not something you can include in a json file (such is your midi remote right now). You should either create a separate midi remote just for feedback, or rebuild the whole remote using js. If you ask me, the latter is more robust. Let me know what caused building a js remote failed.

"Thank you for your clear answer, m.c!

The JavaScript creation failed simply because the Sherlock application historically generates and communicates with Cubase via a classic configuration of MIDI addresses and surface files, not via a native JS script written from scratch. I’m not a JavaScript developer by training, so completely rebuilding the remote control (with its 20 pages and 240 buttons) in native JS code seemed like a colossal undertaking.

However, since feedback is crucial to my workflow, your first solution interests me: Is it difficult to create a “separate MIDI remote control” in JavaScript, containing only this small feedback script to send the track name? This way, my main remote control would remain unchanged, and this mini JS script would serve solely as a gateway for the text."

So we just tested by moving the json file and creating a new js file to recreate my 20 pages of 240 buttons as well as my 20 page assignment buttons so that we can then inject all my functions from the remote midi assistant and there is a problem MidiIn below is the js code if someone could find the solution.

var midiremote_api = require('midiremote_api_v1');

var deviceDriver = midiremote_api.makeDeviceDriver('14bitMIDI', 'Sherlock 2', 'Dominique');

var midiIn = deviceDriver.mPorts.makeMidiIn('SHERLOCK-IN');
var midiOut = deviceDriver.mPorts.makeMidiOut('SHERLOCK-OUT');

deviceDriver.makeDetectionUnit().detectPortPair(midiIn, midiOut)
    .withIdentity('F0 7E 7F 06 02 41 3F 01 01 00 00 00 F7');

var surface = deviceDriver.mSurface;

var pagesGroup = surface.makeSubPageArea('Pages Sherlock');
var subPages = [];
for (var p = 0; p < 20; p++) {
    subPages.push(pagesGroup.makeSubPage('Page ' + (p + 1)));
}

var TOTAL_BOUTONS = 240;
var COLONNES = 12;

for (var btn = 0; btn < TOTAL_BOUTONS; btn++) {
    var ligne = Math.floor(btn / COLONNES);
    var colonne = btn % COLONNES;
    
    var x = colonne * 2;
    var y = (ligne * 2) + 3; 
    
    var monBouton = surface.makeButton(x, y, 1.8, 1.8);
    
    var canalMidi = 0;
    var noteMidi = 0;
    
    if (btn >= 180) {
        canalMidi = 15; 
        noteMidi = btn - 160; 
        monBouton.mSurfaceValue.mMidiBinding.setInputPort(midiIn).bindToNote(canalMidi, noteMidi);
    } else {
        if (btn < 80) {
            canalMidi = 13; 
            noteMidi = btn;
        } else if (btn < 160) {
            canalMidi = 14; 
            noteMidi = btn - 80; 
        } else {
            canalMidi = 15; 
            noteMidi = btn - 160; 
        }
        monBouton.mSurfaceValue.mMidiBinding.setInputPort(midiIn).bindToNote(canalMidi, noteMidi);
        monBouton.mSurfaceValue.bindToSubPageArea(pagesGroup);
    }
}

for (var pg = 0; pg < 20; pg++) {
    var pLigne = Math.floor(pg / 10);
    var pColonne = pg % 10;
    
    var btnPage = surface.makeButton(pColonne * 2.4, pLigne * 1.2, 2.2, 1);
    btnPage.mSurfaceValue.mMidiBinding.setInputPort(midiIn).bindToNote(12, pg);
    
    btnPage.mSurfaceValue.mOnProcessValueChange = (function(indexPage) {
        return function(context, value) {
            if (value > 0) { 
                pagesGroup.mActionCollection.mTargetSubpageIndex.setProcessValue(context, indexPage);
            }
        };
    })(pg);
}

I specify that I am with my current configuration and to try not to break anything in my XAML files of my WPF floating window with the buttons on notes with channel 14 from 0 to 79 channel 15 from 0 to 79 channel 16 from 0 to 79 and channel 13 for page changes from 0 to 19.

To simplify troubleshooting, I have isolated the issue in a much shorter diagnostic script (focusing only on the ports without the button matrix).

Here is the clean test code that still throws the exact same TypeError: undefined not callable (property 'makeMidiIn'...) error on the port declaration line.

How should I declare these ports in JavaScript when my JSON surface is already active?

var midiremote_api = require('midiremote_api_v1');

var deviceDriver = midiremote_api.makeDeviceDriver('14bitMIDI', 'Sherlock 2', 'Dominique');

// Le script plante sur cette ligne :
var midiIn = deviceDriver.mPorts.makeMidiIn('SHERLOCK-IN');
var midiOut = deviceDriver.mPorts.makeMidiOut('SHERLOCK-OUT');

deviceDriver.makeDetectionUnit().detectPortPair(midiIn, midiOut)
.withIdentity('F0 7E 7F 06 02 41 3F 01 01 00 00 00 F7');

deviceDriver.mOnActivate = function(context) {
console.log("Script activé en parallèle du JSON.");
};

Oops, I reversed the ports in my previous message, here is the corrected code with the correct physical ports (Input on SHERLOCK-OUT and Output on SHERLOCK-IN)

var midiremote_api = require('midiremote_api_v1');

var deviceDriver = midiremote_api.makeDeviceDriver('14bitMIDI', 'Sherlock 2', 'Dominique');

// LE CORRECTIF DE DOMINIQUE : Inversion In/Out pour le loopback loopMIDI
var midiIn = deviceDriver.mPorts.makeMidiIn('SHERLOCK-OUT');
var midiOut = deviceDriver.mPorts.makeMidiOut('SHERLOCK-IN');

deviceDriver.makeDetectionUnit().detectPortPair(midiIn, midiOut)
    .withIdentity('F0 7E 7F 06 02 41 3F 01 01 00 00 00 F7');

deviceDriver.mOnActivate = function(context) {
    console.log("Script activé en parallèle du JSON.");
};

Hi, the code is wrong.

Here’s how it should be in order for it to automatically detect the ports:

var midiIn = deviceDriver.mPorts.makeMidiInput("an Input")
var midiOut = deviceDriver.mPorts.makeMidiOutput("an Output")
deviceDriver.makeDetectionUnit().detectPortPair(midiInput, midiOutput)
    .expectInputNameEquals('SHERLOCK-OUT')
    .expectOutputNameEquals('SHERLOCK-IN')

Okay, so we just replaced the JavaScript code and restarted Cubase. No more errors in the console. We’ll continue testing. Thank you very much for the feedback. I’ll keep you updated.

Thanks a lot, mc! Port detection now works perfectly with your syntax.

The second step of my project is to detect track changes in Cubase to send a command to Sherlock (to dynamically change pages depending on the instrument).

What’s the correct JavaScript syntax to listen for changes in the name or focus of the selected track (mTrackSelection)? All my attempts at direct binding or via mHostAccess result in an “undefined” error when the script loads.

Yes, I saw what was wrong in a previous post. The following is wrong.

var hostSelectedTrackTitle = page.mHostAccess.mTrackSelection.mMixerChannel.mValue.mTitle;

hostSelectedTrackTitle.mOnTitleChange = function(activeDeviceInstance, activeMapping, trackTitle) { if (!trackTitle) return; var charCodes = ; for (var i = 0; i < trackTitle.length; i++) { charCodes.push(trackTitle.charCodeAt(i)); } var fullMessage = [0xF0, 0x00, 0x21, 0x1A].concat(charCodes, [0xF7]); console.log("Track changed to: " + trackTitle); };

Replace it with:

var hostSelectedTrack = page.mHostAccess.mTrackSelection.mMixerChannel;

hostSelectedTrack.mOnTitleChange = function(activeDeviceInstance, activeMapping, trackTitle) { 
    if (!trackTitle) return;
    var charCodes=[]
    for (var i = 0; i < trackTitle.length; i++) {
         charCodes.push(trackTitle.charCodeAt(i));  //If you have extended charCodes, this will fail
    }
    var fullMessage = [0xF0, 0x00, 0x21, 0x1A].concat(charCodes, [0xF7]);
    console.log("Track changed to: " + trackTitle); 
}

By the way, you used Gemini, right? Did you point it to the MR API documentation first? I see it’s making some errors, it shouldn’t do. API Reference | MIDI REMOTE API

Yes, absolutely, and I haven’t hidden that fact at all in the other messages I’ve posted on the forum.

He’s the one who helped me design the WPF window, which is already a huge undertaking.

And Gemini wants to show you that he knows how to fix things.
Here’s the code, strictly compliant with the official documentation, to create the page and link the track title:

var midiremote_api = require('midiremote_api_v1');

var deviceDriver = midiremote_api.makeDeviceDriver('14bitMIDI', 'Sherlock 2', 'Dominique');

var midiIn = deviceDriver.mPorts.makeMidiInput("an Input");
var midiOut = deviceDriver.mPorts.makeMidiOutput("an Output");

deviceDriver.makeDetectionUnit().detectPortPair(midiIn, midiOut)
    .expectInputNameEquals('SHERLOCK-OUT')
    .expectOutputNameEquals('SHERLOCK-IN');

// Version officielle : On crée d'abord la surface et la page
var surface = deviceDriver.mSurface;
var page = deviceDriver.makePage('Page Principale');

// On demande poliment à l'application Cubase de nous donner l'accès à la piste sélectionnée
var hostSelectedTrack = page.mHostAccess.mTrackSelection.mMixerChannel;

// On utilise le callback officiel de l'API pour écouter le changement de nom
hostSelectedTrack.mValue.mOnTitleChange = function(context, trackName) {
    console.log("GEEK ALERT -> Piste selectionnee : " + trackName);
};

deviceDriver.mOnActivate = function(context) {
    console.log("Script Sherlock 2 validé et actif !");
};

? Not sure I understand this. What I see below is the code I just provided. Again, I’m not asking if you use Gemini, I ask you to point Gemini to the MIDI Remote API page I linked. This way, it will most probably avoid mistakes in the future, which are mostly due to it not following the API.
For example, when I see deviceDriver.mPorts.makeMidiIn instead of the correct deviceDriver.mPorts.makeMidiInput or when I see .withIdentity instead of the correct
.expectSysexIdentityResponse, I strongly suspect that it didn’t get really into the documentation/examples.

He made me modify the code 3 or 4 more times and there’s still an error in the console.

var midiremote_api = require('midiremote_api_v1');

var deviceDriver = midiremote_api.makeDeviceDriver('14bitMIDI', 'Sherlock 2', 'Dominique');

var midiIn = deviceDriver.mPorts.makeMidiInput("an Input");
var midiOut = deviceDriver.mPorts.makeMidiOutput("an Output");

deviceDriver.makeDetectionUnit().detectPortPair(midiIn, midiOut)
    .expectInputNameEquals('SHERLOCK-OUT')
    .expectOutputNameEquals('SHERLOCK-IN');

// 1. CRÉATION DE LA SURFACE (Obligatoire pour créer un élément)
var surface = deviceDriver.mSurface;
var trackTrigger = surface.makeBlindButton(0, 0, 0, 0); // Un bouton invisible pour tricher proprement

// 2. CRÉATION DE LA PAGE DE MAPPING
var page = deviceDriver.makePage('Page Principale');

// 3. LIAISON AVEC LA PISTE SÉLECTIONNÉE
var hostSelectedTrack = page.mHostAccess.mTrackSelection.mMixerChannel;

// On lie la valeur du bouton invisible au titre de la piste de Cubase
page.makeValueBinding(trackTrigger.mSurfaceValue, hostSelectedTrack.mValue.mTitle);

// 4. L'ÉCOUTEUR DE FOCUS (La syntaxe officielle)
trackTrigger.mSurfaceValue.mOnTitleChange = function(context, trackName) {
    console.log("GEEK ALERT -> Piste selectionnee : " + trackName);
};

deviceDriver.mOnActivate = function(context) {
    console.log("Script Sherlock 2 enfin d'équerre avec la doc !");
};

However, when I see everything we’ve managed to achieve with Visual Studio to create the WPF window, I thought JavaScript would be easy.

Sorry, I didn’t understand.
Anyway, thank you so much for your help. I’ve been following you on the forum for a long time, and you’re the MIDI remote guru!

You’re absolutely right, m.c, and I apologize to you and the community! Gemini is coding completely by feel and inventing functions instead of following the documentation you shared. I was misled by their overconfident answers.

To start over on a sound footing and in accordance with the API: what is the exact method to initialize a page and bind the track change event (mTrackSelection) in the script? A huge thank you for your patience with the “helpful geek” that I am!

Hi, replace this:

var trackTrigger = surface.makeBlindButton(0, 0, 0, 0); // Un bouton invisible pour tricher proprement

With this:

var trackTrigger = surface.makeButton(0, 0, 0, 0); // Un bouton invisible pour tricher proprement

There is no such thing as BlindButton. There is the BlindPanel but it doesn’t support MIDI.

Thank you for your feedback, M.C. There are no longer any error messages in the console, but it’s still impossible to get that feedback.
We’ll keep looking.