Icon Platform M+ MIDI Remote - should work with any MCU based controller

Hi @thomas_martin, if I’m not totally mistaken here, the next update will fix the multi-port issue.


Hi @Jochen_Trappe !

You just made my day!


@dimonskinke and @saxmand thanks for the sample code. That combined with the Cubase 12.0.50 midi remote API updates for VST Instrument parameters banks encouraged me to by a shiny new D2 display for my Icon Platform M+.

I’ve started work on an update script with Display support. The work in progress can be found on the branch add-display - GitHub - woodcockr/midiremote-userscripts at add-display

I modified the way labels are created for the display so they are CamelCase and remove vowels after the first letter then truncate to 6 characters. Volume → Vlm, My really long Text → MyRlly, Convolution Reverb On/Off → Cnvltn

I’m wondering if I can setup a timer call back so it can scroll long labels or something like that on the display. Also wondering about dropping the Value display on the 2nd line and instead using the extra space for longer labels since the value is evident on the Fader location.

Anyway, lot’s to play with now for the update.

If anyone else has updates that like or code to share feel free to list them here or put in a PR on the github page.



I’ve been playing around with the ChannelStrip implementation. It’s quite nice. Ended overcomplicating it as I would like to have some dynamic display when clicking the buttons. kinda managed it, but it’s a bit of a hack.

But here’s a simplified version that should be possible to just add straight in to your code:

function makePageChannelStrip() {
    var page = makePageWithDefaults('Channelstrip')

    var strip = page.makeSubPageArea('strip')
    var gatePage = makeSubPage(strip, 'Gate')
    var compressorPage = makeSubPage(strip, 'Compressor')
    var toolsPage = makeSubPage(strip, 'Tools')
    var saturatorPage = makeSubPage(strip, 'Saturator')
    var limiterPage = makeSubPage(strip, 'Limiter')

    var selectedTrackChannel = page.mHostAccess.mTrackSelection.mMixerChannel
    var stripEffects = selectedTrackChannel.mInsertAndStripEffects.mStripEffects

    for (var idx = 0; idx < surfaceElements.numStrips; ++idx) {
        var knobSurfaceValue = surfaceElements.channelControls[idx].pushEncoder.mEncoderValue;
        var knobPushValue = surfaceElements.channelControls[idx].pushEncoder.mPushValue;
        var faderSurfaceValue = surfaceElements.channelControls[idx].fader.mSurfaceValue;

        page.makeValueBinding(faderSurfaceValue, stripEffects.mGate.mParameterBankZone.makeParameterValue()).setSubPage(gatePage)
        page.makeValueBinding(faderSurfaceValue, stripEffects.mCompressor.mParameterBankZone.makeParameterValue()).setSubPage(compressorPage)
        page.makeValueBinding(faderSurfaceValue, stripEffects.mTools.mParameterBankZone.makeParameterValue()).setSubPage(toolsPage)
        page.makeValueBinding(faderSurfaceValue, stripEffects.mSaturator.mParameterBankZone.makeParameterValue()).setSubPage(saturatorPage)
        page.makeValueBinding(faderSurfaceValue, stripEffects.mLimiter.mParameterBankZone.makeParameterValue()).setSubPage(limiterPage)

  for (var idx = 0; idx < 5; ++idx) {
        var faderStrip = surfaceElements.channelControls[idx]
        var type = ['mGate', 'mCompressor', 'mTools', 'mSaturator', 'mLimiter'][idx]
        page.makeValueBinding(faderStrip.rec_button.mSurfaceValue, stripEffects[type].mOn).setTypeToggle()
        page.makeValueBinding(faderStrip.mute_button.mSurfaceValue, stripEffects[type].mBypass).setTypeToggle()

    page.makeActionBinding(surfaceElements.channelControls[0].sel_button.mSurfaceValue, gatePage.mAction.mActivate)
    page.makeActionBinding(surfaceElements.channelControls[1].sel_button.mSurfaceValue, compressorPage.mAction.mActivate)
    page.makeActionBinding(surfaceElements.channelControls[2].sel_button.mSurfaceValue, toolsPage.mAction.mActivate)
    page.makeActionBinding(surfaceElements.channelControls[3].sel_button.mSurfaceValue, saturatorPage.mAction.mActivate)
    page.makeActionBinding(surfaceElements.channelControls[4].sel_button.mSurfaceValue, limiterPage.mAction.mActivate)

    gatePage.mOnActivate = function (device) { setLeds(device, 24, 'Gate') }
    compressorPage.mOnActivate = function (device) { setLeds(device, 25, 'Compressor') }
    toolsPage.mOnActivate = function (device) { setLeds(device, 26, 'Tools') }
    saturatorPage.mOnActivate = function (device) { setLeds(device, 27, 'Saturator') }
    limiterPage.mOnActivate = function (device) { setLeds(device, 28, 'Limiter') }

    function setLeds(device, value, text) {
        console.log('from script: Platform M+ subpage "' + text + '" activated')
        for (var i = 0; i < 5; ++i) {
            midiOutput.sendMidi(device, [0x90, 24 + i, 0])
        midiOutput.sendMidi(device, [0x90, value, 127])

    return page

Nice @saxmand, I’ll add that in. Feel free to PRs into the github repo if you wish - of course pasting code here works just fine too.

I’m currently trying to figure out how to use label fields and things to sync nicely on the LCD. I also want to try adding some dynamics - when you move a control show the full name across the LCD and then once you let go change it back to the abbreviated version - and similar timed feedback (switch between value and name for a control every 2 seconds).
Looks like it might be possible but there is something about my understanding of how things work in javascript and the midiremote api that is tripping me up right now. I’ll figure it out eventually!

1 Like

@dimonskinke and @radianced Regarding the Mixconsole 1 visibility sync issue - I’ve tested this on my setup and its working as expected when I hide and make visible tracks.
Are you still having this problem? if so I’m going to guess it might be related to preferences and mixconsole1/project visbility sync (which is turned on in my setup). Martin suggested this in his response.

I feel the same. Keep running my head in to the wall, as I guess I don’t fully understand the implementation.

In terms of the dynamic LCD, I don’t think it’s possible it’s possible to do a wait method that is running multi thread meaning. The only way I was able to achieve a sort of wait function that changed the LCD after x seconds was using date.now() and that will lock the device.

The way I’ve ended being able to have some dynamic LCD is by having this dummy function.
It’s a bit simpler for the fader touch functions as we don’t use the “fader_touch” for anything.
So this should be possible to drop this code in to your various scripts.
And then just call the function in any of your pages and you should get the full name of a touched fader and in capitals :slight_smile:

//** ADD TO icon_platformmplus.js script */
function restoreFaderDisplay(activeDevice) {
    for (var idx = 0; idx < surfaceElements.numStrips; ++idx) {
        var faderSurfaceValue = surfaceElements.channelControls[idx];
        midiOutput.sendMidi(activeDevice, helper.sysex.displaySetTextOfColumn(idx, 1, makeStringMax6CharectersAndRemoveSpace(faderSurfaceValue.fader_title.value, idx)))
        midiOutput.sendMidi(activeDevice, helper.sysex.displaySetTextOfColumn(idx, 0, makeStringMax6CharectersAndRemoveSpace(faderSurfaceValue.fader_displayValue.value, idx)))

function touchFaderDisplayFeedback(page) {
    // Can be used for all page types
    var dummy = page.makeSubPageArea('Dummy')
    page.makeActionBinding(surfaceElements.channelControls[0].fader_touch.mSurfaceValue, dummy.mAction.mReset).mOnValueChange = function (activeDevice, mapping, value) {
        showFullControllerNameDisplayFeedback(activeDevice, value, 0)
    page.makeActionBinding(surfaceElements.channelControls[1].fader_touch.mSurfaceValue, dummy.mAction.mReset).mOnValueChange = function (activeDevice, mapping, value) {
        showFullControllerNameDisplayFeedback(activeDevice, value, 1)
    page.makeActionBinding(surfaceElements.channelControls[2].fader_touch.mSurfaceValue, dummy.mAction.mReset).mOnValueChange = function (activeDevice, mapping, value) {
        showFullControllerNameDisplayFeedback(activeDevice, value, 2)
    page.makeActionBinding(surfaceElements.channelControls[3].fader_touch.mSurfaceValue, dummy.mAction.mReset).mOnValueChange = function (activeDevice, mapping, value) {
        showFullControllerNameDisplayFeedback(activeDevice, value, 3)
    page.makeActionBinding(surfaceElements.channelControls[4].fader_touch.mSurfaceValue, dummy.mAction.mReset).mOnValueChange = function (activeDevice, mapping, value) {
        showFullControllerNameDisplayFeedback(activeDevice, value, 4)
    page.makeActionBinding(surfaceElements.channelControls[5].fader_touch.mSurfaceValue, dummy.mAction.mReset).mOnValueChange = function (activeDevice, mapping, value) {
        showFullControllerNameDisplayFeedback(activeDevice, value, 5)
    page.makeActionBinding(surfaceElements.channelControls[6].fader_touch.mSurfaceValue, dummy.mAction.mReset).mOnValueChange = function (activeDevice, mapping, value) {
        showFullControllerNameDisplayFeedback(activeDevice, value, 6)
    page.makeActionBinding(surfaceElements.channelControls[7].fader_touch.mSurfaceValue, dummy.mAction.mReset).mOnValueChange = function (activeDevice, mapping, value) {
        showFullControllerNameDisplayFeedback(activeDevice, value, 7)

    // Will uppercase and show the full title of a touched value
    function showFullControllerNameDisplayFeedback(activeDevice, value, idx) {
        if (value == 1) {
            var name = surfaceElements.channelControls[idx].fader_title.value       
            midiOutput.sendMidi(activeDevice, helper.sysex.make_Sysex_displaySetTextOfColumnFullString(idx, 1, name.toUpperCase() + ' '))
        else {

//** ADD TO helper.js script */

function make_Sysex_displaySetTextOfColumnFullString(columnIndex, textFieldIndex, textString) {
    var pos = columnIndex * 7 + textString.length < 56 ? columnIndex * 7 + textFieldIndex * 56 : 8 * 7 + textFieldIndex * 56 - textString.length + 1
    var data = [0xf0, 0x00, 0x00, 0x66, 0x14, 0x12,
        // setting the pos 1 earlier and adding a space to ensure a space between the words, except when it's the first pos
        columnIndex > 0 ? pos - 1 : pos]

    var text = (columnIndex > 0 ? ' ' : '') + textString
    for (var i = 0; i < text.length; ++i)


    return data

//** ADD TO icon_elements.js */

// function makeChannelControl(surface, midiInput, midiOutput, x, y, instance, deviceDriver) {}

channelControl.fader.mSurfaceValue.mOnTitleChange = function (context, objectTitle, value) {
    //midiOutput.sendMidi(context, helper.sysex.displaySetTextOfColumn(channelIndex, 1, makeStringMax6CharectersAndRemoveSpace(objectTitle)))
    midiOutput.sendMidi(context, helper.sysex.displaySetTextOfColumn(channelIndex, 1, makeStringMax6CharectersAndRemoveSpace(value, channelIndex)))
    channelControl.fader_title = { objectTitle, value }

channelControl.fader.mSurfaceValue.mOnDisplayValueChange = function (context, value, units) {
    midiOutput.sendMidi(context, helper.sysex.displaySetTextOfColumn(channelIndex, 0, makeStringMax6CharectersAndRemoveSpace(value, channelIndex)))
    channelControl.fader_displayValue = { value, units }

// SHOULD ALSO BE AVAILABLE IN THE icon_platformmplus.js

function makeStringMax6CharectersAndRemoveSpace(value, index) {
    if (value.match(' '))
        return (value.replace(/\s/g, '') + '      ').slice(0, index == 7 ? 7 : 6)
        return (value + '      ').slice(0, index == 7 ? 7 : 6)

It was a bit more complicated to do with the mute, solo, sel, rec buttons, as we use them in the .setTypeToggle() which means that it only sends when pressed down and then switching between 1 and 0, instead of giving us 1 on press down and 0 on release inside the . mOnValueChange function.
So had to basically create dummy buttons with the same midi inputs (but without midi outputs) to us them for reading the press down and release of a button.

Maybe there’s a better way. I could also get worried for performance with a hacked system like this :person_tipping_hand:

Ps. Not currently setup for GitHub so easier for me to just share the code here. Will also add a bit more codebase for other users :slight_smile:

It was a bit more complicated with the mute
doing it now is by
that the only way to do a timer thing (the 2 seconds you mention) will be using date and a


This is my issue. The layout of the tracks in mixconsole and on the Platform does not correspond. As you can see in the beginning of the video I have sync enabled - I guess that is the preference you mention?

@dimonskinke I can now repeat your visibility issue - which seems to coincide with my install of Cubase 12.0.50. I can’t be entirely sure but it’s definitely NOT following visiblity anymore - not even in the midiremote display.

For those tracking the GitHub - woodcockr/midiremote-userscripts at add-display branch for developments both the Mixer and Selected Channel Pages now dynamic displayed when you touch a fader.

I had knobs doing it too (code is commented out) but there is no event I can find to trigger to reset the display (you can tap a fader but it was annoying).
Really need some sort of event to trigger once the knobs are idle for a period of say 1 sec.

I’ve not got to the contributed Channel Strip page yet.

Also for reasons unknown (possible Cubase 12.0.50 update related) the Visibility of tracks is no longer being followed in the Mixer. Nothing change at this end but I just retested in my setup and its definitnely failing for me as well now.


EDIT: I’ve added a solution I found below.

Hey @robw

I was wondering if you would have a good idea how to do this.
I have an encoder that I would like to do one command/action when turning it left and another command/action turning it right.

page.makeValueBinding(surfaceElements.channelControls[0].pushEncoder.mEncoderValue, transport.mRewind)

I’ve tried seeing if I could make some dummy values, then using mOnValueChange and then in a function call two different actions. But without luck.
I also tried by filtering the range, but the issue with that is that the second just overwrites the first one:

page.makeCommandBinding(surfaceElements.channelControls[1].pushEncoder.mEncoderValue, 'Transport', 'Step Bar Back').filterByValueRange(0, 0.5).setSubPage(transportPage)
page.makeCommandBinding(surfaceElements.channelControls[1].pushEncoder.mEncoderValue, 'Transport', 'Step Bar').filterByValueRange(0.5, 1).setSubPage(transportPage)

Maybe if it was done with subgroups…

Ps. I’m using the encoder on other pages for regular stuff, so it would not make sense to split it up on the input side

Hi @saxmand,

See attached example, please.

mj_oneController_twoFunctions.js.zip (1.6 KB)

Thank you so much for getting back to me Martin.

Cool to see this example, and it confirms that the way I’ve been thinking my way around some of these things, using dummy’s, is a way to do it. But as it says in the script it isn’t the intended use of the API, and I’ve also found some of the dummy setups I’ve made (probably cause I don’t fully understand the API) wasn’t working fully as I expected it.

With that said, it was actually not what I was trying to achieve. What I wanted was to have an Encoder to do ONE command when turned left, and ONE other command when turned right.

I made a solution that seems to work, which was making another knob with the same input as the original one, and then filter the input on that new one. And then I can choose in the page mapping wether I’ll use the original encoder or the encoder with left and right:

	// Pot encoder
	channelControl.pushEncoder = channelControl.surface.makePushEncoder(posX, posY + 2, 2, 2)
	.bindToControlChange(0, 0 + channelControl.instance)
	.bindToNote(0, 0 + channelControl.instance);

	channelControl.pushEncoderLeft = channelControl.surface.makePushEncoder(posX, posY+10, 2,2)
	.bindToControlChange(0, 0 + channelControl.instance)
	channelControl.pushEncoderRight = channelControl.surface.makePushEncoder(posX, posY+12, 2,2)
	.bindToControlChange(0, 0 + channelControl.instance)

And the command would look like this:

    page.makeCommandBinding(surfaceElements.channelControls[0].pushEncoderLeft.mEncoderValue, 'Transport', 'Step Back Bar').setSubPage(transportPage)
    page.makeCommandBinding(surfaceElements.channelControls[0].pushEncoderRight.mEncoderValue, 'Transport', 'Step Bar').filterByValueRange(0, 0.5).setSubPage(transportPage)

Hi @robw.
@dimonskinke asked me how to make the Icon’s fader send CC.
I’ve made this possible through SoundFlow because multi ports in the MIDI remote API didn’t seem to work properly in the beginning, but they fixed something in 12.0.50.

I just did a quick testing to checked it now. I haven’t tested it much, and some of the ways can probably be done way smoother. Maybe you can continue the research and share when you’ve found better solutions.
I might also merge my own Icon Platform setup to using the script instead of SoundFlow at some point, but currently I’m also writing a completely new script for another controller, which I’m almost more excited about right now.

Anyway just wanted to share with you how it’s sort of possible for now.

  • Make two midi outputs:
// create objects representing the hardware's MIDI ports
var midiInput = deviceDriver.mPorts.makeMidiInput('')
var midiOutput = deviceDriver.mPorts.makeMidiOutput('')
var midiOutput2 = deviceDriver.mPorts.makeMidiOutput('')

Then you need a virtual midi driver for the second output, so that will be needed in to Cubendo. On Mac we have this available through the IAC drivers:

Then you can make faders that sends to the secondary midi output. This sort of works:

    var page = makePageWithDefaults('Midi')
    page.mOnActivate = function (activeDevice) {
        console.log('from script: Platform M+ page "CC" activated')
        clearAllLeds(activeDevice, midiOutput)

        // On load I'm setting the pitchbend fader to the center position. Whenever you release it it will jump back to this point
        midiOutput.sendMidi(activeDevice, [0xE0, 0, 64]) // to put pitchbend in center

    // In order to bind a fader so we can get it's value I'm using a dummy function. 
    var dummy = page.makeSubPageArea('Dummy')
// This fader sends pitchbend
    page.makeActionBinding(surfaceElements.channelControls[0].fader.mSurfaceValue, dummy.mAction.mReset).mOnValueChange = function (activeDevice, mapping, value) {
        var faderNumber = 0
        var valueInput = value
        var pitchBendValue = Math.ceil(valueInput * 16383)
        var value1 = pitchBendValue % 128
        var value2 = Math.floor(pitchBendValue / 128)
        midiOutput.sendMidi(activeDevice, helper.sysex.displaySetTextOfColumn(0, 1, 'PB'))
        midiOutput.sendMidi(activeDevice, helper.sysex.displaySetTextOfColumn(faderNumber, 0, (valueInput * 4 - 2).toFixed(1)))
        midiOutput2.sendMidi(activeDevice, [0xE0, value1, value2])

    // On release we want the pitchbend fader to jump back to the mid position, 
//but for some reason I can get it to work with sending the value. that's why I just added it on the page load
// So here I'm just setting the display back to the "middle" value.
    page.makeActionBinding(surfaceElements.channelControls[0].fader_touch.mSurfaceValue, dummy.mAction.mReset).mOnValueChange = function (activeDevice, mapping, value) {
        if (value == 0) {
            var faderNumber = 0
            var valueInput = 0.5
            var pitchBendValue = Math.ceil(valueInput * 16383)
            var value1 = pitchBendValue % 128
            var value2 = Math.floor(pitchBendValue / 128)
            midiOutput.sendMidi(activeDevice, helper.sysex.displaySetTextOfColumn(faderNumber, 0, (valueInput * 4 - 2).toFixed(1)))
            midiOutput2.sendMidi(activeDevice, [0xE0, value1, value2])
    // This sends CC1. I'm feeding back the values to the Icon, in order for the fader to stay where you release it. Definitely not the best way, but at least one way.
    page.makeActionBinding(surfaceElements.channelControls[1].fader.mSurfaceValue, dummy.mAction.mReset).mOnValueChange = function (activeDevice, mapping, value) {
        var faderNumber = 1
        var ccValue = Math.ceil(value * 127)
        var ccNumber = 1
        var pitchBendValue = Math.ceil(value * 16383)
        var value1 = pitchBendValue % 128
        var value2 = Math.floor(pitchBendValue / 128)
        midiOutput.sendMidi(activeDevice, helper.sysex.displaySetTextOfColumn(1, 1, 'CC' + 1))
        midiOutput.sendMidi(activeDevice, helper.sysex.displaySetTextOfColumn(faderNumber, 0, ccValue))
       // this is the value going back to the icon Fader
        midiOutput.sendMidi(activeDevice, [0xE0 + faderNumber, value1, value2])
       // this is the value going back to Cubendo
        midiOutput2.sendMidi(activeDevice, [0xB0, 1, ccValue])

Since you’ve already worked on a more advanced setup for touch and release fader, you might have an easier time making it better.
I think what needs to be done for it to be nice is to store the various CC values so we get them on load on the display. And we don’t wanna send all the fader values back in to the icon, but rather just send the last value on release. For pitch bend we want the fader to go to the center.

Still seems a bit wonky with the two midi ports, when you reload a script for instance, it create two devices and I have to remove and disable the script, then enable it and then add the device with the ports again :persevere: Maybe it’s because it’s not the intended usage. I don’t know.

But I hope this can get you on your way :slight_smile: Best

Wow! That’s quite neat approach which I hadn’t considered at all. I will take some time to digest this. I do hope Steinberg developers are looking at all the wonderful ways in which we are using the Midi Remote and making at list of features we clearly would like to support fully. There are some seriously creative technology folks on this forum! No doubt they are, and probably wondering about all the “unintended uses” we’re coming up with :stuck_out_tongue:

For the moment I’m looking at rolling back to Cubase 12.0.40 - to see if the track visibility bug goes away. I will lose some midi remote features (like the one you’ve used here) but the track visibility issue is messing with my head more. Then I’ll go back to finishing up the display additions.

I appreciate all the shared ideas and code. Let’s keep them rolling back and forth.


1 Like

Development updates - GitHub - woodcockr/midiremote-userscripts at add-display

Note this is an in-development branch and some things (notably the Zoom handling) are in flux and may not be fully operational.


  • Master Fader - Now only switches between AI Fader and Main Stereo Out Fader. The Mixer light will be OFF when in AI and ON when in Main Stereo Out. I have plans for new control room page to manage the control room levels, amongst other things.
  • Display updates - extensive updates on the display work. Both Mixer and Selected Channel Page show useful output now. Twiddling a channel Knob or Fader will show more details on change being made and its value. Tapping Flip will Change the DISPLAY to show the Knob/Fader detail screens (on the Platform M+ flip is accessed by pressing both Chan and Bank Back buttons at the same time - it’s marked on the unit).


  • My Platform M+ flip function requried some practice to get it to be semi reliable. It’s very picky about what “simultaneous press” means. I found a 2 finger quick tap was reliable enough with practice - if you don’t get it right it will perform one or the other of the functions on the individual buttons). There is no way to control this from Cubase - it’s a fixed hardware thing
  • When you’ve made a Channel Knob change the display will remain on the Knob. There is no way to know you’ve stopped changing the value and Midi Remote doesn’t (yet) have a way to change it back after a period of time. You can lightly touch any Fader to have it switch back (the Faders do this automatically of course).

Still much to do - EQ on the display, Control Room page, Channel Strip, Midi CC, …

1 Like

Now the 12.0.51 makes “A serious error” on Windows 11… this .51 is apparently not the most popular update. Now I have to find out how to roll back

Please rollback to Cubase 12.0.50 for a couple of days. We’ll provide a hotfix soon.

1 Like

@dimonskinke In case you haven’t figured out how to roll back. Simply run the older versions installed and it will show the Cubase component can be Re-installed. Simply as that.

I’ve been back and forth a few times trying to figure out why MIDI Remote won’t follow track visibility any more. @Jochen_Trappe I’d be appreciative if you could confirm/deny this bug and even check my script either here or on the Track Visibility topic you dropped in on a week or two ago. It’s quite maddening.

Thanks, yes I still had the .50 installer so it was very easy :blush: