References for how voice / note allocation systems work from first principles?

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

you still need to track the age of every note until the end if you are killing the oldest.
It's a simple integer increment that happens with a very low interrupt frequency of the modulation (usually around 500 Hz). Even a 40 year old computer can do this with any mentionable CPU or RAM requirement at all

Post

Markus Krause wrote: Wed Feb 08, 2023 8:22 am
you still need to track the age of every note until the end if you are killing the oldest.
It's a simple integer increment that happens with a very low interrupt frequency of the modulation (usually around 500 Hz). Even a 40 year old computer can do this with any mentionable CPU or RAM requirement at all
Doesn't even matter if you track them sample accurately.. 'cos updating them is still something like

Code: Select all

for(auto & v: voices) v.age += nframes;

Post

ah, you don't need to use CPU actually repeatedly calculating the age. (If you are born in 1972 and I in 1965, I can tell you who oldest without bothering to figure out our ages at all).
Likewise, the oldest note is the one that started on the oldest timestamp.

Code: Select all

if (voice1->NoteOnTime < voice2->NoteOnTime)
{
Last edited by Jeff McClintock on Wed Feb 08, 2023 8:10 pm, edited 1 time in total.

Post

here is the 'best voice to steal' logic from SynthEdit..

Code: Select all

// when polyphony limit is exceeded or we failed to allocate, steal a voice.
if (activeVoiceCount > Polyphony || !allocatedVoice)
{
	Voice* best_off{};      // best unused voice to steal (or nullptr if none)
	Voice* best_on{};	// best 'in-use' voice to steal

	// compare sounding note's timestamp.  Any started before 'now' can be stolen.
	// notes started on SAME timestamp can't be stolen because voiceactive messages will ovewright each other causing ADSR to NOT reset.(clicks)
	float bestHeldNoteScore = -100.0f;
	float bestReleasedNoteScore = -100.0f;

	// Search Voice list for unused voice
	for (auto it = begin() + 1; it != end(); ++it)
	{
		auto v = *it;

		// determine best voice to steal from active voices (not already stolen).
		if (v == allocatedVoice || v->voiceState_ != VS_ACTIVE || v->IsRefreshing())
			continue;
					
		// steal the oldest/quetest voice.
		float loudnessEstimate = (std::min)(1.0f, v->peakOutputLevel);

		// Assume attack is clicky or perceptually significant to percieved loudness. Also compensates for 10ms lag updating peak level.
		// attack will be more important up to 0.2 seconds into note, then loudness takes precedence.
		float noteDuration = (timestamp - v->NoteOnTime) * recipricolSampleRate;
		// note attack estimated at 0.2 seconds (1 / 5.0).
		loudnessEstimate = (std::max)(loudnessEstimate, (1.0f - 5.0f * noteDuration));

		float scoreQuietness = 1.0f - loudnessEstimate;

		// Score increases 20% for each overlap.
		float totalScore = scoreQuietness * (1.0f + (overlappedKeys[v->NoteNum] - 1) * 0.2f);

		if (v->NoteOffTime == SE_TIMESTAMP_MAX) // Held Note
		{
			if (v->NoteOnTime < timestamp) // can't steal a voice that only *just* started.
			{
				if (bestHeldNoteScore < totalScore)
				{
					best_on = v;
					bestHeldNoteScore = totalScore;
				}
			}
		}
		else // released note.
		{
			if (bestReleasedNoteScore < totalScore)
			{
				best_off = v;
				bestReleasedNoteScore = totalScore;
			}
		}
	}

	// shutdown a voice if too many active.
	// Not if voice allocation failed because note will be help-back till benchwarmer free. Unless no benchwarmers at all.
	// Determine best note to 'steal'
	if (best_off)
	{
		stealVoice = best_off;
	}
	else
	{
		stealVoice = best_on;
	}

	// Sometimes there's no voice available to steal because all the active voices started on
	// exact same timestamp (can't steal those because voice-active signal timestamps would co-incide/cancel).
	// In this case, we can't allocate any voice (even if benchwarmer free) because that would exceed polyphony.
	if (!stealVoice) // is available.
	{
		// no steal voice available.
		allocatedVoice = nullptr;

		#if defined( DEBUG_VOICE_ALLOCATION )
			_RPT0(_CRT_WARN, "No Voice allocated - exceeded polyphony already on this timestamp.\n");
		#endif
	}
}

Post

Jeff McClintock wrote: Wed Feb 08, 2023 8:02 pm ah, you don't need to use CPU actually repeatedly calculating the age. (If you are born in 1972 and I in 1965, I can tell you who oldest without bothering to figure out our ages at all).
Likewise, the oldest note is the one that started on the oldest timestamp.

Code: Select all

if (voice1->NoteOnTime < voice2->NoteOnTime)
{
Oh wow, I didn't even think about timestamps. That's such a dead simple idea, no need to keep track of indexes or anything. And it matches the concept that MIDI is just a stream.

Post

no real reason to agonise about CPU usage during note/voice stealing though. in fact, considering that MIDI events happen so infrequently relative to samples, it can be advantageous to do more work during voice allocation bookkeeping so that less work can be done during voice processing. need to walk an array for a linear scan/search when stealing a note? it will probably never even show up in a profiler.
owner/operator LHI Audio

Post Reply

Return to “DSP and Plugin Development”