Cloning module components

Hi, i’ve been trying to reverse engineer the source code of some Fundamental modules with the aim to learn more about how they work, and hopefully use this knowledge to eventually be able to build my own, but have come up against some roadblocks…

For example, to start off simple I tried to add 2 separate sine outputs (with two separate knobs obviously) to the Template module… To do this I attempted to just add another Pitch param:

struct MyModule : Module {
	enum ParamId {
	    PITCH_PARAM,
	    PITCH_PARAM2,
            NUM_PARAMS

and clone every instance of PITCH_PARAM with a duplicate line of code for PITCH2_PARAM

void process(const ProcessArgs &args) override {
		// Implement a simple sine oscillator

		// Compute the frequency from the pitch parameter and input
		float pitch = params[PITCH_PARAM].getValue();
                                 params[PITCH2_PARAM].getValue();

And the same for:

	outputs[SINE_OUTPUT].setVoltage(5.f * sine);
        outputs[SINE2_OUTPUT].setVoltage(5.f * sine);

etc…

I knew this would be very crude, and probably wouldn’t work, but I haven’t been able to figure out why. I can post detailed build errors if you want them.

If someone could help I would appreciate it very much.

Surround code with ``` please.

It’s impossible to say what the problem is without seeing the rest of the code unfortunately.

#include "plugin.hpp"


struct MyModule : Module {
	enum ParamId {
		PITCH_PARAM,
	    PITCH2_PARAM,
		NUM_PARAMS
	};
	enum InputId {
		PITCH_INPUT,
		NUM_INPUTS
	};
	enum OutputId {
		SINE_OUTPUT,
		SINE2_OUTPUT,
		NUM_OUTPUTS
	};
	enum LightId {
		BLINK_LIGHT,
		NUM_LIGHTS
	};

	float phase = 0.f;
	float blinkPhase = 0.f;

	MyModule() {
		// Configure the module
		config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);

		// Configure parameters
		// See engine/Param.hpp for config() arguments
		configParam(PITCH_PARAM), -3.f, 3.f, 0.f, "Pitch", " Hz", 2.f, dsp::FREQ_C4);
		configParam(PITCH2_PARAM), -3.f, 3.f, 0.f, "Pitch", " Hz", 2.f, dsp::FREQ_C4);
	}

	void process(const ProcessArgs &args) override {
		// Implement a simple sine oscillator

		// Compute the frequency from the pitch parameter and input
		float pitch = params[PITCH_PARAM].getValue();
                      params[PITCH2_PARAM].getValue();
		
		
		pitch += inputs[PITCH_INPUT].getVoltage();
		pitch = clamp(pitch, -4.f, 4.f);
		// The default pitch is C4 = 261.6256f
		float freq = dsp::FREQ_C4 * std::pow(2.f, pitch);

		// Accumulate the phase
		phase += freq * args.sampleTime;
		if (phase >= 0.5f)
			phase -= 1.f;

		// Compute the sine output
		float sine = std::sin(2.f * M_PI * phase);
		// Audio signals are typically +/-5V
		// https://vcvrack.com/manual/VoltageStandards.html
		outputs[SINE_OUTPUT].setVoltage(5.f * sine);
		outputs[SINE2_OUTPUT].setVoltage(5.f * sine);


		// Blink light at 1Hz
		blinkPhase += args.sampleTime;
		if (blinkPhase >= 1.f)
			blinkPhase -= 1.f;
		lights[BLINK_LIGHT].setBrightness(blinkPhase < 0.5f ? 1.f : 0.f);
	}

	// For more advanced Module features, see engine/Module.hpp in the Rack API.
	// - dataToJson, dataFromJson: serialization of internal data
	// - onSampleRateChange: event triggered by a change of sample rate
	// - onReset, onRandomize: implements custom behavior requested by the user
};


struct MyModuleWidget : ModuleWidget {
	MyModuleWidget(MyModule *module) {
		setModule(module);
		setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/MyModule.svg")));

		addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, 0)));
		addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
		addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
		addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

		addParam(createParam<Davies1900hBlackKnob>(Vec(10, 87), module, MyModule::PITCH_PARAM));
		addParam(createParam<Davies1900hBlackKnob>(Vec(40, 87), module, MyModule::PITCH2_PARAM));




		addInput(createInput<PJ301MPort>(Vec(33, 186), module, MyModule::PITCH_INPUT));

		addOutput(createOutput<PJ301MPort>(Vec(33, 275), module, MyModule::SINE_OUTPUT));
		addOutput(createOutput<PJ301MPort>(Vec(33, 275), module, MyModule::SINE2_OUTPUT));

		addChild(createLight<MediumLight<RedLight>>(Vec(41, 59), module, MyModule::BLINK_LIGHT));
	}
};


// Define the Model with the Module type, ModuleWidget type, and module slug
Model *modelMyModule = createModel<MyModule, MyModuleWidget>("MyModule");

float pitch = params[PITCH_PARAM].getValue(); params[PITCH2_PARAM].getValue();

The PITCH2_PARAM is not assigned to anything assigned being =

Hi Felix!

To follow up on @Coirt’s observation, there’s a deeper issue beyond the build error: this code is going to send the same output value to both outputs.

Just as you need to do configParam twice, once for each parameter ID, you also need to do the whole processing block twice, once for each pitch. I’ll step through what I mean by the “processing block,” which has two pretty different sections.

The first part handles the inputs. We store the value of the PITCH_PARAM knob in a variable called pitch:

float pitch = params[PITCH_PARAM].getValue();

Add the input voltage from the PITCH_INPUT jack (if any). Note that if you want CV control, you’ll need a PITCH_INPUT2 value as well.

pitch += inputs[PITCH_INPUT].getVoltage();

Clamp it within a certain range:

pitch = clamp(pitch, -4.f, 4.f);

And convert it to a frequency:

float freq = dsp::FREQ_C4 * std::pow(2.f, pitch);

The second part actually computes the output. This part is trickier because it’s flipped vertically: the oscillator function (std::sin) is called after we compute where we want to be within it. We start by figuring out how much we want to step forward in the function, which depends on the frequency that we just calculated AND the sample rate (higher sampling rates will yield shorter steps):

phase += freq * args.sampleTime;

We reset if we’re going to go outside of a single cycle:

if (phase >= 0.5f)
	phase -= 1.f;

At last, we calculate the value of the function:

float sine = std::sin(2.f * M_PI * phase);

And, as the last step, we send that value to the SINE_OUTPUT jack.

outputs[SINE_OUTPUT].setVoltage(5.f * sine);

To calculate two different oscillators, you don’t just need separate inputs and outputs, you need a whole second processing block. In other words (to get you started):

float pitch = params[PITCH_PARAM].getValue();
float pitch2 = params[PITCH_PARAM2].getValue();

pitch += inputs[PITCH_INPUT].getVoltage();
pitch2 += inputs[PITCH_INPUT2].getVoltage();

and so forth.

Since you’re doing the same thing twice, the right way to do this is with a function (which would also let you easily switch to, say, four oscillators). I’d get it working as is first, and then turn it into a function; happy to help with that if you post back here.

All the advice, above, is good. But you can try the same thing with the example plugin Template. It is also a sin VCO, but the code is MUCH simpler. It will be much easier to figure it out in there.

Hi @Squinky: I thought this was the Template code, and that OP was starting with Template and trying to modify? I’m probably missing something :stuck_out_tongue: