Is there any basic expander tutorial available?
I saw a similar topic but the most valuable reference (see Module expander proposal) is unavailable.
Is there any basic expander tutorial available?
I saw a similar topic but the most valuable reference (see Module expander proposal) is unavailable.
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.
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.
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!
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:
of course one must never, ever, do this on the audio process thread.