bcbfa88ef492 — Chris Cannam 3 years ago
Merge from branch resampler
M .hgignore +6 -0
@@ 3,3 3,9 @@ syntax: glob
 *.o
 *.a
 test-*
+example/resample
+test/e2e/halve
+test/e2e/random
+test/e2e/undulating
+test/e2e/oversample
+test/e2e/out

          
M Makefile +10 -8
@@ 1,19 1,20 @@ 
 
 # Add to RESAMPLE_DEFINES the relevant options for your desired
-# third-party library support.
+# implementation and/or third-party library support.
 #
-# Available options are
+# Available library options are
 #
 #  -DHAVE_LIBSAMPLERATE  The libsamplerate library is available (recommended)
 #  -DHAVE_IPP            Intel's Integrated Performance Primitives are available
 #  -DHAVE_LIBRESAMPLE    The libresample library is available
+#  -DUSE_BQRESAMPLER     Compile the built-in BQ resampler (pretty good)
 #  -DUSE_SPEEX           Compile the built-in Speex-derived resampler
 #
-# You may define more than one of these. If you define USE_SPEEX, the
-# code will be compiled in and will be used when it is judged to be
-# the best available option for a given quality setting. If no flags
-# are supplied, the code will refuse to compile.
-
+# You may define more than one of these, and the implementation used
+# will depend on the quality setting you request - but it is usually
+# better to stick with a single known library. If no flags are
+# supplied, the code will refuse to compile.
+#
 RESAMPLE_DEFINES	:= -DHAVE_LIBSAMPLERATE
 
 

          
@@ 25,7 26,8 @@ VECTOR_DEFINES 		:=
 ALLOCATOR_DEFINES 	:= 
 
 
-# Add any related includes and libraries here
+# Add any related includes and libraries here (e.g. -lsamplerate in
+# THIRD_PARTY_LIBS if using libsamplerate)
 #
 THIRD_PARTY_INCLUDES	:=
 THIRD_PARTY_LIBS	:= -lsamplerate

          
M README.md +4 -8
@@ 3,13 3,9 @@ bqresample
 ==========
 
 A small C++ library wrapping various audio sample rate conversion
-libraries.
-
-Covers libsamplerate, Intel IPP, libresample, and Speex resampler
-implementations. Note however that libsamplerate is now the
-recommended option on every platform, so for new applications there is
-probably no good reason not to use it directly. Suitable for Windows,
-Mac, and Linux.
+libraries. Contains a built-in implementation and wrappers for
+libsamplerate, Intel IPP, and Speex resampler
+implementations. Suitable for Windows, Mac, and Linux.
 
 Requires the bqvec library.
 

          
@@ 18,7 14,7 @@ same authors (see https://hg.sr.ht/~brea
 It has been pulled out into a separate library and relicensed under a
 more permissive licence.
 
-C++ standard required: C++98 (does not use C++11 or newer features)
+C++ standard required: C++11
 
  * To compile: read and follow the notes in Makefile, edit the Makefile,
    then make test. Or else use one of the pre-edited Makefiles in the

          
M bqresample/Resampler.h +38 -0
@@ 44,6 44,9 @@ class Resampler
 {
 public:
     enum Quality { Best, FastestTolerable, Fastest };
+    enum Dynamism { RatioOftenChanging, RatioMostlyFixed };
+    enum RatioChange { SmoothRatioChange, SuddenRatioChange };
+    
     enum Exception { ImplementationError };
 
     struct Parameters {

          
@@ 53,6 56,22 @@ public:
          */
         Quality quality;
 
+        /**
+         * Performance hint indicating whether the ratio is expected
+         * to change regularly or not. If not, more work may happen on
+         * ratio changes to reduce work when ratio is unchanged.
+         */
+        Dynamism dynamism; 
+
+        /**
+         * Hint indicating whether to smooth transitions, via filter
+         * interpolation or some such method, at ratio change
+         * boundaries, or whether to make a precise switch to the new
+         * ratio without regard to audible artifacts. The actual
+         * effect of this depends on the implementation in use.
+         */
+        RatioChange ratioChange;
+        
         /** 
          * Rate of expected input prior to resampling: may be used to
          * determine the filter bandwidth for the quality setting. If

          
@@ 79,6 98,8 @@ public:
 
         Parameters() :
             quality(FastestTolerable),
+            dynamism(RatioMostlyFixed),
+            ratioChange(SmoothRatioChange),
             initialSampleRate(44100),
             maxBufferSize(0),
             debugLevel(0) { }

          
@@ 137,8 158,25 @@ public:
                             double ratio,
                             bool final = false);
 
+    /**
+     * Return the channel count provided on construction.
+     */
     int getChannelCount() const;
 
+    /**
+     * Return the ratio that will be actually used when the given
+     * ratio is requested. For example, if the resampler internally
+     * uses a rational approximation of the given ratio, this will
+     * return the closest double to that approximation. Not all
+     * implementations support this; an implementation that does not
+     * will just return the given ratio.
+     */
+    double getEffectiveRatio(double ratio) const;
+    
+    /**
+     * Reset the internal processing state so that the next call
+     * behaves as if the resampler had just been constructed.
+     */
     void reset();
 
     class Impl;

          
M build/Makefile.inc +29 -6
@@ 1,6 1,7 @@ 
 
 SRC_DIR	:= src
 TEST_DIR := test
+EXAMPLE_DIR := example
 SPEEX_DIR := speex
 HEADER_DIR := bqresample
 

          
@@ 10,9 11,12 @@ HEADERS	:= $(wildcard $(HEADER_DIR)/*.h)
 OBJECTS	:= $(SOURCES:.cpp=.o)
 OBJECTS	:= $(OBJECTS:.c=.o)
 
-TEST_SOURCES	:= $(wildcard $(TEST_DIR)/*.cpp)
+TEST_SOURCES	:= $(wildcard $(TEST_DIR)/*.cpp) $(wildcard $(TEST_DIR)/e2e/*.cpp)
 TEST_OBJECTS	:= $(TEST_SOURCES:.cpp=.o)
 
+EXAMPLE_SOURCES	:= $(wildcard $(EXAMPLE_DIR)/*.cpp)
+EXAMPLE_OBJECTS	:= $(EXAMPLE_SOURCES:.cpp=.o)
+
 OPTFLAGS ?= -O3 -ffast-math
 
 CXXFLAGS ?= -std=c++98 -fpic -Wall -Wextra -Werror $(RESAMPLE_DEFINES) $(VECTOR_DEFINES) $(ALLOCATOR_DEFINES) $(OPTFLAGS) -I. $(THIRD_PARTY_INCLUDES) -I../bqvec

          
@@ 23,8 27,15 @@ LIBRARY	:= libbqresample.a
 
 all:	$(LIBRARY)
 
-test:	$(LIBRARY) test-resampler
+test:	$(LIBRARY) test-resampler test/e2e/halve test/e2e/oversample test/e2e/random test/e2e/undulating
 	./test-resampler
+	mkdir -p ./test/e2e/out
+	./test/e2e/halve ./test/e2e/sweep-log.wav ./test/e2e/out/halve.wav
+	./test/e2e/oversample ./test/e2e/sweep-log.wav ./test/e2e/out/oversample.wav
+	./test/e2e/random ./test/e2e/sweep-log.wav ./test/e2e/out/random.wav
+	./test/e2e/undulating ./test/e2e/sweep-log.wav ./test/e2e/out/undulating.wav
+
+example:	$(LIBRARY) example/resample
 
 valgrind:	$(LIBRARY) test-resampler
 	valgrind ./test-resampler

          
@@ 35,17 46,29 @@ valgrind:	$(LIBRARY) test-resampler
 test-resampler:	test/TestResampler.o $(LIBRARY)
 	$(CXX) $(CXXFLAGS) -o $@ $^ $(LIBRARY) -lboost_unit_test_framework -L../bqvec -lbqvec $(THIRD_PARTY_LIBS)
 
+example/resample: example/resample.o $(LIBRARY)
+	$(CXX) $(CXXFLAGS) -o $@ $^ $(LIBRARY) -lsndfile -L../bqvec -lbqvec $(THIRD_PARTY_LIBS)
+
+test/e2e/%: test/e2e/%.o $(LIBRARY)
+	$(CXX) $(CXXFLAGS) -o $@ $^ $(LIBRARY) -lsndfile -L../bqvec -lbqvec $(THIRD_PARTY_LIBS)
+
 clean:		
-	rm -f $(OBJECTS) $(TEST_OBJECTS)
+	rm -f $(OBJECTS) $(TEST_OBJECTS) $(EXAMPLE_OBJECTS)
 
 distclean:	clean
-	rm -f $(LIBRARY) test-resampler
+	rm -f $(LIBRARY) test-resampler test/e2e/halve test/e2e/oversample test/e2e/oversample test/e2e/random test/e2e/undulating
 
 depend:
-	makedepend -Y -fbuild/Makefile.inc $(SOURCES) $(HEADERS) $(TEST_SOURCES)
+	makedepend $(RESAMPLE_DEFINES) -Y -fbuild/Makefile.inc $(SOURCES) $(HEADERS) $(TEST_SOURCES) $(EXAMPLE_SOURCES)
 
 
 # DO NOT DELETE
 
-src/Resampler.o: bqresample/Resampler.h
+src/BQResampler.o: src/BQResampler.h
+src/Resampler.o: bqresample/Resampler.h src/BQResampler.h
 test/TestResampler.o: bqresample/Resampler.h
+test/e2e/halve.o: bqresample/Resampler.h test/e2e/e2e.cpp
+test/e2e/oversample.o: bqresample/Resampler.h test/e2e/e2e.cpp
+test/e2e/random.o: bqresample/Resampler.h test/e2e/e2e.cpp
+test/e2e/undulating.o: bqresample/Resampler.h test/e2e/e2e.cpp
+example/resample.o: bqresample/Resampler.h

          
A => example/resample.cpp +163 -0
@@ 0,0 1,163 @@ 
+
+#include "bqresample/Resampler.h"
+
+#include <sndfile.h>
+
+#include <iostream>
+
+#include <cstring>
+#include <cstdlib>
+#include <cmath>
+
+using namespace std;
+
+void usage()
+{
+    cerr << "Usage: resample [-v] [-c <converter>] -to <rate> <infile> <outfile>" << endl;
+    cerr << "where <converter> may be 0, 1, or 2, for best, medium, or fastest respectively." << endl;
+    cerr << "The default converter is 0, best." << endl;
+    cerr << "Supply -v for verbose output." << endl;
+    exit(2);
+}
+
+int main(int argc, char **argv)
+{
+    double target = 0.0;
+    int quality = 0;
+    int arg;
+    bool verbose = false;
+
+    for (arg = 1; arg + 2 < argc; ++arg) {
+        if (!strcmp(argv[arg], "-c")) {
+            char *e = argv[arg+1];
+            quality = strtol(argv[arg+1], &e, 10);
+            if (*e || (quality < 0) || (quality > 2)) {
+                cerr << "error: invalid converter \""
+                     << argv[arg+1] << "\" (must be 0, 1, 2)" << endl;
+                usage();
+            }
+            ++arg;
+            continue;
+        } else if (!strcmp(argv[arg], "-to")) {
+            target = strtod(argv[arg+1], 0);
+            if (!target) {
+                cerr << "error: invalid target \"" << argv[arg+1]
+                     << "\" (must be numeric)" << endl;
+                usage();
+            }
+            ++arg;
+            continue;
+        } else if (!strcmp(argv[arg], "-v")) {
+            verbose = true;
+        } else {
+            cerr << "error: unexpected option \"" << argv[arg] << "\"" << endl;
+            usage();
+        }
+    }
+
+    if (!target || arg + 2 != argc) {
+        usage();
+    }
+
+    string infilename = argv[arg];
+    string outfilename = argv[arg+1];
+    
+    SF_INFO info_in;
+    SNDFILE *file_in = sf_open(infilename.c_str(), SFM_READ, &info_in);
+    if (!file_in) {
+        cerr << "failed to open " << infilename << endl;
+        return 1;
+    }
+
+    int output_rate = round(target);
+    int channels = info_in.channels;
+    
+    if (verbose) {
+        cerr << "input rate = " << info_in.samplerate << endl;
+        cerr << "output rate = " << output_rate << endl;
+    }
+
+    double ratio = target / info_in.samplerate;
+    if (verbose) {
+        cerr << "ratio = " << ratio << endl;
+    }
+
+    breakfastquay::Resampler::Parameters parameters;
+    switch (quality)  {
+    case 0:
+        parameters.quality = breakfastquay::Resampler::Best;
+        if (verbose) {
+            cerr << "quality = best" << endl;
+        }
+        break;
+    case 1:
+        parameters.quality = breakfastquay::Resampler::FastestTolerable;
+        if (verbose) {
+            cerr << "quality = middling" << endl;
+        }
+        break;
+    case 2:
+        parameters.quality = breakfastquay::Resampler::Fastest;
+        if (verbose) {
+            cerr << "quality = worst" << endl;
+        }
+        break;
+    }        
+
+    SF_INFO info_out;
+    memset(&info_out, 0, sizeof(SF_INFO));
+    info_out.channels = channels;
+    info_out.format = info_in.format;
+    info_out.samplerate = output_rate;
+    SNDFILE *file_out = sf_open(outfilename.c_str(), SFM_WRITE, &info_out);
+    if (!file_out) {
+        cerr << "failed to open " << outfilename << endl;
+        return 1;
+    }
+
+    int ibs = 1024;
+    int obs = ceil(ibs * ratio);
+    float *ibuf = new float[ibs * channels];
+    float *obuf = new float[obs * channels];
+
+    parameters.dynamism = breakfastquay::Resampler::RatioMostlyFixed;
+    parameters.ratioChange = breakfastquay::Resampler::SuddenRatioChange;
+    parameters.initialSampleRate = info_in.samplerate;
+    parameters.debugLevel = (verbose ? 1 : 0);
+    breakfastquay::Resampler resampler(parameters, info_in.channels);
+
+    int n = 0;
+    while (true) {
+        int count = sf_readf_float(file_in, ibuf, ibs);
+        if (verbose) {
+            cerr << ".";
+        }
+        if (count < 0) {
+            cerr << "error: count = " << count << endl;
+            break;
+        }
+        bool final = (count < ibs);
+        int got = resampler.resampleInterleaved
+            (obuf, obs, ibuf, count, ratio, final);
+        if (got == 0 && final) {
+            break;
+        } else {
+            for (int i = 0; i < got * channels; ++i) {
+                if (obuf[i] < -1.0) obuf[i] = -1.0;
+                if (obuf[i] > 1.0) obuf[i] = 1.0;
+            }
+            sf_writef_float(file_out, obuf, got);
+        }
+        ++n;
+    }
+
+    if (verbose) {
+        cerr << endl;
+    }
+    
+    sf_close(file_in);
+    sf_close(file_out);
+    
+    delete[] ibuf;
+    delete[] obuf;
+}

          
A => src/BQResampler.cpp +656 -0
@@ 0,0 1,656 @@ 
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    bqresample
+
+    A small library wrapping various audio sample rate conversion
+    implementations in C++.
+
+    Copyright 2007-2021 Particular Programs Ltd.
+
+    Permission is hereby granted, free of charge, to any person
+    obtaining a copy of this software and associated documentation
+    files (the "Software"), to deal in the Software without
+    restriction, including without limitation the rights to use, copy,
+    modify, merge, publish, distribute, sublicense, and/or sell copies
+    of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be
+    included in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
+    ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+    CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+    WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+    Except as contained in this notice, the names of Chris Cannam and
+    Particular Programs Ltd shall not be used in advertising or
+    otherwise to promote the sale, use or other dealings in this
+    Software without prior written authorization.
+*/
+
+#include "BQResampler.h"
+
+#include <cmath>
+
+#include <iostream>
+
+#include <bqvec/Allocators.h>
+#include <bqvec/VectorOps.h>
+
+using std::vector;
+using std::cerr;
+using std::endl;
+using std::min;
+using std::max;
+
+namespace breakfastquay {
+
+BQResampler::BQResampler(Parameters parameters, int channels) :
+    m_qparams(parameters.quality),
+    m_dynamism(parameters.dynamism),
+    m_ratio_change(parameters.ratioChange),
+    m_debug_level(parameters.debugLevel),
+    m_initial_rate(parameters.referenceSampleRate),
+    m_channels(channels),
+    m_fade_count(0),
+    m_initialised(false)
+{
+    if (m_debug_level > 0) {
+        cerr << "BQResampler::BQResampler: "
+             << (m_dynamism == RatioOftenChanging ? "often-changing" : "mostly-fixed")
+             << ", "
+             << (m_ratio_change == SmoothRatioChange ? "smooth" : "sudden")
+             << " ratio changes, ref " << m_initial_rate << " Hz" << endl;
+    }
+    
+    if (m_dynamism == RatioOftenChanging) {
+        m_proto_length = m_qparams.proto_p * m_qparams.p_multiple + 1;
+        if (m_debug_level > 0) {
+            cerr << "BQResampler: creating prototype filter of length "
+                 << m_proto_length << endl;
+        }
+        m_prototype = make_filter(m_proto_length, m_qparams.proto_p);
+        m_prototype.push_back(0.0); // interpolate without fear
+    }
+
+    int phase_reserve = 2 * int(round(m_initial_rate));
+    int buffer_reserve = 1000 * m_channels;
+    m_state_a.phase_info.reserve(phase_reserve);
+    m_state_a.buffer.reserve(buffer_reserve);
+
+    if (m_dynamism == RatioOftenChanging) {
+        m_state_b.phase_info.reserve(phase_reserve);
+        m_state_b.buffer.reserve(buffer_reserve);
+    }
+
+    m_s = &m_state_a;
+    m_fade = &m_state_b;
+}
+
+BQResampler::BQResampler(const BQResampler &other) :
+    m_qparams(other.m_qparams),
+    m_dynamism(other.m_dynamism),
+    m_ratio_change(other.m_ratio_change),
+    m_debug_level(other.m_debug_level),
+    m_initial_rate(other.m_initial_rate),
+    m_channels(other.m_channels),
+    m_state_a(other.m_state_a),
+    m_state_b(other.m_state_b),
+    m_fade_count(other.m_fade_count),
+    m_prototype(other.m_prototype),
+    m_proto_length(other.m_proto_length),
+    m_initialised(other.m_initialised)
+{
+    if (other.m_s == &(other.m_state_a)) {
+        m_s = &m_state_a;
+        m_fade = &m_state_b;
+    } else {
+        m_s = &m_state_b;
+        m_fade = &m_state_a;
+    }
+}
+
+void
+BQResampler::reset()
+{
+    m_initialised = false;
+    m_fade_count = 0;
+}
+
+BQResampler::QualityParams::QualityParams(Quality q)
+{
+    switch (q) {
+    case Fastest:
+        p_multiple = 12;
+        proto_p = 160;
+        k_snr = 70.0;
+        k_transition = 0.2;
+        cut = 0.9;
+        break;
+    case FastestTolerable:
+        p_multiple = 62;
+        proto_p = 160;
+        k_snr = 90.0;
+        k_transition = 0.05;
+        cut = 0.975;
+        break;
+    case Best:
+        p_multiple = 122;
+        proto_p = 800;
+        k_snr = 100.0;
+        k_transition = 0.01;
+        cut = 0.995;
+        break;
+    }
+}
+    
+int
+BQResampler::resampleInterleaved(float *const out,
+                                 int outspace,
+                                 const float *const in,
+                                 int incount,
+                                 double ratio,
+                                 bool final) {
+
+    int fade_length = round(m_initial_rate / 1000.0);
+    if (fade_length < 6) {
+        fade_length = 6;
+    }
+    int max_fade = min(outspace, int(floor(incount * ratio))) / 2;
+    if (fade_length > max_fade) {
+        fade_length = max_fade;
+    }
+        
+    if (!m_initialised) {
+        state_for_ratio(*m_s, ratio, *m_fade);
+        m_initialised = true;
+    } else if (ratio != m_s->parameters.ratio) {
+        state *tmp = m_fade;
+        m_fade = m_s;
+        m_s = tmp;
+        state_for_ratio(*m_s, ratio, *m_fade);
+        if (m_ratio_change == SmoothRatioChange) {
+            if (m_debug_level > 0) {
+                cerr << "BQResampler: ratio changed, beginning fade of length "
+                     << fade_length << endl;
+            }
+            m_fade_count = fade_length;
+        }
+    }
+
+    int i = 0, o = 0;
+    int bufsize = m_s->buffer.size();
+
+    int incount_samples = incount * m_channels;
+    int outspace_samples = outspace * m_channels;
+    
+    while (o < outspace_samples) {
+        while (i < incount_samples && m_s->fill < bufsize) {
+            m_s->buffer[m_s->fill++] = in[i++];
+        }
+        if (m_s->fill == bufsize) {
+            out[o++] = reconstruct_one(m_s);
+        } else if (final && m_s->fill > m_s->centre) {
+            out[o++] = reconstruct_one(m_s);
+        } else if (final && m_s->fill == m_s->centre &&
+                   m_s->current_phase != m_s->initial_phase) {
+            out[o++] = reconstruct_one(m_s);
+        } else {
+            break;
+        }
+    }
+
+    int fbufsize = m_fade->buffer.size();
+    int fi = 0, fo = 0;
+    while (fo < o && m_fade_count > 0) {
+        while (fi < incount_samples && m_fade->fill < fbufsize) {
+            m_fade->buffer[m_fade->fill++] = in[fi++];
+        }
+        if (m_fade->fill == fbufsize) {
+            double r = reconstruct_one(m_fade);
+            double fadeWith = out[fo];
+            double extent = double(m_fade_count - 1) / double(fade_length);
+            double mixture = 0.5 * (1.0 - cos(M_PI * extent));
+            double mixed = r * mixture + fadeWith * (1.0 - mixture);
+            out[fo] = mixed;
+            ++fo;
+            if (m_fade->current_channel == 0) {
+                --m_fade_count;
+            }
+        } else {
+            break;
+        }
+    }
+        
+    return o / m_channels;
+}
+
+double
+BQResampler::getEffectiveRatio(double ratio) const {
+    if (m_initialised && ratio == m_s->parameters.ratio) {
+        return m_s->parameters.effective;
+    } else {
+        return pick_params(ratio).effective;
+    }
+}
+    
+int
+BQResampler::gcd(int a, int b) const
+{
+    int c = a % b;
+    if (c == 0) return b;
+    else return gcd(b, c);
+}
+
+double
+BQResampler::bessel0(double x) const
+{
+    static double facsquared[] = {
+        0.0, 1.0, 4.0, 36.0,
+        576.0, 14400.0, 518400.0, 25401600.0,
+        1625702400.0, 131681894400.0, 1.316818944E13, 1.59335092224E15,
+        2.29442532803E17, 3.87757880436E19, 7.60005445655E21,
+        1.71001225272E24, 4.37763136697E26, 1.26513546506E29,
+        4.09903890678E31, 1.47975304535E34
+    };
+    static int nterms = sizeof(facsquared) / sizeof(facsquared[0]);
+    double b = 1.0;
+    for (int n = 1; n < nterms; ++n) {
+        double ff = facsquared[n];
+        double term = pow(x / 2.0, n * 2.0) / ff;
+        b += term;
+    }
+    return b;
+}
+
+vector<double>
+BQResampler::kaiser(double beta, int len) const
+{
+    double denominator = bessel0(beta);
+    int half = (len % 2 == 0 ? len/2 : (len+1)/2);
+    vector<double> v(len, 0.0);
+    for (int n = 0; n < half; ++n) {
+        double k = (2.0 * n) / (len-1) - 1.0;
+        v[n] = bessel0 (beta * sqrt(1.0 - k*k)) / denominator;
+    }
+    for (int n = half; n < len; ++n) {
+        v[n] = v[len-1 - n];
+    }
+    return v;
+}
+
+void
+BQResampler::kaiser_params(double attenuation,
+                           double transition,
+                           double &beta,
+                           int &len) const
+{
+    if (attenuation > 21.0) {
+        len = 1 + ceil((attenuation - 7.95) / (2.285 * transition));
+    } else {
+        len = 1 + ceil(5.79 / transition);
+    }
+    beta = 0.0;
+    if (attenuation > 50.0) {
+        beta = 0.1102 * (attenuation - 8.7);
+    } else if (attenuation > 21.0) {
+        beta = 0.5842 * (pow (attenuation - 21.0, 0.4)) +
+            0.07886 * (attenuation - 21.0);
+    }
+}
+
+vector<double>
+BQResampler::kaiser_for(double attenuation,
+                        double transition,
+                        int minlen,
+                        int maxlen) const
+{
+    double beta;
+    int m;
+    kaiser_params(attenuation, transition, beta, m);
+    int mb = m;
+    if (maxlen > 0 && mb > maxlen - 1) {
+        mb = maxlen - 1;
+    } else if (minlen > 0 && mb < minlen) {
+        mb = minlen;
+    }
+    if (mb % 2 == 0) ++mb;
+    if (m_debug_level > 0) {
+        cerr << "BQResampler: window attenuation " << attenuation
+             << ", transition " << transition
+             << " -> length " << m << " adjusted to " << mb
+             << ", beta " << beta << endl;
+    }
+    return kaiser(beta, mb);
+}
+    
+void
+BQResampler::sinc_multiply(double peak_to_zero, vector<double> &buf) const
+{
+    int len = int(buf.size());
+    if (len < 2) return;
+
+    int left = len / 2;
+    int right = (len + 1) / 2;
+    double m = M_PI / peak_to_zero;
+
+    for (int i = 1; i <= right; ++i) {
+        double x = i * m;
+        double sinc = sin(x) / x;
+        if (i <= left) {
+            buf[left - i] *= sinc;
+        }
+        if (i < right) {
+            buf[i + left] *= sinc;
+        }
+    }
+}
+
+BQResampler::params
+BQResampler::fill_params(double ratio, int num, int denom) const
+{
+    params p;
+    int g = gcd (num, denom);
+    p.ratio = ratio;
+    p.numerator = num / g;
+    p.denominator = denom / g;
+    p.effective = double(p.numerator) / double(p.denominator);
+    p.peak_to_zero = max(p.denominator, p.numerator);
+    p.peak_to_zero /= m_qparams.cut;
+    p.scale = double(p.numerator) / double(p.peak_to_zero);
+
+    if (m_debug_level > 0) {
+        cerr << "BQResampler: ratio " << p.ratio
+             << " -> fraction " << p.numerator << "/" << p.denominator
+             << " with error " << p.effective - p.ratio
+             << endl;
+        cerr << "BQResampler: peak-to-zero " << p.peak_to_zero
+             << ", scale " << p.scale
+             << endl;
+    }
+    
+    return p;
+}
+    
+BQResampler::params
+BQResampler::pick_params(double ratio) const
+{
+    // Farey algorithm, see
+    // https://www.johndcook.com/blog/2010/10/20/best-rational-approximation/
+    int max_denom = 192000;
+    double a = 0.0, b = 1.0, c = 1.0, d = 0.0;
+    double pa = a, pb = b, pc = c, pd = d;
+    double eps = 1e-9;
+    while (b <= max_denom && d <= max_denom) {
+        double mediant = (a + c) / (b + d);
+        if (fabs(ratio - mediant) < eps) {
+            if (b + d <= max_denom) {
+                return fill_params(ratio, a + c, b + d);
+            } else if (d > b) {
+                return fill_params(ratio, c, d);
+            } else {
+                return fill_params(ratio, a, b);
+            }
+        }
+        if (ratio > mediant) {
+            pa = a; pb = b;
+            a += c; b += d;
+        } else {
+            pc = c; pd = d;
+            c += a; d += b;
+        }
+    }
+    if (fabs(ratio - (pc / pd)) < fabs(ratio - (pa / pb))) {
+        return fill_params(ratio, pc, pd);
+    } else {
+        return fill_params(ratio, pa, pb);
+    }
+}
+
+void
+BQResampler::phase_data_for(vector<BQResampler::phase_rec> &target_phase_data,
+                            floatbuf &target_phase_sorted_filter,
+                            int filter_length,
+                            const vector<double> *filter,
+                            int initial_phase,
+                            int input_spacing,
+                            int output_spacing) const
+{
+    target_phase_data.clear();
+    target_phase_data.reserve(input_spacing);
+        
+    for (int p = 0; p < input_spacing; ++p) {
+        int next_phase = p - output_spacing;
+        while (next_phase < 0) next_phase += input_spacing;
+        next_phase %= input_spacing;
+        double dspace = double(input_spacing);
+        int zip_length = ceil(double(filter_length - p) / dspace);
+        int drop = ceil(double(max(0, output_spacing - p)) / dspace);
+        phase_rec phase;
+        phase.next_phase = next_phase;
+        phase.drop = drop;
+        phase.length = zip_length;
+        phase.start_index = 0; // we fill this in below if needed
+        target_phase_data.push_back(phase);
+    }
+
+    if (m_dynamism == RatioMostlyFixed) {
+        if (!filter) throw std::logic_error("filter required at phase_data_for in RatioMostlyFixed mode");
+        target_phase_sorted_filter.clear();
+        target_phase_sorted_filter.reserve(filter_length);
+        for (int p = initial_phase; ; ) {
+            phase_rec &phase = target_phase_data[p];
+            phase.start_index = target_phase_sorted_filter.size();
+            for (int i = 0; i < phase.length; ++i) {
+                target_phase_sorted_filter.push_back
+                    ((*filter)[i * input_spacing + p]);
+            }
+            p = phase.next_phase;
+            if (p == initial_phase) {
+                break;
+            }
+        }
+    }
+}
+
+vector<double>
+BQResampler::make_filter(int filter_length, double peak_to_zero) const
+{
+    vector<double> filter;
+    filter.reserve(filter_length);
+
+    vector<double> kaiser = kaiser_for(m_qparams.k_snr, m_qparams.k_transition,
+                                       1, filter_length);
+    int k_length = kaiser.size();
+
+    if (k_length == filter_length) {
+        sinc_multiply(peak_to_zero, kaiser);
+        return kaiser;
+    } else {
+        kaiser.push_back(0.0);
+        double m = double(k_length - 1) / double(filter_length - 1);
+        for (int i = 0; i < filter_length; ++i) {
+            double ix = i * m;
+            int iix = floor(ix);
+            double remainder = ix - iix;
+            double value = 0.0;
+            value += kaiser[iix] * (1.0 - remainder);
+            value += kaiser[iix+1] * remainder;
+            filter.push_back(value);
+        }
+        sinc_multiply(peak_to_zero, filter);
+        return filter;
+    }
+}
+
+void
+BQResampler::state_for_ratio(BQResampler::state &target_state,
+                             double ratio,
+                             const BQResampler::state &BQ_R__ prev_state) const
+{
+    params parameters = pick_params(ratio);
+    target_state.parameters = parameters;
+    
+    target_state.filter_length =
+        int(parameters.peak_to_zero * m_qparams.p_multiple + 1);
+
+    if (target_state.filter_length % 2 == 0) {
+        ++target_state.filter_length;
+    }
+
+    int half_length = target_state.filter_length / 2; // nb length is odd
+    int input_spacing = parameters.numerator;
+    int initial_phase = half_length % input_spacing;
+
+    target_state.initial_phase = initial_phase;
+    target_state.current_phase = initial_phase;
+
+    if (m_dynamism == RatioMostlyFixed) {
+        
+        if (m_debug_level > 0) {
+            cerr << "BQResampler: creating filter of length "
+                 << target_state.filter_length << endl;
+        }
+
+        vector<double> filter =
+            make_filter(target_state.filter_length, parameters.peak_to_zero);
+
+        phase_data_for(target_state.phase_info,
+                       target_state.phase_sorted_filter,
+                       target_state.filter_length, &filter,
+                       target_state.initial_phase,
+                       input_spacing,
+                       parameters.denominator);
+    } else {
+        phase_data_for(target_state.phase_info,
+                       target_state.phase_sorted_filter,
+                       target_state.filter_length, 0,
+                       target_state.initial_phase,
+                       input_spacing,
+                       parameters.denominator);
+    }
+
+    int buffer_left = half_length / input_spacing;
+    int buffer_right = buffer_left + 1;
+
+    int buffer_length = buffer_left + buffer_right;
+    buffer_length = max(buffer_length,
+                        int(prev_state.buffer.size() / m_channels));
+
+    target_state.centre = buffer_length / 2;
+    target_state.left = target_state.centre - buffer_left;
+    target_state.fill = target_state.centre;
+
+    buffer_length *= m_channels;
+    target_state.centre *= m_channels;
+    target_state.left *= m_channels;
+    target_state.fill *= m_channels;
+    
+    int n_phases = int(target_state.phase_info.size());
+
+    if (m_debug_level > 0) {
+        cerr << "BQResampler: " << m_channels << " channel(s) interleaved"
+             << ", buffer left " << buffer_left
+             << ", right " << buffer_right
+             << ", total " << buffer_length << endl;
+    
+        cerr << "BQResampler: input spacing " << input_spacing
+             << ", output spacing " << parameters.denominator
+             << ", initial phase " << initial_phase
+             << " of " << n_phases << endl;
+    }
+
+    if (prev_state.buffer.size() > 0) {
+        if (int(prev_state.buffer.size()) == buffer_length) {
+            target_state.buffer = prev_state.buffer;
+            target_state.fill = prev_state.fill;
+        } else {
+            target_state.buffer = floatbuf(buffer_length, 0.0);
+            for (int i = 0; i < prev_state.fill; ++i) {
+                int offset = i - prev_state.centre;
+                int new_ix = offset + target_state.centre;
+                if (new_ix >= 0 && new_ix < buffer_length) {
+                    target_state.buffer[new_ix] = prev_state.buffer[i];
+                    target_state.fill = new_ix + 1;
+                }
+            }
+        }
+
+        int phases_then = int(prev_state.phase_info.size());
+        double distance_through =
+            double(prev_state.current_phase) / double(phases_then);
+        target_state.current_phase = round(n_phases * distance_through);
+        if (target_state.current_phase >= n_phases) {
+            target_state.current_phase = n_phases - 1;
+        }
+    } else {
+        target_state.buffer = floatbuf(buffer_length, 0.0);
+    }
+}
+
+double
+BQResampler::reconstruct_one(state *s) const
+{
+    const phase_rec &pr = s->phase_info[s->current_phase];
+    int phase_length = pr.length;
+    double result = 0.0;
+
+    int dot_length =
+        min(phase_length,
+            (int(s->buffer.size()) - s->left) / m_channels);
+
+    if (m_dynamism == RatioMostlyFixed) {
+        int phase_start = pr.start_index;
+        if (m_channels == 1) {
+            result = v_multiply_and_sum
+                (s->phase_sorted_filter.data() + phase_start,
+                 s->buffer.data() + s->left,
+                 dot_length);
+        } else {
+            for (int i = 0; i < dot_length; ++i) {
+                result +=
+                    s->phase_sorted_filter[phase_start + i] *
+                    s->buffer[s->left + i * m_channels + s->current_channel];
+            }
+        }
+    } else {
+        double m = double(m_proto_length - 1) / double(s->filter_length - 1);
+        for (int i = 0; i < dot_length; ++i) {
+            double sample =
+                s->buffer[s->left + i * m_channels + s->current_channel];
+            int filter_index = i * s->parameters.numerator + s->current_phase;
+            double proto_index = m * filter_index;
+            int iix = floor(proto_index);
+            double remainder = proto_index - iix;
+            double filter_value = m_prototype[iix] * (1.0 - remainder);
+            filter_value += m_prototype[iix+1] * remainder;
+            result += filter_value * sample;
+        }
+    }
+
+    s->current_channel = (s->current_channel + 1) % m_channels;
+    
+    if (s->current_channel == 0) {
+
+        if (pr.drop > 0) {
+            int drop = pr.drop * m_channels;
+            v_move(s->buffer.data(), s->buffer.data() + drop,
+                   int(s->buffer.size()) - drop);
+            for (int i = 1; i <= drop; ++i) {
+                s->buffer[s->buffer.size() - i] = 0.0;
+            }
+            s->fill -= drop;
+        }
+
+        s->current_phase = pr.next_phase;
+    }
+    
+    return result * s->parameters.scale;
+}
+
+}

          
A => src/BQResampler.h +179 -0
@@ 0,0 1,179 @@ 
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    bqresample
+
+    A small library wrapping various audio sample rate conversion
+    implementations in C++.
+
+    Copyright 2007-2021 Particular Programs Ltd.
+
+    Permission is hereby granted, free of charge, to any person
+    obtaining a copy of this software and associated documentation
+    files (the "Software"), to deal in the Software without
+    restriction, including without limitation the rights to use, copy,
+    modify, merge, publish, distribute, sublicense, and/or sell copies
+    of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be
+    included in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
+    ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+    CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+    WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+    Except as contained in this notice, the names of Chris Cannam and
+    Particular Programs Ltd shall not be used in advertising or
+    otherwise to promote the sale, use or other dealings in this
+    Software without prior written authorization.
+*/
+
+#ifndef BQ_BQRESAMPLER_H
+#define BQ_BQRESAMPLER_H
+
+#include <vector>
+
+#include <bqvec/Allocators.h>
+#include <bqvec/Restrict.h>
+
+namespace breakfastquay {
+
+class BQResampler
+{
+public:
+    enum Quality { Best, FastestTolerable, Fastest };
+    enum Dynamism { RatioOftenChanging, RatioMostlyFixed };
+    enum RatioChange { SmoothRatioChange, SuddenRatioChange };
+    
+    struct Parameters {
+        Quality quality;
+        Dynamism dynamism; 
+        RatioChange ratioChange;
+        double referenceSampleRate;
+        int debugLevel;
+
+        Parameters() :
+            quality(FastestTolerable),
+            dynamism(RatioMostlyFixed),
+            ratioChange(SmoothRatioChange),
+            referenceSampleRate(44100),
+            debugLevel(0) { }
+    };
+
+    BQResampler(Parameters parameters, int channels);
+    BQResampler(const BQResampler &);
+
+    int resampleInterleaved(float *const out, int outspace,
+                            const float *const in, int incount,
+                            double ratio, bool final);
+
+    double getEffectiveRatio(double ratio) const;
+    
+    void reset();
+
+private:
+    struct QualityParams {
+        int p_multiple;
+        int proto_p;
+        double k_snr;
+        double k_transition;
+        double cut;
+        QualityParams(Quality);
+    };
+
+    const QualityParams m_qparams;
+    const Dynamism m_dynamism;
+    const RatioChange m_ratio_change;
+    const int m_debug_level;
+    const double m_initial_rate;
+    const int m_channels;
+
+    struct params {
+        double ratio;
+        int numerator;
+        int denominator;
+        double effective;
+        double peak_to_zero;
+        double scale;
+        params() : ratio(1.0), numerator(1), denominator(1),
+                   effective(1.0), peak_to_zero(0), scale(1.0) { }
+    };
+
+    struct phase_rec {
+        int next_phase;
+        int length;
+        int start_index;
+        int drop;
+        phase_rec() : next_phase(0), length(0), start_index(0), drop(0) { }
+    };
+
+    typedef std::vector<float, breakfastquay::StlAllocator<float> > floatbuf;
+    
+    struct state {
+        params parameters;
+        int initial_phase;
+        int current_phase;
+        int current_channel;
+        int filter_length;
+        std::vector<phase_rec> phase_info;
+        floatbuf phase_sorted_filter;
+        floatbuf buffer;
+        int left;
+        int centre;
+        int fill;
+        state() : initial_phase(0), current_phase(0), current_channel(0),
+                  filter_length(0), left(0), centre(0), fill(0) { }
+    };
+
+    state m_state_a;
+    state m_state_b;
+
+    state *m_s;        // points at either m_state_a or m_state_b
+    state *m_fade;     // whichever one m_s does not point to
+    
+    int m_fade_count;
+    
+    std::vector<double> m_prototype;
+    int m_proto_length;
+    bool m_initialised;
+
+    int gcd(int a, int b) const;
+    double bessel0(double x) const;
+    std::vector<double> kaiser(double beta, int len) const;
+    void kaiser_params(double attenuation, double transition,
+                       double &beta, int &len) const;
+    std::vector<double> kaiser_for(double attenuation, double transition,
+                                   int minlen, int maxlen) const;
+    void sinc_multiply(double peak_to_zero, std::vector<double> &buf) const;
+
+    params fill_params(double ratio, int num, int denom) const;
+    params pick_params(double ratio) const;
+
+    std::vector<double> make_filter(int filter_length,
+                                    double peak_to_zero) const;
+    
+    void phase_data_for(std::vector<phase_rec> &target_phase_data,
+                        floatbuf &target_phase_sorted_filter,
+                        int filter_length,
+                        const std::vector<double> *filter,
+                        int initial_phase,
+                        int input_spacing,
+                        int output_spacing) const;
+    
+    void state_for_ratio(state &target_state,
+                         double ratio,
+                         const state &BQ_R__ prev_state) const;
+    
+    double reconstruct_one(state *s) const;
+
+    BQResampler &operator=(const BQResampler &); // not provided
+};
+
+}
+
+#endif

          
M src/Resampler.cpp +212 -16
@@ 69,15 69,21 @@ 
 #include "../speex/speex_resampler.h"
 #endif
 
+#ifdef USE_BQRESAMPLER
+#include "BQResampler.h"
+#endif
+
 #ifndef HAVE_IPP
 #ifndef HAVE_LIBSAMPLERATE
 #ifndef HAVE_LIBRESAMPLE
 #ifndef USE_SPEEX
+#ifndef USE_BQRESAMPLER
 #error No resampler implementation selected!
 #endif
 #endif
 #endif
 #endif
+#endif
 
 using namespace std;
 

          
@@ 103,6 109,7 @@ public:
                                     bool final) = 0;
 
     virtual int getChannelCount() const = 0;
+    virtual double getEffectiveRatio(double ratio) const = 0;
 
     virtual void reset() = 0;
 };

          
@@ 114,7 121,8 @@ namespace Resamplers {
 class D_IPP : public Resampler::Impl
 {
 public:
-    D_IPP(Resampler::Quality quality, int channels, double initialSampleRate,
+    D_IPP(Resampler::Quality quality, Resampler::RatioChange,
+          int channels, double initialSampleRate,
           int maxBufferSize, int debugLevel);
     ~D_IPP();
 

          
@@ 133,6 141,7 @@ public:
                             bool final = false);
 
     int getChannelCount() const { return m_channels; }
+    double getEffectiveRatio(double ratio) const { return ratio; }
 
     void reset();
 

          
@@ 159,6 168,7 @@ protected:
 };
 
 D_IPP::D_IPP(Resampler::Quality /* quality */,
+             Resampler::RatioChange /* ratioChange */,
              int channels, double initialSampleRate,
              int maxBufferSize, int debugLevel) :
     m_state(0),

          
@@ 167,7 177,7 @@ D_IPP::D_IPP(Resampler::Quality /* quali
     m_debugLevel(debugLevel)
 {
     if (m_debugLevel > 0) {
-        cerr << "Resampler::Resampler: using IPP implementation" << endl;
+        cerr << "Resampler::Resampler: using implementation: IPP" << endl;
     }
 
     m_window = 32;

          
@@ 543,7 553,8 @@ D_IPP::reset()
 class D_SRC : public Resampler::Impl
 {
 public:
-    D_SRC(Resampler::Quality quality, int channels, double initialSampleRate,
+    D_SRC(Resampler::Quality quality, Resampler::RatioChange ratioChange,
+          int channels, double initialSampleRate,
           int maxBufferSize, int m_debugLevel);
     ~D_SRC();
 

          
@@ 562,6 573,7 @@ public:
                             bool final = false);
 
     int getChannelCount() const { return m_channels; }
+    double getEffectiveRatio(double ratio) const { return ratio; }
 
     void reset();
 

          
@@ 574,11 586,12 @@ protected:
     int m_ioutsize;
     double m_prevRatio;
     bool m_ratioUnset;
+    bool m_smoothRatios;
     int m_debugLevel;
 };
 
-D_SRC::D_SRC(Resampler::Quality quality, int channels, double,
-             int maxBufferSize, int debugLevel) :
+D_SRC::D_SRC(Resampler::Quality quality, Resampler::RatioChange ratioChange,
+             int channels, double, int maxBufferSize, int debugLevel) :
     m_src(0),
     m_iin(0),
     m_iout(0),

          
@@ 587,10 600,11 @@ D_SRC::D_SRC(Resampler::Quality quality,
     m_ioutsize(0),
     m_prevRatio(1.0),
     m_ratioUnset(true),
+    m_smoothRatios(ratioChange == Resampler::SmoothRatioChange),
     m_debugLevel(debugLevel)
 {
     if (m_debugLevel > 0) {
-        cerr << "Resampler::Resampler: using libsamplerate implementation"
+        cerr << "Resampler::Resampler: using implementation: libsamplerate"
              << endl;
     }
 

          
@@ 692,7 706,7 @@ D_SRC::resampleInterleaved(float *const 
         outcount = int(ceil(incount * ratio) + 5);
     }
 
-    if (m_ratioUnset) {
+    if (m_ratioUnset || !m_smoothRatios) {
 
         // The first time we set a ratio, we want to do it directly
         src_set_ratio(m_src, ratio);

          
@@ 764,7 778,8 @@ D_SRC::reset()
 class D_Resample : public Resampler::Impl
 {
 public:
-    D_Resample(Resampler::Quality quality, int channels, double initialSampleRate,
+    D_Resample(Resampler::Quality quality, Resampler::RatioChange,
+               int channels, double initialSampleRate,
                int maxBufferSize, int m_debugLevel);
     ~D_Resample();
 

          
@@ 783,6 798,7 @@ public:
                             bool final);
 
     int getChannelCount() const { return m_channels; }
+    double getEffectiveRatio(double ratio) const { return ratio; }
 
     void reset();
 

          
@@ 808,7 824,7 @@ D_Resample::D_Resample(Resampler::Qualit
     m_debugLevel(debugLevel)
 {
     if (m_debugLevel > 0) {
-        cerr << "Resampler::Resampler: using libresample implementation"
+        cerr << "Resampler::Resampler: using implementation: libresample"
                   << endl;
     }
 

          
@@ 946,12 962,167 @@ D_Resample::reset()
 
 #endif /* HAVE_LIBRESAMPLE */
 
+#ifdef USE_BQRESAMPLER
+    
+class D_BQResampler : public Resampler::Impl
+{
+public:
+    D_BQResampler(Resampler::Parameters params, int channels);
+    ~D_BQResampler();
+
+    int resample(float *const BQ_R__ *const BQ_R__ out,
+                 int outcount,
+                 const float *const BQ_R__ *const BQ_R__ in,
+                 int incount,
+                 double ratio,
+                 bool final);
+
+    int resampleInterleaved(float *const BQ_R__ out,
+                            int outcount,
+                            const float *const BQ_R__ in,
+                            int incount,
+                            double ratio,
+                            bool final = false);
+
+    int getChannelCount() const {
+        return m_channels;
+    }
+
+    double getEffectiveRatio(double ratio) const {
+        return m_resampler->getEffectiveRatio(ratio);
+    }
+
+    void reset();
+
+protected:
+    BQResampler *m_resampler;
+    float *m_iin;
+    float *m_iout;
+    int m_channels;
+    int m_iinsize;
+    int m_ioutsize;
+    int m_debugLevel;
+};
+
+D_BQResampler::D_BQResampler(Resampler::Parameters params, int channels) :
+    m_resampler(0),
+    m_iin(0),
+    m_iout(0),
+    m_channels(channels),
+    m_iinsize(0),
+    m_ioutsize(0),
+    m_debugLevel(params.debugLevel)
+{
+    if (m_debugLevel > 0) {
+        cerr << "Resampler::Resampler: using implementation: BQResampler" << endl;
+    }
+
+    BQResampler::Parameters rparams;
+    switch (params.quality) {
+    case Resampler::Best:
+        rparams.quality = BQResampler::Best;
+        break;
+    case Resampler::FastestTolerable:
+        rparams.quality = BQResampler::FastestTolerable;
+        break;
+    case Resampler::Fastest:
+        rparams.quality = BQResampler::Fastest;
+        break;
+    }
+    switch (params.dynamism) {
+    case Resampler::RatioOftenChanging:
+        rparams.dynamism = BQResampler::RatioOftenChanging;
+        break;
+    case Resampler::RatioMostlyFixed:
+        rparams.dynamism = BQResampler::RatioMostlyFixed;
+        break;
+    }
+    switch (params.ratioChange) {
+    case Resampler::SmoothRatioChange:
+        rparams.ratioChange = BQResampler::SmoothRatioChange;
+        break;
+    case Resampler::SuddenRatioChange:
+        rparams.ratioChange = BQResampler::SuddenRatioChange;
+        break;
+    }
+    rparams.referenceSampleRate = params.initialSampleRate;
+    rparams.debugLevel = params.debugLevel;
+
+    m_resampler = new BQResampler(rparams, m_channels);
+    
+    if (params.maxBufferSize > 0 && m_channels > 1) {
+        m_iinsize = params.maxBufferSize * m_channels;
+        m_ioutsize = params.maxBufferSize * m_channels * 2;
+        m_iin = allocate<float>(m_iinsize);
+        m_iout = allocate<float>(m_ioutsize);
+    }
+}
+
+D_BQResampler::~D_BQResampler()
+{
+    delete m_resampler;
+    deallocate(m_iin);
+    deallocate(m_iout);
+}
+
+int
+D_BQResampler::resample(float *const BQ_R__ *const BQ_R__ out,
+                        int outcount,
+                        const float *const BQ_R__ *const BQ_R__ in,
+                        int incount,
+                        double ratio,
+                        bool final)
+{
+    if (m_channels == 1) {
+        return resampleInterleaved(*out, outcount, *in, incount, ratio, final);
+    }
+
+    if (incount * m_channels > m_iinsize) {
+        m_iin = reallocate<float>(m_iin, m_iinsize, incount * m_channels);
+        m_iinsize = incount * m_channels;
+    }
+    if (outcount * m_channels > m_ioutsize) {
+        m_iout = reallocate<float>(m_iout, m_ioutsize, outcount * m_channels);
+        m_ioutsize = outcount * m_channels;
+    }
+    
+    v_interleave(m_iin, in, m_channels, incount);
+    
+    int n = resampleInterleaved(m_iout, outcount, m_iin, incount, ratio, final);
+
+    v_deinterleave(out, m_iout, m_channels, n);
+
+    return n;
+}
+
+int
+D_BQResampler::resampleInterleaved(float *const BQ_R__ out,
+                                   int outcount,
+                                   const float *const BQ_R__ in,
+                                   int incount,
+                                   double ratio,
+                                   bool final)
+{
+    return m_resampler->resampleInterleaved(out, outcount,
+                                            in, incount,
+                                            ratio, final);
+}
+
+void
+D_BQResampler::reset()
+{
+    m_resampler->reset();
+}
+
+#endif /* USE_BQRESAMPLER */
+
 #ifdef USE_SPEEX
     
 class D_Speex : public Resampler::Impl
 {
 public:
-    D_Speex(Resampler::Quality quality, int channels, double initialSampleRate,
+    D_Speex(Resampler::Quality quality, Resampler::RatioChange,
+            int channels, double initialSampleRate,
             int maxBufferSize, int debugLevel);
     ~D_Speex();
 

          
@@ 970,6 1141,7 @@ public:
                             bool final = false);
 
     int getChannelCount() const { return m_channels; }
+    double getEffectiveRatio(double ratio) const { return ratio; }
 
     void reset();
 

          
@@ 991,7 1163,7 @@ protected:
                     double ratio, bool final);
 };
 
-D_Speex::D_Speex(Resampler::Quality quality,
+D_Speex::D_Speex(Resampler::Quality quality, Resampler::RatioChange,
                  int channels, double initialSampleRate,
                  int maxBufferSize, int debugLevel) :
     m_resampler(0),

          
@@ 1009,7 1181,7 @@ D_Speex::D_Speex(Resampler::Quality qual
              quality == Resampler::Fastest ? 0 : 4);
 
     if (m_debugLevel > 0) {
-        cerr << "Resampler::Resampler: using Speex implementation with q = "
+        cerr << "Resampler::Resampler: using implementation: Speex with q = "
              << q << endl;
     }
 

          
@@ 1245,6 1417,9 @@ Resampler::Resampler(Resampler::Paramete
 #ifdef HAVE_LIBRESAMPLE
         m_method = 3;
 #endif
+#ifdef USE_BQRESAMPLER
+        m_method = 4;
+#endif
 #ifdef HAVE_LIBSAMPLERATE
         m_method = 1;
 #endif

          
@@ 1260,6 1435,9 @@ Resampler::Resampler(Resampler::Paramete
 #ifdef USE_SPEEX
         m_method = 2;
 #endif
+#ifdef USE_BQRESAMPLER
+        m_method = 4;
+#endif
 #ifdef HAVE_LIBSAMPLERATE
         m_method = 1;
 #endif

          
@@ 1275,6 1453,9 @@ Resampler::Resampler(Resampler::Paramete
 #ifdef USE_SPEEX
         m_method = 2;
 #endif
+#ifdef USE_BQRESAMPLER
+        m_method = 4;
+#endif
 #ifdef HAVE_LIBSAMPLERATE
         m_method = 1;
 #endif

          
@@ 1290,7 1471,7 @@ Resampler::Resampler(Resampler::Paramete
     case 0:
 #ifdef HAVE_IPP
         d = new Resamplers::D_IPP
-            (params.quality,
+            (params.quality, params.ratioChange,
              channels,
              params.initialSampleRate, params.maxBufferSize, params.debugLevel);
 #else

          
@@ 1302,7 1483,7 @@ Resampler::Resampler(Resampler::Paramete
     case 1:
 #ifdef HAVE_LIBSAMPLERATE
         d = new Resamplers::D_SRC
-            (params.quality,
+            (params.quality, params.ratioChange,
              channels,
              params.initialSampleRate, params.maxBufferSize, params.debugLevel);
 #else

          
@@ 1314,7 1495,7 @@ Resampler::Resampler(Resampler::Paramete
     case 2:
 #ifdef USE_SPEEX
         d = new Resamplers::D_Speex
-            (params.quality,
+            (params.quality, params.ratioChange,
              channels,
              params.initialSampleRate, params.maxBufferSize, params.debugLevel);
 #else

          
@@ 1326,7 1507,7 @@ Resampler::Resampler(Resampler::Paramete
     case 3:
 #ifdef HAVE_LIBRESAMPLE
         d = new Resamplers::D_Resample
-            (params.quality,
+            (params.quality, params.ratioChange,
              channels,
              params.initialSampleRate, params.maxBufferSize, params.debugLevel);
 #else

          
@@ 1334,6 1515,15 @@ Resampler::Resampler(Resampler::Paramete
         abort();
 #endif
         break;
+
+    case 4:
+#ifdef USE_BQRESAMPLER
+        d = new Resamplers::D_BQResampler(params, channels);
+#else
+        cerr << "Resampler::Resampler: No implementation available!" << endl;
+        abort();
+#endif
+        break;
     }
 
     if (!d) {

          
@@ 1376,6 1566,12 @@ Resampler::getChannelCount() const
     return d->getChannelCount();
 }
 
+double
+Resampler::getEffectiveRatio(double ratio) const
+{
+    return d->getEffectiveRatio(ratio);
+}
+
 void
 Resampler::reset()
 {

          
A => test/e2e/e2e.cpp +149 -0
@@ 0,0 1,149 @@ 
+
+// Not a standalone source file
+
+void usage()
+{
+    cerr << "This is a test program for bqresample. Please do not try to use it in earnest." << endl;
+    cerr << "Usage: " << programName << " [-v] [-c <converter>] <infile> <outfile>" << endl;
+    cerr << "where <converter> may be 0, 1, or 2, for best, medium, or fastest respectively." << endl;
+    cerr << "The default converter is 0, best." << endl;
+    cerr << "Supply -v for verbose output." << endl;
+    exit(2);
+}
+
+int main(int argc, char **argv)
+{
+    int quality = 0;
+    int arg;
+    bool verbose = false;
+
+    for (arg = 1; arg + 2 < argc; ++arg) {
+        if (!strcmp(argv[arg], "-c")) {
+            char *e = argv[arg+1];
+            quality = strtol(argv[arg+1], &e, 10);
+            if (*e || (quality < 0) || (quality > 2)) {
+                cerr << "error: invalid converter \""
+                     << argv[arg+1] << "\" (must be 0, 1, 2)" << endl;
+                usage();
+            }
+            ++arg;
+            continue;
+        } else if (!strcmp(argv[arg], "-v")) {
+            verbose = true;
+        } else {
+            cerr << "error: unexpected option \"" << argv[arg] << "\"" << endl;
+            usage();
+        }
+    }
+
+    if (arg + 2 != argc) {
+        usage();
+    }
+
+    string infilename = argv[arg];
+    string outfilename = argv[arg+1];
+    
+    SF_INFO info_in;
+    SNDFILE *file_in = sf_open(infilename.c_str(), SFM_READ, &info_in);
+    if (!file_in) {
+        cerr << "failed to open " << infilename << endl;
+        return 1;
+    }
+
+    int channels = info_in.channels;
+
+    if (verbose) {
+        cerr << "input rate = " << info_in.samplerate << endl;
+    }
+
+    double ratio = initialRatio;
+        
+    SF_INFO info_out;
+    memset(&info_out, 0, sizeof(SF_INFO));
+    info_out.channels = channels;
+    info_out.format = info_in.format;
+    info_out.samplerate = info_in.samplerate;
+    SNDFILE *file_out = sf_open(outfilename.c_str(), SFM_WRITE, &info_out);
+    if (!file_out) {
+        cerr << "failed to open " << outfilename << endl;
+        return 1;
+    }
+
+    int ibs = 1024;
+    int obs = ceil(ibs * ratio * 10);
+    float *ibuf = new float[ibs * channels];
+    float *obuf = new float[obs * channels];
+
+    breakfastquay::Resampler::Parameters parameters;
+    switch (quality)  {
+    case 0:
+        parameters.quality = breakfastquay::Resampler::Best;
+        if (verbose) {
+            cerr << "quality = best" << endl;
+        }
+        break;
+    case 1:
+        parameters.quality = breakfastquay::Resampler::FastestTolerable;
+        if (verbose) {
+            cerr << "quality = middling" << endl;
+        }
+        break;
+    case 2:
+        parameters.quality = breakfastquay::Resampler::Fastest;
+        if (verbose) {
+            cerr << "quality = worst" << endl;
+        }
+        break;
+    }        
+
+    if (isRatioChanging()) {
+        parameters.dynamism = breakfastquay::Resampler::RatioOftenChanging;
+        parameters.ratioChange = breakfastquay::Resampler::SmoothRatioChange;
+    } else {
+        parameters.dynamism = breakfastquay::Resampler::RatioMostlyFixed;
+        parameters.ratioChange = breakfastquay::Resampler::SuddenRatioChange;
+    }
+    
+    parameters.initialSampleRate = info_in.samplerate;
+    parameters.debugLevel = (verbose ? 1 : 0);
+    breakfastquay::Resampler resampler(parameters, channels);
+
+    int n = 0;
+    while (true) {
+        int count = sf_readf_float(file_in, ibuf, ibs);
+        if (verbose) {
+            cerr << ".";
+        }
+        if (count < 0) {
+            cerr << "error: count = " << count << endl;
+            break;
+        }
+        
+        bool final = (count < ibs);
+        int got = resampler.resampleInterleaved
+            (obuf, obs, ibuf, count, ratio, final);
+        
+        if (got == 0 && final) {
+            break;
+        } else {
+            for (int i = 0; i < got; ++i) {
+                if (obuf[i] < -1.0) obuf[i] = -1.0;
+                if (obuf[i] > 1.0) obuf[i] = 1.0;
+            }
+            sf_writef_float(file_out, obuf, got);
+        }
+        
+        ratio = nextRatio(ratio);
+        ++n;
+    }
+
+    if (verbose) {
+        cerr << endl;
+    }
+    
+    sf_close(file_in);
+    sf_close(file_out);
+    
+    delete[] ibuf;
+    delete[] obuf;
+}

          
A => test/e2e/halve.cpp +21 -0
@@ 0,0 1,21 @@ 
+
+#include "bqresample/Resampler.h"
+
+#include <sndfile.h>
+#include <cstring>
+#include <cstdlib>
+#include <cmath>
+#include <iostream>
+
+using namespace std;
+
+static const string programName = "halve";
+static const double initialRatio = 0.5;
+static double nextRatio(double ratio) {
+    return ratio;
+}
+static bool isRatioChanging() {
+    return false;
+}
+
+#include "e2e.cpp"

          
A => test/e2e/oversample.cpp +21 -0
@@ 0,0 1,21 @@ 
+
+#include "bqresample/Resampler.h"
+
+#include <sndfile.h>
+#include <cstring>
+#include <cstdlib>
+#include <cmath>
+#include <iostream>
+
+using namespace std;
+
+static const string programName = "oversample";
+static const double initialRatio = 16.0;
+static double nextRatio(double ratio) {
+    return ratio;
+}
+static bool isRatioChanging() {
+    return false;
+}
+
+#include "e2e.cpp"

          
A => test/e2e/random.cpp +21 -0
@@ 0,0 1,21 @@ 
+
+#include "bqresample/Resampler.h"
+
+#include <sndfile.h>
+#include <cstring>
+#include <cstdlib>
+#include <cmath>
+#include <iostream>
+
+using namespace std;
+
+static const string programName = "random";
+static const double initialRatio = 0.5;
+static double nextRatio(double) {
+    return drand48() * 2.5 + 0.1;
+}
+static bool isRatioChanging() {
+    return true;
+}
+
+#include "e2e.cpp"

          
A => test/e2e/sweep-log.wav +0 -0

        
A => test/e2e/undulating.cpp +25 -0
@@ 0,0 1,25 @@ 
+
+#include "bqresample/Resampler.h"
+
+#include <sndfile.h>
+#include <cstring>
+#include <cstdlib>
+#include <cmath>
+#include <iostream>
+
+using namespace std;
+
+static const string programName = "undulating";
+static const double initialRatio = 0.5;
+static double nextRatio(double ratio) {
+    static double factor = 1.01;
+    ratio *= factor;
+    if (ratio > 3.0) factor = 0.99;
+    else if (ratio < 0.3) factor = 1.01;
+    return ratio;
+}
+static bool isRatioChanging() {
+    return true;
+}
+
+#include "e2e.cpp"