diff --git a/.hgignore b/.hgignore index a335526..68d2bc3 100644 --- a/.hgignore +++ b/.hgignore @@ -26,3 +26,7 @@ build build_* build-* UpgradeLog* +out-*/ +playlist-out/* +formant-out-*/ +out*.wav diff --git a/meson.build b/meson.build index f4b05f8..cce1856 100644 --- a/meson.build +++ b/meson.build @@ -83,6 +83,15 @@ lv2_sources = [ 'ladspa-lv2/libmain-lv2.cpp', ] +unit_test_sources = [ + 'src/test/TestAllocators.cpp', + 'src/test/TestFFT.cpp', + 'src/test/TestResampler.cpp', + 'src/test/TestVectorOpsComplex.cpp', + 'src/test/TestVectorOps.cpp', + 'src/test/test.cpp', +] + general_include_dirs = [ 'rubberband', 'src', @@ -100,6 +109,7 @@ fftw3_dep = dependency('fftw3', version: '>= 3.0.0', required: false) samplerate_dep = dependency('samplerate', version: '>= 0.1.8', required: false) sndfile_dep = dependency('sndfile', version: '>= 1.0.16', required: false) vamp_dep = dependency('vamp-sdk', version: '>= 2.9', required: false) +boost_unit_test_dep = dependency('boost_unit_test_framework', required: false) thread_dep = dependency('threads') have_ladspa = cpp.has_header('ladspa.h', args: extra_include_args) have_lv2 = cpp.has_header('lv2.h', args: extra_include_args) @@ -312,6 +322,15 @@ if not sndfile_dep.found() endif have_sndfile = sndfile_dep.found() +if not boost_unit_test_dep.found() + boost_unit_test_dep = cpp.find_library('boost_unit_test_framework', + dirs: get_option('extra_lib_dirs'), + has_headers: ['boost/test/unit_test.hpp'], + header_args: extra_include_args, + required: false) +endif +have_boost_unit_test = boost_unit_test_dep.found() + # General platform and compiler expectations @@ -430,6 +449,7 @@ if cpp.get_id() == 'msvc' rubberband_lv2_name = 'lv2-rubberband' rubberband_vamp_name = 'vamp-rubberband' rubberband_jni_name = 'rubberband-jni' + unit_tests_name = 'tests' else rubberband_library_name = 'rubberband' rubberband_dynamic_name = 'rubberband' @@ -438,6 +458,7 @@ else rubberband_lv2_name = 'lv2-rubberband' rubberband_vamp_name = 'vamp-rubberband' rubberband_jni_name = 'rubberband-jni' + unit_tests_name = 'tests' endif rubberband_objlib = static_library( @@ -686,6 +707,42 @@ else message('Not building command-line utility: libsndfile dependency not found') endif +if have_boost_unit_test + target_summary += { 'Unit tests': [ true, 'Name: ' + unit_tests_name ] } + message('Will build unit tests: use "meson test -C " to run them') + unit_tests = executable( + unit_tests_name, + unit_test_sources, + cpp_args: general_compile_args, + c_args: general_compile_args, + link_args: [ + arch_flags, + feature_libraries, + ], + dependencies: [ + rubberband_objlib_dep, + general_dependencies, + boost_unit_test_dep, + ], + install: false, + build_by_default: false + ) + general_test_args = [ '--log_level=message' ] + test('Allocators', + unit_tests, args: [ '--run_test=TestAllocators', general_test_args ]) + test('FFT', + unit_tests, args: [ '--run_test=TestFFT', general_test_args ]) + test('Resampler', + unit_tests, args: [ '--run_test=TestResampler', general_test_args ]) + test('VectorOps', + unit_tests, args: [ '--run_test=TestVectorOps', general_test_args ]) + test('VectorOpsComplex', + unit_tests, args: [ '--run_test=TestVectorOpsComplex', general_test_args ]) +else + target_summary += { 'Unit tests': false } + message('Not building unit tests: boost_unit_test_framework dependency not found') +endif + pkg.generate( name: 'rubberband', description: 'Audio time-stretching and pitch-shifting library', diff --git a/src/finer/Guide.h b/src/finer/Guide.h index f2fa0d9..5524404 100644 --- a/src/finer/Guide.h +++ b/src/finer/Guide.h @@ -214,17 +214,38 @@ public: guidance.phaseReset.f1 = std::max(segmentation.residualAbove, nextSegmentation.residualAbove); } - +/* double higher = snapToTrough(m_defaultHigher, troughs); if (higher > m_maxHigher) higher = m_maxHigher; double lower = snapToTrough(m_defaultLower, troughs); if (lower > m_maxLower) lower = m_maxLower; - +*/ +/* + double prevHigher = guidance.fftBands[1].f1; + double higher = snapToTrough(prevHigher, troughs, magnitudes); + if (higher < m_minHigher || higher > m_maxHigher) { + higher = snapToTrough(m_defaultHigher, troughs, magnitudes); + if (higher < m_minHigher || higher > m_maxHigher) { + higher = prevHigher; + } + } +*/ + double higher = m_defaultHigher; + + double prevLower = guidance.fftBands[0].f1; + double lower = snapToTrough(prevLower, troughs, magnitudes); + if (lower < m_minLower || lower > m_maxLower) { + lower = snapToTrough(m_defaultLower, troughs, magnitudes); + if (lower < m_minLower || lower > m_maxLower) { + lower = prevLower; + } + } + guidance.fftBands[0].f0 = 0.0; guidance.fftBands[0].f1 = lower; -// std::cout << "x:" << lower << std::endl; + std::cout << "x:" << lower << std::endl; guidance.fftBands[1].f0 = lower; guidance.fftBands[1].f1 = higher; @@ -322,8 +343,22 @@ protected: return (here > 10.e-3 && here > there * 1.4); } - double snapToTrough(double f, const int *const troughs) const { - return frequencyForBin(troughs[binForFrequency(f)]); + double snapToTrough(double f, + const int *const troughs, + const double *const magnitudes) const { +// return frequencyForBin(troughs[binForFrequency(f)]); + int bin = binForFrequency(f); + int snapped = troughs[bin]; + double sf = frequencyForBin(snapped); + std::cout << "snapToTrough: " << f << " -> bin " << bin << " -> snapped " << snapped << " -> " << sf << std::endl; + for (int i = -3; i <= 3; ++i) { + if (i == 0) std::cout << "["; + std::cout << magnitudes[bin + i]; + if (i == 0) std::cout << "]"; + std::cout << " "; + } + std::cout << std::endl; + return sf; } double betaFor(double f, double ratio) const { diff --git a/src/finer/R3StretcherImpl.cpp b/src/finer/R3StretcherImpl.cpp index 8deef82..d2b078b 100644 --- a/src/finer/R3StretcherImpl.cpp +++ b/src/finer/R3StretcherImpl.cpp @@ -654,7 +654,7 @@ R3StretcherImpl::analyseChannel(int c, int inhop, int prevInhop, int prevOuthop) cd->nextSegmentation = cd->segmenter->segment(cd->nextClassification.data()); m_troughPicker.findNearestAndNextPeaks - (classifyScale->mag.data(), 1, nullptr, + (classifyScale->mag.data(), 3, nullptr, classifyScale->troughs.data()); double instantaneousRatio = double(prevOuthop) / double(prevInhop); diff --git a/src/test/TestAllocators.cpp b/src/test/TestAllocators.cpp new file mode 100644 index 0000000..32f547e --- /dev/null +++ b/src/test/TestAllocators.cpp @@ -0,0 +1,104 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Rubber Band Library + An audio time-stretching and pitch-shifting library. + Copyright 2007-2022 Particular Programs Ltd. + + 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. See the file + COPYING included with this distribution for more information. + + Alternatively, if you have a valid commercial licence for the + Rubber Band Library obtained by agreement with the copyright + holders, you may redistribute and/or modify it under the terms + described in that licence. + + If you wish to distribute code using the Rubber Band Library + under terms other than those of the GNU General Public License, + you must obtain a valid commercial licence before doing so. +*/ + +#define BOOST_TEST_DYN_LINK +#include + +#include "../common/VectorOps.h" +#include "../common/Allocators.h" + +#include +#include + +using namespace RubberBand; + +BOOST_AUTO_TEST_SUITE(TestAllocators) + +#define COMPARE_ARRAY(a, b) \ + for (int cmp_i = 0; cmp_i < (int)(sizeof(a)/sizeof(a[0])); ++cmp_i) { \ + BOOST_CHECK_SMALL(a[cmp_i] - b[cmp_i], 1e-14); \ + } + +#define COMPARE_N(a, b, n) \ + for (int cmp_i = 0; cmp_i < n; ++cmp_i) { \ + BOOST_CHECK_SMALL(a[cmp_i] - b[cmp_i], 1e-14); \ + } + +BOOST_AUTO_TEST_CASE(alloc_dealloc) +{ + double *v = allocate(4); + v[0] = 0.1; + v[1] = 2.0; + v[2] = -0.3; + v[3] = 4.0; + double *e = allocate(4); + e[0] = -0.3; + e[1] = 4.0; + e[2] = 0.1; + e[3] = 2.0; + v_fftshift(v, 4); + COMPARE_N(v, e, 4); + deallocate(v); + deallocate(e); +} + +BOOST_AUTO_TEST_CASE(alloc_zero) +{ + double *v = allocate_and_zero(4); + BOOST_CHECK_EQUAL(v[0], 0.f); + BOOST_CHECK_EQUAL(v[1], 0.f); + BOOST_CHECK_EQUAL(v[2], 0.f); + BOOST_CHECK_EQUAL(v[3], 0.f); + deallocate(v); +} + +BOOST_AUTO_TEST_CASE(alloc_dealloc_channels) +{ + double **v = allocate_channels(2, 4); + v[0][0] = 0.1; + v[0][1] = 2.0; + v[0][2] = -0.3; + v[0][3] = 4.0; + v[1][0] = -0.3; + v[1][1] = 4.0; + v[1][2] = 0.1; + v[1][3] = 2.0; + v_fftshift(v[0], 4); + COMPARE_N(v[0], v[1], 4); + deallocate_channels(v, 2); +} + +BOOST_AUTO_TEST_CASE(stl) +{ + std::vector > v; + v.push_back(0.1); + v.push_back(2.0); + v.push_back(-0.3); + v.push_back(4.0); + double e[] = { -0.3, 4.0, 0.1, 2.0 }; + v_fftshift(v.data(), 4); + COMPARE_N(v.data(), e, 4); +} + +BOOST_AUTO_TEST_SUITE_END() + diff --git a/src/test/TestFFT.cpp b/src/test/TestFFT.cpp new file mode 100644 index 0000000..78b5df2 --- /dev/null +++ b/src/test/TestFFT.cpp @@ -0,0 +1,723 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Rubber Band Library + An audio time-stretching and pitch-shifting library. + Copyright 2007-2022 Particular Programs Ltd. + + 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. See the file + COPYING included with this distribution for more information. + + Alternatively, if you have a valid commercial licence for the + Rubber Band Library obtained by agreement with the copyright + holders, you may redistribute and/or modify it under the terms + described in that licence. + + If you wish to distribute code using the Rubber Band Library + under terms other than those of the GNU General Public License, + you must obtain a valid commercial licence before doing so. +*/ + +#define BOOST_TEST_DYN_LINK +#include + +#include "../common/FFT.h" + +#include + +#include +#include + +using namespace RubberBand; + +BOOST_AUTO_TEST_SUITE(TestFFT) + +#define DEFINE_EPS(fft) \ + float epsf = 1e-6f; \ + double eps; \ + if (fft.getSupportedPrecisions() & FFT::DoublePrecision) { \ + eps = 1e-14; \ + } else { \ + eps = epsf; \ + } \ + (void)epsf; (void)eps; + +#define USING_FFT(n) \ + FFT fft(n); \ + DEFINE_EPS(fft); + +#define COMPARE(a, b) BOOST_CHECK_SMALL(a-b, eps) +#define COMPARE_F(a, b) BOOST_CHECK_SMALL(a-b, epsf) + +#define COMPARE_ZERO(a) BOOST_CHECK_SMALL(a, eps) +#define COMPARE_ZERO_F(a) BOOST_CHECK_SMALL(a, epsf) + +#define COMPARE_ALL(a, x) \ + for (int cmp_i = 0; cmp_i < (int)(sizeof(a)/sizeof(a[0])); ++cmp_i) { \ + BOOST_CHECK_SMALL(a[cmp_i] - x, eps); \ + } +#define COMPARE_ALL_F(a, x) \ + for (int cmp_i = 0; cmp_i < (int)(sizeof(a)/sizeof(a[0])); ++cmp_i) { \ + BOOST_CHECK_SMALL(a[cmp_i] - x, epsf); \ + } +#define COMPARE_ARR(a, b, n) \ + for (int cmp_i = 0; cmp_i < n; ++cmp_i) { \ + BOOST_CHECK_SMALL(a[cmp_i] - b[cmp_i], eps); \ + } +#define COMPARE_SCALED(a, b, s) \ + for (int cmp_i = 0; cmp_i < (int)(sizeof(a)/sizeof(a[0])); ++cmp_i) { \ + BOOST_CHECK_SMALL(a[cmp_i]/s - b[cmp_i], eps); \ + } +#define COMPARE_SCALED_N(a, b, n, s) \ + for (int cmp_i = 0; cmp_i < n; ++cmp_i) { \ + BOOST_CHECK_SMALL(a[cmp_i]/s - b[cmp_i], eps); \ + } +#define COMPARE_SCALED_F(a, b, s) \ + for (int cmp_i = 0; cmp_i < (int)(sizeof(a)/sizeof(a[0])); ++cmp_i) { \ + BOOST_CHECK_SMALL(a[cmp_i]/s - b[cmp_i], epsf); \ + } + +#define ONE_IMPL_AUTO_TEST_CASE(name, impl) \ + BOOST_AUTO_TEST_CASE(name##_##impl) \ + { \ + std::set impls = FFT::getImplementations(); \ + if (impls.find(#impl) == impls.end()) return; \ + FFT::setDefaultImplementation(#impl); \ + performTest_##name(); \ + FFT::setDefaultImplementation(""); \ + } + +// If you add an implementation in FFT.cpp, add it also to +// ALL_IMPL_AUTO_TEST_CASE and all_implementations[] below + +#define ALL_IMPL_AUTO_TEST_CASE(name) \ + void performTest_##name (); \ + ONE_IMPL_AUTO_TEST_CASE(name, ipp); \ + ONE_IMPL_AUTO_TEST_CASE(name, vdsp); \ + ONE_IMPL_AUTO_TEST_CASE(name, fftw); \ + ONE_IMPL_AUTO_TEST_CASE(name, kissfft); \ + ONE_IMPL_AUTO_TEST_CASE(name, builtin); \ + ONE_IMPL_AUTO_TEST_CASE(name, dft); \ + void performTest_##name () + +std::string all_implementations[] = { + "ipp", "vdsp", "fftw", "kissfft", "builtin", "dft" +}; + +BOOST_AUTO_TEST_CASE(showImplementations) +{ + std::set impls = FFT::getImplementations(); + BOOST_TEST_MESSAGE("The following implementations are compiled in and will be tested:"); + for (int i = 0; i < int(sizeof(all_implementations)/sizeof(all_implementations[0])); ++i) { + if (impls.find(all_implementations[i]) != impls.end()) { + BOOST_TEST_MESSAGE(" +" << all_implementations[i]); + } + } + BOOST_TEST_MESSAGE("The following implementations are NOT compiled in and will not be tested:"); + for (int i = 0; i < int(sizeof(all_implementations)/sizeof(all_implementations[0])); ++i) { + if (impls.find(all_implementations[i]) == impls.end()) { + BOOST_TEST_MESSAGE(" -" << all_implementations[i]); + } + } +} + + +/* + * 1a. Simple synthetic signals, transforms to separate real/imag arrays, + * double-precision + */ + +ALL_IMPL_AUTO_TEST_CASE(dc) +{ + // DC-only signal. The DC bin is purely real + double in[] = { 1, 1, 1, 1 }; + double re[3], im[3]; + USING_FFT(4); + fft.forward(in, re, im); + COMPARE(re[0], 4.0); + COMPARE_ZERO(re[1]); + COMPARE_ZERO(re[2]); + COMPARE_ALL(im, 0.0); + double back[4]; + fft.inverse(re, im, back); + COMPARE_SCALED(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(sine) +{ + // Sine. Output is purely imaginary + double in[] = { 0, 1, 0, -1 }; + double re[3], im[3]; + USING_FFT(4); + fft.forward(in, re, im); + COMPARE_ALL(re, 0.0); + COMPARE_ZERO(im[0]); + COMPARE(im[1], -2.0); + COMPARE_ZERO(im[2]); + double back[4]; + fft.inverse(re, im, back); + COMPARE_SCALED(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(sine_8) +{ + // Longer sine. With only 4 elements, the real transform only + // needs to get the DC and Nyquist bins right for its two complex + // sub-transforms. We need a longer test to check the real + // transform is working properly. + double cospi4 = 0.5 * sqrt(2.0); + double in[] = { 0, cospi4, 1.0, cospi4, 0.0, -cospi4, -1.0, -cospi4 }; + double re[5], im[5]; + USING_FFT(8); + fft.forward(in, re, im); + COMPARE_ALL(re, 0.0); + COMPARE_ZERO(im[0]); + COMPARE(im[1], -4.0); + COMPARE_ZERO(im[2]); + COMPARE_ZERO(im[3]); + COMPARE_ZERO(im[4]); + double back[8]; + fft.inverse(re, im, back); + COMPARE_SCALED(back, in, 8); +} + +ALL_IMPL_AUTO_TEST_CASE(cosine) +{ + // Cosine. Output is purely real + double in[] = { 1, 0, -1, 0 }; + double re[3], im[3]; + USING_FFT(4); + fft.forward(in, re, im); + COMPARE_ZERO(re[0]); + COMPARE(re[1], 2.0); + COMPARE_ZERO(re[2]); + COMPARE_ALL(im, 0.0); + double back[4]; + fft.inverse(re, im, back); + COMPARE_SCALED(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(cosine_8) +{ + // Longer cosine. + double cospi4 = 0.5 * sqrt(2.0); + double in[] = { 1.0, cospi4, 0.0, -cospi4, -1.0, -cospi4, 0.0, cospi4 }; + double re[5], im[5]; + USING_FFT(8); + fft.forward(in, re, im); + COMPARE_ALL(im, 0.0); + COMPARE_ZERO(re[0]); + COMPARE(re[1], 4.0); + COMPARE_ZERO(re[2]); + COMPARE_ZERO(re[3]); + COMPARE_ZERO(re[4]); + double back[8]; + fft.inverse(re, im, back); + COMPARE_SCALED(back, in, 8); +} + +ALL_IMPL_AUTO_TEST_CASE(sineCosine) +{ + // Sine and cosine mixed + double in[] = { 0.5, 1, -0.5, -1 }; + double re[3], im[3]; + USING_FFT(4); + fft.forward(in, re, im); + COMPARE_ZERO(re[0]); + COMPARE(re[1], 1.0); + COMPARE_ZERO(re[2]); + COMPARE_ZERO(im[0]); + COMPARE(im[1], -2.0); + COMPARE_ZERO(im[2]); + double back[4]; + fft.inverse(re, im, back); + COMPARE_SCALED(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(nyquist) +{ + double in[] = { 1, -1, 1, -1 }; + double re[3], im[3]; + USING_FFT(4); + fft.forward(in, re, im); + COMPARE_ZERO(re[0]); + COMPARE_ZERO(re[1]); + COMPARE(re[2], 4.0); + COMPARE_ALL(im, 0.0); + double back[4]; + fft.inverse(re, im, back); + COMPARE_SCALED(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(dirac) +{ + double in[] = { 1, 0, 0, 0 }; + double re[3], im[3]; + USING_FFT(4); + fft.forward(in, re, im); + COMPARE(re[0], 1.0); + COMPARE(re[1], 1.0); + COMPARE(re[2], 1.0); + COMPARE_ALL(im, 0.0); + double back[4]; + fft.inverse(re, im, back); + COMPARE_SCALED(back, in, 4); +} + + +/* + * 1b. Simple synthetic signals, transforms to separate real/imag arrays, + * single-precision (i.e. single-precision version of 1a) + */ + +ALL_IMPL_AUTO_TEST_CASE(dcF) +{ + // DC-only signal. The DC bin is purely real + float in[] = { 1, 1, 1, 1 }; + float re[3], im[3]; + USING_FFT(4); + fft.forward(in, re, im); + COMPARE_F(re[0], 4.0f); + COMPARE_ZERO_F(re[1]); + COMPARE_ZERO_F(re[2]); + COMPARE_ALL_F(im, 0.0f); + float back[4]; + fft.inverse(re, im, back); + COMPARE_SCALED_F(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(sineF) +{ + // Sine. Output is purely imaginary + float in[] = { 0, 1, 0, -1 }; + float re[3], im[3]; + USING_FFT(4); + fft.forward(in, re, im); + COMPARE_ALL_F(re, 0.0f); + COMPARE_ZERO_F(im[0]); + COMPARE_F(im[1], -2.0f); + COMPARE_ZERO_F(im[2]); + float back[4]; + fft.inverse(re, im, back); + COMPARE_SCALED_F(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(cosineF) +{ + // Cosine. Output is purely real + float in[] = { 1, 0, -1, 0 }; + float re[3], im[3]; + USING_FFT(4); + fft.forward(in, re, im); + COMPARE_ZERO_F(re[0]); + COMPARE_F(re[1], 2.0f); + COMPARE_ZERO_F(re[2]); + COMPARE_ALL_F(im, 0.0f); + float back[4]; + fft.inverse(re, im, back); + COMPARE_SCALED_F(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(sineCosineF) +{ + // Sine and cosine mixed + float in[] = { 0.5, 1, -0.5, -1 }; + float re[3], im[3]; + USING_FFT(4); + fft.forward(in, re, im); + COMPARE_ZERO_F(re[0]); + COMPARE_F(re[1], 1.0f); + COMPARE_ZERO_F(re[2]); + COMPARE_ZERO_F(im[0]); + COMPARE_F(im[1], -2.0f); + COMPARE_ZERO_F(im[2]); + float back[4]; + fft.inverse(re, im, back); + COMPARE_SCALED_F(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(nyquistF) +{ + float in[] = { 1, -1, 1, -1 }; + float re[3], im[3]; + USING_FFT(4); + fft.forward(in, re, im); + COMPARE_ZERO_F(re[0]); + COMPARE_ZERO_F(re[1]); + COMPARE_F(re[2], 4.0f); + COMPARE_ALL_F(im, 0.0f); + float back[4]; + fft.inverse(re, im, back); + COMPARE_SCALED_F(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(diracF) +{ + float in[] = { 1, 0, 0, 0 }; + float re[3], im[3]; + USING_FFT(4); + fft.forward(in, re, im); + COMPARE_F(re[0], 1.0f); + COMPARE_F(re[1], 1.0f); + COMPARE_F(re[2], 1.0f); + COMPARE_ALL_F(im, 0.0f); + float back[4]; + fft.inverse(re, im, back); + COMPARE_SCALED_F(back, in, 4); +} + + +/* + * 2a. Subset of synthetic signals, testing different output formats + * (interleaved complex, polar, magnitude-only, and our weird + * cepstral thing), double-precision + */ + +ALL_IMPL_AUTO_TEST_CASE(interleaved) +{ + // Sine and cosine mixed, test output format + double in[] = { 0.5, 1, -0.5, -1 }; + double out[6]; + USING_FFT(4); + fft.forwardInterleaved(in, out); + COMPARE_ZERO(out[0]); + COMPARE_ZERO(out[1]); + COMPARE(out[2], 1.0); + COMPARE(out[3], -2.0); + COMPARE_ZERO(out[4]); + COMPARE_ZERO(out[5]); + double back[4]; + fft.inverseInterleaved(out, back); + COMPARE_SCALED(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(sinePolar) +{ + double in[] = { 0, 1, 0, -1 }; + double mag[3], phase[3]; + USING_FFT(4); + fft.forwardPolar(in, mag, phase); + COMPARE_ZERO(mag[0]); + COMPARE(mag[1], 2.0); + COMPARE_ZERO(mag[2]); + // No meaningful tests for phase[i] where mag[i]==0 (phase + // could legitimately be anything) + COMPARE(phase[1], -M_PI/2.0); + double back[4]; + fft.inversePolar(mag, phase, back); + COMPARE_SCALED(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(cosinePolar) +{ + double in[] = { 1, 0, -1, 0 }; + double mag[3], phase[3]; + USING_FFT(4); + fft.forwardPolar(in, mag, phase); + COMPARE_ZERO(mag[0]); + COMPARE(mag[1], 2.0); + COMPARE_ZERO(mag[2]); + // No meaningful tests for phase[i] where mag[i]==0 (phase + // could legitimately be anything) + COMPARE_ZERO(phase[1]); + double back[4]; + fft.inversePolar(mag, phase, back); + COMPARE_SCALED(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(magnitude) +{ + // Sine and cosine mixed + double in[] = { 0.5, 1, -0.5, -1 }; + double out[3]; + USING_FFT(4); + fft.forwardMagnitude(in, out); + COMPARE_ZERO(out[0]); + COMPARE_F(float(out[1]), sqrtf(5.0)); + COMPARE_ZERO(out[2]); +} + +ALL_IMPL_AUTO_TEST_CASE(cepstrum) +{ + double in[] = { 1, 0, 0, 0, 1, 0, 0, 0 }; + double mag[5]; + USING_FFT(8); + fft.forwardMagnitude(in, mag); + double cep[8]; + fft.inverseCepstral(mag, cep); + BOOST_CHECK_SMALL(cep[1], 1e-9); + BOOST_CHECK_SMALL(cep[2], 1e-9); + BOOST_CHECK_SMALL(cep[3], 1e-9); + BOOST_CHECK_SMALL(cep[5], 1e-9); + BOOST_CHECK_SMALL(cep[6], 1e-9); + BOOST_CHECK_SMALL(cep[7], 1e-9); + BOOST_CHECK_SMALL(-6.561181 - cep[0]/8, 0.000001); + BOOST_CHECK_SMALL( 7.254329 - cep[4]/8, 0.000001); +} + + +/* + * 2b. Subset of synthetic signals, testing different output formats + * (interleaved complex, polar, magnitude-only, and our weird + * cepstral thing), single-precision (i.e. single-precision + * version of 2a) + */ + +ALL_IMPL_AUTO_TEST_CASE(interleavedF) +{ + // Sine and cosine mixed, test output format + float in[] = { 0.5, 1, -0.5, -1 }; + float out[6]; + USING_FFT(4); + fft.forwardInterleaved(in, out); + COMPARE_ZERO_F(out[0]); + COMPARE_ZERO_F(out[1]); + COMPARE_F(out[2], 1.0f); + COMPARE_F(out[3], -2.0f); + COMPARE_ZERO_F(out[4]); + COMPARE_ZERO_F(out[5]); + float back[4]; + fft.inverseInterleaved(out, back); + COMPARE_SCALED_F(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(cosinePolarF) +{ + float in[] = { 1, 0, -1, 0 }; + float mag[3], phase[3]; + USING_FFT(4); + fft.forwardPolar(in, mag, phase); + COMPARE_ZERO_F(mag[0]); + COMPARE_F(mag[1], 2.0f); + COMPARE_ZERO_F(mag[2]); + // No meaningful tests for phase[i] where mag[i]==0 (phase + // could legitimately be anything) + COMPARE_ZERO_F(phase[1]); + float back[4]; + fft.inversePolar(mag, phase, back); + COMPARE_SCALED_F(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(sinePolarF) +{ + float in[] = { 0, 1, 0, -1 }; + float mag[3], phase[3]; + USING_FFT(4); + fft.forwardPolar(in, mag, phase); + COMPARE_ZERO_F(mag[0]); + COMPARE_F(mag[1], 2.0f); + COMPARE_ZERO_F(mag[2]); + // No meaningful tests for phase[i] where mag[i]==0 (phase + // could legitimately be anything) + COMPARE_F(phase[1], -float(M_PI)/2.0f); + float back[4]; + fft.inversePolar(mag, phase, back); + COMPARE_SCALED_F(back, in, 4); +} + +ALL_IMPL_AUTO_TEST_CASE(magnitudeF) +{ + // Sine and cosine mixed + float in[] = { 0.5, 1, -0.5, -1 }; + float out[3]; + USING_FFT(4); + fft.forwardMagnitude(in, out); + COMPARE_ZERO_F(out[0]); + COMPARE_F(float(out[1]), sqrtf(5.0f)); + COMPARE_ZERO_F(out[2]); +} + +ALL_IMPL_AUTO_TEST_CASE(cepstrumF) +{ + float in[] = { 1, 0, 0, 0, 1, 0, 0, 0 }; + float mag[5]; + USING_FFT(8); + fft.forwardMagnitude(in, mag); + float cep[8]; + fft.inverseCepstral(mag, cep); + COMPARE_ZERO_F(cep[1]); + COMPARE_ZERO_F(cep[2]); + COMPARE_ZERO_F(cep[3]); + COMPARE_ZERO_F(cep[5]); + COMPARE_ZERO_F(cep[6]); + COMPARE_ZERO_F(cep[7]); + BOOST_CHECK_SMALL(-6.561181 - cep[0]/8, 0.000001); + BOOST_CHECK_SMALL( 7.254329 - cep[4]/8, 0.000001); +} + + +/* + * 4. Bounds checking, double-precision and single-precision + */ + +ALL_IMPL_AUTO_TEST_CASE(forwardArrayBounds) +{ + double in[] = { 1, 1, -1, -1 }; + + // Initialise output bins to something recognisable, so we can + // tell if they haven't been written + double re[] = { 999, 999, 999, 999, 999 }; + double im[] = { 999, 999, 999, 999, 999 }; + + USING_FFT(4); + fft.forward(in, re+1, im+1); + + // Check we haven't overrun the output arrays + COMPARE(re[0], 999.0); + COMPARE(im[0], 999.0); + COMPARE(re[4], 999.0); + COMPARE(im[4], 999.0); +} + +ALL_IMPL_AUTO_TEST_CASE(inverseArrayBounds) +{ + // The inverse transform is only supposed to refer to the first + // N/2+1 bins and synthesise the rest rather than read them - so + // initialise the next one to some value that would mess up the + // results if it were used + double re[] = { 0, 1, 0, 456 }; + double im[] = { 0, -2, 0, 456 }; + + // Initialise output bins to something recognisable, so we can + // tell if they haven't been written + double out[] = { 999, 999, 999, 999, 999, 999 }; + + USING_FFT(4); + fft.inverse(re, im, out+1); + + // Check we haven't overrun the output arrays + COMPARE(out[0], 999.0); + COMPARE(out[5], 999.0); + + // And check the results are as we expect, i.e. that we haven't + // used the bogus final bin + COMPARE(out[1] / 4, 0.5); + COMPARE(out[2] / 4, 1.0); + COMPARE(out[3] / 4, -0.5); + COMPARE(out[4] / 4, -1.0); +} + +ALL_IMPL_AUTO_TEST_CASE(forwardArrayBoundsF) +{ + float in[] = { 1, 1, -1, -1 }; + + // Initialise output bins to something recognisable, so we can + // tell if they haven't been written + float re[] = { 999, 999, 999, 999, 999 }; + float im[] = { 999, 999, 999, 999, 999 }; + + USING_FFT(4); + fft.forward(in, re+1, im+1); + + // Check we haven't overrun the output arrays + COMPARE_F(re[0], 999.0f); + COMPARE_F(im[0], 999.0f); + COMPARE_F(re[4], 999.0f); + COMPARE_F(im[4], 999.0f); +} + +ALL_IMPL_AUTO_TEST_CASE(inverseArrayBoundsF) +{ + // The inverse transform is only supposed to refer to the first + // N/2+1 bins and synthesise the rest rather than read them - so + // initialise the next one to some value that would mess up the + // results if it were used + float re[] = { 0, 1, 0, 456 }; + float im[] = { 0, -2, 0, 456 }; + + // Initialise output bins to something recognisable, so we can + // tell if they haven't been written + float out[] = { 999, 999, 999, 999, 999, 999 }; + + USING_FFT(4); + fft.inverse(re, im, out+1); + + // Check we haven't overrun the output arrays + COMPARE_F(out[0], 999.0f); + COMPARE_F(out[5], 999.0f); + + // And check the results are as we expect, i.e. that we haven't + // used the bogus final bin + COMPARE_F(out[1] / 4.0f, 0.5f); + COMPARE_F(out[2] / 4.0f, 1.0f); + COMPARE_F(out[3] / 4.0f, -0.5f); + COMPARE_F(out[4] / 4.0f, -1.0f); +} + + +/* + * 6. Slightly longer transforms of pseudorandom data. + */ + +ALL_IMPL_AUTO_TEST_CASE(random_precalc_16) +{ + double in[] = { + -0.24392125308057722, 0.03443898163344272, 0.3448145656738877, + -0.9625837464603908, 3.366568317669671, 0.9947191221586653, + -1.5038984435999945, 1.3859898682581235, -1.1230576306688778, + -1.6757487116512024, -1.5874436867863229, -2.0794018781307155, + -0.5450152775818973, 0.7530907176983748, 1.0743170685904255, + 3.1787609811018775 + }; + double expected_re[] = { + 1.41162899482, 7.63975551593, -1.20622641052, -1.77829578443, + 3.12678465246, -2.84220463109, -7.17083743716, 0.497290409945, + -1.84690167439, + }; + double expected_im[] = { + 0.0, -4.67826048083, 8.58829211964, 4.96449646815, + 1.41626511493, -3.77219223978, 6.96219662744, 2.23138519225, + 0.0, + }; + double re[9], im[9]; + USING_FFT(16); + if (eps < 1e-11) { + eps = 1e-11; + } + fft.forward(in, re, im); + COMPARE_ARR(re, expected_re, 9); + COMPARE_ARR(im, expected_im, 9); + double back[16]; + fft.inverse(re, im, back); + COMPARE_SCALED(back, in, 16); +} + +/* This one has data from a PRNG, with a fixed seed. Must pass two + * tests: (i) same as DFT; (ii) inverse produces original input (after + * scaling) */ +ALL_IMPL_AUTO_TEST_CASE(random) +{ + const int n = 64; + double *in = new double[n]; + double *re = new double[n/2 + 1]; + double *im = new double[n/2 + 1]; + double *re_compare = new double[n/2 + 1]; + double *im_compare = new double[n/2 + 1]; + double *back = new double[n]; + srand48(0); + for (int i = 0; i < n; ++i) { + in[i] = drand48() * 4.0 - 2.0; + } + USING_FFT(n); + if (eps < 1e-11) { + eps = 1e-11; + } + fft.forward(in, re, im); + fft.inverse(re, im, back); + FFT::setDefaultImplementation("dft"); + fft.forward(in, re_compare, im_compare); + COMPARE_ARR(re, re_compare, n/2 + 1); + COMPARE_ARR(im, im_compare, n/2 + 1); + COMPARE_SCALED_N(back, in, n, n); + delete[] back; + delete[] im_compare; + delete[] re_compare; + delete[] im; + delete[] re; + delete[] in; +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/TestResampler.cpp b/src/test/TestResampler.cpp new file mode 100644 index 0000000..774c2b5 --- /dev/null +++ b/src/test/TestResampler.cpp @@ -0,0 +1,218 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Rubber Band Library + An audio time-stretching and pitch-shifting library. + Copyright 2007-2022 Particular Programs Ltd. + + 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. See the file + COPYING included with this distribution for more information. + + Alternatively, if you have a valid commercial licence for the + Rubber Band Library obtained by agreement with the copyright + holders, you may redistribute and/or modify it under the terms + described in that licence. + + If you wish to distribute code using the Rubber Band Library + under terms other than those of the GNU General Public License, + you must obtain a valid commercial licence before doing so. +*/ + +#define BOOST_TEST_DYN_LINK +#include + +#include "../common/Resampler.h" + +#include +#include +#include +#include + +using namespace std; +using namespace RubberBand; + +BOOST_AUTO_TEST_SUITE(TestResampler) + +#define LEN(a) (int(sizeof(a)/sizeof(a[0]))) + +static vector +sine(double samplerate, double frequency, int nsamples) +{ + vector v(nsamples, 0.f); + for (int i = 0; i < nsamples; ++i) { + v[i] = sin ((i * 2.0 * M_PI * frequency) / samplerate); + } + return v; +} + +#define COMPARE_N(a, b, n) \ + for (int cmp_i = 0; cmp_i < n; ++cmp_i) { \ + BOOST_CHECK_SMALL((a)[cmp_i] - (b)[cmp_i], 1e-4f); \ + } + +static const float guard_value = -999.f; + +BOOST_AUTO_TEST_CASE(interpolated_sine_1ch_interleaved) +{ + // Interpolating a sinusoid should give us a sinusoid, once we've + // dropped the first few samples + vector in = sine(8, 2, 1000); // 2Hz wave at 8Hz: [ 0, 1, 0, -1 ] etc + vector expected = sine(16, 2, 2000); + vector out(in.size() * 2 + 1, guard_value); + Resampler r(Resampler::Parameters(), 1); + int returned = r.resampleInterleaved + (out.data(), out.size(), in.data(), in.size(), 2, true); + + // because final was true, we expect to have exactly the right + // number of samples back + BOOST_CHECK_EQUAL(returned, int(in.size() * 2)); + BOOST_CHECK_NE(out[returned-1], guard_value); + BOOST_CHECK_EQUAL(out[returned], guard_value); + + // and they should match the expected data, at least in the middle + const float *outf = out.data() + 200, *expectedf = expected.data() + 200; + COMPARE_N(outf, expectedf, 600); +} + +BOOST_AUTO_TEST_CASE(interpolated_sine_1ch_noninterleaved) +{ + // Interpolating a sinusoid should give us a sinusoid, once we've + // dropped the first few samples + vector in = sine(8, 2, 1000); // 2Hz wave at 8Hz: [ 0, 1, 0, -1 ] etc + vector expected = sine(16, 2, 2000); + vector out(in.size() * 2 + 1, guard_value); + const float *in_data = in.data(); + float *out_data = out.data(); + Resampler r(Resampler::Parameters(), 1); + int returned = r.resample + (&out_data, out.size(), &in_data, in.size(), 2, true); + + // because final was true, we expect to have exactly the right + // number of samples back + BOOST_CHECK_EQUAL(returned, int(in.size() * 2)); + BOOST_CHECK_NE(out[returned-1], guard_value); + BOOST_CHECK_EQUAL(out[returned], guard_value); + + // and they should match the expected data, at least in the middle + const float *outf = out.data() + 200, *expectedf = expected.data() + 200; + COMPARE_N(outf, expectedf, 600); +} + +BOOST_AUTO_TEST_CASE(overrun_interleaved) +{ + // Check that the outcount argument is correctly used: any samples + // already in the out buffer beyond outcount*channels must be left + // untouched. We test this with short buffers (likely to be + // shorter than the resampler filter length) and longer ones, with + // resampler ratios that reduce, leave unchanged, and raise the + // sample rate, and with all quality settings. + + // Options are ordered from most normal/likely to least. + + int channels = 2; + + int lengths[] = { 6000, 6 }; + int constructionBufferSizes[] = { 0, 1000 }; + double ratios[] = { 1.0, 10.0, 0.1 }; + Resampler::Quality qualities[] = { + Resampler::FastestTolerable, Resampler::Best, Resampler::Fastest + }; + + bool failed = false; + + for (int li = 0; li < LEN(lengths); ++li) { + for (int cbi = 0; cbi < LEN(constructionBufferSizes); ++cbi) { + for (int ri = 0; ri < LEN(ratios); ++ri) { + for (int qi = 0; qi < LEN(qualities); ++qi) { + + int length = lengths[li]; + double ratio = ratios[ri]; + Resampler::Parameters parameters; + parameters.quality = qualities[qi]; + parameters.maxBufferSize = constructionBufferSizes[cbi]; + Resampler r(parameters, channels); + + float *inbuf = new float[length * channels]; + for (int i = 0; i < length; ++i) { + for (int c = 0; c < channels; ++c) { + inbuf[i*channels+c] = + sinf((i * 2.0 * M_PI * 440.0) / 44100.0); + } + } + + int outcount = int(round(length * ratio)); + outcount -= 10; + if (outcount < 1) outcount = 1; + int outbuflen = outcount + 10; + float *outbuf = new float[outbuflen * channels]; + for (int i = 0; i < outbuflen; ++i) { + for (int c = 0; c < channels; ++c) { + outbuf[i*channels+c] = guard_value; + } + } +/* + cerr << "\nTesting with length = " << length << ", ratio = " + << ratio << ", outcount = " << outcount << ", final = false" + << endl; +*/ + int returned = r.resampleInterleaved + (outbuf, outcount, inbuf, length, ratio, false); + + BOOST_CHECK_LE(returned, outcount); + + for (int i = returned; i < outbuflen; ++i) { + for (int c = 0; c < channels; ++c) { + BOOST_CHECK_EQUAL(outbuf[i*channels+c], guard_value); + if (outbuf[i*channels+c] != guard_value) { + failed = true; + } + } + } + + if (failed) { + cerr << "Test failed, abandoning remaining loop cycles" + << endl; + break; + } +/* + cerr << "\nContinuing with length = " << length << ", ratio = " + << ratio << ", outcount = " << outcount << ", final = true" + << endl; +*/ + returned = r.resampleInterleaved + (outbuf, outcount, inbuf, length, ratio, true); + + BOOST_CHECK_LE(returned, outcount); + + for (int i = returned; i < outbuflen; ++i) { + for (int c = 0; c < channels; ++c) { + BOOST_CHECK_EQUAL(outbuf[i*channels+c], guard_value); + if (outbuf[i*channels+c] != guard_value) { + failed = true; + } + } + } + + delete[] outbuf; + delete[] inbuf; + + if (failed) { + cerr << "Test failed, abandoning remaining loop cycles" + << endl; + break; + } + } + + if (failed) break; + } + if (failed) break; + } + if (failed) break; + } +} + +BOOST_AUTO_TEST_SUITE_END() + diff --git a/src/test/TestVectorOps.cpp b/src/test/TestVectorOps.cpp new file mode 100644 index 0000000..615a89f --- /dev/null +++ b/src/test/TestVectorOps.cpp @@ -0,0 +1,249 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Rubber Band Library + An audio time-stretching and pitch-shifting library. + Copyright 2007-2022 Particular Programs Ltd. + + 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. See the file + COPYING included with this distribution for more information. + + Alternatively, if you have a valid commercial licence for the + Rubber Band Library obtained by agreement with the copyright + holders, you may redistribute and/or modify it under the terms + described in that licence. + + If you wish to distribute code using the Rubber Band Library + under terms other than those of the GNU General Public License, + you must obtain a valid commercial licence before doing so. +*/ + +#define BOOST_TEST_DYN_LINK +#include + +#include "../common/VectorOps.h" + +#include +#include + +using namespace RubberBand; + +BOOST_AUTO_TEST_SUITE(TestVectorOps) + +#define COMPARE_ARRAY(a, b) \ + for (int cmp_i = 0; cmp_i < (int)(sizeof(a)/sizeof(a[0])); ++cmp_i) { \ + BOOST_CHECK_SMALL(a[cmp_i] - b[cmp_i], 1e-14); \ + } + +#define COMPARE_N(a, b, n) \ + for (int cmp_i = 0; cmp_i < n; ++cmp_i) { \ + BOOST_CHECK_SMALL(a[cmp_i] - b[cmp_i], 1e-14); \ + } + +BOOST_AUTO_TEST_CASE(add) +{ + double a[] = { 1.0, 2.0, 3.0 }; + double b[] = { -1.0, 3.0, -4.5 }; + double expected[] = { 0.0, 5.0, -1.5 }; + v_add(a, b, 3); + COMPARE_N(a, expected, 3); +} + +BOOST_AUTO_TEST_CASE(add_with_gain) +{ + double a[] = { 1.0, 2.0, 3.0 }; + double b[] = { -1.0, 3.0, -4.5 }; + double expected[] = { -0.5, 6.5, -3.75 }; + v_add_with_gain(a, b, 1.5, 3); + COMPARE_N(a, expected, 3); +} + +BOOST_AUTO_TEST_CASE(subtract) +{ + double a[] = { 1.0, 2.0, 3.0 }; + double b[] = { -1.0, 3.0, -4.5 }; + double expected[] = { 2.0, -1.0, 7.5 }; + v_subtract(a, b, 3); + COMPARE_N(a, expected, 3); +} + +BOOST_AUTO_TEST_CASE(scale) +{ + double a[] = { -1.0, 3.0, -4.5 }; + double scale = -0.5; + double expected[] = { 0.5, -1.5, 2.25 }; + v_scale(a, scale, 3); + COMPARE_N(a, expected, 3); +} + +BOOST_AUTO_TEST_CASE(multiply) +{ + double a[] = { 1.0, 2.0, 3.0 }; + double b[] = { -1.0, 3.0, -4.5 }; + double expected[] = { -1.0, 6.0, -13.5 }; + v_multiply(a, b, 3); + COMPARE_N(a, expected, 3); +} + +BOOST_AUTO_TEST_CASE(multiply_and_add) +{ + double a[] = { 1.0, 2.0, 3.0 }; + double b[] = { -1.0, 3.0, -4.5 }; + double c[] = { 3.0, -1.0, 4.0 }; + double expected[] = { 2.0, 5.0, -9.5 }; + v_multiply_and_add(c, a, b, 3); + COMPARE_N(c, expected, 3); +} + +BOOST_AUTO_TEST_CASE(divide) +{ + double a[] = { 1.0, 2.0, 3.0 }; + double b[] = { -1.0, 3.0, -4.5 }; + double expected[] = { -1.0, 2.0/3.0, 3.0/-4.5 }; + v_divide(a, b, 3); + COMPARE_N(a, expected, 3); +} + +BOOST_AUTO_TEST_CASE(sum) +{ + double a[] = { 1.0, 2.0, -3.5 }; + double s = v_sum(a, 3); + BOOST_CHECK_EQUAL(s, -0.5); +} + +BOOST_AUTO_TEST_CASE(multiply_and_sum) +{ + double a[] = { 2.0, 0.0, -1.5 }; + double b[] = { 3.0, 4.0, 5.0 }; + double s = v_multiply_and_sum(a, b, 3); + BOOST_CHECK_EQUAL(s, -1.5); +} + +BOOST_AUTO_TEST_CASE(log) +{ + double a[] = { 1.0, 1.0 / M_E, M_E }; + double expected[] = { 0.0, -1.0, 1.0 }; + v_log(a, 3); + COMPARE_N(a, expected, 3); +} + +BOOST_AUTO_TEST_CASE(exp) +{ + double a[] = { 0.0, -1.0, 2.0 }; + double expected[] = { 1.0, 1.0 / M_E, M_E * M_E }; + v_exp(a, 3); + COMPARE_N(a, expected, 3); +} + +BOOST_AUTO_TEST_CASE(sqrt) +{ + double a[] = { 0.0, 1.0, 4.0 }; + double expected[] = { 0.0, 1.0, 2.0 }; + v_sqrt(a, 3); + COMPARE_N(a, expected, 3); +} + +BOOST_AUTO_TEST_CASE(square) +{ + double a[] = { 0.0, 1.5, -2.0 }; + double expected[] = { 0.0, 2.25, 4.0 }; + v_square(a, 3); + COMPARE_N(a, expected, 3); +} + +BOOST_AUTO_TEST_CASE(abs) +{ + double a[] = { -1.9, 0.0, 0.01, -0.0 }; + double expected[] = { 1.9, 0.0, 0.01, 0.0 }; + v_abs(a, 4); + COMPARE_N(a, expected, 4); +} + +BOOST_AUTO_TEST_CASE(mean) +{ + double a[] = { -1.0, 1.6, 3.0 }; + double s = v_mean(a, 3); + BOOST_CHECK_EQUAL(s, 1.2); +} + +BOOST_AUTO_TEST_CASE(interleave_1) +{ + double a[] = { 1.0, 2.0, 3.0 }; + double *ch[] = { a }; + double o[3]; + double expected[] = { 1.0, 2.0, 3.0 }; + v_interleave(o, ch, 1, 3); + COMPARE_N(o, expected, 3); +} + +BOOST_AUTO_TEST_CASE(interleave_2) +{ + double a[] = { 1.0, 2.0, 3.0 }; + double b[] = { 4.0, 5.0, 6.0 }; + double *ch[] = { a, b }; + double o[6]; + double expected[] = { 1.0, 4.0, 2.0, 5.0, 3.0, 6.0 }; + v_interleave(o, ch, 2, 3); + COMPARE_N(o, expected, 6); +} + +BOOST_AUTO_TEST_CASE(interleave_3) +{ + double a[] = { 1.0, 2.0 }; + double b[] = { 3.0, 4.0 }; + double c[] = { 5.0, 6.0 }; + double *ch[] = { a, b, c }; + double o[6]; + double expected[] = { 1.0, 3.0, 5.0, 2.0, 4.0, 6.0 }; + v_interleave(o, ch, 3, 2); + COMPARE_N(o, expected, 6); +} + +BOOST_AUTO_TEST_CASE(deinterleave_1) +{ + double a[] = { 1.0, 2.0, 3.0 }; + double o[3]; + double *oo[] = { o }; + double *expected[] = { a }; + v_deinterleave(oo, a, 1, 3); + COMPARE_N(oo[0], expected[0], 3); +} + +BOOST_AUTO_TEST_CASE(deinterleave_2) +{ + double a[] = { 1.0, 4.0, 2.0, 5.0, 3.0, 6.0 }; + double o1[3], o2[3]; + double *oo[] = { o1, o2 }; + double e1[] = { 1.0, 2.0, 3.0 }, e2[] = { 4.0, 5.0, 6.0 }; + double *expected[] = { e1, e2 }; + v_deinterleave(oo, a, 2, 3); + COMPARE_N(oo[0], expected[0], 3); + COMPARE_N(oo[1], expected[1], 3); +} + +BOOST_AUTO_TEST_CASE(deinterleave_3) +{ + double a[] = { 1.0, 3.0, 5.0, 2.0, 4.0, 6.0 }; + double o1[2], o2[2], o3[2]; + double *oo[] = { o1, o2, o3 }; + double e1[] = { 1.0, 2.0 }, e2[] = { 3.0, 4.0 }, e3[] = { 5.0, 6.0 }; + double *expected[] = { e1, e2, e3 }; + v_deinterleave(oo, a, 3, 2); + COMPARE_N(oo[0], expected[0], 2); + COMPARE_N(oo[1], expected[1], 2); + COMPARE_N(oo[2], expected[2], 2); +} + +BOOST_AUTO_TEST_CASE(fftshift) +{ + double a[] = { 0.1, 2.0, -0.3, 4.0 }; + double e[] = { -0.3, 4.0, 0.1, 2.0 }; + v_fftshift(a, 4); + COMPARE_N(a, e, 4); +} + +BOOST_AUTO_TEST_SUITE_END() + diff --git a/src/test/TestVectorOpsComplex.cpp b/src/test/TestVectorOpsComplex.cpp new file mode 100644 index 0000000..dd73cd8 --- /dev/null +++ b/src/test/TestVectorOpsComplex.cpp @@ -0,0 +1,134 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Rubber Band Library + An audio time-stretching and pitch-shifting library. + Copyright 2007-2022 Particular Programs Ltd. + + 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. See the file + COPYING included with this distribution for more information. + + Alternatively, if you have a valid commercial licence for the + Rubber Band Library obtained by agreement with the copyright + holders, you may redistribute and/or modify it under the terms + described in that licence. + + If you wish to distribute code using the Rubber Band Library + under terms other than those of the GNU General Public License, + you must obtain a valid commercial licence before doing so. +*/ + +#define BOOST_TEST_DYN_LINK +#include + +#include "../common/VectorOpsComplex.h" +#include "../common/VectorOps.h" + +#include +#include +#include + +using namespace RubberBand; + +using namespace std; + +BOOST_AUTO_TEST_SUITE(TestVectorOpsComplex) + +#ifdef USE_APPROXIMATE_ATAN2 +static const double eps = 5.0e-3; +static const double eps_approx = eps; +#else +static const double eps = 1.0e-14; +static const double eps_approx = 1.0e-8; +#endif + +#define COMPARE_N(a, b, n) \ + for (int cmp_i = 0; cmp_i < n; ++cmp_i) { \ + BOOST_CHECK_SMALL(a[cmp_i] - b[cmp_i], eps); \ + } + +BOOST_AUTO_TEST_CASE(cartesian_to_magnitudes) +{ + double re[] = { 1.0, 3.0 }; + double im[] = { 2.0, -4.0 }; + double o[2]; + double expected[] = { sqrt(5.0), 5.0 }; + v_cartesian_to_magnitudes(o, re, im, 2); + COMPARE_N(o, expected, 2); +} + +BOOST_AUTO_TEST_CASE(cartesian_interleaved_to_magnitudes) +{ + double a[] = { 1.0, 2.0, 3.0, -4.0 }; + double o[2]; + double expected[] = { sqrt(5.0), 5.0 }; + v_cartesian_interleaved_to_magnitudes(o, a, 2); + COMPARE_N(o, expected, 2); +} + +BOOST_AUTO_TEST_CASE(cartesian_to_polar) +{ + double re[] = { 0.0, 1.0, 0.0 }; + double im[] = { 0.0, 1.0, -1.0 }; + double mo[3], po[3]; + double me[] = { 0.0, sqrt(2.0), 1.0 }; + double pe[] = { 0.0, M_PI / 4.0, -M_PI * 0.5 }; + v_cartesian_to_polar(mo, po, re, im, 3); + COMPARE_N(mo, me, 3); + COMPARE_N(po, pe, 3); +} + +BOOST_AUTO_TEST_CASE(cartesian_to_polar_interleaved_inplace) +{ + double a[] = { 0.0, 0.0, 1.0, 1.0, 0.0, -1.0 }; + double e[] = { 0.0, 0.0, sqrt(2.0), M_PI / 4.0, 1.0, -M_PI * 0.5 }; + v_cartesian_to_polar_interleaved_inplace(a, 3); + COMPARE_N(a, e, 6); +} + +BOOST_AUTO_TEST_CASE(cartesian_interleaved_to_polar) +{ + double a[] = { 0.0, 0.0, 1.0, 1.0, 0.0, -1.0 }; + double mo[3], po[3]; + double me[] = { 0.0, sqrt(2.0), 1.0 }; + double pe[] = { 0.0, M_PI / 4.0, -M_PI * 0.5 }; + v_cartesian_interleaved_to_polar(mo, po, a, 3); + COMPARE_N(mo, me, 3); + COMPARE_N(po, pe, 3); +} + +BOOST_AUTO_TEST_CASE(polar_to_cartesian) +{ + double m[] = { 0.0, sqrt(2.0), 1.0 }; + double p[] = { 0.0, M_PI / 4.0, -M_PI * 0.5 }; + double ro[3], io[3]; + double re[] = { 0.0, 1.0, 0.0 }; + double ie[] = { 0.0, 1.0, -1.0 }; + v_polar_to_cartesian(ro, io, m, p, 3); + COMPARE_N(ro, re, 3); + COMPARE_N(io, ie, 3); +} + +BOOST_AUTO_TEST_CASE(polar_to_cartesian_interleaved_inplace) +{ + double a[] = { 0.0, 0.0, sqrt(2.0), M_PI / 4.0, 1.0, -M_PI * 0.5 }; + double e[] = { 0.0, 0.0, 1.0, 1.0, 0.0, -1.0 }; + v_polar_interleaved_to_cartesian_inplace(a, 3); + COMPARE_N(a, e, 6); +} + +BOOST_AUTO_TEST_CASE(polar_to_cartesian_interleaved) +{ + double m[] = { 0.0, sqrt(2.0), 1.0 }; + double p[] = { 0.0, M_PI / 4.0, -M_PI * 0.5 }; + double o[6]; + double e[] = { 0.0, 0.0, 1.0, 1.0, 0.0, -1.0 }; + v_polar_to_cartesian_interleaved(o, m, p, 3); + COMPARE_N(o, e, 6); +} + +BOOST_AUTO_TEST_SUITE_END() + diff --git a/src/test/test.cpp b/src/test/test.cpp new file mode 100644 index 0000000..e5fda23 --- /dev/null +++ b/src/test/test.cpp @@ -0,0 +1,26 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Rubber Band Library + An audio time-stretching and pitch-shifting library. + Copyright 2007-2022 Particular Programs Ltd. + + 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. See the file + COPYING included with this distribution for more information. + + Alternatively, if you have a valid commercial licence for the + Rubber Band Library obtained by agreement with the copyright + holders, you may redistribute and/or modify it under the terms + described in that licence. + + If you wish to distribute code using the Rubber Band Library + under terms other than those of the GNU General Public License, + you must obtain a valid commercial licence before doing so. +*/ + +#define BOOST_TEST_MODULE RubberBand +#define BOOST_TEST_DYN_LINK +#include