Any expander tutorial around?

Is there any basic expander tutorial available?

I saw a similar topic but the most valuable reference (see Module expander proposal) is unavailable.

1 Like

If you (or other developers) can summarize what questions you have about the module expander API, I can write a tutorial for the manual at some point in the future.

The old “Module Expander Proposal” was an early proposal that became out of date and inaccurate, so it was removed.

2 Likes

Much appreciated Andrew! I don’t think I’ll ask you to write anything, I am sure that you already have quite a few projects running parallelly that are way more beneficial for the VCV community.

I better give another try to find a few of the simplest codes in GitHub and figure out myself. (Any suggestion? 8FACE? StochSeq4X?) For me it’s easier to learn through examples than API.

But the easiest would be to see an expander for something in the Fundamental collection. (Like an additional set of volume knobs for VCV Mix.)

Those Fundamental modules are worth of hundred tutorials anyway!

Do you have something particular in mind your trying to do with the expanders?

I’m more interested in understanding the concept and opportunities than implementing a specific trick or adding an expander to my (half-baked) modules.

But my test project would include something like this I think.

I don’t have a tutorial, but my text formatter module is an expander using the message swapping technique. Feel free to look at the code. The module defines a pair of messages. It writes in one, while the modules either side can read the other, then it swaps them every sample Kind of like double buffering a display.

2 Likes

For all the lazy people searching for their first ever expander project in the future I leave here a simple and short code (< 100 lines) as demo.

// A short demo for a simple module (DemoExpR) and an expander (DemoExpRModule)

#include "plugin.hpp"
struct DemoExpR : Module {

	enum ParamId  {KNOB_PARAM, PARAMS_LEN};
	enum InputId  {INPUTS_LEN};
	enum OutputId {CV_OUTPUT, OUTPUTS_LEN};
	enum LightId  {LIGHTS_LEN};

	DemoExpR() {
		config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
		configParam(KNOB_PARAM, 0.0f, 1.0f, 0.5f, "Set voltage");
		configOutput(CV_OUTPUT, "Voltage"); 
	}

	float newVolt;
	void process(const ProcessArgs& args) override {
		newVolt=params[KNOB_PARAM].getValue();
		outputs[CV_OUTPUT].setVoltage(newVolt);
	}

};

struct DemoExpRWidget : ModuleWidget {

	DemoExpR* module;
	DemoExpRWidget(DemoExpR* module) {

		this->module = module;
		setModule(module);
		setPanel(createPanel(asset::plugin(pluginInstance, "res/DemoExpR.svg")));

		addParam(createParamCentered<RoundSmallBlackKnob>(mm2px(Vec(5.08, 15.24)), module, DemoExpR::KNOB_PARAM));
		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(5.08, 30.48)), module, DemoExpR::CV_OUTPUT));
		
	}

};

Model* modelDemoExpR = createModel<DemoExpR, DemoExpRWidget>("DemoExpR");

/**************************************************/
// EXPANDER CODE - starting here
/**************************************************/

#include "plugin.hpp"
struct DemoExpRModule : Module {

	enum ParamId  {PARAMS_LEN};
	enum InputId  {INPUTS_LEN};
	enum OutputId {CV_INVERT_OUTPUT,OUTPUTS_LEN};
	enum LightId  {LIGHTS_LEN};

	DemoExpRModule() {
		config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
		configOutput(CV_INVERT_OUTPUT, "CV invert"); 
	}

 	DemoExpR* findHostModulePtr(Module* module) {
		if (module) {
			if (module->leftExpander.module) {
				// if it's the mother module, we're done
				if (module->leftExpander.module->model == modelDemoExpR) {
					return reinterpret_cast<DemoExpR*>(module->leftExpander.module);
				}
			}
		}
		return nullptr;
	}
		
	void process(const ProcessArgs& args) override {		
		DemoExpR const* mother = findHostModulePtr(this);
		if (mother) {outputs[CV_INVERT_OUTPUT].setVoltage(mother->newVolt*-1);}
		else {outputs[CV_INVERT_OUTPUT].setVoltage(-0.404);}
	}

};

struct DemoExpRModuleWidget : ModuleWidget {

	DemoExpRModule* module;
	DemoExpRModuleWidget(DemoExpRModule* module) {

		this->module = module;
		setModule(module);
		setPanel(createPanel(asset::plugin(pluginInstance, "res/DemoExpR.svg")));
				
		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(5.08, 30.48)), module, DemoExpRModule::CV_INVERT_OUTPUT));
	}

};

Model* modelDemoExpRModule = createModel<DemoExpRModule, DemoExpRModuleWidget>("DemoExpRModule");

This code creates two modules. One of them (DemoExpR) is the mother with an output and a knob. The other one (DemoExpRModule) is the expander reading mother’s knob value and sending it to it’s own output.

It’s a one-way communication here. It probably can’t be simpler than this. For more options and features study the VCV API!

2 Likes

Thanks for posting this, wish I had it two weeks back! At one point my code was almost this clean, it’s much more convoluted now. A couple things I found along the way:

  • onExpanderChange is your friend. If you can, do any processing there once instead of doing it every process()
  • Daisy chaining and passing data requires some thought. Avoid copying structures of course- but then who owns the struct? Determine the rules on this; it takes some thought to avoid introducing locking and also avoid alloc/delete
  • I found subclassing Module helpful. I put lots of common expander stuff is in the subclass, and the concrete Modules below that subclass. But my wonky use case is daisy chaining different modules together. Maybe this is bad form, I dunno. And hey, it works. :slight_smile:
1 Like

of course one must never, ever, do this on the audio process thread.

1 Like