Advice for programming polyphony into a VSTi?

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

Hi,

I'm looking to design a soft-synth using C++ and the steinberg VST SDK. i'm having a little trouble getting to grips with the polyphony, any advidce?

Post

Davizman wrote:I'm looking to design a soft-synth using C++ and the steinberg VST SDK. i'm having a little trouble getting to grips with the polyphony, any advidce?
I've found it useful to represent "voices" in my synths as C++ classes. A Voice class will have Trigger and Release methods. Then it's a matter of keeping a pool of Voice objects around when a note-on message arrives. If there's a voice in the pool, take it out and trigger it with the new note. If not, steal an already playing voice and trigger it.

When a note-off message arrives, find the voice responsible for playing that note, if any exist, and release it.

That's my basic approach.

Post

my approach is similar to Leslies. the 'pool' is a simple array in my case, finding a free voice involves scanning through the array and stealing a voice is done by keeping track of which voice in the array is the oldest. that requires keeping track of how long a note lasts per voice. i have a simple integer counter per voice which is reset to zero on note-on and gets incremented every sample. ...maybe someone has some more info about proper voice stealing algorithms?
My website: rs-met.com, My presences on: YouTube, GitHub, Facebook

Post

That's pretty much it.

Also, as long as you don't do other midi messages, that approach saves you from having to implement an event system since the new voice can simply wait for it's delta time to arrive (or offset the buffer etc).


WRT to voice stealing, I scan for free, released, and oldest in that order. I determine oldest by a simple counter in the voice object, that increments on every noteOn, the voice with the lowest counter is the oldest one.

Also I use a bench-warmer scheme to avoid clicks when all voices are in use.

Post

i'd like to add, that when i find a voice that currently plays or releases the incoming note, i re-use this voice by re-triggering its envelopes but not retriggering its oscillators.

bench-warmer? what's that? some kind of crossfade? i'm considering something like a poly-glide at the moment. in the sense, that the oldest voice slides to the new incoming note when all voices are used. ..or something like that. could be interesting. and in the monophonic case it just reduces to the usual glide.
My website: rs-met.com, My presences on: YouTube, GitHub, Facebook

Post

Cheers, guys thats great. Thanks for the prompt response.

So let me just clarify, I need to make an array of voices say 16 cells for 16 voice polyphony.

then on a "note on" message, see if there are any free voices.

if there is a free voice use it

if all are being used release the oldest one (using a counter that increments ever sample)

if a note off occurs search for the voice that is responsible for that MIDI note and release it.


Just out of curiosity, the trigger and release methods you speak of, do you use them to trigger and release an amplitude envelope?

Post

Bench warmer is a voice that is not in play, like in football. If you have 8 note polyphony, you have 8 voices in play, but you can also have one or more "on the bench".
When a new note is played you trigger a bench warmer, but if your polyphony is maxed out you also kill (quick release) one of the ringing voices and designate it the next bench warmer.

Davizman > Yeah you pretty much got it. (Except I wouldn't count the samples.)

And yes, to trigger means to restart the envelopes, but also maybe reset things like LFOs and oscillators. It can apply to many things depending on your design.


Here's an old voice class for one of my early synths. Don't rely on it! I'm posting as an example of what might go in to a voice class.

Code: Select all


#ifndef __SUBTRACTIVEVOICE_H__
#define __SUBTRACTIVEVOICE_H__

#include "../cTblOSC.h"
#include "../cADSR.h"

class SubtractiveVoice
{

private:

	cTblOSC * osc1;
	cTblOSC * osc2;

	cADSR * ampADSR;
	cADSR * filterADSR;

	float* Parameters;

	bool isOn,  isReleased;
	int note;

	int Ordinal;

	float velocity;
	float SampleRate, BaseFrequency;

	float low, band, high, q; //main Filter



public:

	SubtractiveVoice(float* P, double s)
	{
		Parameters = P;

		osc1 = new cTblOSC(1);
		osc2 = new cTblOSC(5);

		ampADSR =  		new cADSR();
		filterADSR = 	new cADSR();

		SampleRate = s;

		Ordinal = 1;
		isOn = false;

		note = 0;

		low = 0.0f;
		band = 0.0f;
		high = 0.0f;
		q = 0.0f;

	}


	~SubtractiveVoice()
	{
		delete osc1;
		delete osc2;
		delete ampADSR;
		delete filterADSR;
	}


	void doProcess( float * outBuffer , int numSamples)
	{

		if(!isOn)
		{
			return;
		}


		float Buffer[numSamples]; //work buffer, could be persistant
		for(int i = 0; i < numSamples; i++)
		{
			Buffer[i] = 0.0f;
		}

		//Render Wave
		osc1->doProcess(Buffer, numSamples);
		osc2->doProcess(Buffer, numSamples, Parameters[12]);


		float cut =  0.0f;
		float amp = 0.0f;

		for(int i = 0; i < numSamples; i++)
		{
			
			//filter
			q = 1.1f - Parameters[2];
			cut =  limit(0.001f, 0.999f, Parameters[1] + (filterADSR->getSample() * Parameters[0]));
			low = low + cut * band;
			high =  Buffer[i] - low - q * band;
			band = cut * high + band;
			Buffer[i] = (low * ((q * 0.5f) + 0.5f));// + (high * ((q * 0.5f) + 0.5f));

			//copy to outbuffer
			amp = ampADSR->getSample();
			
			assert(amp == amp || !isnan(amp) || !isinf(amp));
			
			outBuffer[i] += Buffer[i] * Parameters[11] * amp;
		}

		isOn = ampADSR->getState();

	}



	void start(float f, float v, int n, int preroll, float s, int o)
	{
		Ordinal = o;
		SampleRate = s;
		
		assert(int(SampleRate) != 0);
		
		BaseFrequency = f * convPitch(Parameters[13]);


		osc1->reset(BaseFrequency, SampleRate );
		osc2->reset(BaseFrequency, SampleRate );
		note = n;

		isOn = true;
		isReleased = false;
		velocity  = v;
		ampADSR->reset(preroll);
		filterADSR->reset(preroll);
	}



	void release(int p)
	{
		isReleased = true;
		ampADSR->release(p);
		filterADSR->release(p);

	}

	void quickRelease()
	{
		isReleased = true;
		ampADSR->quickRelease();
	}

	int getOrdinal()
	{
		return Ordinal;
	}

	void kill()
	{
		isOn = false;
	}


	void update(int index)
	{
		switch(index)
		{
		case 15: osc1->setOscTbl(int(Parameters[15])); break;
		case 16: osc2->setOscTbl(int(Parameters[16])); break;
		case 7:
		case 8:
		case 9: 
		case 10: ampADSR->setADSR(Parameters[7], Parameters[8], Parameters[9], Parameters[10], SampleRate); break;
		case 3:
		case 4:
		case 5:
		case 6: filterADSR->setADSR(Parameters[3], Parameters[4], Parameters[5], Parameters[6], SampleRate);break;
		case 100:
			osc1->setOscTbl(int(Parameters[15]));
			osc2->setOscTbl(int(Parameters[16]));
			ampADSR->setADSR(Parameters[7], Parameters[8], Parameters[9], Parameters[10], SampleRate); 
			filterADSR->setADSR(Parameters[3], Parameters[4], Parameters[5], Parameters[6], SampleRate);
		break;
		}
	}


	int getNote()
	{
		return note;
	}


	bool getIsOn()
	{
		return isOn;
	}

	bool getIsReleased()
	{
		return isReleased;
	}

	
};

#endif



Post

Robin from www.rs-met.com wrote:i'd like to add, that when i find a voice that currently plays or releases the incoming note, i re-use this voice by re-triggering its envelopes but not retriggering its oscillators.
Good point.

My approach is to leave the phase of the oscillators alone until the voice that's "playing" them has completed its release segment, i.e. is completely done with the current note. Then I like to randomize the phase to give a bit more life to the sound.

Post

@Rock: thanks for clarifying this benchwarmer idea.
@Leslie: i mostly retrigger to some well defined (maybe user adjustable) phase. i like exact reproducability. but that's a matter of taste, of course.
My website: rs-met.com, My presences on: YouTube, GitHub, Facebook

Post

I have two stacks (implemented by linked lists), one for "free voices", one for "used voices". Using a stack like this helps with cache locality of the voice structures (... well, not anymore :hihi:)
Cakewalk by Bandlab / FL Studio
Squire Stratocaster / Chapman ML3 Modern V2 / Fender Precision Bass

Formerly known as arke, VladimirDimitrievich, bslf, and ctmg. Yep, those bans were deserved.

Post

Davizman wrote:Cheers, guys thats great. Thanks for the prompt response.

So let me just clarify, I need to make an array of voices say 16 cells for 16 voice polyphony.

then on a "note on" message, see if there are any free voices.

if there is a free voice use it

if all are being used release the oldest one (using a counter that increments ever sample)

if a note off occurs search for the voice that is responsible for that MIDI note and release it.
I use linked lists to store the voices. I have several lists:

1) A list with all of the voices - kind of master list.
2) A list with all of the voices that have been released.
3) A list with all of the voices that have been triggered (but not yet released)
4) A list with all of the voices that are currently sustaining.

In addition I have an array of 128 pointers to voice objects, a note table. When a voice is triggered, a pointer to the voice is placed in the array using the note number as an index. This makes it easy to find the voice responsible for a specific note when a note-off message is received.

When the voice allocator is reset, it adds all of the voices from the master list (list 1) to the released list (list 2). It then waits for a note to be triggered. It also nulls out all of the pointers in the note table.

The sustaining list is used for voices that are released when the sustain pedal is down. It is kind of a holding list that is used for delaying the actual releasing of voices until the sustain pedal is lifted. When the sustain pedal is lifted, the allocator releases any voices in the sustaining list.

When a note-on event arrives, I check the released list first. If there are any voices there, I get the one at the end of the list. If there are no voices in the released list, I check to see if we're in the sustaining mode. If so, I check the sustaining list for any voices and grab the one at the end if there is one. If not, I finally steal a voice from the end of the triggered list.

I make sure to manage the lists carefully; I put in plenty of assertions to make sure the allocator's invariants are enforce.

I don't keep track of when a voice has completed its release segment. I just let it sit in the released list. This works for me. What is means, though, is that when it's time to mix the voices for the output, I have to iterate through all of the voices in the master list and ignore those that are not playing. I haven't found this to be a problem; the costs of doing so are very small.
Just out of curiosity, the trigger and release methods you speak of, do you use them to trigger and release an amplitude envelope?
Yes, in my synth each voice contains an envelope routed to modulate its amplitude. When a voice is triggered, it delegates the event to any of its internal modules, such as the amplitude envelope, that are interested in the event. For example, a filter may need to know what note was triggered in order to calculate key tracking to modulate the cutoff frequency.

Post

Robin from www.rs-met.com wrote:i'd like to add, that when i find a voice that currently plays or releases the incoming note, i re-use this voice by re-triggering its envelopes but not retriggering its oscillators.
I also find this nicely reduces audible clicking, especially if you don't mind slightly altered attacks on stolen voices, and instead of fully reseting the envelopes, just set 'em back to attack phase (assuming something like ADSR) with whatever value they have. Especially with exponential (with "reverse-exponential" attack) envelopes this usually doesn't have much audible result, and while the loss of a voice is often audible (but that's gonna happen with any limited-polyphony solution), otherwise it gives quite nice results.

Anyway, my other two cents is that I actually mostly represent my voices an array of structures of the different modules (being represented as classes), basicly something like this:

Code: Select all


		struct Voice {
			VCO      vco;
			Filter   filter;
	
			Envelope fenv;
			Envelope aenv;

			bool gate;

			char note;

			Voice() { reset(); }
			void reset() {
				gate = false;
				vco2.reset();
				filter.reset();
				fenv.reset();
				aenv.reset();
			}
		} voices[NVOICES];
In other words, a class with all-public members. That way I can mess with the rendering orders and shared data within the main processing logic without having to maintain separate voice class.

I got my noteOn/noteOff logic simply as members of the main synth. Other than those, the only place where that array's really touched is the synth main processing method, so I'd find pushing that stuff into a separate class just overhead. :)

For finding a free voice, I linearly scan the table twice: once to find the lowest-volume voice (usually just getting current amplitude envelope values), and then again, but only considering those which don't have "gate" set (if there are none, then the best bet from the first scan is selected. I find this O(N) solution to be quite acceptably for what number of voices one normally has.

For turning of voices, I similarly scan the whole array, and setting gate to false for any voices that match the note to be turned off. Means that even if I somehow manage to miss a note-off and have two voices at the same note-number, I'll turn them both properly off at the next note-off anyway.

But you probably don't wanna hear my O(N) solution for monophonic last-note priority? :D

Post

I got 2 mainlist who contains the same noteobjects, but one is sorted by the noteevent order (the way its played), and the other is sorted from lowest to highest notenumber -reason is that I can also enable an arpeggiator in my class.
To handle duplicate notes I have an array [0..127] list, so duplicate notes are possible if I enable that option.

Using those Sorted and Event list gives quick and easy access to notes, and when a note is released (say 67), I just get the first note-object in my dupelist array [67] and no searching involved, since my noteobject tells where it is also located in Sorted and Event list.

...This makes easy and fast coding concerning inherited re-use of my midiclass :)
Image

Post

'k, to elaborate on how I do it :hihi:

I have two linked lists (used as "stacks"). One of them is for the currently playing voices, and one of them is for the free voices. I also have a note to voice map (array of 128) like Leslie does. On plugin startup, I initialize the table to zero, and put all the voices into the free voice stack.

On note-on, a voice is popped off the free list, triggered, then put on the playing list. Note-offs simply look up the corresponding Voice in the table, and trigger its noteoff method, but doesn't do anything else. During processing, the voice's proces() method returns a bool indicating if it's done processing, and if it is, it's removed from the playing list and put on the free list.

If a voice needs to be stolen, one from the back of the playing stack can be killed because it's guaranteed to be the oldest voice.

While I haven't implemented this yet, the voices could be optimized so that short "stab" sounds would actually stop processing when it hits the sustain (= 0) part of the amp envelope, etc.
Cakewalk by Bandlab / FL Studio
Squire Stratocaster / Chapman ML3 Modern V2 / Fender Precision Bass

Formerly known as arke, VladimirDimitrievich, bslf, and ctmg. Yep, those bans were deserved.

Post

What I do not understand, is why some sequencers send the noteoff event (status $80) twice. Minihost is one of them...
Image

Post Reply

Return to “DSP and Plugin Development”