MIDI-KIT - script processor for MIDI - by stoermelder

I gathered quite some knowledge about MIDI since I started developing modules for Rack a few years ago - and so I thought it is time for some generic MIDI processor within a module. As far as I know there is no similar module which can do this (except for VCV Host and using some script VST). The module is completely working though I haven’t pushed the code yet to my GitHub repository for my own reasons.

Since version 2.2.0 Rack has its own Loopback MIDI driver and with this driver MIDI-KIT can be used as an insert: incoming MIDI messages can be processed before reaching the actual MIDI module (like MIDI-CC or MIDI-CV or MIDI-MAP or one of my own modules like MIDI-CAT). Outgoing MIDI messages can be processed the same way.

The module has four CV input and four parameters. These can be used within the scripts for adding some dynamic facets if needed. So, CV-modulated MIDI message are doable - even “crazy things” like selecting the MIDI channel by CV :slight_smile:
There is also a Trigger-input which typically will receive a clock signal for synchonizing (means, delaying) MIDI messages until an upcoming trigger.

The module is purely event-based: It is only active if a MIDI message arrives on the selected MIDI device. But the API provides ability for creating new MIDI messages, one arriving message can result in up to 16 MIDI messages.

The module uses internally a very basic implementation of a JavaScript engine (Elk) for interpreting your custom scripts. It is certainly not the fastest way for running JavaScript from C/C++ but MIDI messages are relatively rare events (in contrast to audio/dsp-processing) and I don’t think perfomence will be an issue. I suspect scripts will be quite simple most of the time.

This is a sample script I’m using for testing:

let processMidi = function(msg) {
    if (midi.isCc(msg)) {
        if (midi.getChannel(msg) === 1) {
            midi.setChannel(msg, 2);
            let msg2 = midi.create();
            midi.setCc(msg2, 4, 11, midi.getValue(msg));
            midi.send(msg2);
        }
        if (midi.getChannel(msg) === 4) {
            let ch = number.ceil(param.getValue(1) * 16);
            midi.setChannel(msg, ch);
            log("ch " + number.toString(ch));
            overlay(number.toString(midi.getValue(msg)));
        }
        midi.send(msg);
    }
    if (midi.isNoteOn(msg)) {
        if (midi.getChannel(msg) === 5) {
            midi.send(msg);
            let msg2 = midi.create();
            midi.setNoteOn(msg2, 6, midi.getNote(msg), midi.getValue(msg));
            midi.sendAfterTrigger(msg2, 2);
        }
    }
};

As the underlaying script engine provides no standard libraries or built-in functions I needed to add everything on my own - which I consider a good thing for security and simplicity reasons. This is the list of API I implemented so far:

Global functions

  • log(string): Logs a text menssage on the display of the module.
  • overlay(string): Displays a message in an overlay widget.
  • processMidi(msg): Main entry point of the script. This function is called by the module on each incoming MIDI message.

input

  • input.getName(arg): This function is used by the module to display a tooltip text for the input. The default implementation can be replaced to display some additional information for the input.
  • input.getVoltage(arg): Reads the current voltage on the input port arg with arg between 1 and 4.

param

  • param.getName(arg): This function is used by the module to display a tooltip text for the parameter. The default implementation can be replaced to display some additional information for the parameter.
  • param.getValueFormat(arg): This function is used by the module to display a formated value on the tooltip for the parameter. The default implementation can be replaced.
  • param.getValue(arg): Reads the value of the parameter with index arg with arg between 1 and 4. The return value is in the range [0, 1].

number

  • number.ceil(arg): Computes the largest integer value not less than arg.
  • number.floor(arg): Computes the largest integer value not greater than arg.
  • number.max(arg1, arg2): Returns the greater of two arguments.
  • number.min(arg1, arg2): Returns the smaller of two arguments.
  • number.random(): Returns a random number in the range [0, 1).
  • number.toString(arg): Converts arg to a string representation.

midi

  • midi.create()
  • midi.getChannel(msg)
  • midi.getNote(msg)
  • midi.getPitchWheel(msg)
  • midi.getValue(msg)
  • midi.isCc(msg)
  • midi.isChanPressure(msg)
  • midi.isClock(msg)
  • midi.isContinue(msg)
  • midi.isNoteOff(msg)
  • midi.isNoteOn(msg)
  • midi.isPitchWheel(msg)
  • midi.isProgramChange(msg)
  • midi.isStart(msg)
  • midi.isStop(msg)
  • midi.send(msg)
  • midi.sendAfterMs(msg, arg)
  • midi.sendAfterTrigger(msg, arg)
  • midi.setCc(msg, channel, cc, value)
  • midi.setChannel(msg, channel)
  • midi.setChanPressure(msg, channel, value)
  • midi.setKeyPressure(msg, channel, note, value)
  • midi.setNote(msg, note)
  • midi.setPitchWheel(msg, channel, value)
  • midi.setProgramChange(msg, channel, prg)
  • midi.setValue(msg, value)

As mentioned the module is fully working but I’m still working on the internal structure for some possible expanders in the future.
I would appreciate any feedback and feature ideas are welcome as always.

23 Likes

It’s not obvious to me - does it support sending/receiving 14-bit midi CC messages (fine + coarse CC/CC+32) ?

Looking forward to see your version of an OSC module too ?.

Yes, but there is currently nothing special for 14-bit CC messages implemented, as these are just two stardard CC messages.
Technically the module supports all MIDI messages, even SysEx, but I’m not sure about any use-cases yet.

1 Like

Also, If it’s not too much work - can you describe how MIDI system realtime messages are handled in rack now ? Do they have higher queue priority than common MIDI messages incl sysex. (less jitter?)

I don’t see any functions related to Start, Stop, Cont, Tick - but I don’t know if it’s relevant either - I haven’t much experience with complex midi setups involving tempo/transport mangling.

edit: found them

  • midi.isClock(msg)
  • midi.isContinue(msg)
  • midi.isStart(msg)
  • midi.isStop(msg)

Looks really handy. As a working coder who would rather not code seriously in my hobby this looks ideal for sketching out midi transformations. Exactly what I used to use CAL for when Cakewalk/Sonar was my DAW for a decade.

1 Like

I haven’t investiged all details yet but MIDI messages have timestamps in v2 and realtime message are priotized in modules which generate different kind of MIDI messages, like CV-MIDI.

Same here, this is why I skipped these for now, except of midi.isClock etc…

1 Like

Exactly this is the idea. No complete development environment, but a simple way of transforming MIDI.
For example for such controllers:

This looks very useful!

The first thing I think of is mapping various controllers to be more useful. A simple example would be velocity scaling (eg my Korg Wavestate only outputs velocities between ~30-100 unless you smash the thing or barely touch it). Another case is making use of the center detent in rotary controllers like the LaunchControl XL to provide dual function for rotating right/left.

Some scaling functions would be useful, such as curve/bezier, log, exp, abs, linear between max/min. The option for higher bit resolution on the function output might be useful in this case.

The LaunchControl is an example where it needs different output than what it sends on the input. In this case I could see MIDI-CAT sending its feedback first to MIDI-KIT on loopback, and MIDI-KIT sending to the LaunchControl. That would require different scripts for handling input from and output to the same controller. That might be a use-case for an expander?

Very cool idea!

One immediate use case for me would be what I did in VCV using about a dozen modules yesterday. I have a vpme.de Trig31 module on my rack that lets me send up to eight CV triggers out as MIDI notes, but when looking to map them at my DAW end the Trig31 sends them on strange notes and different channels e.g. the kick is sent as a C-1 note on channel 10, but the snare as a C#-1 on channel 11, the hi hat as an e2 on channel 12. All come in with velocity 0 and ultra short as they’re triggers not gates. One module with a short script letting me remap those notes to 32nd notes as standard GM MIDI drums on channel 10 would be really handy.

I can do the remapping directly in some DAWs, others using DAW tools and others not at all so this helps with consistency.

Great fun driving an Elektron Analog Rytm from hardware Grids, Bastl Kompas or the vpme Euclidean Circles!

midi.sendSysex(msg)
midi.receiveSysex(msg)

With sysex data we can use the script processor for many hardware.

1 Like

Super cool, I know for me, when max/msp added their javascript object it was a huge convenience!

This is a very nice idea! I don’t think I can send a PM here, and don’t want to hijack this thread, but wanted to suggest that you take a look at the MIDI stuff I’ve been working on.

Specifically my patchable “vMIDI” spec that allows the use of normal patch cables to send MIDI between modules. There is also a MIDI helper class that helps reconnect MIDI devices so you can work offline and plug the hardware in later and it automatically maps it. The code is here:

I also make some hardware USB MIDI controllers with companion VCV modules that implement the same features described above:

Thanks and good luck with your project!

2 Likes