Add basic windowed-sinc audio filters.
authorCarl Hetherington <cth@carlh.net>
Tue, 15 Jul 2014 09:28:06 +0000 (10:28 +0100)
committerCarl Hetherington <cth@carlh.net>
Tue, 15 Jul 2014 09:28:06 +0000 (10:28 +0100)
src/lib/audio_filter.cc [new file with mode: 0644]
src/lib/audio_filter.h [new file with mode: 0644]
src/lib/wscript
test/audio_filter_test.cc [new file with mode: 0644]
test/wscript

diff --git a/src/lib/audio_filter.cc b/src/lib/audio_filter.cc
new file mode 100644 (file)
index 0000000..0cd2b18
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include <cmath>
+#include "audio_filter.h"
+#include "audio_buffers.h"
+
+using std::vector;
+using std::min;
+using boost::shared_ptr;
+
+vector<float>
+AudioFilter::sinc_blackman (float cutoff, bool invert) const
+{
+       vector<float> ir (_M + 1);
+       
+       /* Impulse response */
+       
+       for (int i = 0; i <= _M; ++i) {
+               if (i == (_M / 2)) {
+                       ir[i] = 2 * M_PI * cutoff;
+               } else {
+                       /* sinc */
+                       ir[i] = sin (2 * M_PI * cutoff * (i - _M / 2)) / (i - _M / 2);
+                       /* Blackman window */
+                       ir[i] *= (0.42 - 0.5 * cos (2 * M_PI * i / _M) + 0.08 * cos (4 * M_PI * i / _M));
+               }
+       }
+       
+       /* Normalise */
+       
+       float sum = 0;
+       for (int i = 0; i <= _M; ++i) {
+               sum += ir[i];
+       }
+       
+       for (int i = 0; i <= _M; ++i) {
+               ir[i] /= sum;
+       }
+       
+       /* Frequency inversion (swapping low-pass for high-pass, or whatever) */
+       
+       if (invert) {
+               for (int i = 0; i <= _M; ++i) {
+                       ir[i] = -ir[i];
+               }
+               ir[_M / 2] += 1;
+       }
+       
+       return ir;
+}
+
+shared_ptr<AudioBuffers>
+AudioFilter::run (shared_ptr<AudioBuffers> in)
+{
+       shared_ptr<AudioBuffers> out (new AudioBuffers (in->channels(), in->frames()));
+       
+       if (!_tail) {
+               _tail.reset (new AudioBuffers (in->channels(), _M + 1));
+               _tail->make_silent ();
+       }
+       
+       for (int i = 0; i < in->channels(); ++i) {
+               for (int j = 0; j < in->frames(); ++j) {
+                       float s = 0;
+                       for (int k = 0; k <= _M; ++k) {
+                               if ((j - k) < 0) {
+                                       s += _tail->data(i)[j - k + _M + 1] * _ir[k];
+                               } else {
+                                       s += in->data(i)[j - k] * _ir[k];
+                               }
+                       }
+                       
+                       out->data(i)[j] = s;
+               }
+       }
+       
+       int const amount = min (in->frames(), _tail->frames());
+       if (amount < _tail->frames ()) {
+               _tail->move (amount, 0, _tail->frames() - amount);
+       }
+       _tail->copy_from (in.get(), amount, in->frames() - amount, _tail->frames () - amount);
+       
+       return out;
+}
+
+LowPassAudioFilter::LowPassAudioFilter (float transition_bandwidth, float cutoff)
+       : AudioFilter (transition_bandwidth)
+{
+       _ir = sinc_blackman (cutoff, false);
+}
+
+
+HighPassAudioFilter::HighPassAudioFilter (float transition_bandwidth, float cutoff)
+       : AudioFilter (transition_bandwidth)
+{
+       _ir = sinc_blackman (cutoff, true);
+}
+
+BandPassAudioFilter::BandPassAudioFilter (float transition_bandwidth, float lower, float higher)
+       : AudioFilter (transition_bandwidth)
+{
+       vector<float> lpf = sinc_blackman (lower, false);
+       vector<float> hpf = sinc_blackman (higher, true);
+       
+       _ir.resize (_M + 1);
+       for (int i = 0; i <= _M; ++i) {
+               _ir[i] = lpf[i] + hpf[i];
+       }
+       
+       /* We now have a band-stop, so invert for band-pass */
+       for (int i = 0; i <= _M; ++i) {
+               _ir[i] = -_ir[i];
+       }
+       
+       _ir[_M / 2] += 1;
+}
diff --git a/src/lib/audio_filter.h b/src/lib/audio_filter.h
new file mode 100644 (file)
index 0000000..9dfcec5
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include <vector>
+#include <boost/shared_ptr.hpp>
+
+class AudioBuffers;
+class audio_filter_impulse_kernel_test;
+class audio_filter_impulse_input_test;
+
+class AudioFilter
+{
+public:
+       AudioFilter (float transition_bandwidth)
+       {
+               _M = 4 / transition_bandwidth;
+               if (_M % 2) {
+                       ++_M;
+               }
+       }
+
+       boost::shared_ptr<AudioBuffers> run (boost::shared_ptr<AudioBuffers> in);
+
+protected:
+       friend class audio_filter_impulse_kernel_test;
+       friend class audio_filter_impulse_input_test;
+
+       std::vector<float> sinc_blackman (float cutoff, bool invert) const;
+
+       std::vector<float> _ir;
+       int _M;
+       boost::shared_ptr<AudioBuffers> _tail;
+};
+
+class LowPassAudioFilter : public AudioFilter
+{
+public:
+       /** Construct a windowed-sinc low-pass filter using the Blackman window.
+        *  @param transition_bandwidth Transition bandwidth as a fraction of the sampling rate.
+        *  @param cutoff Cutoff frequency as a fraction of the sampling rate.
+        */
+       LowPassAudioFilter (float transition_bandwidth, float cutoff);
+};
+
+class HighPassAudioFilter : public AudioFilter
+{
+public:
+       /** Construct a windowed-sinc high-pass filter using the Blackman window.
+        *  @param transition_bandwidth Transition bandwidth as a fraction of the sampling rate.
+        *  @param cutoff Cutoff frequency as a fraction of the sampling rate.
+        */
+       HighPassAudioFilter (float transition_bandwidth, float cutoff);
+};
+
+class BandPassAudioFilter : public AudioFilter
+{
+public:
+       /** Construct a windowed-sinc band-pass filter using the Blackman window.
+        *  @param transition_bandwidth Transition bandwidth as a fraction of the sampling rate.
+        *  @param lower Lower cutoff frequency as a fraction of the sampling rate.
+        *  @param higher Higher cutoff frequency as a fraction of the sampling rate.
+        */
+       BandPassAudioFilter (float transition_bandwidth, float lower, float higher);
+};
index 9c929a671c9cbe7bbe8a6cc7f42cb184b8284644..ee8a59c0befb912ab6d043f1ec1be757ef461e55 100644 (file)
@@ -7,6 +7,7 @@ sources = """
           audio_buffers.cc
           audio_content.cc
           audio_decoder.cc
+          audio_filter.cc
           audio_mapping.cc
           audio_processor.cc
           cinema.cc
@@ -73,7 +74,6 @@ sources = """
           single_stream_audio_content.cc
           sndfile_content.cc
           sndfile_decoder.cc
-          sox_audio_processor.cc
           subrip.cc
           subrip_content.cc
           subrip_decoder.cc
diff --git a/test/audio_filter_test.cc b/test/audio_filter_test.cc
new file mode 100644 (file)
index 0000000..bcd16fd
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+/** @file  test/audio_filter_test.cc
+ *  @brief Basic tests of audio filters.
+ */
+
+#include <boost/test/unit_test.hpp>
+#include "lib/audio_filter.h"
+#include "lib/audio_buffers.h"
+
+using boost::shared_ptr;
+
+static void
+audio_filter_impulse_test_one (AudioFilter& f, int block_size, int num_blocks)
+{
+       int c = 0;
+
+       for (int i = 0; i < num_blocks; ++i) {
+
+               shared_ptr<AudioBuffers> in (new AudioBuffers (1, block_size));
+               for (int j = 0; j < block_size; ++j) {
+                       in->data()[0][j] = c + j;
+               }
+
+               shared_ptr<AudioBuffers> out = f.run (in);
+               
+               for (int j = 0; j < out->frames(); ++j) {
+                       BOOST_CHECK_EQUAL (out->data()[0][j], c + j);
+               }
+
+               c += block_size;
+       }
+}
+
+/** Create a filter with an impulse as a kernel and check that it
+ *  passes data through unaltered.
+ */
+BOOST_AUTO_TEST_CASE (audio_filter_impulse_kernel_test)
+{
+       AudioFilter f (0.02);
+       f._ir.resize (f._M + 1);
+
+       f._ir[0] = 1;
+       for (int i = 1; i <= f._M; ++i) {
+               f._ir[i] = 0;
+       }
+
+       audio_filter_impulse_test_one (f, 32, 1);
+       audio_filter_impulse_test_one (f, 256, 1);
+       audio_filter_impulse_test_one (f, 2048, 1);
+}
+
+/** Create filters and pass them impulses as input and check that
+ *  the filter kernels comes back.
+ */
+BOOST_AUTO_TEST_CASE (audio_filter_impulse_input_test)
+{
+       LowPassAudioFilter lpf (0.02, 0.3);
+
+       shared_ptr<AudioBuffers> in (new AudioBuffers (1, 1751));
+       in->make_silent ();
+       in->data(0)[0] = 1;
+       
+       shared_ptr<AudioBuffers> out = lpf.run (in);
+       for (int j = 0; j < out->frames(); ++j) {
+               if (j <= lpf._M) {
+                       BOOST_CHECK_EQUAL (out->data(0)[j], lpf._ir[j]);
+               } else {
+                       BOOST_CHECK_EQUAL (out->data(0)[j], 0);
+               }
+       }
+
+       HighPassAudioFilter hpf (0.02, 0.3);
+
+       in.reset (new AudioBuffers (1, 9133));
+       in->make_silent ();
+       in->data(0)[0] = 1;
+       
+       out = hpf.run (in);
+       for (int j = 0; j < out->frames(); ++j) {
+               if (j <= hpf._M) {
+                       BOOST_CHECK_EQUAL (out->data(0)[j], hpf._ir[j]);
+               } else {
+                       BOOST_CHECK_EQUAL (out->data(0)[j], 0);
+               }
+       }
+}
index f123f844d79e11ba04f6579932f66428aac11320..5bf7ea80eedbc707380bd68e2f70d37c18a9badd 100644 (file)
@@ -20,6 +20,7 @@ def build(bld):
                  audio_buffers_test.cc
                  audio_delay_test.cc
                  audio_decoder_test.cc
+                 audio_filter_test.cc
                  audio_mapping_test.cc
                  black_fill_test.cc
                  burnt_subtitle_test.cc