Need simple help with FFTs in JUCE!

I would like to make a custom-drawn magnitude frequency response to filter a signal, like this:

I believe this is called a “Graphic Equalizer”, and I would need to use a filter bank. Are there any tutorials that exist of this, or can someone please help me figure out the steps to apply this kind of filter to a signal? I believe I should use juce::dsp::FFT::performFrequencyOnlyFowardTransform on my signal, but once I do multiply each bin, I have no way of going back to the time domain.

Also let me know if I can provide more information; I’ve been stuck on this for quite a while now because I lack the DSP knowledge. Thank you!

That’s correct, you can’t perform an inverse FFT on a set of frequency bins as you’d be missing the vital phase information.

FFTs act in the complex domain - they take an audio signal as their input, and their output is a set of complex numbers each representing different points in the frequency domain. The magnitude of those complex numbers tells you how loud each frequency is in the input signal (which is what juce::dsp::FFT::performFrequencyOnlyFowardTransform() returns), and the angle of the numbers tells you the phase. To perform an inverse FFT, which takes complex numbers as its input and outputs an audio signal, you must have both the magnitude and phase information for each bin.

Luckly JUCE makes this very easy as you can simply use juce::dsp::FFT::perform() to do both the forward and inverse transformation. Once you’ve done the forward step, you can apply the same scaling you did before on just the frequencies but this time on the complex numbers - just be sure not to change the angle (phase) of the numbers, only their magnitude.

Hopefully that answers your question about how to use the FFT - however for your specific case where you simply want to apply a certain frequency response, I’d probably recommend simply using some biquad filters. Looking at your graph, it looks like you want a high-pass and a low-pass filter, then 3-5 parameteric filters to get the bumps.

How precise do you need the response to be?

1 Like

funny thing is what’s called a ‘graphic eq’ is pretty much the least ‘graphic’ eq you can think of. it’s the ones that just consist of a bunch of faders next to each other

Well, the faders next to each other show the frequency response.
So that’s pretty graphic, as opposed to the alternative, a parametric equalizer.

1 Like

Thank you for the responses! @ImJimmi That all makes sense to me, thank you for the simple explanation. To answer how precise I would need this, I’m making a vocal synthesizer, and it would be easier for me to experiment with phonemes if I can draw the formants. It’s not the end of the world if I can’t do it, but this way makes far more sense to me than parametric filters (though I used to use biquads for this)!

I experimented with juce::dsp::FFT::perform() for a bit, but I kept getting weird results. I have a small code snippet of what I tried below, with a graph showing the FFT of the samples (not yet filtered):

If I did FFT on 1500hz sine wave at a sample rate of 48kHz with a block size of 512, I believe I should be getting a spike (in this case, -256.0) at the 32nd frequency bin (1500 / (48000 / 2 / 512)), which I do find using juce::dsp::FFT::performRealOnlyForwardTransform() on a float array:
code 2

I’m thinking I’m using complex numbers incorrectly, but I’m not sure what the problem is. No worries if you can’t find the time to respond, I’m feeling much more confident in my approach, thank you for all of the help so far!

The second time you call perform, you need to have the arguments the other way around. On that last line you have

filterFFT->perform(begin(samples), begin(fftBuffer), true);

which will perform an inverse FFT on samples and put the result in fftBuffer. What you want is

filterFFT->perform(begin(fftBuffer), begin(samples), true);

which will perform an inverse FFT on fftBuffer and store the resultant audio signal in samples.

Ah, nice catch! That definitely would’ve been a headache in the future. Let me update the example code:

I noticed later that the notches I was looking for was in the imaginary numbers, though I’m not sure why:

This works for multiples or factors of 1500hz (e.g. 750hz, 3000hz), but this doesn’t work for non-multiples/factors (e.g. 440). I think this is because those waves don’t line up with the sample rate and FFT size, so it produces FFTs that look like this:

And becomes these noisy signals when IFT’d:

Any ideas what this might be caused by? I also noticed you said “not to change the angle (phase) of the numbers”, which I wasn’t able to figure out given real/imaginary parts but not magnitude/angle calculations. Thank you again for the help!

This may be off, but I read a bit more into FFTs and found that it doesn’t like non zero-crossing signals, which is why we use windowing. I went ahead and applied a Hann window, which looks a bit like this:

This, however, is producing strange frequency inaccuracy, with two locale peaks in neighboring bins, like this (for 1500 hz):

This may not be the reason why I get weird transients when filtering, but I thought I would share this information as well! Let me know as well if there’s anything you’d like to see, thanks!

Why are you working directly with the real and imaginary numbers? Usually when you take an FFT, you convert the real+imag parts into magnitude and phase information, which are much easier to work with.

The reason why I’m using real and imaginary numbers is because if I’m just multiplying each frequency bin, I think the transform would be the same between the real and imaginary components, as it would for magnitude and phase. For example:

If r = 3 & i = 4, then magnitude = (32 + 42)1/2 = 5; if I applied a filter that multiplied the signal by 2.5 times, so magnitude = 5 * 2.5 = 12.5, what would r and i equal? If the relation between r and i hasn’t changed, then we could figure this out using the original angle between r and i, θ = arctan(4/3); multiplying with the new hypotenuse, r = 12.5cos(arctan(4/3)) = 7.5 and i = 12.5sin(arctan(4/3)) = 10.

What I could do instead is just multiply r = 3 and i = 4 by 2.5 to get r = 7.5 and i = 10, though. I have the suspicion that it shouldn’t be that easy, however, so please let me know why the math above doesn’t work! One thought I had is that I need to take the imaginary into account, so it would instead be r = 3, i =4i, magnitude = (32 + (4i)2)1/2 = i(7)1/2.

Hi all! I’m pleased to come back to this post with the correct solution. Because I’m making a synth, I realized I could use my oscillator look-up as a complete, zero-crossing cycle to FFT on. This allows me to avoid windowing, non 2^n sizes, and a bunch of other annoyances that would result in messing up the signal. I can also multiply each point of my graphic EQ with the real and imaginary parts of the FFT(signal) to get an accurate filtered signal.

The one complication is that one complete cycle of the signal is not representative of the frequency it will play at, so you will need to shift the indices of the graphic EQ to line up with the signal at a given frequency (1 cycle = 2nd frequency bin). For example, the below graph represents the FFT of a saw wave’s real and imaginary components. This particular example (and code) was done with a scalar float vector, not complex float, so it’s actually the 2nd imaginary component that is the fundamental frequency (x = 3):

The code below shows how to shift and convolve the indices of the graphic EQ (filter) to the correct frequencies of the FFT’d signal (filteredOSC):

Hopefully that helps people in the future!