Cubase 12 midi remote is there a way to have one knob zoom both in and out

I had a similar query before when creating generic remotes in Cubase 10. Ideally I would like a controller knob to zoom in when rotated clockwise and zoom out when rotated counter-clockwise. I can only see an option to assign one type of zoom to the knob or slider. So it will continue to zoom (say) zoom in - regardless of whether the direction of rotation changes.
Also a knob on the physical controller can be assigned as either a midi remote knob or slider. Is the difference only graphical or is there a difference in how they behave?
Thanks

1 Like

I have yet to play much with the new remote system, and not at all with any actual scripting, so someone else will have to actually answer this.

If one can create a function where the command sent depends on the whether 2nd value (because a comparison will be made)sent being more-then or less-than the first it will be possible. (not in the graphic remote surface builder)

Maybe @Jochen_Trappe could let us know if this is within the current scope? @oqion do you have an idea about this?

There is likely a difference on your device. Faders normally have much of the band below 64 and very little above, where as non continuous knobs are 50/50.
Other than that, I can’t find a difference.

Yea I do…sort of. I can think of some ways to code that, but it would take some time. I need to look up the various members and see what could be done. i.e. without spending a coding session on this task, I don’t feel comfortable answering and possibly leading us down the wrong path.

@Jochen_Trappe would know better than me.

Thank you @oqion!

Hi,

As far as I know from top of my head, one of the built in script (AKAI?) uses cross fade to Zoom In/Out.

1 Like

if it is a relative encoder (so called “endless” encoder), and you are on windows, you can use midi-ox to translate, for example:

midi cc 16-value 1 to cc 17-value 1
in generic remote, set midi cc 17 to relative and assign to zoom in
set midi cc 16 to relative and assign it to zoom out

since zoom in and out are separate commands, they need to have discrete triggers (different midi cc’s in this case). this method forces a single knob to send out two different midi cc’s depending on the direction.

if the knob sends midi cc values 1-127 (ie absolute), a calculation is needed, as described above. i have programmed autohotkey to do this but it needs some “pause” function (typically via a button” to allow you to reset the knob). to make it less awkward to use you assign midi cc value ranges to coincide with different degrees of a function (ie zoom x2, zoom x4, etc) so you span the minimum to maximum of the function.

if you happen to have maschine hardware, the jog wheel can be assigned to “relative notes” ie cw to c1 and ccw to d1. from there you can assign the notes to zoom in/out. which actually makes the maschine hardware somewhat useful in the cubase environment.

Oddly, it seems very easy to assign a “pan left and right” to a knob. It automatically assigns the center point at 12 o’clock and then moves the curser either left of right depending on the rotation direction. If only there was a similar command to “zoom in and out”.

I did exactly this in my CC121 Midi Remote Script. CC121 has incremental encoders

1 Like

I implemented a zoom knob into the typescript which can be found here:

I am not procedural scripting because… I don’t do that, it’s just to messy and I make enough mistakes without having to slog through… Also, when at the top level I don’t over do it too much so it get’s procedural when you get up to the page instance level.

This works on a knob that is not incremental.

Here is the pertinent part of the code from top to bottom:

var zoomKnob = function(colour, surface, page) {
    var zoom = new OptionsTrigger(colour, 
        new Commander("zoomOut").
        addTriggerCommand(surface, page, 'Zoom', 'Zoom Out').
        addTriggerCommand(surface, page, 'Zoom', 'Zoom Out Vertically'),
        new Commander("zoomIn").
            addTriggerCommand(surface, page, 'Zoom', 'Zoom In').
            addTriggerCommand(surface, page, 'Zoom', 'Zoom In Vertically'),     
        32);
    return zoom;
}
class OptionsTrigger extends LCXLController {
    count;
    downCommands;
    upCommands;
    slices;
    lastSlice;
    constructor(colour, downCommands: Commander, upCommands: Commander, count=127){
        super(colour, "jump");
        this.downCommands = downCommands;
        this.upCommands = upCommands;
        this.slices = []
        this.registerEventProcessValueChange = true;
        if( count > 127 || count < 0)
        {
            count = 127;
        }
        this.count = count;
        this.lastSlice = -1;

        var size = 1/this.count;
        for(var i = 0; i < this.count; i++)
        {
            this.slices.push( (i+1) * size);
        }
    }

    handleProcessValueChanged(activeDevice, value){
        var sliceNow = 0;
        for(var i=0; i < this.count; i++) {
            if(value <= this.slices[i] || i == this.count-1) {
                sliceNow = i;
                break;
            }
        }

        if( sliceNow < this.lastSlice)
        {
            var difference = this.lastSlice - sliceNow;
            for(var i=0; i < difference; i++)
            {
                this.downCommands.triggerAll(activeDevice)
            }
        } else if( sliceNow > this.lastSlice)
        {
            var difference =  sliceNow - this.lastSlice;
            for(var i=0; i < difference; i++)
            {
                this.upCommands.triggerAll(activeDevice)
            }
        }
        this.lastSlice = sliceNow;
    }
}
class Commander {
    elements;
    name;
    constructor(name) {
        this.name = name;
        this.elements = [];
    }

    length() {
        return this.elements.length;
    }

    addTriggerValue(surface, page, binding) {     
        var element = surface.makeCustomValueVariable(this.name + this.elements.length);
        page.makeValueBinding(element, binding).setTypeToggle();
        this.elements.push(element);
        return this;
    }

    addTriggerCommand(surface, page, commandCategory, commandName) {
        var element = surface.makeCustomValueVariable(this.name + this.elements.length);
        page.makeCommandBinding(element, commandCategory, commandName);
        this.elements.push(element);
        return this;
    }

    addTriggerAction(surface, page, binding) {
        var element = surface.makeCustomValueVariable(this.name + this.elements.length);
        page.makeActionBinding(element, binding);
        this.elements.push(element);
        return this;
    }

    triggerOff(activeDevice)
    {
        this.triggerAll(activeDevice, 0)
    }

    trigger(activeDevice, index = 0, value = 1) {
        this.elements[index].setProcessValue(activeDevice, value)
    }

    triggerAll(activeDevice, value = 1){
        for( var i = 0; i < this.elements.length; i++) {
            this.trigger(activeDevice, i, value);
        }
    }
}
1 Like

@MarcoE

Please take a look at the difference between your code, and mine. If I try and use your style of diff ( or even set my count to 127) I can get Cubase to hang or freeze when the knob is moved quickly.

That is Irrespective of the horizontal/vertical nature of the zoom.

Note that my knob is one knob that does both, rather than two that do vertical and horizontal separately.

To make that clear the factory function could look like this.

var zoomVerticalKnob = function(colour, surface, page) {
    var zoom = new OptionsTrigger(colour, 
        new Commander("zoomOut").
            addTriggerCommand(surface, page, 'Zoom', 'Zoom Out Vertically'),
        new Commander("zoomIn").
            addTriggerCommand(surface, page, 'Zoom', 'Zoom In Vertically'),     
        32);
    return zoom;
}
var zoomHorizontal Knob = function(colour, surface, page) {
    var zoom = new OptionsTrigger(colour, 
        new Commander("zoomOut").
            addTriggerCommand(surface, page, 'Zoom', 'Zoom Out').
        new Commander("zoomIn").
            addTriggerCommand(surface, page, 'Zoom', 'Zoom In').
        32);
    return zoom;
}
1 Like

Even without JavaScript programming it’s possible,

If you have a Native Instruments controller with what they call an encoder, it's quite easy (click the arrow on the left to expand)

On most of those Native Instruments controllers, the “encoders” are knobs with little click stops (compared to the “knobs” which turn smoothly without clicks. On (some of their ) DJ controllers, which feature little fake turntables, those turntable surfaces are also “encoders”.

The special feature, those encoders have is, that they can be configured to send different MIDI Note ON messages repeatedly depending on turning left or turning right. So you could send Note 1 for every click turning left and Note 2 for every click turning right. – And then configure the MIDI Remote so see those 2 note messages as 2 different buttons. (This trick also works in the Generic Remote).

I’m using this all the time for:

  • Zooming
  • Moving the cursor (playhead)
  • Moving between tracks/channels
  • moving between channel groups

for many other controllers with endless encoders, it's more convoluted (click the arrow on the left to expand)
  • configure the remote to send “relative” CC messages ( so the same value is sent repeatedly, one value for turning left, the other for turning right. For example CC 14 with value 0 for turning left and CC14 with value 127 for turning right.
  • then insert midi transformation into the path between the hardware remote and Cubase, changing all CC14 messages to Note messages with the value of CC 14 becoming the note numbers. Bonus points (for additional flexibility) for assigning different notes than 0 and 127.
  • and then use the resulting 2 notes to trigger key commands.
  • this requires virtual midi cabling (a.k.a. “midi loopback”) in software
    • on Windows this requires something like loopMIDI (free utility software)
    • on MacOS MIDI loopback is included in the OS (I think it’s called IAC driver available from the MIDI settings in MacOS)
  • and now it gets a little crazy, but it works:
    • Make a Cubase midi track that does the desired midi transformation (via midi insert (Transformer) or via the track’s “Input Transformer”
  • make that Cubase midi track send to a virtual midi cable, and connect that virtual midi cable to the MIDI Remote (this trick also worked with the old generic remote).

This could all be made so much simpler, if:

The MIDI Remote would become smarter in being able to interpret specific CC values as specific key commands. e.g. that CC 14 could be programmed that specific values trigger different key commands. e.g.

  • value 0 could be Zoom Out and value 127 could be Zoom In, or
  • value 63 could be Zoom Out and value 64 could be Zoom In, etc.

This would open a ton of fabulous possibilities:

  • Nudge Cursor Left, Nudge Cursor Right
  • go to Previous Marker, Next Marker
  • go to Previous Cycle Marker, Next Cycle Marker
  • go to Previous Arranger Chain event, Next Arranger Chain event
  • select Previous track, Next Track
  • select Previous Channel Group, Next Channel Group
  • and of course all kinds of Macro combinations
2 Likes

This style works really well when you have a lot of control over the device and how it behaves. But when you don’t, you have to make up the difference in the code.

What would be wonderful is if the incoming MIDI could be modified as it can in Logic JS Scripting. That would open up the ability to do just about anything.

While I agree with you on the value specific triggers, I think stepping up a level in design would help to solve a lot of the complexity.

To simplify what I want to say I am going to use the word Encoder for any hardware widget, whether it is an Encoder or Button, because the word Controller has a defined meaning when discussing code.

As it is the events are bound to the Surface Element which is bound to the Encoder, and this is where the complexity comes in.

If it were more like:
Hardware Encoder : register incoming events
Cubase Endpoint : register outgoing events

Hardware Encoder → Callback → Cubase Endpoint method (no binding required!)
Cubase Endpoint → Callback → Hardware Encoder method (no binding required!)

But there is a secondary GUI, but we don’t need events from that!!! So it could be a completely separate entity. In fact having the Elements bound doesn’t make a lot of sense. For example, in the example above the knob in question shows up as “No Mapping”.

The whole idea of binding can make sense, but as you try and do something more interesting it starts to become a special case.

So binding the Element to an object that you make or to a pre-defined object makes more sense.

That would be that built in Elements can automatically register for events from Hardware Encoders or Cubase Endpoints, or if you extend the correct type, you can then insert your own with it’s own behavior.

But this is all OO. which of course makes it so much easier to think about.

Unfortunately the term Model, View, Controller, has been improperly used more than it has been used correctly. The Model in this case is the Element, The view is the Element, the Controller is the Element. The Element is everything. One can argue that the binding is the model, but you can’t get events from the binding so, everything has to go through the Element.

In telemetry based systems Visual Proxy concept tends to work a lot better, You have a Model Controller, and a View Controller. And you have them facing both directions. Both incoming and outgoing. That is the model that would work best here I think.

1 Like

oh yes!!!

except for people who have only done procedural programming :rofl:

1 Like

This is Jochen_Trappe’s first response on the matter.

This is pettor’s code.

I timidly tried to modify some factory scripts, and I don’t think I can test these yet, they’re too difficult for me. I’m still doing stuff in the Surface editor.

One more easy way, additionally on what @Nico5 proposes, is allow 1:2 assignments for each element, as it is currently possible in the Generic Remote.

Knob 1 I could have it zoom in for range 50% → 100% and at the same time zoom out for range 50% → 0%. Or 66-100 and 0-33, or 33-0, depending on how the knob functions. Invert value is already there as an option.

This is literally the only thing I’m missing right now from the Surface Editor (ok, apart from Inserts, Strip, Send Pan and other such little things.)

Adding:

  • Nudge Locators
  • Nudge Loop
  • Nudge Events, Starts, Ends
  • Expand Range
  • Locate Next/Previous event
  • Much MOAR
1 Like

Which is why all languages whether Lambda Calculous based or not should be OO. That way you wouldn’t have anyone who had “only done procedural programming”.

Ii think it’s been proven out at this point for over 30 years. Every language that gets traction and isn’t OO has to catch up later to get anyone to do anything important with it. So it’s about time that we as a humans simply stop making inferior programming languages.

It always makes this haves and have-nots sort of schism. Like OO were some kind of complex thing that is hard to learn. If you haven’t ever seen a non OO language, it isn’t a problem, it’s just as easy.

And then you get the anti-inheritance thing. Some how contains is better than is. But the same systems that try and force one away from inheritance will have configurations with JSON or XML. And that isn’t any different than inheritance.

In my code, because of the design of Elements and their position in MVC, I made Controllers where you can either register for events or not. Too register you simply set a value from the Parent Class to TRUE, and implement the method. This is where a lot of people will issue a red card.

But it’s an easy configuration this way. Just like setting “checkJs”: true in the config. It’s simple, and avoids a lot of extra boiler-plate, and additional computation that is unneeded.

The point I am trying to make here is that the reason OO is seen as complex is because there are a lot of these rules someone made up that were specific to very narrow contexts that became baked in to how to “do” OO, and that make writing simple scripts in OO problematic when you try and follow these rules.

SOLID aside, those rules tend to just get in the way. It isn’t OO’s fault.

Yeah- that’s because Pan value zero is full left, 127 is full right and 64 is center. It was built into the MIDI 1.0 spec.

There are a couple possible solutions for this so far.

@windbag – I think this is a super interesting topic with a lot to learn from, but I want to make sure it’s not derailed from your point of view. Let me know if it is, and I can split things off to a second thread.

1 Like

I am delighted by the interest in my post. The answer to this question will open up many possibilities. The midi remote capabilities will suddenly bring life into old controllers that are built like tanks but lack modern programming ( i am thinking of things like the FCB1010 midi foot controller). and completely agree that it is worth pursuing the ability of basic midi controllers ( that can only assign midi channel number, cc number, and maximum values) to control things as mentioned below. This would open a ton of fabulous possibilities: I don’t have the know how yet but feel that it is only a matter of time.

  • Nudge Cursor Left, Nudge Cursor Right
  • go to Previous Marker, Next Marker
  • go to Previous Cycle Marker, Next Cycle Marker
  • go to Previous Arranger Chain event, Next Arranger Chain event
  • select Previous track, Next Track
  • select Previous Channel Group, Next Channel Group
  • and of course all kinds of Macro combinations
2 Likes

Yeah, right on. I see it that way too.

1 Like