Selecting favourite VST Instrument and fx plugins, using MIDI Remote (and shortcuts)

I’ve already made a post with an implementation of the new MIDI Remote feature, the Plugin Manager, introduced in API 1.3 (CB 15.0.20): An HTML-based MIDI Remote demonstrating additions in MR API 1.3
This new post is about how we can bind buttons of our controllers to directly insert into an instrument or insert slot, our favourite plugins. At the same time, I will present a way of binding keyboard shortcuts to these actions.

We have two approaches, the one is using hard-coded plugin names in our script, and this is what I’ve done in my previous post, however, as already mentioned there, this is not the suggested way. The other and suggested way (for me) is to create collections, named something like “Favourites” and then retrieve the plugin lists for these particular collections. We need one for the instrument plugins, and another one for the fxs.

Here are two examples:

When the script is activated. we get this panel, based on the VST collections of our examples:

As we can see we have two rows of the instrument and fx favourite plugins, as already inserted in our collections.
There is also a third row with buttons for selecting one of the first four insert slots of the selected track. Whenever we select an instrument plugin, and if we are already on an instrument track, it will replace whichever plugin we had previously. A note here: I created a template with the number of instrument tracks I usually need, and set their VST to none, so they’re just empty and ready to accept my selections.

These 12 buttons are handled using MIDI CC messages of our choice, by altering the script obviously.

However, there is a way to use keyboard shortcuts for doing these selections without even using external utilities for converting these shortcuts to MIDI messages, as most of us do already.
Here’s the logic behind implementing this:
MIDI Remote can since day one, retrieve the name of the selected track.
With the latest additions (API 1.2 and on) we can even edit the name from our scripts.
How will this help?
Well, we can actually prepare PLE presets, in which we can alter the name of the selected track, by adding a suffix of our choice.
Here’s for example one such PLE:

Now, since MIDI Remote immediately gets notified about the change, we can check it and if the suffix added is compatible with our script’s logic, we can then perform whichever action we want! What’s left is to finally restore the track name to the one before the addition. This can be done by a PLE again, but no need to, since as already mentioned, this is totally doable using just the MIDI Remote.
Finally, what we have to do, is to assign these PLEs to shortcuts. So everything is done and handled internally by Cubase!

Here’s the code of the script for anyone interested in the details, without even having to install it:

// @ts-nocheck
var useMIDIPort=true

var favouriteCollectionsNames=[
	
	"Favourites", //We define the name of the Instrument plugins collection to use as favourite

	"Favourites" //We define the name of the FX plugins collection to use as favourite

]

var selectFavouriteButtonsMIDICCs=[

	[0,1,2,3], //The MIDI CCs we're going to use for recalling our favourite instrument plugins. Here I set it up for 4 favourites. CC 0 is for the first plugin, CC 1 is for the second and so on

	[8,9,10,11], //The MIDI CCs we're going to use for recalling our favourite FX plugins. Here I set it up for 8 favourites. CC 8 is for the first plugin, CC 9 for the second, and so on

	[16,17,18,19], //The MIDI CCs we're going to use for selecting the insert FX slots. I've set it up for the first slots. CC 16 is for activating Slot 1, CC 17 for Slot 2, and so on
	
]

var channelCCs=0 //The channel assigned to the MIDI CCs above

var midiremote_api = require('midiremote_api_v1')
var deviceDriver = midiremote_api.makeDeviceDriver("Test", "Select Favourite Plugin", "mc")

var midiInput

if(useMIDIPort){

	var midiInput = deviceDriver.mPorts.makeMidiInput("anInput")

	var detectionUnitWin = deviceDriver.makeDetectionUnit()
	detectionUnitWin.mPlatformFilter.expectPlatformWindows() //Another feature added in API 1.3 is the ability to chek for OS, so that we can properly define the MIDI Ports names that should be expected
	detectionUnitWin.detectSingleInput(midiInput)
		.expectInputNameEquals("loopMIDI Port 2")
	
	var detectionUnitMac = deviceDriver.makeDetectionUnit()
	detectionUnitMac.mPlatformFilter.expectPlatformMacOS()
	detectionUnitMac.detectSingleInput(midiInput)
		.expectInputNameEquals("IAC Driver Bus 3")
	 
}

var favouritePluginsUIDs=[] //This array will store the UIDs of our favourite plugins per type, i.e. two arrays, one for instrument plugins and the other for FX plugins

for(var i=0;i<2;i++){

	var favouritesPluginsUIDsPerType=[]
	favouritePluginsUIDs.push(favouritesPluginsUIDsPerType)

}

var surface = deviceDriver.mSurface

var buttons=[]

var surfaceButtons=[] //We'll create real buttons (the ones in the buttons array) and fake buttons for surface representation (surfaceButtons)

var labels=[] //These will show the favourite plugin names and the insert slots indexes

var labelsHeaders=[]
var labelHeadersTitles=["Fav. Instruments","Fav. FXs","Insert Slots"]
var headerWidth=7

var controlWidths=[7,7,2.5]
var separatorsX=[0,0,1]

var rowHeight=1
var separatorHeaderHeight=0.5
var separatorSectionHeight=3

var y=0
var buttonCounter=0

for(var i=0;i<3;i++){

	var labelHeader=surface.makeLabelField(0,y,headerWidth,rowHeight)
	labelsHeaders.push(labelHeader)
	y+=separatorHeaderHeight+rowHeight

	var buttonsPerType=[]
	var surfaceButtonsPerType=[]
	var labelsPerType=[]

	for(var j=0;j<selectFavouriteButtonsMIDICCs[i].length;j++){
		
		var x=(controlWidths[i]+separatorsX[i])*j
		
		var button=surface.makeCustomValueVariable("button"+buttonCounter)
		if(useMIDIPort){

			button.mMidiBinding
				.setInputPort(midiInput)
				.bindToControlChange(channelCCs,selectFavouriteButtonsMIDICCs[i][j])

		}
		
		buttonsPerType.push(button)
		
		buttonCounter++

		var surfaceButton=surface.makeLamp(x,y,1,rowHeight)
		
		surfaceButtonsPerType.push(surfaceButton)
		
		var label=surface.makeLabelField(x+1,y,controlWidths[i]-1,rowHeight)
		
		labelsPerType.push(label)

	}

	buttons.push(buttonsPerType)
	surfaceButtons.push(surfaceButtonsPerType)
	labels.push(labelsPerType)
	
	y+=separatorSectionHeight

}

var customDelayVar=surface.makeCustomValueVariable("customDelayVar") //This is a variable we will use for delaying setting a lamp off, after turning it on, upon selecting a plugin. This is just for visual feedback purposes

var customDelayCycles=10 //The number of cycles after which we will turn off the lamp

var mapping = deviceDriver.mMapping

var page=mapping.makePage("Default")

var numOfSlots=Math.min(midiremote_api.mDefaults.getNumberOfInsertEffectSlots(),selectFavouriteButtonsMIDICCs[2].length) //Some users (including me) might not want to control all (16) available slots. At the same time, even if we do wish so, if using a Cubase version below the Artist, we won't have that many slots

for(var i=0;i<labelsHeaders.length;i++){

	page.setLabelFieldText(labelsHeaders[i],labelHeadersTitles[i])

}

for(var i=0;i<numOfSlots;i++){
	
	page.setLabelFieldText(labels[2][i],(i+1).toString())

}

var daMixer=page.mHostAccess.makeDirectAccess(page.mHostAccess.mMixConsole) //This is the direct access object we will use for handling the plugin manager and the selection of the plugins

var selectedTrackMixerChannel=page.mHostAccess.mTrackSelection.mMixerChannel //We will use this for ensuring that we set a plugin to an instrument slot.
//This (at least for now) is important, since we may try to accidentally set a plugin to a MIDI Track, or a Drum Track etc

var instrumentSlot=selectedTrackMixerChannel.mInstrumentPluginSlot //We will use this to grab the objectID of the instrument slot when we want to set a plugin to it

var insertsViewerSlots=[] //These will hold the insert fx slots. We will use them for getting their object IDs and properly set the plugin we want to them 

var selectedInsertSlot=0
var selectedInstrumentOrPlugin=0
var selectedInstrumentOrPluginIndex=-1

for(var i=0;i<numOfSlots;i++){

	var insertViewerSlot=page.mHostAccess.mTrackSelection.mMixerChannel.mInsertAndStripEffects.makeInsertEffectViewer("insertSlot"+i)

	insertViewerSlot.accessSlotAtIndex(i)

	insertsViewerSlots.push(insertViewerSlot)

	var customHostSelectSlot=page.mCustom.makeHostValueVariable("customSelectSlot"+i) //We will use this for selecting the insert slot we want

	page.makeValueBinding(buttons[2][i],customHostSelectSlot).mOnValueChange=function(activeDevice,activeMapiing,value,diff){

		if(value==1){

			buttons[2][this.i].setProcessValue(activeDevice,0)

			if(this.i!=selectedInsertSlot){

				surfaceButtons[2][selectedInsertSlot].mSurfaceValue.setProcessValue(activeDevice,0)
				
				selectedInsertSlot=this.i
				
				surfaceButtons[2][selectedInsertSlot].mSurfaceValue.setProcessValue(activeDevice,1)
			
			}
		
		}
	
	}.bind({i})

}

var tmpCounter=0
for(var i=0;i<2;i++){

	for(var j=0;j<buttons[i].length;j++){

		var customHostSelectPlugin=page.mCustom.makeHostValueVariable("customHostSelectPlugin"+tmpCounter) //This will be used for selecting a plugin we want to set to a slot (instrument or insert)

		tmpCounter++

		page.makeValueBinding(buttons[i][j],customHostSelectPlugin).mOnValueChange=function(activeDevice,activeMapping,value,diff){

			if(value==1){

				buttons[this.i][this.j].setProcessValue(activeDevice,0)

				setPlugin(activeMapping,this.i,this.j) //This is the function for selecting the plugin indexed [i] for setting it to either the instrument or an insert slot [j= 0: instrument, 1: insert]

				selectedInstrumentOrPlugin=this.i 
				selectedInstrumentOrPluginIndex=this.j 

				surfaceButtons[selectedInstrumentOrPlugin][selectedInstrumentOrPluginIndex].mSurfaceValue.setProcessValue(activeDevice,1) //Turning the lamp on for visual feedback

				customDelayVar.setProcessValue(activeDevice,customDelayCycles) //activating the delay for turning the lamp off

			}

		}.bind({i,j})

	}

}

customDelayVar.mOnProcessValueChange=function(activeDevice,value,diff){

	value--

	if(value>0){

		customDelayVar.setProcessValue(activeDevice,value)

	} else {

		surfaceButtons[selectedInstrumentOrPlugin][selectedInstrumentOrPluginIndex].mSurfaceValue.setProcessValue(activeDevice,0)

	}

}

function setPlugin(activeMapping,instrumentOrFx,pluginIndexInCollection){

	//Here we're setting the chosen plugin at the desired slot

	var objectID=-1

	var channelID=selectedTrackMixerChannel.getRuntimeID(activeMapping)
	var channelType=daMixer.getObjectTypeName(activeMapping,channelID)

	if(instrumentOrFx==0 && channelType=="Synth"){
		
		//Making sure that we are in an instrument track
		
		objectID=instrumentSlot.getRuntimeID(activeMapping) //When we want to set to the instrument slot, we're choosing the instrumentSlot object and recalling its runtime ID using the method getRunTimeID
	
	} else if(instrumentOrFx==1 && channelType!="MidiChannel"){
		
		//Making sure we are NOT in a MIDI track
		
		objectID=insertsViewerSlots[selectedInsertSlot].getRuntimeID(activeMapping) //When we want to set to an FX slot, we're choosing the insertViewer object for this slot, and recalling its runtime ID using the method getRunTimeID

	}
		
	if(objectID<1) return //This means that we made an illegal track selection earlier

	var manager=daMixer.mPluginManager //This is the plugin manager, introduced in API 1.3

	if(favouritePluginsUIDs[instrumentOrFx].length<pluginIndexInCollection+1) return //Needed when our favourite plugin buttons are more than the collection size

	manager.trySetSlotPlugin(activeMapping,objectID,favouritePluginsUIDs[instrumentOrFx][pluginIndexInCollection],false) //Here we see the syntax of the method trySetSlotPlugin
	//We provide (apart from the activeMapping):
	// - the objectID which is the ID of the instrument or an fx slot
	// - the UID of the plugin we want
	// - a flag set to true or false. If set to true, we will get a warning for changing the plugin in the slot

}

var fetchPluginsFinished=[false,false] //These vars will be set to true, once we have fetched their corresponding collection

function loadPlugins(activeMapping,instrumentOrFx){

	//Here I will scan the mixConsole channels
	//I want to find an instrument track, when needing the instrument plugins collections, or any track (except from MIDI) for the fx plugins collections
	//Identifying the type of a track is easy with the new getObjectTypeName method in API 1.3
	//Instrument tracks are of type "Synth"

	var mixerID=daMixer.getBaseObjectID(activeMapping)
	//This is our basis, the mixConsole ID

	if(mixerID==-1) return 

	var numOfChannels=daMixer.getNumberOfChildObjects(activeMapping,mixerID)
	//Here we get the number of available channels in the mixConsole
	//Even on an empty project, we should still have at least an output normally

	if(numOfChannels<1) return 

	var channelIDToBeUsed=-1

	for(var i=0;i<numOfChannels;i++){
	
		var channelID=daMixer.getChildObjectID(activeMapping,mixerID,i)
		var channelType=daMixer.getObjectTypeName(activeMapping,channelID)
		
		if(channelType=="Synth" || (channelType!="MidiChannel" && instrumentOrFx==1)){
	
			channelIDToBeUsed=channelID
			break 
	
		}
	
	}

	if(channelIDToBeUsed==-1) return

	var finalObjectID=-1

	if(instrumentOrFx==0){
	
		finalObjectID=daMixer.getChildObjectID(activeMapping,channelIDToBeUsed,9)
		//When we find an instrument track, we need its 10th child (index 9), which is its instrument slot. THIS is the final object we will use for quering the instrument plugin collections later
	
	} else {

		var insertsID=daMixer.getChildObjectID(activeMapping,channelIDToBeUsed,3)
		//The insert slots object is the 4th child of the track (index 3)
	
		finalObjectID=daMixer.getChildObjectID(activeMapping,insertsID,0)
		//Here I'm getting the ID of the very first insert slot, which is the first child of the insert slots object (index 0)
	
	}

	if(finalObjectID==-1) return 

	fetchPluginsFinished[instrumentOrFx]=true 

	var manager=daMixer.mPluginManager

	var numOfCollections=manager.getNumberOfPluginCollections(activeMapping, finalObjectID) //Getting the total count of available collections for this slot

	for(var i=0;i<numOfCollections;i++){

		var collection=manager.getPluginCollectionByIndex(activeMapping, finalObjectID,i)
		//we get a collection by its index
		//Every collection is an object which stores its name (mName) and the list of the plugins in the collection (mEntries)

		var collectionName=collection.mName.trim().toLowerCase()
		
		if(collectionName==favouriteCollectionsNames[instrumentOrFx].trim().toLowerCase()){

			//Favourite collection found
			var mEntries = collection.mEntries
			//This is the list of the plugins in the collection

			var minEntries=Math.min(mEntries.length,selectFavouriteButtonsMIDICCs[instrumentOrFx].length)

			if(minEntries>0){

				for(var j=0;j<minEntries;j++) {
			
					var entry=mEntries[j]
					//This is an element of the plugins list
					
					//It is an object with keys:

					//mPluginName -> the name of the plugin
					//mPluginVendor -> the vendor 
					//mPluginVersion -> the version as provided by the vendor
					//mFormatVersion -> the format (example: VST 3.7.7)
					
					//mSubCategories -> The categories of the plugin, separated by | (examples: Instrument|Synth, Fx|Delay)
					
					//mCollectionPath -> the collection path. If the plugin is inside a folder or a subfolder, it will return the folder or subfolder (example: /folder1). This comes handy when we want a more accurate representation in an external UI
					
					//and finally the most important one, the mPluginUID. This is the one we need for storing, since this will be used upon setting a plugin to a slot 

					var pluginName = entry.mPluginName
					
					var pluginUID = entry.mPluginUID
			
					favouritePluginsUIDs[instrumentOrFx].push(pluginUID)
				
					page.setLabelFieldText(labels[instrumentOrFx][j],pluginName)
				
				}
			
			}

			break
			
		}

	}

}

page.mOnActivate=function(activeDevice,activeMapping){

	surfaceButtons[2][0].mSurfaceValue.setProcessValue(activeDevice,1) // Showing that the first insert slot is activated when script starts

}

page.mOnIdle=function(activeDevice,activeMapping){

	//Here we take advantage of the mOnIdle event
	
	//We keep scanning the mixConsole until we find an instrument track for getting the instrument collection, and for a track of any type (other than MIDI) for getting the fx collection

	if(fetchPluginsFinished[0]==false){

		loadPlugins(activeMapping,0)

	}

	if(fetchPluginsFinished[1]==false){

		loadPlugins(activeMapping,1)

	}



}

//Assigning MIDI Remote handles to keyboard shortcuts using PLEs, no external utility used

//The idea is this:

//At the later versions of MR API, we can use direct access object for altering the names of channels

//At the same time, we can always get the name of the selected channel

//This means that we can "sniff" the name, and if it contains a special character sequence, we can then perform whichever action we want inside our MIDI Remote, and at the same time restore the name of the selected channel by replacing the special character sequence we appended using the PLE

//So if we construct a PLE, say for example ->

				// Target filters
				// 		Container Type is → Equal → Track
				// 		Property → Property Is Set → Is Selected

				// Transform actions
				// 		Name → Append → [* An Integer here *]
						
				//Action → Transform

// we can then assign it to a shortcut using the ordinary procedure from the key commands menu of Cubendo

// And then, when we execute it, it will append the special keyword to the selected channel name

// Then, our MIDI Remote will read it, and parse the "Integer" we've put inside the placeholders in our PLE

// By having an array of custom process variables, we can then call the one indexed the same as the integer we just parsed, and execute an assigned function to it, and

// finally, we restore the channel name. No harm made (I hope)

//Getting shortcuts from altered selected track name, using PLE preset
var maxCustomVars=12
var pleCustomVars=[]
for(var i=0;i<maxCustomVars;i++){
	
	pleCustomVars.push(surface.makeCustomValueVariable("pleCustomVar"+i))

}

var regExp=/\[\*(\d+)\*\]/ //This searches inside a string for the format [*Integer*] , examples: Track01 [*1*], Bass [*4*] 
selectedTrackMixerChannel.mOnTitleChange=function(activeDevice,activeMapping,title){

	var match = title.match(regExp)

	if(match){
    	
		var number = parseInt(match[1], 10)
		var newTitle=title.replace(regExp,"")
		if(newTitle.length==0){newTitle=" "}
		
    	daMixer.setParameterDisplayValue(activeMapping,selectedTrackMixerChannel.getRuntimeID(activeMapping),1024,newTitle)
		
		if(number<maxCustomVars){
			
			pleCustomVars[number].setProcessValue(activeDevice,1)
		
		}
		
	}

}

//Here we can bind these ple-driven custom variables to whatever MR function we want

//In this example, I will drive the first 4 vars to selecting a corresponding favourite instrument, next 4 for favourite fx plugins, and another group of 4 for choosing one of the first 4 inserts slots

var pleCustomVarIndex=0

for(var i=0;i<buttons.length;i++){
	
	
	for(var j=0;j<buttons[i].length;j++){
	
		pleCustomVars[pleCustomVarIndex].mOnProcessValueChange=function(activeDevice,value,diff){
	
			if(value==1){
	
				pleCustomVars[this.pleCustomVarIndex].setProcessValue(activeDevice,0)
				
				buttons[this.i][this.j].setProcessValue(activeDevice,1)
	
			}
	
		}.bind({i,j,pleCustomVarIndex})

		pleCustomVarIndex++
	
	}

}

I have heavily commented things (heavily, relative to my very bad habit of not commenting at all) when I thought that some further info is needed.

And here’s the midiRemote file that needs to be installed by anyone wishing to give it a try:
Test_Select Favourite Plugin.midiremote (21.3 KB)

Some notes about the installation:

  • This script needs one MIDI Input Port, I’ve chosen a port named “loopMIDI Port 2” under Windows and “IAC Driver Bus 3” , however you can always choose a different port as long as you have one installed.
  • Inside the script folder, I have a sub folder containing 12 PLE presets for providing the shortcuts functionality described above. You should copy these presets to the User Presets Folder for PLEs.
  • If you open a project which does not already contain an instrument track, you will notice that the row of Favourite Instrument Plugins is not populated. This is due to the design of the plugin manager, which correctly expects a compatible object in order to properly get the collections it can handle.
  • My final note: Some time ago, a good friend in this forum, asked me whether it is mandatory to use MIDI Ports. Well, it’s MIDI Remote we’re talking about, so the trivial answer should be yes. Still, we can actually create remotes with no MIDI ports at all. I’m not sure if this is by design, or a miss, but what works works :thinking: :downcast_face_with_sweat: I must be clear though, I would never suggest this, since I have no idea what implications this may have, but it’s there.
    The only issue you’ll immediately notice with this approach is that Cubendo won’t remember our script upon the next launch, and this is totally logical, since MIDI Port names are crucial for auto recognition of our remotes. So, one should reselect the script and activate it.
    In my script, at the top of it, you can find this line:
    εικόνα
    If you change true to false, the script will not provide a MIDI Port Detection Unit to Cubendo and when we open the MIDI Remote tab, and want to select it, we will see this:

    No ports available. However we can activate the script.

A short video demonstrating this script:

Your script works great, thanks a lot! I do have one problem though; every time I open Cubase I have to reinstall this MIDI remote. Do you know how to have the MIDI remote stuck so I don’t have to reinstall it every time I open Cubase?

Do you use the option of starting it with no MIDI port or do you have a specific MIDI port other than the one in my script?

Thanks a lot for your reply, I just found the cause of the problem: I stupidly renamed the existing IAC Driver as IAC Driver Bus 3 instead of adding 2 more busses to the existing one. Once I did this step Cubase started to automatically load the MIDI Remote. Very useful script now I want to try the one you mentioned in the other thread!

@m.c Is it possible to load a track preset (insert chain) using that method?

Unfortunately no. See this post for how I personally handle such cases: How to create a macro that loads a track preset - #2 by m.c

The case of insert chain, however, is doable to an extent, but without using presets.
If for example, we hard-code in the script, load this delay at slot 1, with these parameters altered, load this compressor at slot 2, etc, yes it’s totally doable. But I stay away from this approach, I prefer the presets method. If you really want to dive into something like this, I can help obviously.

Hi @m.c !

I am going to try to implement this, using the version 1.3 of MR API.

For loading the version 1.3, do we have to specify 1.3 or just 1 as always?

I mean this:
var midiremote_api = require(‘midiremote_api_v1’)

or this:
var midiremote_api = require(‘midiremote_api_v1.3’)

Thanks!

For compatibility reasons, Steinberg kept the original name. You can see it in my snippet as well, there’s no 1.3 reference, thankfully :slight_smile:
So use the “old” one: var midiremote_api = require('midiremote_api_v1')