Module expanders - sharing a minimal example

Hi everyone,

I’ve spent some time recently trying to implement expander modules (with bi-directional communication) for a project I’m working on. Thanks to the Module expanders tutorial thread and a fair amount of experimentation and reading others’ code, it’s working fine. Many thanks to @clone45, @CountModula, and @marc_boule!

Because things are somewhat buried in that thread, I wanted to share a simple example in the hopes that it can save somebody else some time. Here’s a little module that is an expander for itself - slide as many together as you like. Each displays its own position and the total number in the group.

The widget implements the beautiful seamless expander behaviour seen in MindMeld’s MixMaster & AuxSpander.

This is what it looks like.

The module code is:

struct expMessage { // contains both directions
	unsigned int numModulesSoFar = 0; // This will travel rightward
	unsigned int numModulesTotal = 0; // This will travel leftward
};

struct TwoWayExpander : Module {
	expMessage leftMessages[2][1]; // messages to & from left module
	expMessage rightMessages[2][1]; // messages to & from right module
	unsigned int numMe = 0;
	unsigned int numModules = 0;

	TwoWayExpander() {
		leftExpander.producerMessage = leftMessages[0];
		leftExpander.consumerMessage = leftMessages[1];	
		rightExpander.producerMessage = rightMessages[0];
		rightExpander.consumerMessage = rightMessages[1];
	} // module constructor
	
	void process(const ProcessArgs &args) override {
		bool expandsLeftward = leftExpander.module && leftExpander.module->model == modelTwoWayExpander;
		bool expandsRightward = rightExpander.module && rightExpander.module->model == modelTwoWayExpander;

		expMessage* leftSink = expandsLeftward ? (expMessage*)(leftExpander.module->rightExpander.producerMessage) : nullptr; // this is the left module's; I write to it and request flip
		expMessage* leftSource = (expMessage*)(leftExpander.consumerMessage); // this is mine; my leftExpander.producer message is written by the left module, which requests flip
		expMessage* rightSink = expandsRightward ? (expMessage*)(rightExpander.module->leftExpander.producerMessage) : nullptr; // this is the right module's; I write to it and request flip
		expMessage* rightSource = (expMessage*)(rightExpander.consumerMessage); // this is mine; my rightExpander.producer message is written by the right module, which requests flip
		
		if (expandsLeftward) { // Add to the number of modules
			numMe = leftSource->numModulesSoFar + 1;
		} else { // I'm the leftmost. Count myself.
			numMe = 1;
		}

		if (expandsRightward) {
			rightSink->numModulesSoFar = numMe; // current count
			numModules = rightSource->numModulesTotal; // total from right
			rightExpander.module->leftExpander.messageFlipRequested = true; // tell the right module to flip its leftExpander, putting the producer I wrote to into its consumer
		} else { // I'm the rightmost. Close the count loop.
			numModules = numMe;
		}

		if (expandsLeftward) {
			leftSink->numModulesTotal = numModules; // total goes left
			leftExpander.module->rightExpander.messageFlipRequested = true; // tell the left module to flip its rightExpander, putting the producer I wrote to into its consumer
		}
	} // process
}; // TwoWayExpander

The repo can be found at https://github.com/landgrvi/VGLabs-TwoWayExpander. Thanks again, and enjoy!

16 Likes

Very cool! I like the example of “melding” of connected panels like MindMeld does.

Thanks! This is a very helpful example.

It would be really great if we could somehow link this up to the Plugin API Guide on the VCV tutorial page. The examples they point to currently (Greyscale) are closed source.

Thanks, I’ve been looking exactly for such succinct example of expander implementation. Bookmarked.

In my experience VCV does not link to “other” stuff. ymmv, but they have never linked to any of my stuff.

Thank you so much! This really helped me to understand expanders!

One question: does it take n frames (audio samples) until a message has propagated across n daisy chained modules or does it happen in one frame?

As documented in the manual, each message passed from one module to another happens in the next frame after you set the flag. So an implementation that communicates 1:1 with its neighbor will take n frames to go from, for example, the leftmost module to the rightmost.

It’s not hard to follow the chain of expanders, so if you want to communicate with all expanders at once, this is possible to do, but of course much more complex to orchestrate.

I took the other approach in my Solim module with its expanders: the expander modules themselves just provide the additional UI (inputs, outputs and params), but the processing logic is all in the main module. It checks the chain of expanders to the left and right of it (since multiple expanders can be chained in a variable way), and accesses the UI components of the expanders directly to retrieve their current values (it determines the type(s) of expander(s) that are connected, so can access their UI components safely). Everything gets done in the process call of the main module, which can thus use the most up-to-date values within that single sample/frame.

That only works of course if you don’t do any processing in the expanders that influences the main module, since multithreading can cause issues if the main module and the expander modules were both processing and manipulating data.

I thought it was impossible to get even the inputs from “the wrong” thread. This is reliable?

For sure reliable. It is also a documented option within the VCV development guide. It is how all my Venom expanders work as well. I chose this method because it does not introduce any sample delays.

The only thing that is moderately tricky is having to “compute” the connected expander chain each and every process call. Not a big deal if only allowing one expander left and/or right. But if supporting chains, then it becomes a bigger deal.

I also “compute” the expander chain for each module within the widget step procedure in order to maintain connection indicator lights.

3 Likes

The inputs/outputs/… of the modules themselves aren’t bound to a thread. As long as you’re not doing anything in the process() of the expanders (or at least nothing such as calculating variables or constructing/destructing objects that the main module then tries to use), multithreading shouldn’t have an impact.

I did wonder back then if there was a risk that an expander module might get moved/removed on another thread while the main module was in its process() method and using the inputs of that expander module, so I asked it in another post (Accessing expanders when Rack is set to multiple threads), and Andrew confirmed that the VCV Rack engine stops this from happening (and that accessing the inputs/outputs of an expander is safe since there is no relevant process() logic in the expander itself)

3 Likes

Looks like we came to a very similar approach to our expander implementations :slight_smile: I also added connection indication lights on them, which I update in the widget draw method to reduce number of times they have to be re-checked

2 Likes