DSP tutorial: Lowpass filtering using FFT

This example records audio and saves the lowpass filtered data to a .wav file. There’s also an example of bandpass filtering. Filtering is done with FFT.

Note: this simple brickwall filtering method is not applicable in some cases because it introduces ringing in the resulting filtered sound. See this section of my tutorial to read about this more.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
public class LowPassFilter implements Runnable {
    private final static int SAMPLERATE = 44100;
    private final static int BUFFERSIZE = SAMPLERATE * 2;

    private TargetDataLine tdl;
    private DoubleFFT_1D fft;

    LowPassFilter(TargetDataLine tdl) {
        this.tdl = tdl;
    }

    // converts float array to byte array
    private byte[] getBytesFromDoubles(final double[] audioData, final int storedSamples) {
        byte[] audioDataBytes = new byte[storedSamples * 2];

        for (int i = 0; i < storedSamples; i++) {
            // saturation
            audioData[i] = Math.min(1.0, Math.max(-1.0, audioData[i]));

            // scaling and conversion to integer
            int sample = (int) Math.round((audioData[i] + 1.0) * 32767.5) - 32768;

            byte high = (byte) ((sample >> 8) & 0xFF);
            byte low = (byte) (sample & 0xFF);
            audioDataBytes[i * 2] = low;
            audioDataBytes[i * 2 + 1] = high;
        }

        return audioDataBytes;
    }

    // saves the audio data given in audioDataBytes to a .wav file
    private void writeWavFile(final byte[] audioDataBytes, final int storedSamples, final String fileName) {
        AudioFormat audioFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, SAMPLERATE, 16, 1, 2, SAMPLERATE, false);
        AudioInputStream audioInputStream = new AudioInputStream(new ByteArrayInputStream(audioDataBytes), audioFormat, storedSamples);

        try {
            FileOutputStream fileOutputStream = new FileOutputStream(fileName);
            AudioSystem.write(audioInputStream, AudioFileFormat.Type.WAVE, fileOutputStream);
            audioInputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void lowPassFilter(double[] audioData, final int storedSamples) {
        // we need to initialize a buffer where we store our samples as complex numbers. first value is the real part, second is the imaginary.
        double[] fftData = new double[audioData.length * 2];
        for (int i = 0; i < storedSamples; i++) {
            // copying audio data to the fft data buffer, imaginary part is 0
            fftData[2 * i] = audioData[i];
            fftData[2 * i + 1] = 0;
        }

        // calculating the fft of the data, so we will have spectral power of each frequency component
        fft.complexForward(fftData);

        // zeroing out all components above this frequency
        int cutFreq = 2000;

        for (int i = 0; i < fftData.length; i += 2) {
            // lowpass
            if (i > ((cutFreq * (fftData.length/2)) / SAMPLERATE)*2)
                fftData[i] = fftData[i + 1] = 0;
        }
   
        // bandpass
        //if (i < (((cutFreq - 1000) * (fftData.length/2)) / SAMPLERATE)*2 || i > (((cutFreq + 1000) * (fftData.length/2)) / SAMPLERATE)*2)
            //fftData[i] = fftData[i + 1] = 0;

        // built-in scaling hangs the thread, so we don't use it
        fft.complexInverse(fftData, false);

        for (int i = 0; i < storedSamples; i++) {
            audioData[i] = fftData[2 * i] / (SAMPLERATE/4.0); // applying scaling
        }
    }

    @Override
    public void run() {
        byte[] abBuffer = new byte[tdl.getBufferSize()];
        double[] abBufferDouble = new double[abBuffer.length / 2];
        ByteArrayOutputStream baos = new ByteArrayOutputStream(); // this will store sound data

        fft = new DoubleFFT_1D(abBufferDouble.length);

        tdl.start();

        try {
            while (!Thread.interrupted()) {
                // waiting for the buffer to get filled
                while (tdl.available() < tdl.getBufferSize() * 0.5)
                    Thread.sleep(0, 1); // without this, the audio will be choppy

                int bytesRead = tdl.read(abBuffer, 0, tdl.available());

                // converting frames stored as bytes to double values
                int samplesRead = bytesRead / tdl.getFormat().getFrameSize();
                for (int i = 0; i < samplesRead; i++)
                    abBufferDouble[i] = ((abBuffer[i * 2] & 0xFF) | (abBuffer[i * 2 + 1] << 8)) / 32768.0;

                lowPassFilter(abBufferDouble, samplesRead);
                baos.write(getBytesFromDoubles(abBufferDouble, samplesRead), 0, samplesRead * 2);
            }
        } catch (InterruptedException e) {
        }

        tdl.stop();
        tdl.close();

        writeWavFile(baos.toByteArray(), baos.size() / 2, "output.wav");
    }

    public static void main(String[] args) {
        AudioFormat audioFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, SAMPLERATE, 16, 1, 2, SAMPLERATE, false);
        DataLine.Info info = new DataLine.Info(TargetDataLine.class, audioFormat, BUFFERSIZE);

        TargetDataLine targetDataLine = null;
        try {
            targetDataLine = (TargetDataLine) AudioSystem.getLine(info);
            targetDataLine.open(audioFormat, BUFFERSIZE);
            System.out.println("Buffer size: " + targetDataLine.getBufferSize());
        } catch (LineUnavailableException e1) {
            e1.printStackTrace();
        }

        // creating the recorder thread from this class' instance
        LowPassFilter lowPassFilter = new LowPassFilter(targetDataLine);
        Thread lowPassFilterThread = new Thread(lowPassFilter);

        // we use this to read line from the standard input
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        lowPassFilterThread.setPriority(Thread.MAX_PRIORITY);
        lowPassFilterThread.start();

        System.out.println("Recording... press ENTER to stop recording!");
        try {
            br.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }

        lowPassFilterThread.interrupt();

        try {
            // waiting for the recorder thread to stop
            lowPassFilterThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Recording stopped.");
    }
}

download (mirror2) (850.3 kb, 152 dls, today: 0)

Freddy 2013-07-26 06:08:18

hey thanks for the tutorial, helping me a bunch, just wanted to point out a broken link on this page:
http://dp.nonoo.hu/projects/ham-dsp-tutorial/07-fft-lowpass-filtering/
the link under the text ‘this section’ points to
http://dp.nonoo.hu/projects/ham-dsp-tutorial/19-dsp-tutorial-lowpass-fir-filtering-using-fft-convolution/
but I think it needs to point to
http://dp.nonoo.hu/projects/ham-dsp-tutorial/19-lowpass-fir-filtering-using-fft-convolution/
cheers!

Nonoo 2013-07-26 10:28:33

Thanks, I fixed it!

 
 
Name (required)
E-mail (required - never shown publicly)
Webpage URL
Comment:
You may use <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> in your comment.

Click for dp.nonoo.hu »

About me

Nonoo
I'm Nonoo. This is my blog about music, sounds, filmmaking, amateur radio, computers, programming, electronics and other things I'm obsessed with. ... »

Twitter

Listening now

My favorite artists