Generic Remote to Remote Midi Conversion Using JS

Hi,

I thought I just share my perhaps poorly written conversion in case it will save some else some time down the road. I simply parsed through the generic remote XMLs to save my time from editing hundred of entries from my generic remotes. The below script will take a given path to the generic remote XML, and hopefully (worked on my end with my 7 generic remotes) convert it to a JSON file in the same directory this script runs.

this will require Node & a XML to JSON package

// Warning: If the generic remote has any command that is NOT CONNECTED, the
// script will fail. This is because if there is a command that is not
// connected, it is not saved in the XML as a "blank connected" command, rather
// Cubase will skip the node entirely in the bank. This will result in <ctrltable> having more
// entries than <bank>. I simply just removed any non-connected entries in my
// generic remote, save up a new version of the xml before using this script below.

import { readFileSync, writeFile } from "fs"
import { parse } from "path"
import { xml2json } from "xml-js"

main("PATH_TO_XML")

function main(filepath) {
  const readXML = readFileSync(filepath, { encoding: "utf-8" })
  const xmlToString = xml2json(readXML, {
    compact: true,
    spaces: 4,
    ignoreDeclaration: true,
  })
  const xmlToJSON = JSON.parse(xmlToString).remotedescription

  const itemsCtrl = xmlToJSON.ctrltable.ctrl
  const itemsBank1 = xmlToJSON.bank?.entry ?? xmlToJSON.bank[0]?.entry // could be multiple banks
  const itemsToBtnJSON = []

  if (itemsCtrl.length !== itemsBank1.length) {
    console.log(itemsCtrl.length, xmlToJSON.bank?.entry.length, "lengths don't match")
    return
  }

  itemsCtrl.forEach((item, i) => {
    if (itemsBank1[i].value !== undefined) {
      itemsToBtnJSON.push({
        i,
        chan: item.chan,
        addr: item.addr,
        category: itemsBank1[i]?.value?.device,
        action: itemsBank1[i]?.value?.chan,
      })
      return
    }
    itemsToBtnJSON.push({
      i,
      chan: item.chan,
      addr: item.addr,
      category: itemsBank1[i]?.command?.category,
      action: itemsBank1[i]?.command?.action,
    })
  })

  writeToFiles(itemsToBtnJSON, filepath)
}

function writeToFiles(data, filepath) {
  writeFile(
    `./${parse(filepath).name}.json`,
    JSON.stringify(data),
    { flag: "a+" },
    (err) => {
      console.log(err)
    }
  )
}

package.json

{
  "name": "_scratchpad-js",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "type": "module",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "xml-js": "^1.6.11"
  }
}

After, I just created a for loop to create a series of buttons in the remote midi API, reading the JSON created from the above script. I also added a small perhaps bad math example to create a grid of buttons. Here’s what I did below:

//-----------------------------------------------------------------------------
// 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(
  "Test Studios",
  "genericRemote_01",
  "Test Studios"
)

// 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
deviceDriver
  .makeDetectionUnit()
  .detectPortPair(midiInput, midiOutput)
  .expectInputNameEquals("MIDI_In")
  .expectOutputNameEquals("MIDI_Out")

//-----------------------------------------------------------------------------
// 2. SURFACE LAYOUT - create control elements and midi bindings, midi cc channels are 0 based
//-----------------------------------------------------------------------------
function bindMidiCC(button, chn, num) {
  button.mSurfaceValue.mMidiBinding
    .setInputPort(midiInput)
    .bindToControlChange(parseInt(chn), parseInt(num))
}

var genericRemote_01 = require("./imports/genericRemote_01.js")
var midiCCBinds = genericRemote_01.midiCCBinds

var button = []
var btnPos = 0
var yPos = 0

// creating a grid of buttons
for (var index = 0; index < midiCCBinds.length; ++index) {
  if (btnPos >= 9) {
    btnPos = 0
    yPos = yPos + 1
  }
  var x = btnPos++ * 2
  var y = yPos
  var w = 2
  var h = 1
  var newButton = deviceDriver.mSurface.makeButton(x, y, w, h)
  button.push(newButton)
}

//-----------------------------------------------------------------------------
// 3. HOST MAPPING - create mapping pages and host bindings
//-----------------------------------------------------------------------------
var page = deviceDriver.mMapping.makePage("Test Page 1")

for (var index = 0; index < midiCCBinds.length; index++) {
  var btn = midiCCBinds[index]
  bindMidiCC(button[btn.i], btn.chan, btn.addr)
  page.makeCommandBinding(button[btn.i].mSurfaceValue, btn.category, btn.action)
}

picture of the button grid

Hopefully, this saves someone else sometime. Feel free to make changes.

Cheers,
DMDComposer

6 Likes

Hi,

Wow, very great job! I had the same idea, I just haven’t find time to realise it so far.

1 Like

How awesome!

Thanks a lot for sharing this!

Thomas

1 Like