Linear Interpolation between formant filter coefficients

DSP, Plug-in and Host development discussion.
KVRist
112 posts since 29 May, 2004 from UK

Post Mon Nov 01, 2004 10:36 am

After successfully implementing the formant filter from the music-dsp archives as a VST plugin, I decided to try to add some interpolation between the 5 vowel coefficients (ie. originally there were five possible coefficient selections corresponding to the five vowels. I want to be able to blend smoothly between these settings now).

With my current version of the code it seems to work somewhat with the A, E, and I vowels, there is definitely some change between these parameter positions, however there is still a jump when the integer part changes. Also, as soon as I hit the O vowel the plugin becomes unstable, I thought perhaps this was a denormal issue so I added my usual method of inverting an anti denormal variable with each process() block and therefore adding either 1e-15 or -1e-15 to each sample, but I still get this problem so I assume something else must be causing it.

My Code:

Code: Select all

// FormantFilter.cpp: implementation of the FormantFilter class.
//
//////////////////////////////////////////////////////////////////////

#include "FormantFilter.h"
	
const double voweltable[5][11] = {
	{								///A
		8.11044e-06, 
		8.943665402,    -36.83889529,    92.01697887,    -154.337906,    
		181.6233289,
		-151.8651235,   89.09614114,    -35.10298511,    8.388101016,    
		-0.923313471  
	},

	{								///E
		4.36215e-06,
		8.90438318,    -36.55179099,    91.05750846,    -152.422234,    
		179.1170248, 
		-149.6496211,87.78352223,    -34.60687431,    8.282228154,    
		-0.914150747  
	},

	{								///I
		3.33819e-06,
		8.893102966,    -36.49532826,    90.96543286,    -152.4545478,    
		179.4835618,
		-150.315433,    88.43409371,    -34.98612086,    8.407803364,    
		-0.932568035  
	},

	{								///O
		1.13572e-06,
		8.994734087,    -37.2084849,    93.22900521,    -156.6929844,    
		184.596544,   
		-154.3755513,    90.49663749,    -35.58964535,    8.478996281,    
		-0.929252233
	},

	{								///U
		4.09431e-07,
		8.997322763,    -37.20218544,    93.11385476,    -156.2530937,    
		183.7080141,  
		-153.2631681,    89.59539726,    -35.12454591,    8.338655623,    
		-0.910251753
	}
}; 

static double memory[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

FormantFilter::FormantFilter()
{
	vowel = 0;

	for(int i=0; i<11; i++)
		coeff[i] = voweltable[0][i];	// Initialize coefficient to A
}

FormantFilter::~FormantFilter()
{

}

float FormantFilter::process(float input)
{
	float res = (float)(	coeff[0] * input +
							coeff[1] * memory[0] +
							coeff[2] * memory[1] +
							coeff[3] * memory[2] +
							coeff[4] * memory[3] +
							coeff[5] * memory[4] +
							coeff[6] * memory[5] +
							coeff[7] * memory[6] +
							coeff[8] * memory[7] +
							coeff[9] * memory[8] +
							coeff[10] * memory[9]  
	);

	memory[9] = memory[8];
	memory[8] = memory[7];
	memory[7] = memory[6];
	memory[6] = memory[5];
	memory[5] = memory[4];
	memory[4] = memory[3];
	memory[3] = memory[2];
	memory[2] = memory[1];
	memory[1] = memory[0];
	memory[0] = (double) res;

	return res;
}

void FormantFilter::setVowel(float vowelnum)
{
	int		intpart = (int)(vowelnum * 4.0f);
	float	fracpart = vowelnum - intpart;

	for(int i=0; i<11; i++)
	{
		coeff[i] = voweltable[intpart][i] * (1.0f - fracpart) 
			+ (voweltable[intpart+1][i] * fracpart);			// Linear Interpolation
	}
}
I'm guessing there's something wrong with my interpolation formula, although something similar seems to work fine in another plugin I did... Any suggestions appreciated.

Thanks

Dan

User avatar
Urs
u-he
26016 posts since 8 Aug, 2002 from Berlin

Post Mon Nov 01, 2004 10:54 am

One possible bug:

If vowelnum is 1.0, the intpart will be 4 while you use intpart+1 in your calculations. Hence you will access memory out of the boundaries of the coefficients...

Furtherly, as this seems to be kinda 10-zeros BiQuad Filter, you should leave res a double when feeding it back. You can cast it to float upon return.

Hmmm, honestly, I have no idea how the coefficients were calculated. I can imagine that a linear interpolation between them is not the best idea...

Cheers,

;) Urs

KVRist

Topic Starter

112 posts since 29 May, 2004 from UK

Post Mon Nov 01, 2004 11:27 am

OK, my setVowel function now looks like this:

Code: Select all

void FormantFilter::setVowel(float vowelnum)
{
	int		intpart = (int)(vowelnum * 4.0f);
	float	fracpart = vowelnum - intpart;

	for(int i=0; i<11; i++)
	{
		if(intpart <= 3)
		{
			coeff[i] = voweltable[intpart][i] * (1.0f - fracpart) 
				+ (voweltable[intpart+1][i] * fracpart);		// Linear Interpolation
		}

		else if(intpart == 4)
		{
			coeff[i] = voweltable[4][i];
		}
	}
}
Is there a way to do this without the branch?

I've also made it so the res variable is a double and is only cast to a float when the function returns. I've still got the problem with the O coefficients though. It doesn't seem to get quite as unstable any more; O causes HF noise, but doesn't cause the program to stop responding quite so soon so I'm able to select the U vowel, which doesn't seem to cause any problems. The transition still isn't smooth by any means, so I'm guessing linear interp. probably isn't the way to go about doing this. One of the comments for the code on the music-dsp archives does, however say:
morphing lineary between the coefficients works just fine
I assumed this was the same as linear interpolation; a google search for linear morphing didn't seem to bring up anything relevant at any rate.

I have just noticed there are some corrections in the comments. I'll try those and post back if the problem is solved.

- edit -

ok the corrections are things I'd already accounted for, never mind.
dan

User avatar
Urs
u-he
26016 posts since 8 Aug, 2002 from Berlin

Post Mon Nov 01, 2004 11:39 am

Hehehe,

here's a little trick for doing it without a branch:

Code: Select all

int intValue1 = intValue + (intValue < 4);
Nifty, huh? - As long as intValue is 0-3, the comparison result returns 1. If intValue is 4, it returns 0 and thus intValue1 is always in the correct boundaries for your case, without any branch.

Hmmm, as for the smoothness of the morphing... I have no idea. I'm using other algorithms for my formant type filters...

Cheers,

;) Urs

User avatar
Urs
u-he
26016 posts since 8 Aug, 2002 from Berlin

Post Mon Nov 01, 2004 11:43 am

ooops... replace "intValue" with "intpart" :oops:

;) Urs

KVRist

Topic Starter

112 posts since 29 May, 2004 from UK

Post Mon Nov 01, 2004 12:45 pm

Nifty, huh? - As long as intValue is 0-3, the comparison result returns 1. If intValue is 4, it returns 0 and thus intValue1 is always in the correct boundaries for your case, without any branch.
Ah, yes, that is clever. So doing this should work?:

Code: Select all

	for(int i=0; i<11; i++)
	{
		coeff[i] = voweltable[intpart][i] * (1.0f - fracpart) 
			+ (voweltable[intpart + (intpart < 4)][i] * fracpart);		// Linear Interpolation
	}

Regarding the coefficients, the main reason for using the music-dsp one was to see what the effect is like on (for example) a guitar signal. Those coefficients only work for 44kHz sampling rate, but I have a textbook listing formant frequencies, bandwidths and amplitudes for each vowel sound and also for male and female voices. So ultimately I would like to design my own filter that calculates coefficients on construction depending on sample rate, and then give you the option to morph between each vowel sound and between male and female coefficients.

I'm guessing one of the many freeware filter designers and some hefty DSP textbooks would be the best way to go about doing this. What kind of filter would be best for this? and what would be the best method of interpolating between vowels, etc.?

Dan

User avatar
Urs
u-he
26016 posts since 8 Aug, 2002 from Berlin

Post Mon Nov 01, 2004 1:03 pm

Yep! - It should work 8)

I'm doing such stuff (using comparison results for calculation rather than branching) all day long. It works in gcc/Mac, so I pretty much assume it works on PC as well...

Well, the easiest way to start building your own formant filter would be using 3 to 5 parallel bandpass filters. Using biquads it's also possible to create nearly real formant filters (their frequency response looks like a triangle rather than the bandpass bell shape), but I havn't seen any transfer function for that.

A phantastic sound can be achieved by lowering the input with a shelfving filter and using *extreme* peaking filters in series (!) to bring back the resonating frequencies. This has the cool side effect that you don't have phase cancellations from parallel processing and that you can control the overall spectrum, so that the filter doesn't necessarily sound too "mute"...

Latter can give you that really big sound of the vowel effect that comes with Korg's Legacy Collection.

Cheers,

;) Urs

User avatar
Urs
u-he
26016 posts since 8 Aug, 2002 from Berlin

Post Mon Nov 01, 2004 1:06 pm

Oh yes, before I forget. When you have your seperated formant filters, you can just linearly interpolate their frequencies between the vowels. That's much safer than interpolating coefficients ;)

Cheers,

;) Urs

KVRist

Topic Starter

112 posts since 29 May, 2004 from UK

Post Mon Nov 01, 2004 1:44 pm

Oh yes, before I forget. When you have your seperated formant filters, you can just linearly interpolate their frequencies between the vowels. That's much safer than interpolating coefficients
Right, that doesn't sound too painful to do. So I'd have 3 to 5 separate bandpass filters and set the cutoff, bandwidth and amplitude of each one according to a table containing that information, which I can pretty much take straight out of the book? Is there any advantage to using a filter package to combine the 3 to 5 separate bandpass filters into some kind of uber-filter? I assume that's more or less what's happening with the music-dsp code, so I'm guessing it'd just make blending between vowel sounds too complicated...
A phantastic sound can be achieved by lowering the input with a shelfving filter and using *extreme* peaking filters in series (!) to bring back the resonating frequencies. This has the cool side effect that you don't have phase cancellations from parallel processing and that you can control the overall spectrum, so that the filter doesn't necessarily sound too "mute"...
I don't fully understand what you mean here, but the result sounds very cool... do you mean run on low pass shelving filter on the input and then pass it through the peaking filters... and by *extreme* peaking filters do you mean bandpass filters with high Q/narrow bandwidth? How does this allow you to control the overall spectrum?

Thanks for the help!

Dan

User avatar
Urs
u-he
26016 posts since 8 Aug, 2002 from Berlin

Post Mon Nov 01, 2004 2:09 pm

Well...

You can use bandpass filters in parallel. This works. There's just no transfer function to combine parallel filters. You can only combine filters in series.

But, as with most filter designs, there's almost no point combining them into one big transform. It's usually easier to use them as cascaded stages.

Why peaking filters? - Unlike bandpass filters, peaking filters (such as the ones in the RBJ Audio EQ Cookbook on musicdsp) do not touch the frequencies outside of their working band. Hence, they can be used to emphasize a signal with several peaks in the spectrum.

Just, if you want to emphasize several peaks with peaking filters, you have to lower the overall volume. This is where I'd use a shelving filter with negative gain and a soft slope. I think I'd use a hishelf and lower all frequencies above 100 Hz by -24dB and set the peaking filters to roughly add between 15 and 30 dB for each peak. But this has to be done by experiment and with your ears ;-)

Moment... I'll post an example...

;) Urs

User avatar
Urs
u-he
26016 posts since 8 Aug, 2002 from Berlin

Post Mon Nov 01, 2004 2:15 pm

Here's an mp3:

http://www.u-he.com/music/eq_yaiyy.mp3

It's a simple sawtooth going through an eq plugin I'm working on.

A highself (blue ball) was used to lower the overall gain, and only 2 peaking filters (orange and green) were used to create the formants, so it's not ideal, but you get the point. The blue line shows how the linear modulation (an envelope follower) creates the "morphing" between the vowels:

Image

Now, unlike parallel bandpass filters, you can retain some crispness in the overall sound.

Cheers,

;) Urs

edit: The frequencies were chosen arbitrarily, I havn't had a fomant chart at my hands - I just launched Logic and did the patch in a couple of seconds. You'll probably get better sounds if you use a formant table and at least 1 or two more peaks. Also, the q was chosen a bit high, so that there's somewhat too much "resonance" in the sweep...

User avatar
KVRAF
2209 posts since 7 Jul, 2003 from Huntington, WV

Post Mon Nov 01, 2004 2:21 pm

If I wanted to implement a bank of parallel formant filters, where would I be able to find a description of appropriate center frequencies and Q-factors (or bandwidths, if you prefer) for those filters? I'm not looking for source code, but for descriptions of the filters' response curves.

thanks,
McLilith

User avatar
Urs
u-he
26016 posts since 8 Aug, 2002 from Berlin

Post Mon Nov 01, 2004 2:25 pm

McLilith wrote:If I wanted to implement a bank of parallel formant filters, where would I be able to find a description of appropriate center frequencies and Q-factors (or bandwidths, if you prefer) for those filters? I'm not looking for source code, but for descriptions of the filters' response curves.

thanks,
McLilith
Just google for "formant frequency vowel". There should be quite some tables around in the net.

Resonance/Q must be chosen by experiment, taste and ears...

Cheers,

;) Urs

User avatar
Urs
u-he
26016 posts since 8 Aug, 2002 from Berlin

Post Mon Nov 01, 2004 2:27 pm


KVRist

Topic Starter

112 posts since 29 May, 2004 from UK

Post Mon Nov 01, 2004 2:35 pm

The blue line shows how the linear modulation (an envelope follower) creates the "morphing" between the vowels:
Image
So the 2 formant peaks there (orange and green balls) modulate from those positions to the positions of the blue-line peaks according to the output value of the envelope follower? Sorry to keep asking for clarification, just want to be sure I understand the screenshot...

So, basically, if I used bandpass filters in parallel, there would be a slight "tail" in the frequency response outside each boundary of the bandwidth? Also, the parallel processing introduces phase cancellation at some frequencies... so a better method is to use peaking filters in series... but I still don't quite understand why a high shelf is used... if it's just to lower the overall gain why not just lower the gain by multiplying by zero point some low digit?

Dan

Return to “DSP and Plug-in Development”