Extending the Focused Quick Controls to more than 8 using MIDI Remote

For me, 8 Focused Quick Controls are more than enough, since I do use them either at the very early stages of my drafts, so I need to quickly change “important” parameters only, or the late stages of mix/master, when again I tend to focus only on a few parameters. So 8 is more than OK for me.
However, not everyone is the same, nor the needs.

Here’s a small midi remote, which tries to extend the Focus functionality, so that we can handle more than the 8 focused quick controls.

Here’s the idea behind it:

MIDI Remote provides complete access to our Instrument and Inserts Quick Controls. These, contrary to the Focused ones, can be of no real limit, by using the concept of parameter banks. Controls can be banked by a specific parameters’ size, and we can have dedicated buttons for choosing previous/next/first bank. The bank size doesn’t even have to be 8. We can choose much bigger numbers.

So, if we find a way to know what is the nature and place of the focused plugin, we can set our remote to handle the instrument/insert slot corresponding to this plugin, and we can now have the new banking system helping us override the FQC limitation.

Using the relatively new Direct Access entity, we can query the path of the currently focused plugin, while at the same time, by having our channels mapped by their IDs, we can find both the slot (instrument or insert) the plugin lives in, and the channel that hosts it. So then, we can programmatically select the channel, and then set our remote to a subPage for handling the correct slot.

Here’s the midiRemote installation file, for those wanting to give this a try. I’ve set it to handle 16 parameters per bank. CB15.0.20 is required due to the MIDI Ports recognition unit I prepared. You can alter this in the code provided below, and use prior CB versions (down to 13.0.50 if I recall correctly).

Test_FQC Extender.midiremote (2.9 KB)

You will need a virtual MIDI Port to activate it.

And here’s the code for those interested in such implementations (without having to install the script):

// @ts-nocheck
var numOfParamsPerBank=16

var midiremote_api = require('midiremote_api_v1')
var deviceDriver = midiremote_api.makeDeviceDriver("Test", "FQC Extender", "mc")

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")

var detectionUnitMac = deviceDriver.makeDetectionUnit()
detectionUnitMac.mPlatformFilter.expectPlatformMacOS()
detectionUnitMac.detectSingleInput(midiInput)
	.expectInputNameEquals("IAC Driver Bus 3")
	 
var surface = deviceDriver.mSurface

var knobs=[]

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

	var knob=surface.makeKnob(i,1,1,1)
	knob.mSurfaceValue.mMidiBinding
		.setInputPort(midiInput)
		.bindToControlChange(0,i+3)

	knobs.push(knob)

}

var buttonPreviousBank=surface.makeButton(0,2.5,1,0.5)
buttonPreviousBank.mSurfaceValue.mMidiBinding
	.setInputPort(midiInput)
	.bindToControlChange(0,0)

var buttonNextBank=surface.makeButton(1,2.5,1,0.5)
buttonNextBank.mSurfaceValue.mMidiBinding
	.setInputPort(midiInput)
	.bindToControlChange(0,1)

var buttonResetBank=surface.makeButton(2,2.5,1,0.5)
buttonResetBank.mSurfaceValue.mMidiBinding
	.setInputPort(midiInput)
	.bindToControlChange(0,2)

var labelSubPage=surface.makeLabelField(0,0,3,0.5)

var mapping = deviceDriver.mMapping

var page=mapping.makePage("Default")

var numOfInsertSlots=midiremote_api.mDefaults.getNumberOfInsertEffectSlots()

var subPagesArea=page.makeSubPageArea("subPagesArea")
var subPages=[]
for(var i=0;i<numOfInsertSlots+2;i++){
	var subPagesName= i==0 ? "Track" : i==1 ? "Instrument" : "Slot "+(i-1)
	subPages.push(subPagesArea.makeSubPage(subPagesName))
}

page.setLabelFieldSubPageArea(labelSubPage,subPagesArea)

var fqc=page.mHostAccess.mFocusedQuickControls

var daMixer=page.mHostAccess.makeDirectAccess(page.mHostAccess.mMixConsole)
var daFQC=page.mHostAccess.makeDirectAccess(fqc)

var selectedTrack=page.mHostAccess.mTrackSelection.mMixerChannel
var instrumentQuickControls=selectedTrack.mInstrumentPluginSlot.mParameterBankZone 
var instrumentParams=[]
var insertViewers=[]
var insertParams=[]

var nullHostValue=page.mCustom.makeHostValueVariable("nullHostValue")

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

	var tmpNumOfParams= i==0 ? Math.min(numOfParamsPerBank,midiremote_api.mDefaults.getNumberOfQuickControls()) : numOfParamsPerBank

	for(var j=0;j<tmpNumOfParams;j++){

		if(i==0){

			page.makeValueBinding(knobs[j].mSurfaceValue,selectedTrack.mQuickControls.getByIndex(j)).setSubPage(subPages[0])
		
		} else if (i==1){

			instrumentParams.push(instrumentQuickControls.makeParameterValue())
			page.makeValueBinding(knobs[j].mSurfaceValue,instrumentParams[j]).setSubPage(subPages[1])
		
		} else {
		
			if(j==0){
		
				insertViewers.push(selectedTrack.mInsertAndStripEffects.makeInsertEffectViewer("insertViewer"+i))
				insertViewers[i-2].accessSlotAtIndex(i-2)
				insertParams.push([])
		
			}
		
			insertParams[i-2].push(insertViewers[i-2].mParameterBankZone.makeParameterValue())
			page.makeValueBinding(knobs[j].mSurfaceValue,insertParams[i-2][j]).setSubPage(subPages[i])
		
		}
	
	}

	if(i==0){

		page.makeValueBinding(buttonPreviousBank.mSurfaceValue,nullHostValue).setSubPage(subPages[0])

		page.makeValueBinding(buttonNextBank.mSurfaceValue,nullHostValue).setSubPage(subPages[0])

		page.makeValueBinding(buttonResetBank.mSurfaceValue,nullHostValue).setSubPage(subPages[0])
	
	} else if(i==1){
	
		page.makeActionBinding(buttonPreviousBank.mSurfaceValue,instrumentQuickControls.mAction.mPrevBank).setSubPage(subPages[1])

		page.makeActionBinding(buttonNextBank.mSurfaceValue,instrumentQuickControls.mAction.mNextBank).setSubPage(subPages[1])

		page.makeActionBinding(buttonResetBank.mSurfaceValue,instrumentQuickControls.mAction.mResetBank).setSubPage(subPages[1])
	
	} else {
	
		page.makeActionBinding(buttonPreviousBank.mSurfaceValue,insertViewers[i-2].mParameterBankZone.mAction.mPrevBank).setSubPage(subPages[i])

		page.makeActionBinding(buttonNextBank.mSurfaceValue,insertViewers[i-2].mParameterBankZone.mAction.mNextBank).setSubPage(subPages[i])

		page.makeActionBinding(buttonResetBank.mSurfaceValue,insertViewers[i-2].mParameterBankZone.mAction.mResetBank).setSubPage(subPages[i])

	}

	if(tmpNumOfParams<numOfParamsPerBank){

		for(var j=tmpNumOfParams;j<numOfParamsPerBank;j++){
		
			page.makeValueBinding(knobs[j].mSurfaceValue,nullHostValue).setSubPage(subPages[i])
		
		}
	
	}

}

var currentPluginPath=""

var focusedObjectID=-1

var channelsUniqueNames=[]
var channelsIds=[]


page.mOnActivate=function(activeDevice,activeMapping){

	daMixer.activate(activeMapping)
	daFQC.activate(activeMapping)

}

page.mOnDeactivate=function(activeDevice,activeMapping){

	daMixer.deactivate(activeMapping)
	daFQC.deactivate(activeMapping)

}

daMixer.mOnObjectChange=function(activeDevice,activeMapping,objectID){

	if(objectID==daMixer.getBaseObjectID(activeMapping)){

		var size=daMixer.getNumberOfChildObjects(activeMapping,objectID)-1

		if(size!=channelsUniqueNames.length){

			channelsUniqueNames=[]
			channelsIds=[]

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

				var channelID=daMixer.getChildObjectID(activeMapping,objectID,i)
				var name=daMixer.getObjectUniqueName(activeMapping,channelID)
				var uniqueID=daMixer.getObjectUniqueIDString(activeMapping,channelID)
				var channelName="VST Mixer\\Channels\\"+name
				channelsUniqueNames.push(channelName)
				channelsIds.push(channelID)
				
			}

		} 

	}		

}

daMixer.mOnObjectWillBeRemoved=function(activeDevice,activeMapping,objectID){

	var channelIDIndex=channelsIds.indexOf(objectID)

	if(channelIDIndex!=-1){
	
		channelsIds.splice(channelIDIndex,1)
		channelsUniqueNames.splice(channelIDIndex,1)
	
	}

}

daFQC.mOnObjectChange=function(activeDevice,activeMapping,objectID){

	 if(objectID==daFQC.getBaseObjectID(activeMapping)){

		var pluginPathTag=4
		var newPluginPath=daMixer.getParameterDisplayValue(activeMapping,objectID,pluginPathTag)
		
		if(newPluginPath!=currentPluginPath){
			
			currentPluginPath=newPluginPath
			
			if(currentPluginPath.length==0){
			
				//Track focus
				subPages[0].mAction.mActivate.trigger(activeMapping)
				return 
			}

			var channelPath=currentPluginPath.split("\\").slice(0,3).join("\\")

			var indexOfChannel=channelsUniqueNames.indexOf(channelPath)
			
			if(indexOfChannel!=-1){
			
				var channelID=channelsIds[indexOfChannel]
				var subPageToSwitch=-1
				
				var instrumentPrefix=channelPath+"\\Slot\\"
				var insertsPrefix=channelPath+"\\Inserts\\Slot"
				var start,end,slotIndex

				if(currentPluginPath.indexOf(instrumentPrefix)==0){
				
					subPageToSwitch=1

				} else {
					
					start=currentPluginPath.indexOf(insertsPrefix)

					if(start!==-1){

						start+=insertsPrefix.length
						end=currentPluginPath.indexOf("\\",start)
						
						slotIndex= end!=-1 ? parseInt(currentPluginPath.substring(start,end),10) : 1

						subPageToSwitch = isNaN(slotIndex) ? 2 : slotIndex+1
					
					}
				
				}

				if(subPageToSwitch!=-1){

					//Selecting the track which hosts the focused plugin
					var prevLock=daMixer.getParameterEditLockState(activeMapping,channelID,4000)
					daMixer.setParameterEditLockState(activeMapping,channelID,4000,true)
					daMixer.setParameterProcessValue(activeMapping,channelID,4000,1)
					daMixer.setParameterEditLockState(activeMapping,channelID,4000,prevLock)

					subPages[subPageToSwitch].mAction.mActivate.trigger(activeMapping)
				
				}
				
			} else {

				console.log("Channel not found. This shouldn't happen when project not empty.")
			
			}
		
		}
	 
	}
	
}

A video demonstrating the above. Watch how the MIDI Remote UI changes accordingly to the plugin (or track) we currently have in focus:

This is great. Thanks.

Do you know if these parameter names will be displayed on an external controller screen? Let’s say on an S49 mk3?

Hi, the way this script is structured, no.
However, If one knows the messages sent to the NI’s S MK3 series, (and I do know they have prepared a script for Cubase) it is totally doable, yes, when the device is in DAW mode. If I recall correctly, the displays are locked when in MIDI/User mode. Don’t you use the official implementation of NI for Cubase?

I was more giving NI as an example. I’m actually interested in how complex it could be to get other controllers to display these parameters. Things like the Korg Keystage or others with individual screens for each control.

Not particularly complex. If you know the sysex messages for the displays, it is pretty straight forward.

In this case, you can actually see how I implemented the display feedback using the proper sysex messages. Korg Keystage 49/61 - Custom MIDI Remote Script

This is so cool!
It gets me one step closer to the idea of building a unique MIDI controller that covers the essential functions of pretty much any synth: 3 oscillator blocks, mixer, ADSR sections for amp and filter

Thanks a lot @m.c for sharing this!

Hi @m.c

Thanks again for this — the FQC Extender approach is working great in my script.

While documenting my implementation, I ran into two things I couldn’t fully confirm and wanted to ask, since they came straight from your snippet:

1. pluginPathTag = 4
I understand this is the tag read via daMixer.getParameterDisplayValue(activeMapping, focusQCObjectID, 4) to get the path of the currently focused plugin. Is this tag officially documented anywhere, or is it something you found by logging, the same way as the track tags you shared in the “Full Scripting API” thread? If it’s the latter, do you happen to have a similar tag table for the FocusedQuickControls base object (the way you posted one for a track root)? That would help a lot in understanding what else is available there.

2. getParameterEditLockState / setParameterEditLockState — In the snippet, the edit lock is set to true before writing to tag 4000 (Selected), then restored to its previous state afterward. I couldn’t find any explanation of what this lock actually protects against (the official API reference only lists the method signature, no description). Is this something that’s required specifically for the “Selected” parameter, or is it a general precaution needed whenever writing to any DirectAccess parameter via setParameterProcessValue? And what would happen if the lock/unlock steps were skipped — would the write silently fail, get overridden, or something else?

Thanks again for all the work you put into documenting DirectAccess!!

Tag Index Title
1 0 locked
2 1 focusedDevicePath
3 2 selectedTrackDevicePath
4 3 pluginWindowDevicePath
5 4 isFollowingSelectedTrack
6 5 isFollowingPluginWindowFocus

Note that you can log pretty much anything using either code I posted, or more officially the one posted by Trappe here:

I used it following Trappe’s advice here:

Generally I don’t pay much attention to this, I mean I usually just alter the host value :fearful:

Never seen it fail. Most probably checking the lock state is for protecting the host value, i.e. don’t try to alter it when it’s locked by a process, rather than something else.

Thanks a lot for the reply, @m.c !

@m.c You’ve made my day!

I’d set up my Faderfox EC4 to control parameter on UAD SSL channelstrip plugin. But then I realized that (if I’m not using Quickcontrol) the controller isn’t updating when focusing on different plugin-windows. This only works with Quickcontrol 1-8, but I need all 16 knobs and 16 button on the EC4.

Your script solved it, that selects the appropriate track for the plugin, so that “Insert & strip effects parameters” functions as intended.

I wish Steinberg would expand the number of Quickcontrols, because control 1-8 works wonderfully :slight_smile: .

Again, thank you so much!

Although I think I’ve found a catch :smiley: . It works on regular tracks, group- and FX-tracks. But not on “Folder with Group Channel”. That is folders with built-in audio group. I think it is a new feature starting Cubase 15.0.20.

Could this be added?

Interesting. I never actually tested group folders. So, have you setup track quick controls with a group folder and then these qcs are not controllable using the script?

Just checked it, it seems to work with group folders too. In the following video, when I select a group folder track, the Quick controls are properly updated.

Hm, how weird. I’ll have to confirm this. Thanks for checking.

(2 min. later) I stand corrected :smiley:. Wtf? I’ll test this further during the evening.

Perhaps you’re changing track in the project window and you don’t have Preferences→Project & MixConsole→Sync Selection in Project Window and MixConsole enabled?

Sync Selection is enabled.