The video below is a three-minute preview of a cable protocol I have been working on, called MPX, Modular Polyphonic Expression, along with four modules that use it. It is a convention between plugins rather than a change to Rack: it is built on Rack’s own cables and needs nothing from VCV.
The modules in the patch are mpxChart, which plays chord charts; mpxArp, which arpeggiates them; mpxRand, which adds variation; and fromMPX, which converts an MPX cable back to ordinary VCV signal types. They are on GitHub as a pre-release, built for all four platforms, if you would rather try them than watch: https://github.com/chrisgr99/MPX/releases
The short technical description. A note cable is an ordinary Rack cable and it carries no data; it is a declaration of topology. A note port will only hold a cable to another note port: it refuses the connection to an ordinary port, and removes a cable it finds is not to another MPX port. So there is no case where an MPX output is left patched into a VCO’s V/oct with nothing coming out of it. The events travel out of band. Not an expander, because Rack’s message passing reaches the module physically next door and no further, and a voice belongs among the modules that implement it wherever they sit in the rack. A source implements one capability interface that maps an output id to a bus slot; a consumer walks the cables on its input, asks each upstream module that question, and attaches a cursor. Notes are events — on, off and update — each carrying a handle that is unique for the session, so a message can reach a note that is already sounding. A note-on carries pitch, level, duration, pan and the bend range in semitones, the range travelling with the note so the far end can produce a control voltage without knowing the source’s knob. Updates carry bend, pressure or timbre against a handle.
One cable is one instrument rather than one note, which is what a polyphonic cable of voltages cannot do: describing one expressive note takes about nine of the sixteen channels, so voltages give you one note per cable rather than one part per cable. This is the same corner MPE is in, incidentally. MPE spends one MIDI channel per sounding note so that bend and pressure can be per note, which caps a part at fifteen. The voltage version of that idea runs out at one.
The handle is the other difference from MIDI, and the one that matters most in practice. MIDI addresses a sounding note by channel and key number, which is unique only until the channel is reused, and a note lives until a note-off arrives for it. An MPX note has an identity of its own for as long as it sounds, and it carries its own duration, so a module downstream can bend it, move it, change its level or end it early by name — and a note whose note-off is never sent still stops, because the far end already knows how long it was meant to last. That is what makes a module like mpxRand possible: it takes hold of notes made by something upstream and rewrites them as they pass.
The harmony travels the same way, but as state rather than events. The block holds the key, the current chord and the next two, beats until the current one gives way, position in beats, cycle length, time signature, and a master random seed with a pass counter. It is read rather than received, so a module that starts listening halfway through a phrase knows where it is immediately, and because it names the chords ahead, a harmony processor has lookahead that a note processor does not. In the video the arpeggiator is reading chords from that block rather than notes from a keyboard.
On the audio thread the whole thing is a store into a ring, an atomic increment, and a bounded seqlock read of the harmony. No searching, no casts, no locks and no allocation per sample. Discovery — which cables are patched, and what is on the other end — happens off the audio thread, on the UI thread, when the patching changes.
Where it stands, and the reason for this post. The bus table is a static in the plugin binary, so today this works between my own modules and nowhere else. The rendezvous that would fix that looks like this. On Mac and Linux, Rack loads plugins with RTLD_LOCAL, so the symbols are not in the global namespace and dlsym against the default handle finds nothing — but an explicit handle still resolves: dlopen with RTLD_NOLOAD on a library that is already loaded, then dlsym on that handle. On Windows the equivalent is GetModuleHandleEx, and it has to be given the plugin’s full path, because every Rack plugin binary in the process is named plugin.dll and a name-only lookup returns whichever one loaded first — not the one you asked for. Rack knows each plugin’s directory, so the path is available; enumerating the loaded modules and matching paths is the other way to do it. Participating plugins can then find each other at load time and elect one table to share, with no plugin depending on any other being installed.
Two things about that election are worth saying before anyone asks. It is not a race: Rack loads plugins sequentially at startup and does not load them dynamically afterwards, so the first participating plugin to initialise finds no table and creates one, and every plugin initialised after it finds that table and adopts it. And because each plugin carries its own copy of the source, the table that wins can be older or newer than the copy a given plugin was built against, which is the real compatibility problem here rather than the symbol lookup. The table therefore carries its own version, the layout only ever grows at the end, and each side works to the lower of the two versions: a plugin built against a newer library that adopts an older table uses the older feature set, and an older plugin reading a newer table sees only the fields it knows about.
The other thing anyone trying this will hit immediately is symbol visibility. The SDK sets no visibility flags today, so a stock plugin exports its symbols on all three platforms and this works by accident. It should not be left to accident: the rendezvous function wants an explicit default-visibility attribute on Mac and Linux, since a plugin built with -fvisibility=hidden will otherwise hide it, and dllexport on Windows. And there is a trap in that last part — MinGW auto-exports everything only while nothing is explicitly exported, so adding dllexport to one function turns auto-export off for the whole binary and Rack’s own init stops being found. A library doing this has to export init as well, or link with --export-all-symbols.
My question is whether there is interest in that being packaged as source: a permissively licensed header and one translation unit that a plugin drops into its tree, so that the rendezvous, the ring, the cursors, the version negotiation and the export macros are written once rather than by hand in each plugin, with the implementation details isolated from the plugin that uses it. The API a developer would see is small: an init call, an object bound to an output with note on, note off and update, an object bound to an input with a refresh on the UI thread and a drain on the audio thread, and a couple of questions for cable colouring.
If you maintain a sequencer, a voice, or anything that would send or receive notes, yours is the reaction I am after. If that is of use to anyone, I will write it that way. If the reaction is that the out-of-band table is the wrong approach and events belong on the cable as voltages, I would rather hear that now than after the library exists.