8
$\begingroup$

I recently started playing with the Fourier transform (after spending a few weeks learning about the mathematics behind it). I decided to try hacking together a low-pass filter on the following sound bite:

In particular, I took the Fourier transform, zeroed out the highest 1/2 of the frequencies, and then took the inverse Fourier transform. This is what I got

Why is there that crackling noise?

$\endgroup$
  • $\begingroup$ Also, I should mention that I have no clue why I'm applying a low-pass filter to a sound clip. It is purely experimental. Does that operation even make sense for a sound clip? $\endgroup$ – JeremyKun Jul 17 '12 at 6:23
  • $\begingroup$ you should make those samples downloadable $\endgroup$ – endolith Jul 17 '12 at 14:26
11
$\begingroup$

Two potential issues:

  1. Doing filtering in the frequency domain (using an FFT) requires overlap-add, overlap-save, or a related algorithm. This is caused by the difference between linear and circular convolution. Otherwise you get time-domain aliasing
  2. Some of the noise sounds like simple clipping. Filtering can actually increase the time domain amplitude for some samples and if that exceeds the available range it either clips or wraps around.
$\endgroup$
  • 1
    $\begingroup$ Sounded like clipping/wrapping to me. $\endgroup$ – heltonbiker Jul 17 '12 at 17:12
  • $\begingroup$ What can I do to stop the clipping/wrapping? Should I just not have done this operation in the first place? $\endgroup$ – JeremyKun Jul 18 '12 at 3:18
  • $\begingroup$ It is definitely clipping. I scaled the amplitude down on the input signal and the crackling sounds went away. $\endgroup$ – JeremyKun Jul 18 '12 at 3:49
7
$\begingroup$

First just a note, fourier transforms are not ideal for low/high pass filters. Butterworth filters are a good place to start and following that Chebyshev/Elliptical filters if you get more ambitious.

It looks like your are trying to implement an ideal filter. There is no way we can implement these 'brick-wall' filters where we cut off all frequencies above/below a given value. All well developed filters will taper from 1 to 0 around our cut-off frequency.

Ideal filters are only possible theoretical and if you had a Continuous Fourier Transform your method above would work.

But we're doing Discrete Fourier Transforms so there is more to worry about. Since I'm not sure the method of your implementation, I'll guess that you are doing windowing since just pulling out frequencies is a sure way to get crackling in a windowed DFT.

When windowing in a DFT, one might think that the frequency amplitudes between windows are relatively continuous. e.g. if the 400Hz frequency has an amplitude of 0.5 in the current window that in the next window the amplitude will be near 0.5. This unfortunately is not true, so if we were to simply remove the 400Hz frequency from our DFT, we may hear loud pops or cracks between windows.

A small example: Cut off rate is 600Hz Window 1 is playing a sine of 800Hz Window 2 connects 'continuously' with window 1 and plays 400Hz. Then we will hear a pop between the window because window 1 will be silent and window 2 will turn on immediately.

Another thing to keep in mind is that we can only represent a finite amount of frequencies with a DFT. If we have an audio file with a sine wave of a frequency between two of our discrete frequencies in our DFT, then we actually represent it with a lot of our discrete frequencies. So even though an example audio file may contain a sine wave lower than our cutoff, if its frequency is inbetween our DFT frequencies than we might cut part of it out and distort it with the method above, since higher frequencies are necessary to represent the audio file.

Hope that helps

$\endgroup$
  • $\begingroup$ Ah, I retract my windowing comment (It's more a real-time DFT problem). Hilmar's answer seems more accurate. $\endgroup$ – Matt Tytel Jul 17 '12 at 23:47
4
$\begingroup$

It is indeed possible to do as you suggest but not without some side effects. Say we form a simple test signal $s(t) = s_\mathrm{low}(t) + s_\mathrm{high}(t)$ where:

$$ s_\mathrm{low}(t) = \cos(2\pi f_0 t) + \cos(2\pi f_1 t + \frac{\pi}{3}) $$

$$ s_\mathrm{high}(t) = \frac{1}{2} \cdot \cos(2\pi f_2 t + 0.2) $$

where we say that both $f_0,f_1$ are below a chosen lowpass cut-off frequency $f_\mathrm{cut}$ such that $f_0 < f_1 < f_\mathrm{cut}$, and we choose $f_2 > f_\mathrm{cut}$. We can of course choose the amplitudes as we wish and I have just chosen the ones above to keep things simple. By having two frequency contributions below the cut-off frequency and one above makes it easy to follow and compare the signals.

In the following I assume that we have $N$ samples taken with the frequency $f_\mathrm{s} > 2\cdot f_2$. In reality we choose $f_\mathrm{s} \gg 2\cdot f_2$ to make the observed signal smooth. It is also assumed that we only consider one bunch of data samples. If you need to handle several time frames check the paper by Fred Harris named "On the Use of Windows for Harmonic Analysis with the Discrete Fourier Transform" from Proc. IEEE in 1978.

I have combined a small Python program to illustrate some of the concepts - the code is pretty awful but I just took some old code that I had for similar problems. Although there are hardly any comments it should be fairly easy to follow due to the small modules. The are two dft / idft functions; two functions fshiftn / fshiftp to frequency shift the signal i DFT domain for filtering; a function dftlpass to perform the filtering in DFT domain; a function zpblpass to do the filtering by use of a Butterworth filter; a function bbdftsig to form the test signal and perform the filtering; and finally a small function plotsigs to plot the signals. At the end of the script, the different parameters are set and the different figures are made.

"""
   Test of DFT versus scipy.signal.butter filtering with respect to
   signal reconstruction.

"""

# import ############################################################ import #
import matplotlib as mpl;   mpl.rcParams['backend'] = 'Agg'
import matplotlib.pyplot as mplpp
import matplotlib.mlab as mplml
import numpy as np
import scipy.signal as sps


# initialize #################################################### initialize #
try:
    mpl.rc('text', usetex=False)
    mpl.rc('font', family='serif')
    mpl.rc('font', serif='STIXGeneral')
    mpl.rc('font', size=8)
except AttributeError:
    None


# dft ################################################################## dft #
def dft(xt, fs, t0):
    N, d = len(xt), -2j*np.pi/len(xt)
    w = np.arange(N, dtype=np.float).reshape((N,1))
    c = np.exp(d*t0*fs*w)
    W = np.exp(d*np.dot(w,np.transpose(w)))
    xf = np.multiply(c,np.dot(W,xt)) / float(N)
    f = w*fs/float(N)
    return xf, f


# idft ################################################################ idft #
def idft( X, FS, T0 ):
    N, d = len(X), 2j*np.pi/len(X)
    w = np.arange(N, dtype=float).reshape((N,1))
    cc = np.exp(d*T0*FS*w)
    Wc = np.exp(d*np.dot(w, np.transpose(w)))
    Y = np.dot(Wc, np.multiply(cc, X))
    return Y



# fshiftn ########################################################## fshiftn #
def fshiftn( xf, f ):
    assert type(f) == np.ndarray, "f must be a np.ndarray"
    assert f.shape[1] == 1, "f must be a column array"
    assert xf.shape[1] == 1, "xf must be a column array"
    assert sum(f<0) == 0, "All frequency components must be 0 or positive"

    # Determine sampling rate, tolerance, and allocate output array
    fs, tol = len(f)*(np.abs(f[1,0]-f[0,0])), 1.E-2
    fshift = np.zeros((len(f),1), dtype=float)
    xfshift = np.zeros((len(f),1), dtype=complex)

    # Determine index where f > fs/2
    Nm = np.floor(len(f)/2.0)
    Np = np.floor((len(f)-1.0)/2.0)

    # Compute output frequency array such that -fs/2 <= f < fs/2 and the
    # corresponding Fourier coefficients
    fshift[:Nm,0] = f[Np+1:,0] - fs
    fshift[Nm,0] = f[0,0]
    fshift[Nm+1:,0] = f[1:Np+1,0]

    xfshift[:Nm,0] = xf[Np+1:,0]
    xfshift[Nm,0] = xf[0,0]
    xfshift[Nm+1:,0] = xf[1:Np+1,0]

    return xfshift, fshift


# fshiftp ########################################################## fshiftp #
def fshiftp(xf, f):
    assert type(f) == np.ndarray, "f must be a np.ndarray"
    assert f.shape[1] == 1, "f must be a column array"
    assert xf.shape[1] == 1, "xf must be a column array"
    assert sum(f<0) > 0, "Some input frequencies must be negative"

    # Determine sampling rate, tolerance, and allocate output array
    fs, tol = len(f)*(np.abs(f[1,0]-f[0,0])), 1.E-2
    fshift = np.zeros((len(f),1), dtype=float)
    xfshift = np.zeros((len(f),1), dtype=complex)

    # Determine index where f > fs/2
    #Nx = np.floor((len(f)+1+tol)/2)
    Nm = np.floor(len(f)/2.0)
    Np = np.floor((len(f)-1.0)/2.0)

    # Compute output frequency array such that -fs/2 <= f < fs/2 and the
    # corresponding Fourier coefficients
    fshift[Np+1:,0] = f[:Nm:,0] + fs
    fshift[0,0] = f[Nm,0]
    fshift[1:Np+1:,0] = f[Nm+1:,0]

    xfshift[Np+1:,0] = xf[:Nm:,0]
    xfshift[0,0] = xf[Nm,0]
    xfshift[1:Np+1:,0] = xf[Nm+1:,0]

    return xfshift, fshift


# dftlpass ######################################################## dftlpass #
def dftlpass(xt, fs, fcut):
    # Perform Discrete Fourier Transform
    xf, f = dft(xt, fs, 0.0)

    # Shift frequencies to -fs/2 <= f < fs/2 ... and coefficients
    xfshift, fshift = fshiftn(xf, f)

    # Perform filtration
    xfshift = xfshift * (np.abs(fshift) <= fcut)

    # Re-shift frequencies to 0 <= f < fs ... and coefficients
    xfrecon, frecon = fshiftp(xfshift, fshift)

    # Perform inverse Discrete Fourier Transform
    yt = idft(xfrecon, fs, 0.0)
    return yt.real


# zpblpass ######################################################## zpblpass #
def zpblpass(xn, fcal, fs, fcut):
    bz, az = sps.butter(5, fcut/(fs/2))

    # Gain calibration
    Ncal = np.max([np.int(20*fs/fcal), 30000])
    Nguard = np.int(0.1*Ncal)    
    t = np.arange(Ncal) / fs
    x0_cal = 1.0 * np.cos(2*np.pi*fcal*t)
    yi_cal = sps.filtfilt(bz, az, 2.0*x0_cal*np.cos(2*np.pi*fcal*t))
    k = 1.0/np.mean(yi_cal[Nguard:Ncal-Nguard])

    # Scaled output
    yn = k * sps.filtfilt(bz, az, xn)
    return yn


# bbdftsig ######################################################## bbdftsig #
def bbdftsig(f0, f1, f2, fcut, fs, N):
    t = np.arange(N).reshape((N,1)) / fs
    s0 = np.sin(2*np.pi*f0*t)
    s1 = np.sin(2*np.pi*f1*t + 0.2)
    s2 = 0.7 * np.sin(2*np.pi*f2*t + np.pi/3.0)
    slow = s0 + s1
    s = slow + s2

    sf = dftlpass(s, fs, fcut)
    sfdftv = sf.reshape((N))
    sv = s.reshape((N))
    slowv = slow.reshape((N))

    sv = s.reshape((N))
    sfzpbv = zpblpass(sv, f1, fs, fcut)
    #sfzpbv = sfzpb.reshape((N))
    return sv, slowv, sfdftv, sfzpbv


# plotsigs ######################################################## plotsigs #
def plotsigs(s, slow, sfdft, sfzpb, Nstart, Nstop, fname):
    n = np.arange(s.shape[0])

    # Plot results
    mplpp.figure(1, (5.0,2.25))
    mplpp.clf()
    mplpp.plot(n[Nstart:Nstop], s[Nstart:Nstop], 'm-',
               n[Nstart:Nstop:4], s[Nstart:Nstop:4], 'mx',
               n[Nstart:Nstop], slow[Nstart:Nstop], 'g-',
               n[Nstart:Nstop:10], slow[Nstart:Nstop:10], 'gx',
               n[Nstart:Nstop], sfdft[Nstart:Nstop], 'r-',
               n[Nstart:Nstop:15], sfdft[Nstart:Nstop:15], 'rx',
               n[Nstart:Nstop], sfzpb[Nstart:Nstop], 'b-',
               linewidth=1.5)
    mplpp.legend([r'$s$', r'$s$', r'$s_{\rm low}$', r'$s_{\rm low}$',
                  r'DFT', r'DFT', r'ZPB'], loc='upper right')
    mplpp.ylabel(r'Signal')
    mplpp.xlabel(r'$n$')
    #mplpp.axis([-10.0, 10.0, 1.0E-2, 1.0E2])
    mplpp.grid(True)
    mplpp.savefig(fname, dpi=600,
                bbox_inches='tight', pad_inches=0.05)
    mplpp.close()


# __main__ ######################################################## __main__ #
if __name__ == '__main__':
    # Initialize
    f0 = 3.0
    f1 = 11.5
    f2 = 20.0
    fcut = 15.0
    fs = 1000.0
    N = 5000

    s, slow, sfdft, sfzpb = bbdftsig(f0, f1, f2, fcut, fs, N)
    n = np.arange(s.shape[0])

    # Fig. 1: full data set
    Nstart = 0
    Nstop = N
    fname = 'full.pdf'
    plotsigs(s, slow, sfdft, sfzpb, Nstart, Nstop, fname)

    # Fig. 2: beginning
    Nstart = 0
    Nstop = 150
    fname = 'beginning.pdf'
    plotsigs(s, slow, sfdft, sfzpb, Nstart, Nstop, fname)

    # Fig. 3: middle
    Nstart = np.floor(N/2.0) - 75
    Nstop = Nstart + 100
    fname = 'middle.pdf'
    plotsigs(s, slow, sfdft, sfzpb, Nstart, Nstop, fname)

    # Fig. 4: ending
    Nstart = N - 150
    Nstop = N
    fname = 'ending.pdf'
    plotsigs(s, slow, sfdft, sfzpb, Nstart, Nstop, fname)

Choosing $N=5000$ and $f_\mathrm{s} = 1000$ gives us a frequency resolution of $f_\mathrm{s}/N = 0.2\:$Hz. If we choose $f_0,f_1,f_2$ according to this we can get perfect agreement by choosing the frequencies as shown above. If we first choose frequencies which are on the grid as $f_0=3$, $f_1=11$, $f_2=21$ and we have $f_\mathrm{cut}=15$ we get the first set of results. The first, middle and last parts of the relevant signals are shown below:

Beginning of signals - on-grid Middle of signals - on-grid Ending of signals - on-grid

As seen from the figure we have the combined input $s$ as the magenta signal; the green signal as we can only see from the 'x' markings is $s_\mathrm{low}$ (the raw input signal when we simply only include the input signal below the cut-off frequency); the red signal is the one we get when using the DFT filtering; and the blue signal is the one we get from the Butterworth filter. As seen above we obtain perfect agreement between $s_\mathrm{low}$ and the DFT filtered signal - but the Butterworth filter has some impact on the in-band signal (in particular the component at $f_1$. As is quite typical for this type of processing we have some differences at the beginning and end of the sequence due to edge effects and reasonably good agreement between both types of filtering in the middle section.

If we change the frequency $f_1$ to $f_1=11.5$ which is not on the frequency grid (and further it is quite close to the cut-off frequency) we see some different results as shown below.

Beginning of signals - off-grid Middle of signals - off-grid Ending of signals - off-grid

Now we see substantial differences between green, blue and red signals which in the ideal situation should be identical. In the middle of the signal they all agree fairly well - the DFT and the reference $s_\mathrm{low}$ agree best though.

So in conclusion it is possible to use direct filtering by forcing Fourier coefficients to zero which is also done sometimes in compressive sensing to reduce the support of a signal to force sparsity on a signal. However, there are consequences of this as increased errors in particular at the edges of the signal. Further, the above is a best case where the entire signal is treated as one sequence. If the signal must be split in time frames it gets complicated as we then need to consider some windowing or other technique to ensure continuity of the signal between frames. So my advice is similar to some of the other posts in recommending to normally use a Butterworth / Elliptic / .. or whatever filter.

$\endgroup$
0
$\begingroup$

Zeroing bins in an FFT can actually increase the amplitude of other frequencies near but not centered at the zero-ed bin or its adjacent bins. This increase can cause clipping.

Furthermore, if you are doing the FFT using un-zero-padded (and not overlap-added) blocks (as opposed to the entire song in one big FFT), any FFT data modifications will wrap around from the back to the front of the time domain sequence in each block, thus adding other weird discontinuities in the wrong places.

$\endgroup$
0
$\begingroup$

Here's a quick-and-dirty bin-zeroing FFT bandpass filter, with the FFT code as well.

void FFT(int n, int inverse, double *gRe, double *gIm, double *GRe, double *GIm)
{

    int m = 0;
    int p = 1;
    int j = 0;
    int i1=0;
    int k=0;
    double ca=0;
    double sa=0;
    int l1,l2,l3;
    double u1,u2;
    double t1 = 0;
    double t2 = 0;
    int i2=0;
    double z;
    /* Calculate m=log_2(n) */
    while(p < n)
    {
        p *= 2;
        m++;
    }
    /* Bit reversal */
    GRe[n - 1] = gRe[n - 1];
    GIm[n - 1] = gIm[n - 1];
    for(i1 = 0; i1 < n - 1; i1++)
    {
        GRe[i1] = gRe[j];
        GIm[i1] = gIm[j];
        k = n / 2;
        while(k <= j)
        {
            j -= k;
            k /= 2;
        }
        j += k;
    }
    /* Calculate the FFT */
    ca = -1.0;
    sa = 0.0;
    l1 = 1;
    l2 = 1;
    l3=0;
    for(l3 = 0; l3 < m; l3++)
    {
        l1 = l2;
        l2 *= 2;
        u1 = 1.0;
        u2 = 0.0;       
    for(j = 0; j < l1; j++)
        {
            i2=j;
            for(i2 = j; i2 < n; i2 += l2)
            {
                i1 = i2 + l1;
                t1 = u1 * GRe[i1] - u2 * GIm[i1];
                t2 = u1 * GIm[i1] + u2 * GRe[i1];
                GRe[i1] = GRe[i2] - t1;
                GIm[i1] = GIm[i2] - t2;
                GRe[i2] += t1;
                GIm[i2] += t2;
            }
            z =  u1 * ca - u2 * sa;
            u2 = u1 * sa + u2 * ca;
            u1 = z;
        }
        sa = sqrt((1.0 - ca) / 2.0);
        if(!inverse) sa =- sa;
        ca = sqrt((1.0 + ca) / 2.0);

    }
    /* Divide through n if it isn't the IDFT */
    if(!inverse)
    {
        int i3=0;
        for(i3 = 0; i3 < n; i3++)
        {
            GRe[i3] /= n;
            GIm[i3] /= n;
        }
    }
}


void mainfftBandPass(double *insamples, double *outsamples, unsigned long fftsize, long lowfreq, long highfreq, long srate)
{
    static double *inbuf=NULL;
    static double *realn=NULL;
    static double *imags=NULL;
    static double *spectr=NULL;
    static double *zer0=NULL;
    static double *olds=NULL;
    static double *infader=NULL;
    static double *outfader=NULL;
    int notched=(highfreq<lowfreq) ? 1 : 0;
    long incounter=0;
    /* treble is the highest baseband frequency */
    /* bass the the lowest baseband frequency */
    /* this function is called twice per FFT block */
    long midcounter=0;
    long outcounter=0;
    long bass=lowfreq*(fftsize/(double)srate);
    long treble=(highfreq)*(fftsize/(double)srate);
    static long halffft=2;
    static long old_fftsize=0;
    static short first=1;
    if(first==1 || fftsize!=old_fftsize)
    {
        if(inbuf)
             free(inbuf);
        if(realn)
            free(realn);
        if(imags)
            free(imags);
        if(spectr)
            free(spectr);
        if(zer0)
            free(zer0);
        if(olds)
            free(olds);
        if(infader)
            free(infader);
        if(outfader)
            free(outfader);
        infader=(double*)malloc(fftsize*sizeof(double));
        outfader=(double*)malloc(fftsize*sizeof(double));
        inbuf=(double*)malloc(fftsize*sizeof(double));
        realn=(double*)malloc(fftsize*sizeof(double));
        imags=(double*)malloc(fftsize*sizeof(double));
        spectr=(double*)malloc(fftsize*sizeof(double));
        zer0=(double*)malloc(fftsize*sizeof(double));
        olds=(double*)malloc(fftsize*sizeof(double));
        if((!inbuf) || (!realn) ||(!imags) ||(!spectr)||(!zer0)||(!ol   ds))
        {
            printf("Not enough memory for FFT!\n");
                    exit(1);
        }
        halffft=fftsize/2;
        long infade=0;
        long outfade=halffft;
        for(infade=0;infade<halffft;infade++)
        {
            outfade--;
            outfader[infade]=(0.5 * cos((infade) *  M_PI/(double)(halffft))+0.5);
            infader[outfade]=outfader[infade];
        }
        first=0;
    }
    memset(realn,0,sizeof(double)*fftsize);
    for(incounter=0;incounter<halffft;incounter++)
    {
        inbuf[incounter]=inbuf[incounter+halffft];
    }
    for(incounter=0;incounter<halffft;incounter++)
    {
        inbuf[incounter+halffft]=insamples[incounter];
    }
    for(incounter=0;incounter<fftsize;incounter++)
    {
        realn[incounter]=inbuf[incounter];
    }   
    memset(imags,0,sizeof(double)*fftsize);
    FFT(fftsize, 0, realn,imags, spectr,zer0);
    memset(realn,0,sizeof(double)*fftsize);
    memset(imags,0,sizeof(double)*fftsize);
    if(notched==0)
    {
        for(midcounter=bass;midcounter<treble;midcounter++)
        {
            realn[midcounter]=spectr[midcounter] * 2.0;
            imags[midcounter]= zer0[midcounter] * 2.0;
        }
        if(bass==0)
            realn[0]=spectr[0];

    }
    else if(notched==1)
    {
        for(midcounter=0;midcounter<halffft;midcounter++)
        {
            if((midcounter<treble) ||(midcounter>bass))
            {
                realn[midcounter]=spectr[midcounter] * 2.0;
                imags[midcounter]= zer0[midcounter] * 2.0;
            }
        }
        if(bass==0)
        {
            realn[0]=0;
        }
        else
        {
            realn[0]=spectr[0];
        }
    }
    FFT(fftsize, 1, realn, imags,spectr,zer0);
    for(outcounter=0;outcounter<halffft;outcounter++)
    {
        outsamples[outcounter]=(((spectr[outcounter] )*infader[outcounter])+(olds[outcounter+halffft]*outfader[outcounter])) ;
    }
    for(outcounter=0;outcounter<fftsize;outcounter++)
    {
        olds[outcounter]=spectr[outcounter];
    }
    memset(spectr,0,fftsize*sizeof(double));
    memset(zer0,0,fftsize*sizeof(double));
    old_fftsize=fftsize;
}

signed short mainbandpass(signed short input, double lowcut, double highcut,long rate,long fftsize)
{
    double retvalue=0;
    static double *insamp=NULL;
    static double *outsamp=NULL;
    static int first=1;
    static int q=0;
    if(first==1)
    {
            insamp=(double*)malloc(fftsize * sizeof(double));
            outsamp=(double*)malloc(fftsize * sizeof(double));
            if(insamp==NULL || outsamp==NULL)
            {
                   printf("Not enough memory for FFT buffers.\n");
                   exit(1);
            }
        memset(insamp,0,fftsize * sizeof(double));
        memset(outsamp,0,fftsize * sizeof(double));
        first=0;
    }

    insamp[q]=input;
    retvalue=outsamp[q];
    if(retvalue> 32767)
        retvalue=32767;
    if(retvalue <-32768)
        retvalue=-32768;
    q++;
    if(q>(fftsize/2)-1)
    {
        mainfftBandPass(insamp,outsamp, fftsize, lowcut,highcut,rate);
        q=0;
    }
    return (signed short)retvalue;
}

for each sample of the input audio, call mainbandpass with the input sample and the range of frequencies you want to keep in lowcut and highcut. If lowcut is greater than highcut the result will be a band-reject filter. There is circular convolution going on, but there will be no out of band emissions which is good for modems.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.