Dynamic midi listener via Midi Remote Script?

Lets take this script as a starting point:

//-----------------------------------------------------------------------------
// 1. DRIVER SETUP - create driver object, midi ports and detection information
//-----------------------------------------------------------------------------

// get the api's entry point
var midiremote_api = require('midiremote_api_v1')

// create the device driver main object
var deviceDriver = midiremote_api.makeDeviceDriver('AHK', 'AHK_v2', 'Thomas')

// create objects representing the hardware's MIDI ports
var midiInput = deviceDriver.mPorts.makeMidiInput()
var midiOutput = deviceDriver.mPorts.makeMidiOutput()

// define all possible namings the devices MIDI ports could have
// NOTE: Windows and MacOS handle port naming differently
deviceDriver.makeDetectionUnit().detectPortPair(midiInput, midiOutput)
    .expectInputNameEquals('loopMIDI Port 3 AHK')
    
//-----------------------------------------------------------------------------
// 2. SURFACE LAYOUT - create control elements and midi bindings
//-----------------------------------------------------------------------------

// create control element representing your hardware's surface
var button1 = deviceDriver.mSurface.makeButton(0, 0, 0.5 , 0.5)


// bind midi ports to surface elements
button1.mSurfaceValue.mMidiBinding.setInputPort(midiInput).bindToControlChange(15, 119) // channel 15, cc 21


//-----------------------------------------------------------------------------
// 3. HOST MAPPING - create mapping pages and host bindings
//-----------------------------------------------------------------------------

// create at least one mapping page
var page = deviceDriver.mMapping.makePage('Default')

// create host accessing objects
var hostSelectedTrackChannel = page.mHostAccess.mTrackSelection.mMixerChannel


// bind surface elements to host accessing object values
page.makeCommandBinding(button1.mSurfaceValue, 'AddTrack', 'Instrument')

Is it possible to have the script listen for any cc message, and based on the message, binds a command? The only way I would think of to do this now is to make a surface element for each command. Is there any limit to the amount of buttons?

Bonus question? is it possible to listen for incoming sysex? I don’t see any bindToSysex function in the API so probably not I guess.

If I can get all this to work, I could make a very cool AutoHotKey ^.^

Hi,

Do you mean based on the Message (i.e. MIDI CC1 or MIDI CC2), or do you mean based on the Value (i.e. MIDI CC1 value 10, or MIDI CC1 value 20)?

Based on the message. Ideally I’d want to use program changes but those are not available. I tried making 127 * 16 buttons and it seems to be able to handle it though.

Hi,

In this case, yes, you can do so. You can use RPN and NRPN to get 14bit values.

No.

Yes.

midiInput.mOnSysex=function(activeDevice,sysex){

    //sysex is an array of bytes, sysex[0] first byte, sysex[1] second and so on...

}
1 Like

Could you then also fire a command based on the incoming sysex?

Provide an example with some pseudo code.

Hi,

Be aware, that SysEx data are quite “expensive”. There is much more data transfer needed in comparison to the MIDI CCs. So using them in real-time is rather limited.

1 Like

True, Martin. Usually we tend to use them for “states” reading, concerning the controller, and some more exotic things like for example when wanting to read a Mackie response. This is actually why I’ve asked for an example earlier, in order to avoid if possible the sysex option.

Yeah I think SysEx is not really needed. Program changes would be perfect but cc data will probably do the trick as well.

Pseudo code how I would prefer it:

lookupTable = [
[{category: “Edit”, command: “Undo”}, {category: “Edit”, command: “Redo”}],
[{category: “Edit”, command: “Open”}, {category: “Edit”, command: “Preferences…”}], //etc…
]

onMidiCC => {
//check for incoming cc messages
var channel = received.channel
var cc = received.cc
var command = lookupTable[channel][cc]

executeCommand(command.category, commmand.command)
}

But i’ll probably have to generate the whole lookup table first, and then make a button for each command. But if you have an easier solution, let me know! :slight_smile:

Thanks for all the help!

Here’s a snippet:

var lookupTable = [
    
    {ch: 0, cc: 1, category: 'Edit', command: 'Undo'},
    {ch: 0, cc:2, category: 'Edit', command: 'Redo'},
    {ch: 0, cc: 3, category: 'Edit', command: 'Open'},
    {ch: 0, cc: 4, category: 'Edit', command: 'Preferences…'}

]

//define the page prior to this:

lookupTable.forEach(function(assignment){

    var customVar=surface.makeCustomValueVariable("ch"+assignment.ch+"cc"+assignment.cc)
    customVar.mMidiBinding
        .setInputPort(midiInput)
        .bindToControlChange(assignment.ch,assignment.cc)

    page.makeCommandBinding(customVar,assignment.category,assignment.command)

})

I referred to the MIDI Remote API documentation’s Command Bindings. Since Steinberg doesn’t directly provide a snippet list, you have to click each item one by one. Therefore, I created a macro in Keyboard Maestro to simulate the clicks and another macro to generate the code.

Cool, that looks like what i’m trying to make with AutoHotkey! A Dorico Jump Bar feature for Cubase :slight_smile:

Could you explain how RPN and NRPN could get me more options? As far as i understand it it still leaves me with only 16 * 127 options to assign to elements right?

Hi,

(N)RPN are 16 * 127 * 127 = 258.064, as they are 14bit messages.

Well, strictly speaking, this is actually 128*128 :slight_smile: And if we add the fine adjustment, it’s another *128.

Anyway, in the MR concept with scripting, and when we’re talking strictly about commands and not parameters, CC is already a good choice, since it can give us 16 channels * 128 cc numbers * 128 values.

Example:

var aCC=surface.makeCustomValueVariable("aCC")
aCC.mMidiBinding.setInputPort(midiInput).bindToControlChange(0,20)

var customProcessesPerValue=[]
for(var i=0;i<128;i++){

    customProcessesPerValue.push(surface.makeCustomValueVariable("aCC"+i))

}

aCC.mOnProcessValueChange=function(activeDevice,value,diff){
    
    var value127=Math.round(127*value)
    customProcessesPerValue[value127].setProcessValue(activeDevice,1)
    
}

page.makeCommandBinding(customProcessesPerValue[0],'Edit','Undo')
page.makeCommandBinding(customProcessesPerValue[1],'Edit','Redo')
//and so on, up to 127...
page.makeCommandBinding(customProcessesPerValue[127],'File','Save')

The above obviously is not for normal controllers (using buttons and knobs, etc) but rather for custom software implementations, where one can for example assign a shortcut to a very specific cc/value combination.

1 Like

Ah, so that is how you also make seperate commandbindings based on values. I tried to create a function that generates this automatically but can’t get it to work. Altered the code a bit so it is easier to read:

//-----------------------------------------------------------------------------
// 1. DRIVER SETUP - create driver object, midi ports and detection information
//-----------------------------------------------------------------------------

// get the api's entry point
var midiremote_api = require("midiremote_api_v1");

// create the device driver main object
var deviceDriver = midiremote_api.makeDeviceDriver("AHK", "AHK_v2", "Thomas");

// create objects representing the hardware's MIDI ports
var midiInput = deviceDriver.mPorts.makeMidiInput();
var midiOutput = deviceDriver.mPorts.makeMidiOutput();

// define all possible namings the devices MIDI ports could have
// NOTE: Windows and MacOS handle port naming differently
deviceDriver
  .makeDetectionUnit()
  .detectPortPair(midiInput, midiOutput)
  .expectInputNameEquals("loopMIDI Port 3 AHK");

  var jsonData = {
    "02,119,001": {
      Preferences: "Controls - Value Box/Time Control Mode",
    },
    "02,119,002": {
      Preferences: "MediaBay - Scan Folders only when MediaBay is open",
    },
    // etc...
}
//-----------------------------------------------------------------------------
// 2. SURFACE LAYOUT - create control elements and midi bindings
//-----------------------------------------------------------------------------

var page = deviceDriver.mMapping.makePage("Default");

/**
 * @param {number} amount
 */

function createCustomValueBindings(amount) {
  var customValueVariables = [];
  var prevCC = 0;
  var prevChannel = 0;

  for (var i = 0; i < amount; i++) {
    var key = Object.keys(jsonData)[i];
    var seperate = key.split(",");
    var chString = seperate[0];
    var ccString = seperate[1];
    var channel = parseInt(chString);
    var cc = parseInt(ccString);

    // if cc and channel already have been used, skip this one
    if (cc === prevCC && channel === prevChannel) {
      continue;
    }
    console.log("cc: " + cc + " and channel: " + channel);
    var mainElement = deviceDriver.mSurface.makeCustomValueVariable("ch" + channel + "cc" + cc);

    mainElement.mMidiBinding.setInputPort(midiInput).bindToControlChange(channel, cc);

    var elementPerValue = [];

    for (var j = 1; j < 128; j++) {
      elementPerValue.push(
        deviceDriver.mSurface.makeCustomValueVariable("ch" + channel + "cc" + cc + "v" + j)
      );
    }
    mainElement.mOnProcessValueChange = function (activeDevice, value, diff) {
      var value127 = Math.round(127 * value);
      elementPerValue[value127].setProcessValue(activeDevice, 1);
    };

    for (var k = 1; k < 128; k++) {
      var valueString;
      if (k < 10) {
        valueString = "00" + k;
      } else if (k >= 10 && k < 100) {
        valueString = "0" + k;
      } else {
        valueString = k.toString();
      }
      var string = chString + "," + ccString + "," + valueString;
      var command = jsonData[string];

      if (!command) {
        break;
      }
      var category = Object.keys(command)[0];
      var name = command[category];
      page.makeCommandBinding(elementPerValue[k - 1], category, name);
    }
    customValueVariables.push(mainElement);
    prevCC = cc;
    prevChannel = channel;
  }

  return customValueVariables;
}

// Generate bindings for 128 CC numbers starting from the specified CC
var customBindings = createCustomValueBindings(Object.keys(jsonData).length);

Any idea why it is not working? When I check the mapping page inside cubase, I can see all the mappings in the list. But when sending the midi cc data, nothing happens.

Yes. For example, you have a continue and this prevents the second entry to be read. Unfortunately, I don’t have much time for debugging codes of others, hopefully another user may chime in.
Anyway, here’s an altered version, tested here:

var midiremote_api = require("midiremote_api_v1")
var deviceDriver = midiremote_api.makeDeviceDriver("AHK", "AHK_v2", "Thomas")

var midiInput = deviceDriver.mPorts.makeMidiInput("MIDI Input")
var midiOutput = deviceDriver.mPorts.makeMidiOutput("MIDI Output")

deviceDriver.makeDetectionUnit().detectPortPair(midiInput, midiOutput)
  .expectInputNameEquals("loopMIDI Port 3 AHK")

var jsonData ={
    
    "2,119":
        [

            [1,'Preferences','Controls - Value Box/Time Control Mode'],
            [2,'Preferences','MediaBay - Scan Folders only when MediaBay is open']
                    
        ]

}


var surface=deviceDriver.mSurface

var page = deviceDriver.mMapping.makePage("Default")

function createCustomValueBindings(){
  
    Object.keys(jsonData).forEach(function(channelAndCC){

        var channelAndCcSplit=channelAndCC.split(",")
        var channel = +channelAndCcSplit[0]
        var cc = +channelAndCcSplit[1]
        var mainElement = surface.makeCustomValueVariable("ch" + channel + "cc" + cc)
        mainElement.mMidiBinding.setInputPort(midiInput).bindToControlChange(channel, cc)

        var mainElementsArrOfValues=[]
        var customProcesses=[]

        var bindings=jsonData[channelAndCC]
        
        bindings.forEach(function(binding){

            var ccValue=+binding[0]

            if(mainElementsArrOfValues.indexOf(ccValue)==-1){

                var commandCategory=binding[1]
                var commandName=binding[2]

                var customProcess=surface.makeCustomValueVariable("ch"+channel+"cc"+cc+"val"+ccValue)
                customProcesses.push(customProcess)
                
                page.makeCommandBinding(customProcess,commandCategory,commandName)
                
                mainElementsArrOfValues.push(ccValue)

            }
            
        })

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

            var value127=Math.round(127*value)
            var index=mainElementsArrOfValues.indexOf(value127)
        
            if(index>-1){
        
                customProcesses[index].setProcessValue(activeDevice,1)
        
            }
        
        }
        
    })

}

createCustomValueBindings()

Thanks! Had some issues with your script sometimes working, sometimes not. No idea what caused it, but my script never worked and yours is more logical anyway.

Changed up my json data and now it works like a charm!