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: See updated post.

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.

2 Likes

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

MIDI-KIT is still “work-in-progress” and I haven’t done much coding in the last few months. This is its programming interface right of today, most of the stuff is working and tested. I haven’t released anything because I want the API being stable before.

Global functions

  • processMidi(midiPort, msg): Main entry point of the script. This function is called by the module on each incoming MIDI message msg, received from MIDI input port midiPort (always 1 for this version).
  • log(str): Prints string str to the display of the module.
  • overlay(str1, [str2], [str3]): Displays string str1 in an overlay widget.

input

  • input.enable(port): Enables input with index port (1…4).
  • input.getName(port): Callback function 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(port, [channel]): Reads the current voltage on the input port port (1…4) of polyphonic channel (1…16).
  • input.isHigh(port, [channel]): Returns true if the voltage on input port port (1…4) of polyphonic channel (1…16) is above 0.7V.
  • input.isLow(port, [channel]): Returns true if the voltage on input port port (1…4) of polyphonic channel (1…16) is below 0.7V.

trig

  • trig.getTicks([trigPort]): Returns the number of triggers on trigger port trigPort (only 1 is supported in this version) since loading the script. If trigPort is omitted the default trigger port is selected.
  • trig.isHigh([trigPort]): Returns true if the voltage on trigger port trigPort (only 1 is supported in this version) is above 0.7V. If trigPort is omitted the default trigger port is selected.
  • trig.isLow([trigPort]): Returns true if the voltage on trigger port trigPort (only 1 is supported in this version) is below 0.7V. If trigPort is omitted the default trigger port is selected.

param

  • param.enable(arg): Enables parameter with index arg (1…4).
  • param.getName(arg): Callback function 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 (1…4). The return value is interval [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 of interval [0, 1).
  • number.toString(arg): Converts arg to a string representation.

midi

  • midi.create(): Creates an empty MIDI message.
  • midi.createNRPN(): Creates an empty NRPN MIDI message (actually 4 MIDI messages).
  • midi.getChannel(msg): Returns the MIDI channel (1…16) of msg.
  • midi.getLength(msg): Returns the length of the MIDI message msg, for all common messages this will return 3.
  • midi.getNote(msg): Returns the MIDI note number (0…127) of msg (byte 2 of the MIDI message).
  • midi.getSysExData(msg): Returns the data of a MIDI SysEx message msg as hexstring.
  • midi.getPitchWheel(msg): Returns the MIDI pitch wheel (0…16383) value of msg.
  • midi.getValue(msg). Returns the MIDI value field (0…127) of msg (byte 3 of the MIDI message).
  • midi.isCc(msg): Returns true if msg is a MIDI CC message.
  • midi.isChanPressure(msg): Returns true if msg is a MIDI channel pressure message.
  • midi.isClock(msg): Returns true if msg is a MIDI clock message.
  • midi.isContinue(msg): Returns true if msg is a MIDI continue message.
  • midi.isNoteOff(msg): Returns true if msg is a MIDI note off message.
  • midi.isNoteOn(msg): Returns true if msg is a MIDI note on message.
  • midi.isPitchWheel(msg): Returns true if msg is a MIDI pitch wheel message.
  • midi.isProgramChange(msg): Returns true if msg is a MIDI program change message.
  • midi.isStart(msg): Returns true if msg is a MIDI start message.
  • midi.isStop(msg): Returns true if msg is a MIDI stop message.
  • midi.isSysEx(msg): Returns true if msg is a MIDI SysEx message.
  • midi.setCc(msg, channel, cc, value): Sets msg as a MIDI CC message with the specified MIDI channel channel (1…16), CC number cc (0…127) and value (0…127).
  • midi.setCc14bit(msg1, msg2, channel, cc, value): Sets msg1 and msg2 as a 14-bit MIDI CC message pair, with the MIDI channel channel (1…16), CC number cc (0…127) and value (0…16383).
  • midi.setChannel(msg, channel): Sets the MIDI channel channel (1…16) for msg.
  • midi.setChanPressure(msg, channel, value): Sets msg as a MIDI channel pressure message, with MIDI channel channel (1…16) and pressure value (0…127).
  • midi.setKeyPressure(msg, channel, note, value): Sets msg as MIDI key pressure/aftertouch message, with the MIDI channel channel (1…16), MIDI note number note (0…127) and pressure value (0…127).
  • midi.setNote(msg, note): Sets the MIDI note number (0…127) for msg (byte 2 of the MIDI message).
  • midi.setNoteOff(msg, channel, note): Sets msg as MIDI note off message, with MIDI channel channel (1…16) and MIDI note number note (0…127). Please be aware, some MIDI devices need a MIDI note on message with velocity 0 instead of a MIDI note off message.
  • midi.setNoteOn(msg, channel, note, velocity): Sets msg as MIDI note on message, with MIDI channel channel (1…16), MIDI note number note (0…127) and velocity (0…127).
  • midi.setNRPN(nrpn, channel, number, value): Sets the NRPN number and NRPN value of nrpn.
  • midi.setPitchWheel(msg, channel, value): Sets msg as a MIDI pitch wheel message, with the specified MIDI channel (1…16) and pitch wheel value (0…16383).
  • midi.setProgramChange(msg, channel, prg): Sets msg as a MIDI program change message, with the MIDI channel channel (1…16) and program number prg (0…127).
  • midi.setSysEx(msg, str): Sets msg as a MIDI SysEx message with string str representing a hexstring of data (e.g. “ab0fad050fdd”, whitespaces are ignored).
  • midi.setValue(msg, value): Sets the MIDI value field (0…127) for msg (byte 3 of the MIDI message).

midiOut

  • midiOut.send([midiPort], msg): Sends msg on MIDI port midiPort (default port = 1). If midiPort is omitted the default MIDI output port is used.
  • midiOut.sendAfterMs([midiPort], msg, ms): Sends msg delayed on MIDI port midiPort (default port = 1). The delay ms is specified in milliseconds. If midiPort is omitted the default MIDI output port is used.
  • midiOut.sendAfterTrigger([midiPort], msg, [trigPort], ticks): Sends msg delayed on MIDI port midiPort (default output = 1). The delay is specified in ticks of triggers on CV trigger input trigPort. If midiPort is omitted the default MIDI output port is used. If trigPort is omitted the default trigger port is selected.
4 Likes

In relation to the korg minilogue NRPN example i posted in another thread. I will link the MIDI implementation for the Korg minilogue XD, In case you want to look at it.

I don’t think i will succeed using patchmaster, cv-map and ModScript for my minilogue modulations. Rack keeps crashing(it hangs) when i get to mapping CV’s to the knobs using cv-map (or midi-map).

Edit add: had to slow down the processing to make it stable.

config.frameDivider = 128
config.bufferSize = 8

Do you want some knobs for controlling your MIDI device or do want to send NRPNs for some CV source in Rack?

can i have both ? hehe

when the knobs are not cv-mapped, they can be used manually.

This is how i have it set up now. I plan on making more knows to control everything that is midi controllable on the minilogue xd.

Ok, this is super-complicated for sending some MIDI messages :slight_smile: Why don’t you use the input ports of ModScript? Would this not be easier?

Probably, but it only has 4 input ports. I could just make the LFO’s in modcript too … so many choices…

Ah, I see. My module has 4 (right now), but you can access polyphonic channels, so 4*16. Not sure about ModScript, I never tried it.

Very interesting! I work on MIDI stuff to better understanding this area. And I try to make a patch to interface a controler like L’aunch Control XT with MindMeld Mix and stuff around. Is your module available somewhere now? Thanks. Alain