No Output Gain from ProcessorChain

OBJECTIVE:
Experimenting and Learning

TARGET:
VST3 on Windows Platform

PROBLEM:
What I notice is that I have to bypass Band-2, and Band-3 (see processor chain below) in order for me to hear output.

I’m using makeLowShelf, makePeakFilter, and makeHighShelf to generate the coefficients for my filters.

IN HEADER FILE

	using FilterBand = dsp::ProcessorDuplicator<dsp::IIR::Filter<float>, 
       dsp::IIR::Coefficients<float>>;
	using Gain = dsp::Gain<float>;

	dsp::ProcessorChain<
		Gain,		
		FilterBand,  //Band-1...........LowShelf
		FilterBand,  //Band-2...........Peak
		FilterBand,  //Band-3...........Peak
		FilterBand,  //Band-4...........Peak
		FilterBand,  //Band-5...........Peak
		FilterBand,  //Band-6...........Peak
		FilterBand,  //Band-7...........Peak
		FilterBand,  //Band-8...........Peak
		FilterBand,  //Band-9...........High Shelf
		Gain> filter;

PREPARE-TO-PLAY

void eqAlphaAudioProcessor::prepareToPlay(double newSampleRate, int newSamplesPerBlock)
{
	sampleRate = newSampleRate;
	
	dsp::ProcessSpec spec{ sampleRate,  uint32(newSamplesPerBlock), uint32(getTotalNumOutputChannels()) };
	

	

	for (size_t i = 0; i < bands.size(); ++i)
	{
		updateFilters(i);
	}

	auto& inputGain = filter.template get<0>();
	auto& outputGain = filter.template get <10>();

	inputGain.setGainLinear(*state.getRawParameterValue(paramInput));
	outputGain.setGainLinear(*state.getRawParameterValue(paramOutput));

	

	filter.prepare(spec);


}

PROCESS-BLOCK

void eqAlphaAudioProcessor::processBlock(AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
	ScopedNoDenormals noDenormals;
	ignoreUnused(midiMessages);



	// -- Update Analyzer with input buffer data

	if (bypassed)
	{
		filter.reset();

		bypassed = false;
	}
	dsp::AudioBlock<float>              block(buffer);
	dsp::ProcessContextReplacing<float> context(block);


	filter.process(context);

	// Update analyzer with output data
}

How and where do you apply the filter coefficients?

To update filters I call:

I call updateFilters to get/apply the coefficients. I call it first from prepareToPlay() (Note: I’ve updated my sample code for prepareToPlay(). I accidentally left it out, by trying to include only the pertinent code)

I also call it whenever a parameter changes (e.g. a frequency is updated).

void eqAlphaAudioProcessor::updateFilters(const size_t index)
{
	if (sampleRate > 0)
	{
		dsp::IIR::Coefficients<float>::Ptr ptrCoefficients;
		
                size_t filterIndex = index + 1;
		switch (filterIndex )
		{
		case 1:
			ptrCoefficients = dsp::IIR::Coefficients<float>::makeLowShelf(sampleRate, bands[index].frequency, bands[index].quality, bands[index].gain);
			break;
		case 2:
		case 3:
		case 4:
		case 5:
		case 6:
		case 7:
                case 8:
			ptrCoefficients = dsp::IIR::Coefficients<float>::makePeakFilter(sampleRate, bands[index].frequency, bands[index].quality, bands[index].gain);
			break;
		case 9:
			ptrCoefficients = dsp::IIR::Coefficients<float>::makeHighShelf(sampleRate, bands[index].frequency, bands[index].quality, bands[index].gain);
			break;
		default:
			break;
		}


		if (ptrCoefficients)
		{

			
			switch (filterIndex)
			{
			case 1:
				*filter.get<1>().state = *ptrCoefficients;
				break;
			case 2:
				*filter.get<2>().state = *ptrCoefficients;
				break;
			case 4:
				*filter.get<4>().state = *ptrCoefficients;
				break;
			case 5:
				*filter.get<5>().state = *ptrCoefficients;
				break;
			case 6:
				*filter.get<6>().state = *ptrCoefficients;
				break;
			case 7:
				*filter.get<7>().state = *ptrCoefficients;
				break;
			case 8:
				*filter.get<8>().state = *ptrCoefficients;
				break;
			case 9:
				*filter.get<9>().state = *ptrCoefficients;
				break;
			}

			ptrCoefficients->getMagnitudeForFrequencyArray(frequencies.data(),
				bands[index].magnitudes.data(),
				frequencies.size(), sampleRate);
		}
	}

      ....
}

I may have found my problem. I’m missing a case statement.

“case 3:” is missing. The coefficient is never being assigned to that band.

OOFA!!!

Glad you found it!

To me, that big switch/case statement does not look like the best choice, since it is not very generic and makes it easy to introduce subtle bugs like the one you just experienced. What I’d do would be

  • Divide the chain into two chains, an inner filter chain and an outer overall chain with the gains and the filter chain. This would allow for a 1 to 1 mapping of the indices.
  • Make the according size and index sequence a static constexpr member to the class.
  • Store pointers to the filter instances into an array that can be indexed.
  • Store function pointers to the make functions associated with each filter (and use the new allocation free ArrayCoefficient functions)

So my implementation would probably look something like that (untested)

    // probably in the private section of your class header
    using FilterBand = juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>,
                                                      juce::dsp::IIR::Coefficients<float>>;

    using MakeCoeffFn = std::array<float, 6> (*)(double sampleRate, float freq, float q, float gain);

    using FilterChain = juce::dsp::ProcessorChain<FilterBand, FilterBand, FilterBand, FilterBand, FilterBand, FilterBand, FilterBand, FilterBand, FilterBand>;

    static constexpr auto numFilters = std::tuple_size_v<FilterChain>;
    static constexpr auto filterIndices = std::make_index_sequence<numFilters>();

    juce::dsp::ProcessorChain<juce::dsp::Gain<float>, FilterChain, juce::dsp::Gain<float>> chain;
    struct ChainIndices { enum { preGain, filterChain, postGain }};
    
    // Helper functions to fill the arrays
    struct detail
    {
        using CF = juce::dsp::IIR::ArrayCoefficients<float>;

        template <size_t i> struct GetCoeffFn                 { static constexpr auto get() { return CF::makePeakFilter; } };
        template <>         struct GetCoeffFn<0>              { static constexpr auto get() { return CF::makeLowShelf; } };
        template <>         struct GetCoeffFn<numFilters - 1> { static constexpr auto get() { return CF::makeHighShelf; } };

        template <size_t... i>
        static std::array<MakeCoeffFn, numFilters> makeCoeffFnArray (std::index_sequence<i...>)
        {
            return std::array<MakeCoeffFn, numFilters> { (GetCoeffFn<i>::get())... };
        }

        template <size_t... i>
        static std::array<FilterBand*, numFilters> makeFiltersArray (FilterChain& c, std::index_sequence<i...>)
        {
            return std::array<FilterBand*, numFilters> { (&c.get<i>())... };
        }
    };

    const std::array<FilterBand*, numFilters> filters      { detail::makeFiltersArray (chain.get<ChainIndices::filterChain>(), filterIndices) };
    const std::array<MakeCoeffFn, numFilters> makeCoeffFns { detail::makeCoeffFnArray (filterIndices) };


    // Implementation of the updateFilters function
    void updateFilters (size_t index)
    {
        filters[index]->state = makeCoeffFns[index] (sampleRate, bands[index].frequency, bands[index].quality, bands[index].gain);
        filters[index]->state->getMagnitudeForFrequencyArray (frequencies.data(), bands[index].magnitudes.data(), frequencies.size(), sampleRate);
    }

Of course there is a lot boilerplate code now needed to expand all the templated indexed values into the arrays via index sequence and pack expansion and to make the template specialisation technique used to assign the right coefficient creation function to the function pointer array work. But the updateFilters function just became a two liner and whenever you’ll change the number of filters in your filter chain, everything will keep working, which is what such generic solutions are all about.

One last note: Are you aware that – from a signal processing point of view – it doesn’t really matter if you apply gain before or after the filter since the filter is completely linear? So you don’t really need a pre and post gain. But it doesn’t harm, of course :slight_smile:

1 Like

Firstly, THANK YOU!

I’m trying to separate Input Gain from Output Gain. This is the only way I knew how. I got the idea about pre/post gain from WaveShaper Tutorial which does something like this.

Change the transfer function

    //==============================================================================
    enum
    {
        preGainIndex,    // [2]
        waveshaperIndex,
        postGainIndex    // [3]
    };
 
    juce::dsp::ProcessorChain<juce::dsp::Gain<Type>, juce::dsp::WaveShaper<Type>, juce::dsp::Gain<Type>> processorChain; // [1]

Later on it does this:
ADJUST THE FILTER


    //==============================================================================
    enum
    {
        filterIndex,        // [2]
        preGainIndex,
        waveshaperIndex,
        postGainIndex
    };
 
    using Filter = juce::dsp::IIR::Filter<Type>;
    using FilterCoefs = juce::dsp::IIR::Coefficients<Type>;
 
    juce::dsp::ProcessorChain<juce::dsp::ProcessorDuplicator<Filter, FilterCoefs>,
                              juce::dsp::Gain<Type>, juce::dsp::WaveShaper<Type>, juce::dsp::Gain<Type>> processorChain;

I’m a c/c++ programmer, but I’m still learning Audio. I’m a musician as well, but knowing my way around 17th century counterpoint does nothing for learning how a IIR/FIR filters work.

I have a good background in math, and am trying to learn this stuff inside out, but it’s difficult to know where to begin.

Thanks again!

I’ll be STUDYING your sample code, and adjust what doesn’t work since it hasn’t been tested; but this is great stuff I’ve not even thought of. I dig learning.

I don’t mind starting from the bottom, up. If you know a website that has solid beginner info please pass it along. Until I can get to an actual audio engineering class, I’m gonna have to learn it on my own the best I can.