ProcessBlock: Dropouts when processing more than one channel


#1

I guess my gui minimization question… Well… Moving on with what I hope is a more interesting question… :slight_smile:

I am implementing a basic peaking filter with gain, Q, and frequency controls (variables in uppercase). as follows.

[code]void PeakEqAudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
const int numChannels = buffer.getNumChannels();
const int numSamples = buffer.getNumSamples();
const int numOutputChannels = getNumOutputChannels();
const float pi = 3.14159265358979323846264338f;

for (int channel = 0; channel < numChannels; ++channel)
{
float* channelData = buffer.getSampleData (channel);

    for (int i = 0; i < numSamples; ++i)
	  {

		//Input
		const float xn = channelData[i];

		// Delay samples
	        float xn_1 = m_f_xz_1;
		float xn_2 = m_f_xz_2;
		float yn_1 = m_f_yz_1;
		float yn_2 = m_f_yz_2;

		//Calculate intermediate values


/* edited out long and boring difference equation stuff here for posting online */

		// Replace new delay values
		m_f_xz_2 = xn_1;
		m_f_xz_1 = xn;
		m_f_yz_2 = yn_1;
		m_f_yz_1 = yn;

		//Output, rewrite to buffer.
		channelData[i] = yn;

	} //End of sample buffer loop

}// End of channel increment loop

for (int i = numChannels; i < numOutputChannels; ++i) //clear out output buffers
{
buffer.clear (i, 0, buffer.getNumSamples());
}
// End of Process Block
}[/code]

Here’s the issue - when I have the channel “for” loop in the code so it will process multiple channels I get a lot of dropouts (say the plug is on a stereo output master), that tend towards channel 1 (so right in the case of a stereo output). However, If I force it to only process one channel for example:

It works perfectly, just on one channel though (so EQ math is correct).

I have messed a lot with it changing things around to try to zero down what the issue might be. One interesting thing I stumbled onto if I replace the EQ code with a simple channelData[i] = xn * VOLUME. The output is perfect no glitches with 2 channels.

I think I understand processBlock member of AudioProcessor. I’m giving it 2 channels in, and there are 2 channels out so it expects those 2 channels to be re filled with the output.

Also, I wouldn’t think the EQ equation vs a simple volume should make any difference. Or maybe the extra processing time does. I’ve tried it in a couple different hosts with the same result.

Any ideas appreciated. I’m sure I’m missing something obvious. I’m learning…

Jim


#2

Hi Jim,

It looks to me like you need different sets of delay data stored for each channel. At the moment your state is being passed from one channel to the other when each channel should have its own. The volume example works because it is stateless.

So you need each of your delay values to be stored in an array dimensioned to the current number of channels. As the number of channels is not known until prepareToPlay(), that’s where you need to dimension the array. I can post a bit of code later to show you how to implement a dynamic array of primitives if that would help.

-Andrew


#3

Thanks Andrew - yes some code example would be great… I admit that this rings a bell, but have no idea where to start.

Thanks very much, I’ll be back at this tonight.

Jim


#4

OK, first up some notes on dynamic arrays of primitives.

[list][]Don’t use ScopedPointer<> as delete[] won’t be called. Instead, use HeapBlock<>.[/]
[]For dynamic arrays, the default constructor of HeapBlock can be used (i.e. no need to explicitly initialise if using it as a class member)
[list][
]In prepareToPlay() call the allocate method of the HeapBlock to set the required dimensions and optionally clear to zero[/]
[
]Access items directly using [][/][/list][/][/list]

			// Declare member variable in header
			HeapBlock<float> filterState;	// Multi-channel filter state variable
			
			// In prepareToPlay()
			filterState.allocate (numChannels, true)

			// In processBlock()
			filterState[currentChannel] = 0.2f;		// To access items directly, just use []
			float* tmp = filterState.getData()		// To access as a float array, use getData()

[list][]If static and a class member, use the class initialisation list - e.g. myStaticCoeffs(3)[/]
[]HeapBlock tidies up after itself, no need to call free in class destructor[/][/list]

Now, an edit of your code:

[code]// Class member declarations
HeapBlock m_f_xz_1;
HeapBlock m_f_xz_2;
HeapBlock m_f_yz_1;
HeapBlock m_f_yz_2;

void PeakEqAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
m_f_xz_1.allocate (getNumInputChannels(), true);
m_f_xz_2.allocate (getNumInputChannels(), true);
m_f_yz_1.allocate (getNumInputChannels(), true);
m_f_yz_2.allocate (getNumInputChannels(), true);

// Anything else you already had here...

}

void PeakEqAudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
const int numChannels = buffer.getNumChannels();
const int numSamples = buffer.getNumSamples();
const int numOutputChannels = getNumOutputChannels();
const float pi = 3.14159265358979323846264338f;

for (int channel = 0; channel < numChannels; ++channel)
{
float* channelData = buffer.getSampleData (channel);

    for (int i = 0; i < numSamples; ++i)
    {

     //Input
     const float xn = channelData[i];

     // Delay samples
     float xn_1 = m_f_xz_1[channel];
     float xn_2 = m_f_xz_2[channel];
     float yn_1 = m_f_yz_1[channel];
     float yn_2 = m_f_yz_2[channel];

     //Calculate intermediate values

     float m_f_theta_c = 2.0f*pi*FREQUENCY/(float)44100;
     float m_f_U = pow(10.0f,VOLUME/20.0f);
     float m_f_zeta = 4.0f/(1.0f + m_f_U);
     float m_f_K = m_f_theta_c/(2.0f*QUALITYFACTOR);

/* edited out long and boring difference equation stuff here for posting online */

     // Replace new delay values
     m_f_xz_2[channel] = xn_1;
     m_f_xz_1[channel] = xn;
     m_f_yz_2[channel] = yn_1;
     m_f_yz_1[channel] = yn;

     //Output, rewrite to buffer.
     channelData[i] = yn;

  } //End of sample buffer loop

}// End of channel increment loop

for (int i = numChannels; i < numOutputChannels; ++i) //clear out output buffers
{
buffer.clear (i, 0, buffer.getNumSamples());
}
// End of Process Block
}[/code]


#5

Bear with me, I’m still a little confused.

I call HeapBlock, and allocate 4 arrays in the heap named:

m_f_xz_1; m_f_xz_2; m_f_yz_1; m_f_yz_2;

Allocate x number of elements, based on the number of input channels returned by preparetoplay, initialize to 0. OK I have that.

But then I am trying to fill up the array with the samples that come for a particular channel in a block whose size is not known until processblock is called. But the array(s) for the 4 delay samples will only contain slots for 2 elements, so as the i++ loops if there are more than numchannels than samples in a block the arrays will overflow. So should I not dimension the arrays based on numsamples?

What am I missing? Thanks again.

[code]for (int channel = 0; channel < numChannels; ++channel)
{
float* channelData = buffer.getSampleData (channel);

    for (int i = 0; i < numSamples; ++i)
	  {

		//Input
		float xn = channelData[i];

		// Delay samples
	    float xn_1 = m_f_xz_1[i];
		float xn_2 = m_f_xz_2[i];
		float yn_1 = m_f_yz_1[i];
		float yn_2 = m_f_yz_2[i];

		//Calculate intermediate values

	

		// Replace new delay values
		 m_f_xz_2[i] = xn_1;
		 m_f_xz_1[i] = xn;
		 m_f_yz_2[i] = yn_1;
		 m_f_yz_1[i] = yn;

		//Output, rewrite to buffer.
		channelData[i] = yn;

	} //End of sample buffer loop

}// End of channel increment loop

for (int i = numChannels; i < numOutputChannels; ++i)
{
buffer.clear (i, 0, buffer.getNumSamples());
}
// End of Process Block
}[/code]


#6

There’s no need to redimension the arrays according to numSamples.

Your four state variables (m_f_xz_1, m_f_xz_2, m_f_yz_1, m_f_yz_2) are used to store the state of previous input and output samples. m_f_xz_1 & m_f_xz_2 hold the previous two input values for example.

Your code iterates through the sample block one sample at a time, updating the state variables at each iteration. This is in effect a running mini history, so you don’t need to hold a history of the whole block. The arrays only come into it because each channel needs to have it’s own history that is kept separate from the other channels.


#7

Got it! It works now. Thanks Andrew.

What I was confused about was the [i] in the delay registers, they need the channel # so we know where in the array(s) the sample will be inserted (position 0, 1, 2, etc).

[quote] float xn_1 = m_f_xz_1[channel];
float xn_2 = m_f_xz_2[channel];
float yn_1 = m_f_yz_1[channel];
float yn_2 = m_f_yz_2[channel];[/quote]

Oh, I’m sure this won’t be my last question! (collective groan).

One more tool in the kit…

Jim


#8

Oh dear, sorry about that - I looked at the last for loop where i is used for the channel number :oops: I should use a bigger screen and avoid all this scrolling up and down!

I’ll edit my post so we don’t trap anyone else in future.


#9

No worries friend! The troubleshooting made me work through it, so now I really know how it’s put together.

Jim