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