MPX — a proposed cable type for notes and harmony between plugins, and a question about packaging it

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.

Hello Chris(?)

Thanks for putting your ideas out here, it’s always interesting to contemplate new ones for sure. So here’s my first reaction to what I percieve you are suggesting. Mind you, I’m not quite sure I have accurately understood everything you’re saying, so keep that in mind; I may have misunderstood.

First of all, the modules you are showing - playing chords, arpeggiating, mutating notes etc. are all interesting in their own right, and other modules exist that explore the same space, and the more the better I say.

Now to the deeper idea you seem to be suggesting. What I’m hearing is something like this:

Cables are annoying and limiting, and there’s too many of them. What if instead we have the idea of a “hidden” signal backplane, where lots of signals can travel, then we can get rid of a lot of cables. And maybe we can model that on the digital MIDI signals, which compresses down a lot of info, that can travel on various channels on that backplane. And maybe in that new protocol we accept some new limitations but it will be worth it for saving the cables. That’s roughly what I’m hearing, but correct me where I’m wrong.

So, I’m not saying that it’s wrong to make a digital synthesizer along those lines/ideas, and it might even be very interesting, but I’m inclined to say that “then it won’t be Rack”. Let me elaborate…

Rack is a Eurorack emulator. That means it utilizes a simple paradigm - the modular, Eurorack, voltage-controlled paradigm, to construct a synthesizer. This has many advantages:

  • It’s already familiar.
  • It’s quite simple to wrap your head around and understand the basics, because it uses only a few, basic concepts, that are tried and well known.
  • Because those concepts are basic, few and orthogonal, there’s enormous power in that, because you can combine those basic concepts in almost endless ways. It’s kind of like the power of assembly language. It’s the true power of modular.
  • There is great power in the simple maxim: Everything is just a voltage.
  • Signals have a huge resolution, much greater than MIDI (v1).
  • It’s not limited to one form of music making, but can accomodate virtually all forms of music making.
  • There is an almost endless solution-space to explore, using just those basic concepts, and we are no where near done with that yet.
  • That also means that as long as you make modules that adhere to those basic concepts and standards, they are all interoperable, which means endless patching and combination opportunities. This is the power we are all making use of when we’re patching.
  • Rack added one meaningful addition to modular: Simple polyphonic cables, which admittedly are a break with classic Eurorack. It’s simply a bundle of the known concepts in one cable and there’s huge power in that, which I think we are no where near done exploring yet. And they blend quite well with mono-cables. It might be Rack’s biggest super power yet, and talk about saving cables…

Those advantages are very well worth keeping, and is what makes the Rack patching experience enjoyable and rewarding.

Personally I don’t see anything in your proposal, as I understand it, that would be worth sacrificing any of those advantages for, especially not if it would fragment the Rack module ecosystem into “protocol 1” and “protocol 2” modules, thus loosing the power of endless recombination of modules.

So my feeling is - Let Rack be the simple, voltage-controlled, modular paradigm that it is, limitations and all, because there is great freedom, usability and coherence in that.

Again, I’m not saying your idea is not worth exploring, for building a synthesizer system with, I’m just not seeing that Rack is the natural home for that.

That’s my sincere view, written in the best meaning, and not meant to put you down at all. Hope it makes sense.

I may be misremembering this, but IIRC some early approaches to polyphony support handled it through context-menu options. I wonder if that approach could offer a nice middle ground—maintaining Rack’s interoperability while still allowing for deeper experimentation where desired.

I personally love having my cables represent direct connections, ones that I can easily intercept with a scope to understand. It may sound silly or inconsequential, but I sometimes feel like more technical or nuanced poly workflows can abstract that ability away for me. To me, this proposal feels like an extension of that abstraction, with a cable containing additional layers and disparate types of information that become harder to access, visualize, or edit.

Pros and cons, I suppose. I probably wouldn’t personally use something like this, but I can imagine a lot of folks being excited about the options it provides.

@LarsBjerregaard @sounds — thanks, both. Lars, and yes, I’m Chris.

There are two separate questions in this, so I will take them one at a time.

Where MPX came from. I did not set out to design a protocol. Fifteen years ago I wrote a generative polyphonic sequencer called GeoSonix — C++ on Qt — which produced notes in several independent voices and played them out over MIDI. That version is no longer maintained, but about a year ago I decided to take the idea further and reimplemented it in Web Audio, and alongside it built a Web Audio modular synthesiser, DreamRack, to play it, with voices as a first-class thing: each voice is a tab holding that voice’s own group of modules. It runs in a browser at DreamRack — rack, and here it is playing polyphonic voices, using Strudel as the note source to test the idea: https://youtu.be/-zu7oc7BWHI. What I needed between sequencer and synth was an interface that was polyphonic in the way the sequencer already was, and that is where MPX came from. A good deal of it works in Web Audio now, and I am porting it to VCV because I want access to a far wider ecosystem of modules than I can write myself.

That origin is why the design looks the way it does. A generative sequencer does not emit gates and pitches; it emits notes, in a key, against a chord, with a duration known at note-on, and it goes on making decisions about those notes while they sound. Getting that into VCV meant either throwing most of it away or inventing somewhere to put it.

What is actually on the cable. Lars, one correction to your summary: it is not a compressed, MIDI-style backplane, and saving cables is the smaller half of it.

One part of it is coordination. Seven signals per voice, kept in step, arriving in one place. That part is ergonomics: for a four-module chain it is 21 cables against 3.

The other part is information that is not a voltage. Some of what a note carries has no sensible voltage representation at all:

  • The chord currently in force, and the key — not as a CV, but as a set of pitch classes a downstream module can quantise or voice against.
  • The identity of a note that is already sounding. Every note carries a handle, so the source can come back to it after note-on: end it early, or change it, because of something that happened at the source since it started. In a generative sequencer that is the ordinary case, not an edge case — the decision to cut a note or move it is made while the note is playing. With gates and pitches you can only address a voice slot, and once two notes overlap on the same pitch nothing downstream can work out which note-off belongs to which note-on.

Neither of those can be sent down a wire that carries a number. The handle is also where this departs from MIDI rather than copies it: MIDI addresses a sounding note by channel and key number, which stops being unique the moment the channel is reused.

Poly cables, since you say there is room left in them. I agree that there is, and a poly cable was the first thing I tried for this. You can carry an expressive voice on one if you give each channel over to a single attribute — pitch on one channel, then level, bend, pressure, timbre, pan and the rest — and it works. It costs about nine of the sixteen channels to describe one note that way. So a poly cable used like this becomes one voice per cable rather than one part per cable, and what I was after was this kind of control over several voices at once. That is the same corner MPE is in, incidentally: it spends a 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.

That cuts the other way too, and it is one of the things MPX is a natural outlet for. An MPE controller — a Seaboard, a LinnStrument — is already sending per-note bend, pressure and timbre, and at the moment there is nowhere in Rack for all of that to land intact. Since MPX carries everything MPE does, the module for it is fromMPX with a MIDI input in place of the MPX one: the same seven outputs, driven straight from the controller, so an expressive instrument breaks out into ordinary Rack signals with nothing thrown away.

The converters, and the two-ecosystems worry. Keaton, your context-menu instinct is the right shape; the difference is that the opt-in is a module rather than a menu item, so there is no mode to be in. MPX has converters at both ends, and they are ordinary modules with ordinary jacks:

  • toMPX takes seven conventional inputs — Gate, 1V/oct, Level, Duration, Pan, Pressure, Timbre — and bundles them into one MPX signal.
  • fromMPX takes an MPX signal and breaks it back out to seven conventional outputs — Gate, 1V/oct with bend applied, Level, Bend, Pressure, Timbre, Pan.

So the boundary is a module, not a mode. Anything in the rack can generate an MPX stream, and anything in the rack can consume one. Processors in the middle forward what they do not consume — the monitor module exists partly to prove that, since it changes nothing and passes the stream on, and it also shows everything travelling on the cable, so the contents are not hidden from you — so you can tap a chain anywhere with a fromMPX and take conventional signals off it without breaking the chain. Keaton, that is also the answer to the scope: put a fromMPX in at any point and you have seven ordinary jacks to meter, scope and patch by hand.

That is what I would put against the fragmentation worry, Lars. MPX asks nothing of Rack and changes nothing about voltages, every chain that uses it begins and ends in ordinary jacks, and a module that never hears of it loses nothing — a patch still has to come back to 1V/oct to make a sound. It is an extra way for a few modules to talk to each other, not a second Rack.

Expanders could carry this. A left/right expander chain is a sanctioned non-voltage channel in Rack already, and MPX could have been built on it. The cost is adjacency: an expander chain forces every module in a voice to sit physically next to every other one, in order. A cable lets me build the voice’s identity in one place and run it to a cluster of modules somewhere else in the rack — several clusters, if the same note stream drives more than one.

What I am asking here. I am going to write a number of MPX modules either way, since I need them for my own sequencer. The question is narrower than a proposal to change anything about Rack: it is whether anyone else working on polyphonic expression has a use for the mechanism itself. If a few of you do, I would package it as a library, so that sending or receiving notes this way does not mean reimplementing the bus each time.

Thanks for your clarification Chris! It seems well considered, if not a bit over my head. Excited to try out your sequencer and MPX if/when you release it.

Cheers!

Further discussion of MPX, both the cable protocol and the mpxChart, mpxArp, mpxRand and fromMPX modules, is in Dreamer Development: Dreamer Development: Clarity, Test Gear, Dark, Help and MPX (latest: Help 2.0.1)

MPX is still a work in progress and not officially released; the pre-release builds remain on GitHub.