Making your monophonic module polyphonic

Making your monophonic module polyphonic

Here’s an example of how one would add polyphony to your Module class, say an oscillator. To isolate the problem, I’m wrapping all your custom functionality into an imaginary class VcoEngine.

Monophonic:

VcoEngine vcoEngine;

void process(const ProcessArgs &args) override {
	float pitch = inputs[PITCH_INPUT].getVoltage();
	pitch += params[PITCH_PARAM].getValue();
	vcoEngine.setPitch(pitch);
	vcoEngine.process(args.sampleTime);
	outputs[SINE_OUTPUT].setVoltage(vcoEngine.getSine());
}

Polyphonic:

VcoEngine vcoEngine[16];

void process(const ProcessArgs &args) override {
	int channels = inputs[PITCH_INPUT].getChannels();
	for (int c = 0; c < channels; c++) {
		float pitch = inputs[PITCH_INPUT].getVoltage(c);
		pitch += params[PITCH_PARAM].getValue();
		vcoEngine[c].setPitch(pitch);
		vcoEngine[c].process(args.sampleTime);
		outputs[SINE_OUTPUT].setVoltage(vcoEngine[c].getSine(), c);
	}
	// Don't forget to set the number of output channels
	outputs[SINE_OUTPUT].setChannels(channels);
}

This is all you need to get polyphony working.

Advanced: using SIMD

Using 16 voices would take roughly 16x the CPU compared to the monophonic version for the 16 calls to the expensive VcoEngine::process() method. What if I told you it was possible to use only a bit more than 4x the CPU? (And ~1x for using 4 voices.) You can use 128-bit SIMD, using Rack’s simd/vector.hpp header.

using namespace simd;
// 4 engines processing 4 floats simultaneously
VcoEngine<float_4> vcoEngine[4];

void process(const ProcessArgs &args) override {
	int channels = inputs[PITCH_INPUT].getChannels();
	// Advance by 4 channels each time
	for (int c = 0; c < channels; c += 4) {
		float_4 pitch = float_4::load(inputs[PITCH_INPUT].getVoltages(c));
		// The simd API allows you to add single floats to vectors
		pitch += params[PITCH_PARAM].getValue();
		vcoEngine[c / 4].setPitch(pitch);
		vcoEngine[c / 4].process(args.sampleTime);
		float_4 sine = vcoEngine[c / 4].getSine();
		sine.store(outputs[SINE_OUTPUT].getVoltages(c));
	}
	outputs[SINE_OUTPUT].setChannels(channels);
}

Note that this requires the hypothetical VcoEngine code to be written to support simd::float_4. Here I’ve used a template (template <typename T> struct VcoEngine {...), but you can use the type directly if you want.

7 Likes

Do you plan to eventually make dsp::RCFilter and company SIMD also? Just wondering since some mixing consoles have many filters (mscHack comes to mind), and those could be imrproved also if they used SIMD filter code. I may also want an SIMD RCFilter for a Geodesics module, and I will probably parallelize your RCFilter, but I would ideally like to avoid the code duplication if possible (I could even send you my parallel version of your RCFilter if ever you don’t plan on making it and are interested).

2 Likes

Yes, one day I’ll turn all classes and functions in dsp:: into template <typename T = float>. The difficulty is handling things like logic and branching.

1 Like

Went ahead and made RCFilter and a few others SIMD-ified, since I’ll need them for Fundamental modules. More SIMD coming soon.

5 Likes

Logic is handled with float_4 masks (e.g. x > y returns either 0x00000000 or 0xffffffff for each of the 4 f32’s), which you can use with the float_4 ifelse(float_4 mask, float_4 a, float_4 b) function. You can do stuff like

template <typename T>
T sinc(T x) {
	x *= M_PI;
	T y = simd::sin(x) / x;
	return simd::ifelse(x == 0.f, 1.f, x);
}

and it’ll automatically work for both float and float_4.

Use TRCFilter<float_4> filter; for creating a filter that processes a float_4.

2 Likes

Awesome, thanks, I’ll give it a try! Do you know offhand if there is much of an overhead associated with loading into SIMD space, so to speak, and then storing back to normal float array space? Just trying to see if there are instances, perhaps short calculations, that would be better to just do normally instead of SIMD, or is it always a gain in performance? Profiling would give a definite answer, but just wondering in your experience what you have noticed regarding this.

1 Like

_mm_storeu_ps/_mm_loadu_ps take about 1 CPU cycle, plus overhead of copying the scalars and SIMD vectors between cache and registers, which depends on where your scalars are.

1 Like

@Vortico
What is the status of polyphony for Fundamental VCO.
I tried it in the Linux dev version, that is linked in the other thread.
But seems it is not polyphonic yet?
I could be wrong?

I’ve updated Fundamental’s manifest with Polyphonic tags for the current poly-supporting modules. https://github.com/VCVRack/Fundamental/blob/v1/plugin.json

Ah ok! :+1:t4:

Unrelated, but I was wondering if Octave is deprected or it will be ported to v1 at a later time?

Thanks, added.

2 Likes

i have like a basic polyphony question, cause i think i’m not approaching it correctly yet.
does it work like this: “send 4 CV pitch signals to the MUX module, then patch the poly out of the MUX module into a polyphonic VCO v/oct input” ?

Here’s polyphony working with Blamsoft XFX Wave

I guess there are some different ways of approaching it, since at first try I was expecting the XFX to mix all the channels into a single output wire on its output, but it actually outputs all the channels separately in a poly cable, and we have to split them to mix them. I guess it offers more control this way.

5 Likes

owwww ok, I also need to use a split afterwards! thanks Marc! I will try that out tonight after work. :wink::+1:t4:

Could someone post how poly works with a filter? Preferable F-35

Never mind figured it out not enough audio outputs!

why split and not sums?

Would be great to see 8 channel mixer get a poly input at the top, and then distribute the channels automatically in numerical order. :thinking:

1 Like

Yes. That module is called Fundamental Merge now. Some modules like MIDI-CV can generate poly cables as a source.

Typically you don’t need to adjust channel levels independently. You can just use Fundamental Sum to add all poly channels of an input into a mono output.

1 Like