From: Carl Hetherington Date: Tue, 15 Jul 2014 09:28:06 +0000 (+0100) Subject: Add basic windowed-sinc audio filters. X-Git-Tag: v2.0.48~725 X-Git-Url: https://main.carlh.net/gitweb/?p=dcpomatic.git;a=commitdiff_plain;h=e5f79c57e1123754b1325f964123fcb56a2572b3 Add basic windowed-sinc audio filters. --- diff --git a/src/lib/audio_filter.cc b/src/lib/audio_filter.cc new file mode 100644 index 000000000..0cd2b18fb --- /dev/null +++ b/src/lib/audio_filter.cc @@ -0,0 +1,133 @@ +/* + Copyright (C) 2014 Carl Hetherington + + 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 +#include "audio_filter.h" +#include "audio_buffers.h" + +using std::vector; +using std::min; +using boost::shared_ptr; + +vector +AudioFilter::sinc_blackman (float cutoff, bool invert) const +{ + vector 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 +AudioFilter::run (shared_ptr in) +{ + shared_ptr 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 lpf = sinc_blackman (lower, false); + vector 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 index 000000000..9dfcec58b --- /dev/null +++ b/src/lib/audio_filter.h @@ -0,0 +1,80 @@ +/* + Copyright (C) 2014 Carl Hetherington + + 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 +#include + +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 run (boost::shared_ptr in); + +protected: + friend class audio_filter_impulse_kernel_test; + friend class audio_filter_impulse_input_test; + + std::vector sinc_blackman (float cutoff, bool invert) const; + + std::vector _ir; + int _M; + boost::shared_ptr _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); +}; diff --git a/src/lib/wscript b/src/lib/wscript index 9c929a671..ee8a59c0b 100644 --- a/src/lib/wscript +++ b/src/lib/wscript @@ -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 index 000000000..bcd16fd4e --- /dev/null +++ b/test/audio_filter_test.cc @@ -0,0 +1,104 @@ +/* + Copyright (C) 2014 Carl Hetherington + + 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 +#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 in (new AudioBuffers (1, block_size)); + for (int j = 0; j < block_size; ++j) { + in->data()[0][j] = c + j; + } + + shared_ptr 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 in (new AudioBuffers (1, 1751)); + in->make_silent (); + in->data(0)[0] = 1; + + shared_ptr 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); + } + } +} diff --git a/test/wscript b/test/wscript index f123f844d..5bf7ea80e 100644 --- a/test/wscript +++ b/test/wscript @@ -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