/* * Copyright 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include using ::testing::Pointwise; using ::testing::FloatNear; using namespace android::audio_utils; /************************************************************************************ * Reference data, must not change. * The reference output data is from running in matlab or octave y = filter(b, a, x), where * b = [2.0 3.0 4.0] * a = [1.0 0.2 0.3] * x = [-0.1 -0.2 -0.3 -0.4 -0.5 0.1 0.2 0.3 0.4 0.5] * filter(b, a, x) * * The output y = [-0.2 -0.66 -1.4080 -2.0204 -2.5735 -1.7792 -0.1721 2.1682 2.1180 2.3259]. * The reference data construct the input and output as 2D array so that it can be * use to practice calling BiquadFilter::process multiple times. ************************************************************************************/ constexpr size_t FRAME_COUNT = 5; constexpr size_t PERIOD = 2; constexpr float INPUT[PERIOD][FRAME_COUNT] = { {-0.1f, -0.2f, -0.3f, -0.4f, -0.5f}, {0.1f, 0.2f, 0.3f, 0.4f, 0.5f}}; // COEFS in order of [ b0 b1 b2 a1 a2 ], normalized form where a0 = 1. constexpr std::array COEFS = { 2.0f, 3.0f, 4.0f, 0.2f, 0.3f }; constexpr float OUTPUT[PERIOD][FRAME_COUNT] = { {-0.2f, -0.66f, -1.4080f, -2.0204f, -2.5735f}, {-1.7792f, -0.1721f, 2.1682f, 2.1180f, 2.3259f}}; constexpr float EPS = 1e-4f; template static void populateBuffer(const S *singleChannelBuffer, size_t frameCount, size_t channelCount, size_t zeroChannels, D *buffer) { const size_t stride = channelCount + zeroChannels; for (size_t i = 0; i < frameCount; ++i) { size_t j = 0; for (; j < channelCount; ++j) { buffer[i * stride + j] = singleChannelBuffer[i]; } for (; j < stride; ++j) { buffer[i * stride + j] = D{}; } } } template static void randomBuffer(D *buffer, size_t frameCount, size_t channelCount) { static std::minstd_rand gen(42); constexpr float amplitude = 1.0f; std::uniform_real_distribution<> dis(-amplitude, amplitude); for (size_t i = 0; i < frameCount * channelCount; ++i) { buffer[i] = dis(gen); } } template static std::array randomFilter() { static std::minstd_rand gen(42); constexpr float amplitude = 0.9f; std::uniform_real_distribution<> dis(-amplitude, amplitude); const D p1 = (D)dis(gen); const D p2 = (D)dis(gen); return {(D)dis(gen), (D)dis(gen), (D)dis(gen), -(p1 + p2), p1 * p2}; } template static std::array randomUnstableFilter() { static std::minstd_rand gen(42); constexpr float amplitude = 3.; std::uniform_real_distribution<> dis(-amplitude, amplitude); // symmetric in p1 and p2. const D p1 = (D)dis(gen); D p2; while (true) { p2 = (D)dis(gen); if (fabs(p2) > 1.1) break; } return {(D)dis(gen), (D)dis(gen), (D)dis(gen), -(p1 + p2), p1 * p2}; } // The BiquadFilterTest is parameterized on channel count. class BiquadFilterTest : public ::testing::TestWithParam { protected: template static void testProcess(size_t zeroChannels = 0) { const size_t channelCount = static_cast(GetParam()); const size_t stride = channelCount + zeroChannels; const size_t sampleCount = FRAME_COUNT * stride; T inputBuffer[PERIOD][sampleCount]; T outputBuffer[sampleCount]; T expectedOutputBuffer[PERIOD][sampleCount]; for (size_t i = 0; i < PERIOD; ++i) { populateBuffer(INPUT[i], FRAME_COUNT, channelCount, zeroChannels, inputBuffer[i]); populateBuffer( OUTPUT[i], FRAME_COUNT, channelCount, zeroChannels, expectedOutputBuffer[i]); } BiquadFilter filter(channelCount, COEFS); for (size_t i = 0; i < PERIOD; ++i) { filter.process(outputBuffer, inputBuffer[i], FRAME_COUNT, stride); EXPECT_THAT(std::vector(outputBuffer, outputBuffer + sampleCount), Pointwise(FloatNear(EPS), std::vector( expectedOutputBuffer[i], expectedOutputBuffer[i] + sampleCount))); } // After clear, the previous delays should be cleared. filter.clear(); filter.process(outputBuffer, inputBuffer[0], FRAME_COUNT, stride); EXPECT_THAT(std::vector(outputBuffer, outputBuffer + sampleCount), Pointwise(FloatNear(EPS), std::vector( expectedOutputBuffer[0], expectedOutputBuffer[0] + sampleCount))); } }; struct StateSpaceOptions { template using FilterType = BiquadStateSpace; }; struct StateSpaceChannelOptimizedOptions { template using FilterType = BiquadStateSpace; }; struct Direct2TransposeOptions { template using FilterType = BiquadDirect2Transpose; }; TEST_P(BiquadFilterTest, ConstructAndProcessSSFilterFloat) { testProcess(); } TEST_P(BiquadFilterTest, ConstructAndProcessSSFilterDouble) { testProcess(); } TEST_P(BiquadFilterTest, ConstructAndProcessSSFilterFloatZero3) { testProcess(3 /* zeroChannels */); } TEST_P(BiquadFilterTest, ConstructAndProcessSSFilterDoubleZero5) { testProcess(5 /* zeroChannels */); } TEST_P(BiquadFilterTest, ConstructAndProcessSSChanelOptimizedFilterFloat) { testProcess(); } TEST_P(BiquadFilterTest, ConstructAndProcessSSChannelOptimizedFilterDouble) { testProcess(); } TEST_P(BiquadFilterTest, ConstructAndProcessDT2FilterFloat) { testProcess(); } TEST_P(BiquadFilterTest, ConstructAndProcessDT2FilterDouble) { testProcess(); } INSTANTIATE_TEST_CASE_P( CstrAndRunBiquadFilter, BiquadFilterTest, ::testing::Values(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24) ); // Test the experimental 1D mode. TEST(BiquadBasicTest, OneDee) { using D = float; constexpr size_t TEST_LENGTH = 1024; constexpr size_t FILTERS = 3; std::vector reference(TEST_LENGTH); randomBuffer(reference.data(), TEST_LENGTH, 1 /* channelCount */); BiquadFilter parallel(FILTERS, COEFS); std::vector>> biquads(FILTERS); for (auto& biquad : biquads) { biquad.reset(new BiquadFilter(1, COEFS)); } auto test1 = reference; parallel.process1D(test1.data(), TEST_LENGTH); auto test2 = reference; for (auto& biquad : biquads) { biquad->process(test2.data(), test2.data(), TEST_LENGTH); } EXPECT_THAT(test1, Pointwise(FloatNear(EPS), test2)); } // The BiquadBasicTest is parameterized on floating point type (float or double). template class BiquadBasicTest : public ::testing::Test { protected: // Multichannel biquad test where each channel has different filter coefficients. static void testDifferentFiltersPerChannel() { constexpr size_t FILTERS = 3; constexpr size_t TEST_LENGTH = 1024; std::vector reference(TEST_LENGTH * FILTERS); randomBuffer(reference.data(), TEST_LENGTH, FILTERS); std::array, FILTERS> filters; for (auto &filter : filters) { filter = randomFilter(); } BiquadFilter multichannel(FILTERS); std::vector>> biquads(FILTERS); for (size_t i = 0; i < filters.size(); ++i) { ASSERT_TRUE(multichannel.setCoefficients(filters[i], i)); biquads[i].reset(new BiquadFilter(1 /* channels */, filters[i])); } // Single multichannel Biquad with different filters per channel. auto test1 = reference; multichannel.process(test1.data(), test1.data(), TEST_LENGTH); // Multiple different single channel Biquads applied to the test data, with a stride. auto test2 = reference; for (size_t i = 0; i < biquads.size(); ++i) { biquads[i]->process(test2.data() + i, test2.data() + i, TEST_LENGTH, FILTERS); } // Must be equivalent. EXPECT_THAT(test1, Pointwise(FloatNear(EPS), test2)); } // Test zero fill with coefficients all zero. static void testZeroFill() { constexpr size_t TEST_LENGTH = 1024; // Randomize input and output. std::vector reference(TEST_LENGTH); randomBuffer(reference.data(), TEST_LENGTH, 1); std::vector output(TEST_LENGTH); randomBuffer(output.data(), TEST_LENGTH, 1); // Single channel Biquad BiquadFilter bqf(1 /* channelCount */, {} /* coefs */); bqf.process(output.data(), reference.data(), TEST_LENGTH); // Result is zero. const std::vector zero(TEST_LENGTH); ASSERT_EQ(zero, output); ASSERT_NE(zero, reference); } // Stability check static void testStability() { BiquadFilter bqf(1 /* channels */); constexpr size_t TRIALS = 1000; for (size_t i = 0; i < TRIALS; ++i) { ASSERT_TRUE(bqf.setCoefficients(randomFilter())); ASSERT_FALSE(bqf.setCoefficients(randomUnstableFilter())); } } // Constructor, assignment equivalence check static void testEquivalence() { for (size_t channelCount = 1; channelCount < 3; ++channelCount) { BiquadFilter bqf1(channelCount); BiquadFilter bqf2(channelCount); ASSERT_TRUE(bqf1.setCoefficients(randomFilter())); ASSERT_FALSE(bqf2.setCoefficients(randomUnstableFilter())); ASSERT_NE(bqf1, bqf2); // one is stable one isn't, can't be the same. constexpr size_t TRIALS = 10; // try a few different filters, just to be sure. for (size_t i = 0; i < TRIALS; ++i) { ASSERT_TRUE(bqf1.setCoefficients(randomFilter())); // Copy construction/assignment is equivalent. const auto bqf3 = bqf1; ASSERT_EQ(bqf1, bqf3); const auto bqf4(bqf1); ASSERT_EQ(bqf1, bqf4); BiquadFilter bqf5(channelCount); bqf5.setCoefficients(bqf1.getCoefficients()); ASSERT_EQ(bqf1, bqf5); } } } // Test that 6 coefficient definition reduces to same 5 coefficient definition static void testCoefReductionEquivalence() { std::array coef5 = randomFilter(); // The 6 coefficient version has a0. // This should be a power of 2 to be exact for IEEE binary float for (size_t shift = 0; shift < 4; ++shift) { const D a0 = 1 << shift; std::array coef6 = { coef5[0] * a0, coef5[1] * a0, coef5[2] * a0, a0, coef5[3] * a0, coef5[4] * a0 }; for (size_t channelCount = 1; channelCount < 2; ++channelCount) { BiquadFilter bqf1(channelCount, coef5); BiquadFilter bqf2(channelCount, coef6); ASSERT_EQ(bqf1, bqf2); } } } }; using FloatTypes = ::testing::Types; TYPED_TEST_CASE(BiquadBasicTest, FloatTypes); TYPED_TEST(BiquadBasicTest, DifferentFiltersPerChannel) { this->testDifferentFiltersPerChannel(); } TYPED_TEST(BiquadBasicTest, ZeroFill) { this->testZeroFill(); } TYPED_TEST(BiquadBasicTest, Stability) { this->testStability(); } TYPED_TEST(BiquadBasicTest, Equivalence) { this->testEquivalence(); } TYPED_TEST(BiquadBasicTest, CoefReductionEquivalence) { this->testCoefReductionEquivalence(); }