diff --git a/.github/workflows/flucoma-core-ci.yml b/.github/workflows/flucoma-core-ci.yml index ec1e34b50..d56dda8ef 100644 --- a/.github/workflows/flucoma-core-ci.yml +++ b/.github/workflows/flucoma-core-ci.yml @@ -35,5 +35,5 @@ jobs: - name: Test working-directory: ${{github.workspace}}/build - run: ctest -C ${{env.BUILD_TYPE}} + run: ctest -C ${{env.BUILD_TYPE}} -j3 diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 000000000..1c9a99104 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +*.received.* diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c0f3e3f32..f1b4c7954 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required (VERSION 3.11) ###### Utils +add_subdirectory(test_signals) ##### Assert Death Testing @@ -131,6 +132,23 @@ add_test_executable(TestFluidSource clients/common/TestFluidSource.cpp) add_test_executable(TestFluidSink clients/common/TestFluidSink.cpp) add_test_executable(TestBufferedProcess clients/common/TestBufferedProcess.cpp) +add_test_executable(TestNoveltySeg + algorithms/public/TestNoveltySegmentation.cpp +) +add_test_executable(TestOnsetSeg algorithms/public/TestOnsetSegmentation.cpp) +add_test_executable(TestEnvelopeSeg algorithms/public/TestEnvelopeSegmentation.cpp) + +add_test_executable(TestEnvelopeGate algorithms/public/TestEnvelopeGate.cpp) + +add_test_executable(TestTransientSlice algorithms/public/TestTransientSlice.cpp) + + +target_link_libraries(TestNoveltySeg PRIVATE TestSignals) +target_link_libraries(TestOnsetSeg PRIVATE TestSignals) +target_link_libraries(TestEnvelopeSeg PRIVATE TestSignals) +target_link_libraries(TestEnvelopeGate PRIVATE TestSignals) +target_link_libraries(TestTransientSlice PRIVATE TestSignals) + include(CTest) include(Catch) @@ -145,6 +163,12 @@ catch_discover_tests(TestFluidTensorView WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" catch_discover_tests(TestFluidTensorSupport WORKING_DIRECTORY "${CMAKE_BINARY_DIR}") catch_discover_tests(TestFluidDataSet WORKING_DIRECTORY "${CMAKE_BINARY_DIR}") +catch_discover_tests(TestNoveltySeg WORKING_DIRECTORY "${CMAKE_BINARY_DIR}") +catch_discover_tests(TestOnsetSeg WORKING_DIRECTORY "${CMAKE_BINARY_DIR}") +catch_discover_tests(TestEnvelopeSeg WORKING_DIRECTORY "${CMAKE_BINARY_DIR}") +catch_discover_tests(TestEnvelopeGate WORKING_DIRECTORY "${CMAKE_BINARY_DIR}") +catch_discover_tests(TestTransientSlice WORKING_DIRECTORY "${CMAKE_BINARY_DIR}") + catch_discover_tests(TestFluidSource WORKING_DIRECTORY "${CMAKE_BINARY_DIR}") catch_discover_tests(TestFluidSink WORKING_DIRECTORY "${CMAKE_BINARY_DIR}") catch_discover_tests(TestBufferedProcess WORKING_DIRECTORY "${CMAKE_BINARY_DIR}") diff --git a/tests/algorithms/public/SlicerTestHarness.hpp b/tests/algorithms/public/SlicerTestHarness.hpp new file mode 100644 index 000000000..12407f7db --- /dev/null +++ b/tests/algorithms/public/SlicerTestHarness.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace fluid { + +template +std::vector +SlicerTestHarness(FluidTensorView testSignal, Params p, + PrepareSlicerFn&& prepareSlicer, MakeInputsFn&& makeInputs, + InvokeSlicerFn&& invokeSlicer, index addedLatency) +{ + const index halfWindow = p.window; // >> 1; + const index padding = addedLatency; + FluidTensor padded(p.window + halfWindow + padding + + testSignal.size()); + padded.fill(0); + padded(Slice(halfWindow, testSignal.size())) = testSignal; + const fluid::index nHops = + std::floor((padded.size() - p.window) / p.hop); + auto slicer = prepareSlicer(p); + std::vector spikePositions; + for (index i = 0; i < nHops; ++i) + { + auto input = makeInputs(padded(Slice(i * p.hop, p.window))); + if (invokeSlicer(slicer, input, p) > 0) + { + spikePositions.push_back((i * p.hop) - padding - p.hop); + } + } + + // This reproduces what the NRT wrapper does (and hence the result that the + // existing test in SC sees). I'm dubious that + // it really ought to be needed though. I think we're adjusting the latency + // by a hop too much + std::transform(spikePositions.begin(), spikePositions.end(), + spikePositions.begin(), + [&p](index x) { return std::max(0, x); }); + + spikePositions.erase( + std::unique(spikePositions.begin(), spikePositions.end()), + spikePositions.end()); + + return spikePositions; +} + + +struct STFTMagnitudeInput +{ + + template + STFTMagnitudeInput(const Params& p) + : mSTFT(index(p.window), index(p.fft), index(p.hop)), + mSTFTFrame((index(p.fft) / 2) + 1), mMagnitudes((index(p.fft) / 2) + 1) + {} + + FluidTensorView operator()(FluidTensorView source) + { + mSTFT.processFrame(source, mSTFTFrame); + mSTFT.magnitude(mSTFTFrame, mMagnitudes); + return FluidTensorView(mMagnitudes); + } + +private: + algorithm::STFT mSTFT; + FluidTensor, 1> mSTFTFrame; + FluidTensor mMagnitudes; +}; + + +} // namespace fluid diff --git a/tests/algorithms/public/TestEnvelopeGate.cpp b/tests/algorithms/public/TestEnvelopeGate.cpp new file mode 100644 index 000000000..8b67ca58b --- /dev/null +++ b/tests/algorithms/public/TestEnvelopeGate.cpp @@ -0,0 +1,466 @@ +#define CATCH_CONFIG_MAIN + +#include "SlicerTestHarness.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fluid { + +using RampUp = StrongType; +using RampDown = StrongType; +using OnThreshold = StrongType; +using OffThreshold = StrongType; +using MinLengthAbove = StrongType; +using MinLengthBelow = StrongType; +using MinSliceLength = StrongType; +using MinSilenceLength = StrongType; +using HighPassFreq = StrongType; +using LookAhead = StrongType; +using LookBack = StrongType; +using Data = StrongType, struct DataTag>; +using Expected = StrongType, struct ExpectedTag>; +using Margin = StrongType; + + +struct TestParams +{ + RampUp rampUp; + RampDown rampDown; + OnThreshold onThreshold; + OffThreshold offThreshold; + MinLengthAbove minLengthAbove; + MinLengthBelow minLengthBelow; + MinSliceLength minSliceLength; + MinSilenceLength minSilenceLength; + LookAhead lookahead; + LookBack lookback; + HighPassFreq highPassFreq; +}; + +std::pair, std::vector> +runTest(FluidTensorView testSignal, TestParams const& p) +{ + + double hiPassFreq = std::min(p.highPassFreq / 44100, 0.5); + + auto algo = algorithm::EnvelopeGate(88200); + algo.init(p.onThreshold, p.offThreshold, hiPassFreq, p.minLengthAbove, + p.lookback, p.minLengthBelow, p.lookahead); + + + index latency = + std::max(p.minLengthAbove + p.lookback, + std::max(p.minLengthBelow, p.lookahead)); + + std::vector onsetPositions; + std::vector offsetPositions; + + index i{0}; + + bool state = false; + + for (auto&& x : testSignal) + { + + + double response = algo.processSample(x, p.onThreshold, p.offThreshold, + p.rampUp, p.rampDown, hiPassFreq, + p.minSliceLength, p.minSilenceLength); + if (response > 0 && !state) + { + onsetPositions.push_back(i - latency); + state = true; + } + else if (response == 0 && state) + { + offsetPositions.push_back(i - latency); + state = false; + } + i++; + } + + return {onsetPositions, offsetPositions}; +} + + +TEST_CASE("EnvelopeGate is almost exact with impulses", "[AmpGate][slicers]") +{ + auto sig = testsignals::monoImpulses(); + auto expectedOnsets = testsignals::stereoImpulsePositions(); + auto expectedOffsets = testsignals::stereoImpulsePositions(); + + std::transform(expectedOffsets.begin(), expectedOffsets.end(), + expectedOffsets.begin(), [](index x) { return x + 596; }); + + auto params = + TestParams{RampUp(1), RampDown(10), OnThreshold(-30), + OffThreshold(-90), MinLengthAbove(1), MinLengthBelow(1), + MinSliceLength(1), MinSilenceLength(1), LookAhead(0), + LookBack(0), HighPassFreq(85)}; + + auto result = runTest(sig, params); + + REQUIRE(result.first.size() == result.second.size()); + + auto matcherOn = Catch::Matchers::Approx(expectedOnsets); + index margin = 2; + matcherOn.margin(margin); + + CHECK_THAT(result.first, matcherOn); + + auto matcherOff = Catch::Matchers::Approx(expectedOffsets); + matcherOff.margin(margin); + + CHECK_THAT(result.second, matcherOff); +} + +TEST_CASE("EnvelopeGate is predictable with smooth sine bursts", + "[AmpGate][slicers]") +{ + auto sig = testsignals::smoothSine(); + auto expectedOnsets = std::vector{ + 1876, 1942, 2009, 2076, 2144, 2212, 2279, 2347, 2415, 2483, + 2551, 2619, 2687, 19431, 19501, 19571, 19641, 19711, 19781, 19851, + 19921, 19991, 20062, 20133, 20205, 23926, 23992, 24059, 24126, 24194, + 24262, 24329, 24397, 24465, 24533, 24601, 24669, 24737, 41481, 41551, + 41621, 41691, 41761, 41831, 41901, 41971, 42041, 42112, 42183, 42255}; + + auto expectedOffsets = std::vector{ + 1890, 1965, 2039, 2113, 2185, 2257, 2329, 2400, 2471, 2542, + 2613, 2684, 19430, 19497, 19564, 19631, 19698, 19764, 19831, 19897, + 19963, 20028, 20092, 20156, 20219, 23940, 24015, 24089, 24163, 24235, + 24307, 24379, 24450, 24521, 24592, 24663, 24734, 41480, 41547, 41614, + 41681, 41748, 41814, 41881, 41947, 42013, 42078, 42142, 42206, 42269}; + + + auto params = + TestParams{RampUp(5), RampDown(25), OnThreshold(-12), + OffThreshold(-12), MinLengthAbove(1), MinLengthBelow(1), + MinSliceLength(1), MinSilenceLength(1), LookAhead(0), + LookBack(0), HighPassFreq(85)}; + + + auto result = runTest(sig, params); + + REQUIRE(result.first.size() == result.second.size()); + + auto matcherOn = Catch::Matchers::Approx(expectedOnsets); + index margin = 1; + matcherOn.margin(margin); + + CHECK_THAT(result.first, matcherOn); + + auto matcherOff = Catch::Matchers::Approx(expectedOffsets); + matcherOff.margin(margin); + + CHECK_THAT(result.second, matcherOff); +} + +TEST_CASE("EnvelopeGate is predictable with smooth sine bursts and hysteresis", + "[AmpGate][slicers]") +{ + auto sig = testsignals::smoothSine(); + auto expectedOnsets = std::vector{1878, 23928}; + + auto expectedOffsets = std::vector{20462, 42512}; + + auto params = + TestParams{RampUp(5), RampDown(25), OnThreshold(-12), + OffThreshold(-16), MinLengthAbove(1), MinLengthBelow(1), + MinSliceLength(1), MinSilenceLength(1), LookAhead(0), + LookBack(0), HighPassFreq(85)}; + + + auto result = runTest(sig, params); + + REQUIRE(result.first.size() == result.second.size()); + + auto matcherOn = Catch::Matchers::Approx(expectedOnsets); + index margin = 1; + matcherOn.margin(margin); + + CHECK_THAT(result.first, matcherOn); + + auto matcherOff = Catch::Matchers::Approx(expectedOffsets); + matcherOff.margin(margin); + + CHECK_THAT(result.second, matcherOff); +} + +TEST_CASE("EnvelopeGate is predictable with smooth sine bursts and debouncing", + "[AmpGate][slicers]") +{ + auto sig = testsignals::smoothSine(); + auto expectedOnsets = + std::vector{1876, 2347, 19431, 19921, 23926, 24397, 41481, 41971}; + + auto expectedOffsets = + std::vector{2329, 19430, 19897, 20362, 24379, 41480, 41947, 42412}; + + auto params = + TestParams{RampUp(5), RampDown(25), OnThreshold(-12), + OffThreshold(-12), MinLengthAbove(1), MinLengthBelow(1), + MinSliceLength(441), MinSilenceLength(1), LookAhead(0), + LookBack(0), HighPassFreq(85)}; + + + auto result = runTest(sig, params); + + REQUIRE(result.first.size() == result.second.size()); + + auto matcherOn = Catch::Matchers::Approx(expectedOnsets); + index margin = 1; + matcherOn.margin(margin); + + CHECK_THAT(result.first, matcherOn); + + auto matcherOff = Catch::Matchers::Approx(expectedOffsets); + matcherOff.margin(margin); + + CHECK_THAT(result.second, matcherOff); +} + +TEST_CASE( + "EnvelopeGate is predictable with smooth sine bursts and gap debouncing", + "[AmpGate][slicers]") +{ + auto sig = testsignals::smoothSine(); + auto expectedOnsets = + std::vector{1876, 2347, 2841, 19871, 23926, 24397, 24891, 41921}; + + auto expectedOffsets = + std::vector{1890, 2400, 19430, 19897, 23940, 24450, 41480, 41947}; + + auto params = + TestParams{RampUp(5), RampDown(25), OnThreshold(-12), + OffThreshold(-12), MinLengthAbove(1), MinLengthBelow(1), + MinSliceLength(1), MinSilenceLength(441), LookAhead(0), + LookBack(0), HighPassFreq(85)}; + + + auto result = runTest(sig, params); + + REQUIRE(result.first.size() == result.second.size()); + + auto matcherOn = Catch::Matchers::Approx(expectedOnsets); + index margin = 1; + matcherOn.margin(margin); + + CHECK_THAT(result.first, matcherOn); + + auto matcherOff = Catch::Matchers::Approx(expectedOffsets); + matcherOff.margin(margin); + + CHECK_THAT(result.second, matcherOff); +} + + +TEST_CASE( + "EnvelopeGate is predictable with smooth sine bursts and min time above", + "[AmpGate][slicers]") +{ + auto sig = testsignals::smoothSine(); + auto expectedOnsets = std::vector{2687, 24737}; + + auto expectedOffsets = std::vector{19429, 41479}; + + auto params = + TestParams{RampUp(5), RampDown(25), OnThreshold(-12), + OffThreshold(-12), MinLengthAbove(441), MinLengthBelow(1), + MinSliceLength(1), MinSilenceLength(1), LookAhead(0), + LookBack(0), HighPassFreq(85)}; + + + auto result = runTest(sig, params); + + REQUIRE(result.first.size() == result.second.size()); + + auto matcherOn = Catch::Matchers::Approx(expectedOnsets); + index margin = 1; + matcherOn.margin(margin); + + CHECK_THAT(result.first, matcherOn); + + auto matcherOff = Catch::Matchers::Approx(expectedOffsets); + matcherOff.margin(margin); + + CHECK_THAT(result.second, matcherOff); +} + +TEST_CASE( + "EnvelopeGate is predictable with smooth sine bursts and min time below", + "[AmpGate][slicers]") +{ + auto sig = testsignals::smoothSine(); + auto expectedOnsets = std::vector{1875, 23925}; + + auto expectedOffsets = std::vector{20219, 42269}; + + auto params = + TestParams{RampUp(5), RampDown(25), OnThreshold(-12), + OffThreshold(-12), MinLengthAbove(1), MinLengthBelow(441), + MinSliceLength(1), MinSilenceLength(1), LookAhead(0), + LookBack(0), HighPassFreq(85)}; + + + auto result = runTest(sig, params); + + REQUIRE(result.first.size() == result.second.size()); + + auto matcherOn = Catch::Matchers::Approx(expectedOnsets); + index margin = 1; + matcherOn.margin(margin); + + CHECK_THAT(result.first, matcherOn); + + auto matcherOff = Catch::Matchers::Approx(expectedOffsets); + matcherOff.margin(margin); + + CHECK_THAT(result.second, matcherOff); +} + +TEST_CASE("EnvelopeGate is predictable with smooth sine bursts and lookahead", + "[AmpGate][slicers]") +{ + auto sig = testsignals::smoothSine(); + auto expectedOnsets = std::vector{1875, 23925}; + + auto expectedOffsets = std::vector{20658, 42708}; + + auto params = + TestParams{RampUp(5), RampDown(25), OnThreshold(-12), + OffThreshold(-12), MinLengthAbove(1), MinLengthBelow(1), + MinSliceLength(1), MinSilenceLength(1), LookAhead(441), + LookBack(0), HighPassFreq(85)}; + + + auto result = runTest(sig, params); + + REQUIRE(result.first.size() == result.second.size()); + + auto matcherOn = Catch::Matchers::Approx(expectedOnsets); + index margin = 1; + matcherOn.margin(margin); + + CHECK_THAT(result.first, matcherOn); + + auto matcherOff = Catch::Matchers::Approx(expectedOffsets); + matcherOff.margin(margin); + + CHECK_THAT(result.second, matcherOff); +} + + +TEST_CASE("EnvelopeGate is predictable with smooth sine bursts and lookback", + "[AmpGate][slicers]") +{ + auto sig = testsignals::smoothSine(); + auto expectedOnsets = std::vector{ + 1435, 19499, 19568, 19638, 19707, 19776, 19846, 19915, + 19985, 20055, 20125, 20195, 23485, 41549, 41618, 41688, + 41757, 41826, 41896, 41965, 42035, 42105, 42175, 42245}; + + auto expectedOffsets = std::vector{ + 19496, 19563, 19630, 19697, 19763, 19830, 19896, 19962, + 20027, 20091, 20155, 20218, 41546, 41613, 41680, 41747, + 41813, 41880, 41946, 42012, 42077, 42141, 42205, 42268}; + + auto params = + TestParams{RampUp(5), RampDown(25), OnThreshold(-12), + OffThreshold(-12), MinLengthAbove(1), MinLengthBelow(1), + MinSliceLength(1), MinSilenceLength(1), LookAhead(0), + LookBack(441), HighPassFreq(85)}; + + + auto result = runTest(sig, params); + + REQUIRE(result.first.size() == result.second.size()); + + auto matcherOn = Catch::Matchers::Approx(expectedOnsets); + index margin = 1; + matcherOn.margin(margin); + + CHECK_THAT(result.first, matcherOn); + + auto matcherOff = Catch::Matchers::Approx(expectedOffsets); + matcherOff.margin(margin); + + CHECK_THAT(result.second, matcherOff); +} + +TEST_CASE("EnvelopeGate is predictable with smooth sine bursts and both " + "lookahead and lookback", + "[AmpGate][slicers]") +{ + auto sig = testsignals::smoothSine(); + auto expectedOnsets = std::vector{1654, 23704}; + + auto expectedOffsets = std::vector{20658, 42708}; + + auto params = + TestParams{RampUp(5), RampDown(25), OnThreshold(-12), + OffThreshold(-12), MinLengthAbove(1), MinLengthBelow(1), + MinSliceLength(1), MinSilenceLength(1), LookAhead(441), + LookBack(221), HighPassFreq(85)}; + + + auto result = runTest(sig, params); + + REQUIRE(result.first.size() == result.second.size()); + + auto matcherOn = Catch::Matchers::Approx(expectedOnsets); + index margin = 1; + matcherOn.margin(margin); + + CHECK_THAT(result.first, matcherOn); + + auto matcherOff = Catch::Matchers::Approx(expectedOffsets); + matcherOff.margin(margin); + + CHECK_THAT(result.second, matcherOff); +} + +TEST_CASE("EnvelopeGate is predictable on a real signal", "[AmpGate][slicers]") +{ + auto sig = testsignals::monoDrums(); + auto expectedOnsets = std::vector{ + 1269, 38394, 69830, 88034, 114533, 151761, 176327, 202300, 220866, + 239779, 252598, 276315, 283982, 326755, 353444, 372504, 390197, 417174}; + + auto expectedOffsets = std::vector{ + 23328, 65751, 81905, 96307, 124975, 172742, 184798, 216023, 232738, + 249671, 272516, 283322, 310995, 343433, 366663, 384191, 398299, 428473}; + + auto params = + TestParams{RampUp(110), RampDown(2205), OnThreshold(-27), + OffThreshold(-31), MinLengthAbove(1), MinLengthBelow(1), + MinSliceLength(1), MinSilenceLength(1100), LookAhead(0), + LookBack(441), HighPassFreq(40)}; + + + auto result = runTest(sig, params); + + REQUIRE(result.first.size() == result.second.size()); + + auto matcherOn = Catch::Matchers::Approx(expectedOnsets); + index margin = 1; + matcherOn.margin(margin); + + CHECK_THAT(result.first, matcherOn); + + auto matcherOff = Catch::Matchers::Approx(expectedOffsets); + matcherOff.margin(margin); + + CHECK_THAT(result.second, matcherOff); +} + + +} // namespace fluid diff --git a/tests/algorithms/public/TestEnvelopeSegmentation.cpp b/tests/algorithms/public/TestEnvelopeSegmentation.cpp new file mode 100644 index 000000000..214372e47 --- /dev/null +++ b/tests/algorithms/public/TestEnvelopeSegmentation.cpp @@ -0,0 +1,181 @@ +#define CATCH_CONFIG_MAIN + +#include "SlicerTestHarness.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fluid { + +using FastRampUp = StrongType; +using FastRampDown = StrongType; +using SlowRampUp = StrongType; +using SlowRampDown = StrongType; +using OnThreshold = StrongType; +using OffThreshold = StrongType; +using Floor = StrongType; +using MinSliceLength = StrongType; +using HighPassFreq = StrongType; +using Data = StrongType, struct DataTag>; +using Expected = StrongType, struct ExpectedTag>; +using Margin = StrongType; + +struct TestParams +{ + FastRampUp fastRampUp; + FastRampDown fastRampDown; + SlowRampUp slowRampUp; + SlowRampDown slowRampDown; + OnThreshold onThreshold; + OffThreshold offThreshold; + Floor floor; + MinSliceLength minSliceLength; + HighPassFreq highPassFreq; + Data data; + Expected expected; + Margin margin; +}; + +std::vector runTest(FluidTensorView testSignal, + TestParams const& p) +{ + + double hiPassFreq = std::min(p.highPassFreq / 44100, 0.5); + + auto algo = algorithm::EnvelopeSegmentation(); + algo.init(p.floor, hiPassFreq); + + std::vector spikePositions; + + index i{0}; + + for (auto&& x : testSignal) + { + if (algo.processSample(x, p.onThreshold, p.offThreshold, p.floor, + p.fastRampUp, p.slowRampUp, p.fastRampDown, + p.slowRampDown, hiPassFreq, p.minSliceLength) > 0) + { + spikePositions.push_back(i); + } + + i++; + } + + return spikePositions; +} + + +TEST_CASE("EnvSeg can be exactly precise with impulses", "[slicers][AmpSlice]") +{ + + auto data = testsignals::monoImpulses(); + auto exp = testsignals::stereoImpulsePositions(); + + auto params = + TestParams{FastRampUp(10), FastRampDown(2205), SlowRampUp(4410), + SlowRampDown(4410), OnThreshold(10), OffThreshold(5), + Floor(-144), MinSliceLength(2), HighPassFreq(85), + Data(data), Expected(exp), Margin(1)}; + + auto result = runTest(params.data(), params); + REQUIRE_THAT(result, Catch::Equals(exp)); +} + +TEST_CASE("EnvSeg is predictable with sharp sine bursts", "[slicers][AmpSlice]") +{ + + auto data = testsignals::sharpSines(); + auto exp = std::vector{1001, 1455, 1493, 11028, 11412, 11450, 22053, + 22399, 22437, 22475, 33078, 33462, 33500}; + + auto params = TestParams{ + FastRampUp(5), FastRampDown(50), SlowRampUp(220), SlowRampDown(220), + OnThreshold(10), OffThreshold(10), Floor(-60), MinSliceLength(2), + HighPassFreq(85), Data(data), Expected(exp), Margin(1)}; + + auto result = runTest(params.data(), params); + + + + REQUIRE_THAT(result, Catch::Matchers::Approx(exp).margin(1)); +} + + +TEST_CASE("EnvSeg schmitt triggering is predictable", "[slicers][AmpSlice]") +{ + + auto data = testsignals::sharpSines(); + auto exp = std::vector{1001, 11028, 22053, 33078}; + + auto params = TestParams{ + FastRampUp(5), FastRampDown(50), SlowRampUp(220), SlowRampDown(220), + OnThreshold(10), OffThreshold(5), Floor(-60), MinSliceLength(2), + HighPassFreq(85), Data(data), Expected(exp), Margin(1)}; + + auto result = runTest(params.data(), params); +// REQUIRE_THAT(result, Catch::Equals(exp)); + REQUIRE_THAT(result, Catch::Matchers::Approx(exp).margin(1)); +} + +TEST_CASE("EnvSeg debouncing is predictable", "[slicers][AmpSlice]") +{ + + auto data = testsignals::sharpSines(); + auto exp = std::vector{1001, 11028, 22053, 33078}; + + auto params = TestParams{ + FastRampUp(5), FastRampDown(50), SlowRampUp(220), SlowRampDown(220), + OnThreshold(10), OffThreshold(10), Floor(-60), MinSliceLength(800), + HighPassFreq(85), Data(data), Expected(exp), Margin(1)}; + + auto result = runTest(params.data(), params); +// REQUIRE_THAT(result, Catch::Equals(exp)); +REQUIRE_THAT(result, Catch::Matchers::Approx(exp).margin(1)); +} + +TEST_CASE("EnvSeg debouncing and Schmitt trigger together are predictable", + "[slicers][AmpSlice]") +{ + + auto data = testsignals::sharpSines(); + auto exp = std::vector{1001, 22053}; + + auto params = + TestParams{FastRampUp(5), FastRampDown(50), SlowRampUp(220), + SlowRampDown(220), OnThreshold(10), OffThreshold(5), + Floor(-60), MinSliceLength(15000), HighPassFreq(85), + Data(data), Expected(exp), Margin(1)}; + + auto result = runTest(params.data(), params); +// REQUIRE_THAT(result, Catch::Equals(exp)); + REQUIRE_THAT(result, Catch::Matchers::Approx(exp).margin(1)); +} + +TEST_CASE("EnvSeg is predictable on real meaterial", "[slicers][AmpSlice]") +{ + + auto data = testsignals::monoDrums(); + auto exp = std::vector{1685, 38411, 51140, 69840, 88051, 114540, + 151768, 176349, 202307, 220877, 239981, 252606, + 259266, 276511, 283738, 289695, 296181, 302794, + 326863, 353451, 372514, 390215, 417181}; + + auto params = + TestParams{FastRampUp(10), FastRampDown(2205), SlowRampUp(4410), + SlowRampDown(4410), OnThreshold(10), OffThreshold(5), + Floor(-40), MinSliceLength(4410), HighPassFreq(20), + Data(data), Expected(exp), Margin(1)}; + + auto result = runTest(params.data(), params); + REQUIRE_THAT(result, Catch::Equals(exp)); +} + + +} // namespace fluid diff --git a/tests/algorithms/public/TestNoveltySegmentation.cpp b/tests/algorithms/public/TestNoveltySegmentation.cpp new file mode 100644 index 000000000..cc6e1f0c2 --- /dev/null +++ b/tests/algorithms/public/TestNoveltySegmentation.cpp @@ -0,0 +1,371 @@ +#define CATCH_CONFIG_MAIN +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +using fluid::FluidTensor; +using fluid::FluidTensorView; +using fluid::Slice; +using fluid::algorithm::NoveltySegmentation; +using fluid::algorithm::STFT; + + +static std::string audio_path; + +struct Params +{ + fluid::index window; + fluid::index hop; + fluid::index fft; + fluid::index minSlice; + fluid::index kernel; + fluid::index filter; + fluid::index dims; + double threshold; +}; + + +namespace fluid { + +template +std::vector NoveltyTestHarness(FluidTensorView testSignal, + Params p, F&& f) +{ + + // NB: This has a lot of arcana about padding amounts and adjustments to get + // the same results as the equivalent tests already implemented in SC. + // However, I think the latency calculation in NoveltySlice, and the padding + // assumptions in NRTWrapper could do with another look, as I think we can get + // closer than we do + + const index filt = p.filter % 2 ? p.filter + 1 : p.filter; + const index halfWindow = p.window; // >> 1; + const index padding = p.hop * (((p.kernel + 1) >> 1) + (filt >> 1)); + FluidTensor padded(p.window + halfWindow + padding + + testSignal.size()); + padded.fill(0); + padded(Slice(halfWindow, testSignal.size())) = testSignal; + const fluid::index nHops = + std::floor((padded.size() - p.window) / p.hop); + + auto slicer = NoveltySegmentation(p.kernel, p.filter); + slicer.init(p.kernel, p.filter, p.dims); // sigh + + std::vector spikePositions; + + for (index i = 0; i < nHops; ++i) + { + auto input = f(padded(Slice(i * p.hop, p.window))); + if (slicer.processFrame(input, p.threshold, p.minSlice) > 0) + { + spikePositions.push_back((i * p.hop) - padding - p.hop); + } + } + + // This reproduces what the NRT wrapper does (and hence the result that the + // existing test in SC sees). I'm dubious that + // it really ought to be needed though. I think we're adjusting the latency + // by a hop too much + std::transform(spikePositions.begin(), spikePositions.end(), + spikePositions.begin(), + [&p](index x) { return std::max(0, x); }); + + spikePositions.erase( + std::unique(spikePositions.begin(), spikePositions.end()), + spikePositions.end()); + + return spikePositions; +} +} // namespace fluid + +std::vector +NoveltySTFTTest(fluid::FluidTensorView testSignal, Params p) +{ + FluidTensor, 1> stftFrame(p.dims); + FluidTensor magnitudes(p.dims); + + auto stft = STFT{p.window, p.fft, p.hop}; + + auto makeInput = [&stft, &stftFrame, &magnitudes](auto source) { + stft.processFrame(source, stftFrame); + stft.magnitude(stftFrame, magnitudes); + return fluid::FluidTensorView(magnitudes); + }; + + return NoveltyTestHarness(testSignal, p, makeInput); +} + + +std::vector +NoveltyMFCCTest(fluid::FluidTensorView testSignal, Params p) +{ + FluidTensor, 1> stftFrame((p.fft / 2) + 1); + FluidTensor magnitudes((p.fft / 2) + 1); + FluidTensor melFrame(40); + FluidTensor mfccFrame(13); + + auto stft = STFT{p.window, p.fft, p.hop}; + auto mels = fluid::algorithm::MelBands(40, p.fft); + auto dct = fluid::algorithm::DCT(40, 13); + + // The NoveltySliceClient inits mels only up to 2k, which I'm not is correct + mels.init(20, 2000, 40, (p.fft / 2) + 1, 44100, p.window); + dct.init(40, 13); + auto makeInput = [&stft, &mels, &dct, &stftFrame, &magnitudes, &melFrame, + &mfccFrame](auto source) { + stft.processFrame(source, stftFrame); + stft.magnitude(stftFrame, magnitudes); + mels.processFrame(magnitudes, melFrame, false, false, true); + dct.processFrame(melFrame, mfccFrame); + return fluid::FluidTensorView(mfccFrame); + }; + + return NoveltyTestHarness(testSignal, p, makeInput); +} + +std::vector +NoveltyPitchTest(fluid::FluidTensorView testSignal, Params p) +{ + FluidTensor, 1> stftFrame((p.fft / 2) + 1); + FluidTensor magnitudes((p.fft / 2) + 1); + FluidTensor pitchFrame(2); + + auto stft = STFT{p.window, p.fft, p.hop}; + auto pitch = fluid::algorithm::YINFFT(); + + auto makeInput = [&stft, &pitch, &stftFrame, &magnitudes, + &pitchFrame](auto source) { + stft.processFrame(source, stftFrame); + stft.magnitude(stftFrame, magnitudes); + pitch.processFrame(magnitudes, pitchFrame, 20, 5000, 44100); + return fluid::FluidTensorView(pitchFrame); + }; + + return NoveltyTestHarness(testSignal, p, makeInput); +} + +std::vector +NoveltyLoudnessTest(fluid::FluidTensorView testSignal, Params p) +{ + FluidTensor loudnessFrame(2); + + auto loudness = fluid::algorithm::Loudness(p.fft); + loudness.init(p.window, 44100); + + auto makeInput = [&loudness, &loudnessFrame](auto source) { + loudness.processFrame(source, loudnessFrame, true, true); + return fluid::FluidTensorView(loudnessFrame); + }; + + return NoveltyTestHarness(testSignal, p, makeInput); +} + +TEST_CASE("NoveltySegmentation will segment on clicks with some predictability", + "[Novelty][slicers]") +{ + + using fluid::index; + + auto monoInput = fluid::testsignals::monoImpulses(); + + Params p; + p.window = 128; + p.fft = 128; + p.hop = 64; + p.threshold = 0.5; + p.minSlice = 2; + p.kernel = 3; + p.filter = 1; + p.dims = (p.fft / 2) + 1; + + // FluidTensor monoInput(testSignal.cols()); + // monoInput = testSignal.row(0); + // monoInput.apply(testSignal.row(1), [](double& x, double y) { x += y; }); + + const std::vector spikePositions = NoveltySTFTTest(monoInput, p); + + const std::vector expected{1000, 12025, 23051, 34076}; + + auto matcher = Catch::Matchers::Approx(expected); + index margin = 128; + matcher.margin(margin); + + REQUIRE(spikePositions.size() == 4); + REQUIRE_THAT(spikePositions, matcher); +} + +TEST_CASE("NoveltySegmentation will segment sine bursts STFT mags accurately", + "[Novelty][slicers]") +{ + using fluid::index; + Params p; + p.window = 512; + p.fft = 1024; + p.hop = 256; + p.threshold = 0.38; + p.minSlice = 4; + p.kernel = 3; + p.filter = 1; + p.dims = (p.fft / 2) + 1; + + const auto testSignal = fluid::testsignals::sharpSines(); + + const std::vector spikePositions = NoveltySTFTTest(testSignal, p); + + const std::vector expected{512, 11008, 22016, 33024}; + REQUIRE(spikePositions.size() == 4); + REQUIRE_THAT(spikePositions, Catch::Matchers::Equals(expected)); +} + +TEST_CASE( + "NoveltySegmentation will do something predictable with a smooth AM sine", + "[Novelty][slicers]") +{ + using fluid::index; + Params p; + p.window = 512; + p.fft = 1024; + p.hop = 256; + p.threshold = 0.34; + p.minSlice = 30; + p.kernel = 3; + p.filter = 1; + p.dims = (p.fft / 2) + 1; + + const auto testSignal = fluid::testsignals::smoothSine(); + + const std::vector spikePositions = NoveltySTFTTest(testSignal, p); + + const std::vector expected{0, 22016}; + REQUIRE(spikePositions.size() == 2); + REQUIRE_THAT(spikePositions, Catch::Matchers::Equals(expected)); +} + +TEST_CASE("NoveltySegmentation behaves with different filter sizes","[Novelty][slicers]"){ + + using fluid::index; + + struct Settings + { + index filterSize; + std::vector expected; + }; + + auto settings = GENERATE( + Settings{1, + {0, 292352, 558592, 563712, 617984, 669696, 722432, 774656, + 826368, 973824, 1000960}}, + Settings{4, + {0, 292352, 564224, 617984, 670208, 722944, 774656, 826880, + 974848, 1000960}}, + Settings{12, + {512, 292352, 564224, 617984, 723456, 774656, 827392, 1000960}}); + + Params p; + p.window = 1024; + p.fft = 1024; + p.hop = 512; + p.threshold = 0.1; + p.minSlice = 2; + p.kernel = 31; + p.filter = settings.filterSize; + p.dims = (p.fft / 2) + 1; + + const auto testSignal = fluid::testsignals::guitarStrums(); + + const std::vector spikePositions = NoveltySTFTTest(testSignal.row(0), p); + CHECK(spikePositions.size() == settings.expected.size()); + REQUIRE_THAT(spikePositions, Catch::Matchers::Equals(settings.expected)); +} + +TEST_CASE("NoveltySegmentation works with MFCC feature","[Novelty][slicers]"){ + + using fluid::index; + + std::vector expected{320, 34880, 105856, 117504, 179200, + 186496, 205248, 223936, 238208, 256448, + 346944, 352512, 368512, 401088, 414016, + 455424, 465600, 481728, 494784, 512640}; + + Params p; + p.window = 2048; + p.fft = 2048; + p.hop = 64; + p.threshold = 0.6; + p.minSlice = 50; + p.kernel = 17; + p.filter = 5; + p.dims = 13; + + const auto testSignal = fluid::testsignals::eurorackSynth(); + + const std::vector spikePositions = NoveltyMFCCTest(testSignal.row(0), p); + CHECK(spikePositions.size() == expected.size()); + REQUIRE_THAT(spikePositions, Catch::Matchers::Equals(expected)); +} + +TEST_CASE("NoveltySegmentation works with pitch feature","[Novelty][slicers]"){ + + + using fluid::index; + + std::vector expected{ + 128, 34880, 47360, 145280, 181888, 186496, 191040, 195648, 200320, + 204928, 230976, 266880, 349056, 354944, 358784, 362688, 367552, 371456, + 375360, 414080, 425728, 465600, 471616, 481664, 487744, 492992}; + + Params p; + p.window = 2048; + p.fft = 2048; + p.hop = 64; + p.threshold = 0.2; + p.minSlice = 50; + p.kernel = 9; + p.filter = 5; + p.dims = 2; + + const auto testSignal = fluid::testsignals::eurorackSynth(); + + const std::vector spikePositions = NoveltyPitchTest(testSignal.row(0), p); + CHECK(spikePositions.size() == expected.size()); + REQUIRE_THAT(spikePositions, Catch::Matchers::Equals(expected)); +} + +TEST_CASE("NoveltySegmentation works with loudness feature","[Novelty][slicers]"){ + + using fluid::index; + + std::vector expected{0, 19008, 24640, 34624, 58240, 117696, + 122048, 179392, 229376, 256832, 260288, 265536, + 287488, 306752, 335616, 401280, 413888, 464896, + 471936, 477184, 483456, 488064, 493376, 513664}; + + Params p; + p.window = 2048; + p.fft = 2048; + p.hop = 64; + p.threshold = 0.0145; + p.minSlice = 50; + p.kernel = 17; + p.filter = 5; + p.dims = 2; + + const auto testSignal = fluid::testsignals::eurorackSynth(); + + const std::vector spikePositions = NoveltyLoudnessTest(testSignal.row(0), p); + CHECK(spikePositions.size() == expected.size()); + REQUIRE_THAT(spikePositions, Catch::Matchers::Equals(expected)); +} diff --git a/tests/algorithms/public/TestOnsetSegmentation.cpp b/tests/algorithms/public/TestOnsetSegmentation.cpp new file mode 100644 index 000000000..fc01df2b4 --- /dev/null +++ b/tests/algorithms/public/TestOnsetSegmentation.cpp @@ -0,0 +1,297 @@ +#define CATCH_CONFIG_MAIN + +#include "SlicerTestHarness.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace fluid { + +using testsignals::drums; +using testsignals::monoDrums; +using testsignals::monoImpulses; +using testsignals::oneImpulse; +using testsignals::stereoImpulses; + +std::vector spikeExpected{22050}; + +using Window = StrongType; +using Hop = StrongType; +using FFT = StrongType; +using Metric = StrongType; +using MinSliceLen = StrongType; +using FilterSize = StrongType; +using Threshold = StrongType; +using FrameDelta = StrongType; +using Data = StrongType, struct DataTag>; +using Expected = StrongType, struct ExpectedTag>; +using Margin = StrongType; + +struct TestParams +{ + Window window; + Hop hop; + FFT fft; + Metric metric; + MinSliceLen minSlice; + FilterSize filterSize; + Threshold threshold; + FrameDelta frameDelta{1}; + Data data; + Expected expected; + Margin margin; +}; + + +std::vector runOneTest(const TestParams& params) +{ + auto makeSlicer = [](const TestParams& p) { + auto res = algorithm::OnsetSegmentation(p.fft); + res.init(p.window, p.fft, p.filterSize); + return res; + }; + + auto invokeSlicer = [](algorithm::OnsetSegmentation& slicer, auto source, + const TestParams& p) { + return slicer.processFrame(source, p.metric, p.filterSize, p.threshold, + p.minSlice, p.frameDelta); + }; + + auto inputFn = [](auto source) { return source; }; + return SlicerTestHarness(params.data, params, makeSlicer, inputFn, + invokeSlicer, 0); // params.hop - (params.window/2)); +} + + +TEST_CASE("OnsetSegmentation can produce the same results as SC tests", + "[OnsetSegmentation][slicers]") +{ + + SECTION("single impulse tests") + { + + + auto metric = GENERATE(as{}, 0, 1, 2, 3, 4, 8, 9); + auto params = + TestParams{Window(1024), + Hop(512), + FFT(1024), + metric, + MinSliceLen(2), + FilterSize(5), + Threshold(0.5), + FrameDelta(0), + Data(FluidTensorView(oneImpulse())), + Expected(spikeExpected), + Margin(34)}; + + INFO("Click Test with Metric " << index(params.metric)); + + auto result = runOneTest(params); + const std::vector& points = params.expected(); + auto matcher = Catch::Matchers::Approx(points); + index margin = params.margin; + matcher.margin(margin); + + CHECK(result.size() == points.size()); + CHECK_THAT(result, matcher); + } + + SECTION("stereo impulses") + { + + auto params = TestParams{Window(512), + Hop(64), + FFT(512), + Metric(9), + MinSliceLen(2), + FilterSize(5), + Threshold(0.1), + FrameDelta(0), + Data(monoImpulses()), + Expected({1000, 12025, 23051, 34076}), + Margin(64)}; + + auto result = runOneTest(params); + const std::vector& points = params.expected(); + auto matcher = Catch::Matchers::Approx(points); + index margin = params.margin; + matcher.margin(margin); + + INFO("stereo impulse test") + CHECK(result.size() == points.size()); + CHECK_THAT(result, matcher); + } + + + SECTION("drum tests") + { + + struct LabelledParams + { + std::string label; + TestParams p; + }; + + auto makeDrumParams = [](std::string label, Metric m, Threshold t, Window w, + Hop h, FFT f, MinSliceLen l, Expected e) { + return LabelledParams{ + label, TestParams{w, h, f, m, l, FilterSize(5), t, FrameDelta(0), + Data(FluidTensorView(monoDrums())), + e, Margin(1)}}; + }; + + auto params = GENERATE_REF( + makeDrumParams( + "test_drums_energy", Metric(0), Threshold(0.5), Window(1024), + Hop(512), FFT(1024), MinSliceLen(2), + Expected({1536, 8192, 38400, 51200, 69632, 88064, 114688, + 151552, 157184, 176640, 202240, 220672, 240128, 252416, + 259072, 276480, 283648, 289792, 296448, 302592, 327168, + 353280, 372736, 390144, 417280})), + makeDrumParams("test_drums_hfc", Metric(1), Threshold(20), Window(512), + Hop(128), FFT(512), MinSliceLen(20), + Expected({1792, 7936, 38784, 51200, 70016, 88320, + 114688, 151808, 157568, 176768, 202368, 221056, + 240256, 252800, 259328, 284032, 289792, 302976, + 327040, 353536, 372608, 390528, 417280})), + makeDrumParams( + "test_drums_SpectralFlux", Metric(2), Threshold(0.2), Window(1000), + Hop(220), FFT(1024), MinSliceLen(2), + Expected({1760, 8580, 38720, 51260, 69960, 88220, + 114620, 151800, 157520, 176660, 202400, 221100, + 240240, 252780, 259380, 284020, 289740, 296560, + 302940, 326920, 353540, 372680, 390500, 417340})), + makeDrumParams( + "test_drums_MKL", Metric(3), Threshold(2), Window(800), Hop(330), + FFT(1024), MinSliceLen(2), + Expected({0, 1650, 69960, 88110, 100650, 114510, 127050, + 146190, 151800, 189420, 202290, 220770, 239910, 252780, + 259380, 277530, 283800, 289740, 302940, 326700, 353430, + 372570, 378840, 403920, 417120, 429990, 449130})), + makeDrumParams( + "test_drums_cosine", Metric(5), Threshold(0.2), Window(1000), + Hop(200), FFT(1024), MinSliceLen(5), + Expected({0, 1600, 38400, 51200, 69800, 88000, + 146200, 151800, 157400, 176400, 202200, 240000, + 243000, 252600, 276400, 280600, 289800, 302800, + 326800, 353400, 390200, 417200, 449000, 453600})), + makeDrumParams( + "test_drums_phase_dev", Metric(6), Threshold(0.1), Window(2000), + Hop(200), FFT(2048), MinSliceLen(5), + Expected({2200, 8800, 40200, 51600, 70600, 115200, 152200, 158400, + 202800, 241000, 253400, 259800, 290200, 303400, 327600, + 354000, 373200, 417600})), + makeDrumParams( + "test_drums_Wphase_dev", Metric(7), Threshold(0.1), Window(1500), + Hop(300), FFT(2048), MinSliceLen(5), + Expected({1800, 8400, 38700, 51300, 70200, 88200, 114600, + 151800, 157500, 165000, 176700, 202500, 221100, 240300, + 252900, 259500, 276900, 284100, 289800, 296400, 303000, + 327300, 353700, 372600, 390600, 417300})), + makeDrumParams("test_drums_complex", Metric(8), Threshold(0.1), + Window(512), Hop(50), FFT(512), MinSliceLen(50), + Expected({1750, 5200, 7900, 38550, 51200, 54650, + 69900, 88150, 114600, 151800, 154550, 157300, + 176550, 202350, 206100, 220950, 240050, 252700, + 259350, 276800, 283900, 289750, 296350, 302850, + 326900, 353500, 372600, 390350, 417250})), + makeDrumParams("test_drums_Rcomplex", Metric(9), Threshold(0.2), + Window(1950), Hop(40), FFT(2048), MinSliceLen(50), + Expected({2040, 9000, 39560, 51760, 89200, 115000, + 152200, 158280, 177480, 202720, 240760, 253120, + 259800, 277840, 284880, 290200, 297560, 303360, + 327440, 354040, 373560, 391360, 417600}))); + + INFO("" << params.label); + + auto result = runOneTest(params.p); + + CHECK(result.size() == params.p.expected().size()); + CHECK_THAT(result, Catch::Equals(params.p.expected())); + } + + SECTION("Test Filtersize") + { + + auto makeDrumParams = [](FilterSize f, Expected e) { + return TestParams{Window(512), + Hop(50), + FFT(512), + Metric(8), + MinSliceLen(50), + f, + Threshold(0.1), + FrameDelta(0), + Data(FluidTensorView(monoDrums())), + e, + Margin(1)}; + }; + + auto params = GENERATE_REF( + makeDrumParams( + FilterSize(3), + Expected({1750, 5200, 8400, 38800, 51200, 53750, 69950, + 88200, 114600, 151800, 157300, 176750, 202350, 204950, + 220950, 240150, 252700, 259350, 276800, 284000, 289750, + 296550, 302850, 326950, 353500, 372600, 390450, 417250})), + makeDrumParams( + FilterSize(7), + Expected({1750, 5200, 7850, 38550, 51200, 54650, + 69900, 76800, 88150, 100800, 114600, 151800, + 157300, 166200, 176550, 202350, 206100, 220950, + 228450, 240050, 252700, 259350, 276800, 283900, + 289750, 296350, 302850, 326900, 332050, 353500, + 372600, 379000, 390350, 404050, 417250, 430250})), + makeDrumParams( + FilterSize(29), + Expected({1750, 7850, 13750, 38550, 46400, 51200, 69900, + 76800, 88150, 100800, 114600, 127300, 151800, 157300, + 164350, 176550, 189650, 202350, 220950, 228400, 240000, + 252700, 259350, 276600, 283900, 289750, 296350, 302850, + 326900, 332050, 353500, 372600, 379000, 390350, 404050, + 417250, 430200, 449350}))); + + INFO("Filter Size " << index(params.filterSize)); + auto result = runOneTest(params); + CHECK(result.size() == params.expected().size()); + CHECK_THAT(result, Catch::Equals(params.expected())); + } + + SECTION("frame delta") + { + auto params = TestParams{ + Window(1000), + Hop(200), + FFT(1024), + Metric(5), + MinSliceLen(5), + FilterSize(7), + Threshold(0.2), + FrameDelta(100), + Data(monoDrums()), + Expected({0, 1600, 38400, 51200, 69800, 88000, 114600, + 146200, 151800, 157400, 176400, 202200, 240000, 243000, + 252600, 276400, 278400, 280600, 289800, 302800, 326800, + 353400, 390200, 404000, 417200, 449000, 453600}), + Margin(1)}; + + INFO("frame delta test"); + auto result = runOneTest(params); + CHECK(result.size() == params.expected().size()); + CHECK_THAT(result, Catch::Equals(params.expected())); + } +} + +} // namespace fluid diff --git a/tests/algorithms/public/TestTransientSlice.cpp b/tests/algorithms/public/TestTransientSlice.cpp new file mode 100644 index 000000000..b50f50104 --- /dev/null +++ b/tests/algorithms/public/TestTransientSlice.cpp @@ -0,0 +1,176 @@ +#define CATCH_CONFIG_MAIN + +#include "SlicerTestHarness.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fluid { + +using Order = StrongType; +using BlockSize = StrongType; +using Padding = StrongType; +using Skew = StrongType; +using ThreshFwd = StrongType; +using ThreshBack = StrongType; +using WindowSize = StrongType; +using ClumpLength = StrongType; +using MinSliceLength = StrongType; + +struct TestParams +{ + Order order; + BlockSize blocksize; + Padding padding; + Skew skew; + ThreshFwd threshfwd; + ThreshBack threshback; + WindowSize windowsize; + ClumpLength clumplength; + MinSliceLength minslicelength; +}; + +std::vector runTest(FluidTensorView testSignal, + TestParams const& p) +{ + auto algo = algorithm::TransientSegmentation(); + algo.init(p.order, p.blocksize, p.padding); + + const double skew = pow(2, p.skew); + + const index maxWinIn = 2 * p.blocksize + p.padding; + const index maxWinOut = maxWinIn; + const index halfWindow = lrint(p.windowsize / 2); + const index latency = p.padding + p.blocksize - p.order; + + algo.setDetectionParameters(skew, p.threshfwd, p.threshback, halfWindow, + p.clumplength, p.minslicelength); + + + const index hopSize = algo.hopSize(); + index nHops = std::ceil(testSignal.size() / hopSize); + FluidTensor paddedInput(testSignal.size() + latency + hopSize); + paddedInput(Slice(latency, testSignal.size())) = testSignal; + + FluidTensor output(hopSize); + + std::vector spikePositions; + + index i{0}; + + for (index i = 0; i < paddedInput.size(); i += hopSize) + { + algo.process(paddedInput(Slice(i, algo.inputSize())), output); + + auto it = std::find_if(output.begin(), output.end(), + [](double x) { return x > 0; }); + while (it != output.end()) + { + spikePositions.push_back(std::distance(output.begin(), it) + i - latency); + it = std::find_if(std::next(it), output.end(), + [](double x) { return x > 0; }); + } + } + + return spikePositions; +} + +TEST_CASE("TransientSlice is predictable on impulses", + "[TransientSlice][slicers]") +{ + auto source = testsignals::monoImpulses(); + auto expected = testsignals::stereoImpulsePositions(); + + auto params = + TestParams{Order(20), BlockSize(256), Padding(128), + Skew(0), ThreshFwd(2), ThreshBack(1.1), + WindowSize(14), ClumpLength(25), MinSliceLength(1000)}; + + auto matcher = Catch::Matchers::Approx(expected); + index margin = 8; + matcher.margin(margin); + + auto result = runTest(source, params); + + CHECK_THAT(result, matcher); +} + + +TEST_CASE("TransientSlice is predictable on sharp sine bursts", + "[TransientSlice][slicers]") +{ + auto source = testsignals::sharpSines(); + auto expected = std::vector{1000, 22050, 33075}; + + auto params = + TestParams{Order(20), BlockSize(256), Padding(128), + Skew(0), ThreshFwd(2), ThreshBack(1.1), + WindowSize(14), ClumpLength(25), MinSliceLength(1000)}; + + auto matcher = Catch::Matchers::Approx(expected); + index margin = 8; + matcher.margin(margin); + + auto result = runTest(source, params); + + CHECK_THAT(result, matcher); +} + + +TEST_CASE("TransientSlice is predictable on real material", + "[TransientSlice][slicers]") +{ + auto source = testsignals::monoEurorackSynth(); + auto expected = std::vector{ + 144, 19188, 34706, 47223, 49465, 58299, 68185, 86942, 105689, + 106751, 117438, 139521, 152879, 161525, 167573, 179045, 186295, 205049, + 223795, 248985, 250356, 256304, 263609, 280169, 297483, 306502, 310674, + 312505, 319114, 327659, 335217, 346778, 364673, 368356, 384718, 400937, + 431226, 433295, 434501, 435764, 439536, 441625, 444028, 445795, 452031, + 453392, 465467, 481514, 494518, 496119, 505754, 512477, 514270}; + + auto params = + TestParams{Order(20), BlockSize(256), Padding(128), + Skew(0), ThreshFwd(2), ThreshBack(1.1), + WindowSize(14), ClumpLength(25), MinSliceLength(1000)}; + + auto matcher = Catch::Matchers::Approx(expected); + index margin = 1; + matcher.margin(margin); + + auto result = runTest(source, params); + + CHECK_THAT(result, matcher); +} + +TEST_CASE("TransientSlice is predictable on real material with heavy settings", + "[TransientSlice][slicers]") +{ + auto source = testsignals::monoEurorackSynth(); + auto expected = std::vector{ + 140, 19182, 34704, 47217, 58297, 68182, 86941, 105688, 117356, + 122134, 139498, 150485, 161516, 167571, 179043, 186293, 205047, 220493}; + + auto params = + TestParams{Order(200), BlockSize(2048), Padding(1024), + Skew(1), ThreshFwd(3), ThreshBack(1), + WindowSize(15), ClumpLength(30), MinSliceLength(4410)}; + + auto matcher = Catch::Matchers::Approx(expected); + index margin = 1; + matcher.margin(margin); + + auto result = runTest(source(Slice(0, 220500)), params); + + CHECK_THAT(result, matcher); +} + + +} // namespace fluid diff --git a/tests/clients/common/TestFluidSource.cpp b/tests/clients/common/TestFluidSource.cpp index 1e59e360d..81241348c 100644 --- a/tests/clients/common/TestFluidSource.cpp +++ b/tests/clients/common/TestFluidSource.cpp @@ -28,34 +28,32 @@ TEMPLATE_TEST_CASE( std::array data; std::array output; -// std::array emptyFrame; -// emptyFrame.fill(0); std::iota(data.begin(), data.end(), 0); // run the test with each of these frame sizes auto frameSize = GENERATE(32, 43, 64, 96, 128, 512); - + // and for each frame size above, we test with these hops - auto hop = GENERATE_REF(int(frameSize / 4), int(frameSize / 3), - int(frameSize / 2), int(frameSize)); - + auto overlap = GENERATE(4, 3, 2, 1); + int hop = frameSize / overlap; FluidTensor expected(data.size() + frameSize); - expected(Slice(frameSize)) = FluidTensorView(data.data(),0,data.size()); + expected(Slice(frameSize)) = + FluidTensorView(data.data(), 0, data.size()); for (int i = 0, j = 0, k = 0; i < data.size() - hostSize; i += hostSize) { - auto input = FluidTensorView{ data.data(), i, 1, hostSize }; + auto input = FluidTensorView{data.data(), i, 1, hostSize}; framer.push(input); - auto outputView = FluidTensorView{ output.data(), 0, 1, frameSize }; - + auto outputView = + FluidTensorView{output.data(), 0, 1, frameSize}; for (; j < hostSize; j += hop, k += hop) { framer.pull(outputView, j); - CHECK_THAT(outputView, EqualsRange(expected(Slice(k,frameSize)))); + CHECK_THAT(outputView, EqualsRange(expected(Slice(k, frameSize)))); } j = j < hostSize ? j : j - hostSize; diff --git a/tests/include/CatchUtils.hpp b/tests/include/CatchUtils.hpp index 274bf0fbb..1c7583044 100644 --- a/tests/include/CatchUtils.hpp +++ b/tests/include/CatchUtils.hpp @@ -1,5 +1,8 @@ // #include // #include + +#pragma once + #include namespace fluid { @@ -34,4 +37,4 @@ auto EqualsRange(Range&& range) -> EqualsRangeMatcher { return EqualsRangeMatcher{std::forward(range)}; } -} \ No newline at end of file +} diff --git a/tests/include/TestUtils.hpp b/tests/include/TestUtils.hpp new file mode 100644 index 000000000..fa21c9a9d --- /dev/null +++ b/tests/include/TestUtils.hpp @@ -0,0 +1,14 @@ +#pragma once + +namespace fluid { +template +struct StrongType +{ + StrongType(T val) : mValue{val} {} + operator const T&() const { return mValue; } + const T& operator()() const { return mValue; } + +private: + T mValue; +}; +} // namespace fluid diff --git a/tests/test_signals/.gitignore b/tests/test_signals/.gitignore new file mode 100644 index 000000000..19b97d72d --- /dev/null +++ b/tests/test_signals/.gitignore @@ -0,0 +1 @@ +Signals.cpp diff --git a/tests/test_signals/CMakeLists.txt b/tests/test_signals/CMakeLists.txt new file mode 100644 index 000000000..30233e41a --- /dev/null +++ b/tests/test_signals/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required (VERSION 3.11) + +get_filename_component(FLUCOMA_CORE_AUDIO + "${CMAKE_CURRENT_SOURCE_DIR}/../../Resources/AudioFiles" ABSOLUTE +) + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Signals.cpp.in ${CMAKE_CURRENT_SOURCE_DIR}/Signals.cpp @ONLY) + +add_library(TestSignals STATIC Signals.cpp) +target_include_directories(TestSignals PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(TestSignals PRIVATE + FLUID_DECOMPOSITION HISSTools_AudioFile +) +target_compile_features(TestSignals PUBLIC cxx_std_14) diff --git a/tests/test_signals/Signals.cpp.in b/tests/test_signals/Signals.cpp.in new file mode 100644 index 000000000..6af28f9dd --- /dev/null +++ b/tests/test_signals/Signals.cpp.in @@ -0,0 +1,161 @@ +#include "Signals.hpp" +#include +#include +#include +#include +#include +#include + +namespace fluid { +namespace testsignals { + +constexpr size_t fs = 44100; + +const std::string audio_path("@FLUCOMA_CORE_AUDIO@"); + +FluidTensor mono(const FluidTensor& x) +{ + FluidTensor monoInput(x.cols()); + monoInput = x.row(0); + for (index i = 1; i < x.rows(); ++i) + monoInput.apply(x.row(i), [](double& x, double y) { x += y; }); + return monoInput; +} + +FluidTensor make_oneImpulse() +{ + FluidTensor oneImpulse(fs); + std::fill(oneImpulse.begin(), oneImpulse.end(), 0); + oneImpulse[(fs / 2) - 1] = 1.0; + return oneImpulse; +} + +std::vector stereoImpulsePositions() +{ + return {1000, 12025, 23051, 34076}; +} + +FluidTensor make_stereoImpulses() +{ + FluidTensor impulses(2, fs); + impulses.fill(0); + // 1000.0, 12025.0, 23051.0, 34076.0 + impulses.row(0)[1000] = 1; + impulses.row(0)[23051] = 1; + + impulses.row(1)[12025] = 1; + impulses.row(1)[34076] = 1; + return impulses; +} + +FluidTensor make_sharpSines() +{ + FluidTensor sharpSines(fs); + std::generate(sharpSines.begin() + 1000, sharpSines.end(), + [i = 1000]() mutable { + constexpr double freq = 640; + double sinx = sin(2 * M_PI * i * freq / (fs - 1)); + constexpr double nPeriods = 4; + double phasor = + (((fs - 1 - i) % index(fs / nPeriods)) / (fs / nPeriods)); + i++; + return sinx * phasor; + }); + return sharpSines; +} + +FluidTensor make_smoothSine() +{ + FluidTensor smoothSine(fs); + std::generate(smoothSine.begin(), smoothSine.end(), [i = 0]() mutable { + double res = sin(2 * M_PI * 320 * i / fs) * fabs(sin(2 * M_PI * i / fs)); + i++; + return res; + }); + return smoothSine; +} + +FluidTensor load(const std::string& audio_path, + const std::string& file) +{ + HISSTools::IAudioFile f(audio_path + "/" + file); + auto e = f.getErrors(); + if (e.size()) + { + throw std::runtime_error(HISSTools::BaseAudioFile::getErrorString(e[0])); + } + + FluidTensor data(f.getChannels(), f.getFrames()); + f.readInterleaved(data.data(), f.getFrames()); + e = f.getErrors(); + if (e.size()) + { + throw std::runtime_error(HISSTools::BaseAudioFile::getErrorString(e[0])); + } + + return data; +} + +FluidTensor& oneImpulse() +{ + static auto resource = make_oneImpulse(); + return resource; +} + +FluidTensor& stereoImpulses() +{ + static auto resource = make_stereoImpulses(); + return resource; +} + +FluidTensor& sharpSines() +{ + static auto resource = make_sharpSines(); + return resource; +} + +FluidTensor& smoothSine() +{ + static auto resource = make_smoothSine(); + return resource; +} + +FluidTensor& guitarStrums() +{ + static auto resource = load(audio_path,"Tremblay-AaS-AcousticStrums-M.wav"); + return resource; +} + +FluidTensor& eurorackSynth() +{ + static auto resource = load(audio_path,"Tremblay-AaS-SynthTwoVoices-M.wav"); + return resource; +} + +FluidTensor& monoEurorackSynth() +{ + static auto resource = mono(eurorackSynth()); + return resource; +} + +FluidTensor& drums() +{ + static auto resource = load(audio_path,"Nicol-LoopE-M.wav"); + return resource; +} + +FluidTensor& monoDrums() +{ + static auto resource = mono(drums()); + return resource; +} + +FluidTensor& monoImpulses() +{ + static auto resource = mono(stereoImpulses()); + return resource; +} + + +} // namespace testsignals +} // namespace fluid diff --git a/tests/test_signals/Signals.hpp b/tests/test_signals/Signals.hpp new file mode 100644 index 000000000..b6ecaf8e5 --- /dev/null +++ b/tests/test_signals/Signals.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace fluid { +namespace testsignals { + +FluidTensor& oneImpulse(); +FluidTensor& stereoImpulses(); + +FluidTensor& sharpSines(); +FluidTensor& smoothSine(); +FluidTensor& guitarStrums(); +FluidTensor& eurorackSynth(); +FluidTensor& monoEurorackSynth(); +FluidTensor& drums(); +FluidTensor& monoDrums(); +FluidTensor& monoImpulses(); + +std::vector stereoImpulsePositions(); +} // namespace testsignals +} // namespace fluid