diff --git a/.gitmodules b/.gitmodules index e679824..97bbd46 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,3 +17,6 @@ [submodule "thirdparty/miniaudio"] path = thirdparty/miniaudio url = https://github.com/mackron/miniaudio.git +[submodule "thirdparty/googletest"] + path = thirdparty/googletest + url = https://github.com/google/googletest.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 3471dbe..0f21319 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,9 +76,22 @@ endif() include(CTest) if(BUILD_TESTING) find_package(Python3 REQUIRED COMPONENTS Interpreter) + if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/googletest/CMakeLists.txt") + message(FATAL_ERROR + "GoogleTest submodule is missing. Run: git submodule update --init --recursive") + endif() + set(BUILD_GMOCK OFF CACHE BOOL "Build GoogleMock" FORCE) + set(INSTALL_GTEST OFF CACHE BOOL "Install GoogleTest" FORCE) + set(gtest_force_shared_crt ON CACHE BOOL "Use the shared MSVC runtime" FORCE) + add_subdirectory(thirdparty/googletest EXCLUDE_FROM_ALL) + if(BUILD_SHARED_LIBS) + # GoogleTest only sets this for installed consumers, not build-tree users. + target_compile_definitions(gtest INTERFACE GTEST_LINKED_AS_SHARED_LIBRARY=1) + endif() + if(NOT BUILD_SHARED_LIBS) - add_executable(clap-c-api-allocation-test tests/c-api-allocation-test.cpp) - target_link_libraries(clap-c-api-allocation-test PRIVATE clap) + add_executable(clap-c-api-allocation-test tests/clap-test.cpp) + target_link_libraries(clap-c-api-allocation-test PRIVATE clap GTest::gtest_main) add_test(NAME clap_c_api_allocation COMMAND clap-c-api-allocation-test) endif() add_test(NAME clap_conversion_imports @@ -99,11 +112,14 @@ if(BUILD_TESTING) COMMENT "Generating audio test fixtures" VERBATIM) add_custom_target(clap-audio-fixtures ALL DEPENDS ${CLAP_AUDIO_FIXTURES}) - add_executable(clap-resample-test tests/resample-test.cpp) - target_link_libraries(clap-resample-test PRIVATE clap) - add_test(NAME clap_resample COMMAND clap-resample-test) add_executable(clap-audio-test tests/audio-test.cpp) - target_link_libraries(clap-audio-test PRIVATE clap) + target_link_libraries(clap-audio-test PRIVATE clap GTest::gtest) + if(WIN32 AND BUILD_SHARED_LIBS) + add_custom_command(TARGET clap-audio-test POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "$" "$" + VERBATIM) + endif() add_dependencies(clap-audio-test clap-audio-fixtures) add_test(NAME clap_audio COMMAND clap-audio-test "${CLAP_AUDIO_FIXTURE_DIR}") add_executable(clap-audio-path-test tests/audio-path-test.cpp) @@ -373,7 +389,7 @@ if(BUILD_TESTING) set_tests_properties(clap_embedding_cli PROPERTIES TIMEOUT 30) endif() # Audio preprocessing helpers are private to the core and its tests. - foreach(target IN ITEMS clap-resample-test clap-audio-test clap-audio-path-test + foreach(target IN ITEMS clap-audio-test clap-audio-path-test mert2-fft-test mert-model-test clamp3-bundled-test muq-model-test muq-mulan-model-test) if(TARGET ${target}) target_include_directories(${target} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") diff --git a/tests/audio-test.cpp b/tests/audio-test.cpp index 7504721..90a58cc 100644 --- a/tests/audio-test.cpp +++ b/tests/audio-test.cpp @@ -1,86 +1,226 @@ #include "audio.h" +#include #include #include #include -#include +#include #include +#include +#include #include +#include -static void require(bool value, const std::string &message) { - if (!value) - throw std::runtime_error(message); +namespace { +constexpr double pi = 3.14159265358979323846; +clap::Audio tone(int rate, double frequency) { + clap::Audio audio; + audio.sample_rate = rate; + audio.samples.resize(rate / 10); + for (size_t i = 0; i < audio.samples.size(); ++i) + audio.samples[i] = static_cast(0.5 * std::sin(2 * pi * frequency * i / rate)); + return audio; } -int main(int argc, char **argv) { - try { - require(argc == 2, "usage: clap-audio-test FIXTURE_DIR"); - const std::filesystem::path dir(argv[1]); - for (const auto &name : {"mono.wav", "mono.flac", "mono.mp3", "stereo.wav", "stereo.flac", "stereo.mp3", - "unknown-length.flac", "pcm24.wav", "float.wav"}) { - const auto audio = clap::decode_audio((dir / name).string()); - std::ifstream file(dir / name, std::ios::binary); - const std::vector bytes((std::istreambuf_iterator(file)), {}); - const auto memory = clap::decode_audio(bytes.data(), bytes.size()); - require(memory.sample_rate == audio.sample_rate && memory.samples == audio.samples, - std::string(name) + " memory/file decoding parity"); - require(audio.sample_rate == 48000, std::string(name) + " sample rate"); - require(audio.samples.size() == 4800, std::string(name) + " frame count"); - const bool stereo = std::string(name).find("stereo") == 0; - double error = 0; - for (size_t i = 0; i < audio.samples.size(); ++i) { - require(std::isfinite(audio.samples[i]), std::string(name) + " non-finite sample"); - const double left = 12000.0 / 32768 * std::sin(2 * 3.141592653589793 * 440 * i / 48000); - const double right = 12000.0 / 32768 * std::sin(2 * 3.141592653589793 * 880 * i / 48000); - const double expected = stereo ? (left + right) / 2 : left; - error += std::pow(audio.samples[i] - expected, 2); - } - const double rmse = std::sqrt(error / audio.samples.size()); - const double tolerance = std::string(name).find(".mp3") != std::string::npos ? 0.02 : 0.00002; - require(rmse < tolerance, std::string(name) + " waveform mismatch: " + std::to_string(rmse)); - std::cout << name << " RMSE=" << rmse << '\n'; + +TEST(Resample, PreservesDurationAmplitudeAndPhase) { + for (const auto rates : {std::pair{8000, 48000}, + {16000, 48000}, + {22050, 48000}, + {44100, 48000}, + {96000, 48000}, + {48000, 24000}, + {44100, 24000}, + {16000, 24000}}) { + const int source = rates.first, target = rates.second; + SCOPED_TRACE(testing::Message() << source << " -> " << target); + const auto output = clap::resample_audio(tone(source, 1000), target); + ASSERT_EQ(target, output.sample_rate); + ASSERT_EQ(static_cast(target / 10), output.samples.size()); + // Linear interpolation + causal LPF is not a zero-phase sinc. + // Fit sine/cosine coefficients: check frequency, gain and bounded delay + // independently, without requiring the previous algorithm's phase. + double sine = 0, cosine = 0; + const size_t begin = target / 100, end = target * 9 / 100; // whole periods, after startup + for (size_t i = begin; i < end; ++i) { + const double phase = 2 * pi * 1000 * i / target; + sine += output.samples[i] * std::sin(phase); + cosine += output.samples[i] * std::cos(phase); + } + sine *= 2.0 / (end - begin); + cosine *= 2.0 / (end - begin); + const double gain = std::hypot(sine, cosine) / 0.5; + const double phase = std::atan2(cosine, sine); + double error = 0; + for (size_t i = begin; i < end; ++i) { + const double angle = 2 * pi * 1000 * i / target; + error += std::pow(output.samples[i] - sine * std::sin(angle) - cosine * std::cos(angle), 2); + } + const double rmse = std::sqrt(error / (end - begin)); + std::cout << source << " -> " << target << " gain=" << gain << " phase=" << phase << " residual=" << rmse + << '\n'; + ASSERT_GT(gain, 0.94); + ASSERT_LT(gain, 1.01); + ASSERT_LE(phase, 0.01); + ASSERT_GT(phase, -pi); + ASSERT_LT(rmse, 0.002); + } +} + +TEST(Resample, SuppressesFrequenciesAboveTargetNyquist) { + for (const auto rates : {std::pair{48000, 16000}, {96000, 48000}, {48000, 24000}}) { + SCOPED_TRACE(testing::Message() << rates.first << " -> " << rates.second); + const auto output = clap::resample_audio(tone(rates.first, rates.second * 0.75), rates.second); + ASSERT_GT(output.samples.size(), 256u); + double energy = 0; + for (size_t i = 128; i < output.samples.size() - 128; ++i) + energy += output.samples[i] * output.samples[i]; + const double rms = std::sqrt(energy / (output.samples.size() - 256)); + std::cout << rates.first << " -> " << rates.second << " alias RMS=" << rms << '\n'; + // At least 35 dB attenuation relative to the 0.5-amplitude input tone. + ASSERT_LT(rms, 0.006); + } +} + +TEST(Resample, SameRateOutputIsExact) { + const auto original = tone(48000, 1000); + ASSERT_EQ(original.samples, clap::resample_audio(original).samples); +} + +TEST(Resample, RoundsDurationUpAndPreservesSettledDcGain) { + for (int source : {8000, 44100, 96000}) { + SCOPED_TRACE(source); + const clap::Audio constant{std::vector(source / 10 + 1, 0.25f), source}; + const auto output = clap::resample_audio(constant); + const auto expected = (static_cast(constant.samples.size()) * 48000 + source - 1) / source; + ASSERT_EQ(expected, output.samples.size()); + ASSERT_LT(std::abs(output.samples.front()), 1e-6f); + ASSERT_TRUE(std::all_of(output.samples.begin() + 480, output.samples.end(), + [](float x) { return std::abs(x - 0.25f) < 1e-6f; })); + } +} + +TEST(Resample, HandlesSingleSampleClips) { + for (int source : {8000, 44100, 96000}) { + SCOPED_TRACE(source); + const auto short_clip = clap::resample_audio({{0.5f}, source}); + ASSERT_EQ((48000ULL + source - 1) / source, short_clip.samples.size()); + for (float x : short_clip.samples) + ASSERT_TRUE(std::isfinite(x)); + } +} + +TEST(Resample, BoundsCausalImpulseLatency) { + clap::Audio impulse{std::vector(4800, 0), 16000}; + impulse.samples[100] = 1; + const auto response = clap::resample_audio(impulse); + const auto peak = std::max_element(response.samples.begin(), response.samples.end()); + const auto delay = peak - response.samples.begin() - 300; + std::cout << "16000 -> 48000 impulse peak delay=" << delay << " samples\n"; + ASSERT_GT(delay, 0); + ASSERT_LT(delay, 64); +} + +TEST(Resample, PreservesShortAndFractionalSilence) { + for (const auto rates : {std::pair{44100, 48000}, {48000, 16000}, {96000, 44100}}) { + for (size_t count : {size_t{1}, size_t{2}, size_t{17}, size_t{4097}}) { + SCOPED_TRACE(testing::Message() << rates.first << " -> " << rates.second << " count=" << count); + const auto out = clap::resample_audio({std::vector(count, 0), rates.first}, rates.second); + ASSERT_EQ((count * rates.second + rates.first - 1) / rates.first, out.samples.size()); + for (float x : out.samples) + ASSERT_EQ(0, x); } - const auto other_rate = clap::decode_audio((dir / "44100.flac").string()); - require(other_rate.sample_rate == 44100 && other_rate.samples.size() == 4410, - "decoding must preserve the sample rate"); - // Format detection must use contents, not filename extensions. - const auto decoded = clap::decode_audio((dir / "audio.dat").string()); - require(decoded.samples.size() == 4800, "content-based format detection"); - const auto surround = clap::decode_audio((dir / "surround.wav").string()); - require(surround.samples.size() == 4800, "surround frame count"); - for (float value : surround.samples) - require(std::abs(value - 10500.0f / 32768) < 1e-6f, "equal-weight surround downmix"); - for (const auto &path : {dir / "truncated.wav", dir / "truncated.flac", dir / "nonfinite.wav", - dir / "missing.wav", dir / "invalid.bin", dir / "empty.wav"}) { - bool rejected = false; - try { - clap::decode_audio(path.string()); - } catch (const std::exception &) { - rejected = true; - } - require(rejected, "must reject " + path.string()); - if (std::filesystem::exists(path)) { - std::ifstream file(path, std::ios::binary); - const std::vector bytes((std::istreambuf_iterator(file)), {}); - rejected = false; - try { - clap::decode_audio(bytes.data(), bytes.size()); - } catch (const std::exception &) { - rejected = true; - } - require(rejected, "must reject memory audio " + path.string()); - } + } +} + +TEST(Resample, RejectsInvalidInputs) { + ASSERT_THROW(clap::resample_audio({{}, 48000}, 48000), std::exception); + ASSERT_THROW(clap::resample_audio({{0}, 0}, 48000), std::exception); + ASSERT_THROW(clap::resample_audio({{0}, -1}, 48000), std::exception); + ASSERT_THROW(clap::resample_audio({{0}, 48000}, 0), std::exception); + ASSERT_THROW(clap::resample_audio({{0}, 48000}, -1), std::exception); + ASSERT_THROW(clap::resample_audio({{std::numeric_limits::quiet_NaN()}, 48000}, 48000), std::exception); + ASSERT_THROW(clap::resample_audio({{std::numeric_limits::infinity()}, 44100}, 48000), std::exception); + ASSERT_THROW(clap::resample_audio({{0}, 1}, std::numeric_limits::max()), std::exception); +} +} // namespace + +namespace { +std::filesystem::path fixture_dir; + +TEST(AudioDecode, PreservesWaveformsAndMatchesMemoryDecoding) { + for (const auto &name : {"mono.wav", "mono.flac", "mono.mp3", "stereo.wav", "stereo.flac", "stereo.mp3", + "unknown-length.flac", "pcm24.wav", "float.wav"}) { + SCOPED_TRACE(name); + const auto audio = clap::decode_audio((fixture_dir / name).string()); + std::ifstream file(fixture_dir / name, std::ios::binary); + const std::vector bytes((std::istreambuf_iterator(file)), {}); + const auto memory = clap::decode_audio(bytes.data(), bytes.size()); + ASSERT_EQ(audio.sample_rate, memory.sample_rate); + ASSERT_EQ(audio.samples, memory.samples); + ASSERT_EQ(48000, audio.sample_rate); + ASSERT_EQ(4800u, audio.samples.size()); + const bool stereo = std::string(name).find("stereo") == 0; + double error = 0; + for (size_t i = 0; i < audio.samples.size(); ++i) { + ASSERT_TRUE(std::isfinite(audio.samples[i])) << "sample " << i; + const double left = 12000.0 / 32768 * std::sin(2 * 3.141592653589793 * 440 * i / 48000); + const double right = 12000.0 / 32768 * std::sin(2 * 3.141592653589793 * 880 * i / 48000); + const double expected = stereo ? (left + right) / 2 : left; + error += std::pow(audio.samples[i] - expected, 2); } - for (size_t size : {size_t{0}, size_t{4}}) { - bool rejected = false; - try { - clap::decode_audio(nullptr, size); - } catch (const std::exception &) { - rejected = true; - } - require(rejected, "must reject null audio buffer"); + const double rmse = std::sqrt(error / audio.samples.size()); + const double tolerance = std::string(name).find(".mp3") != std::string::npos ? 0.02 : 0.00002; + ASSERT_LT(rmse, tolerance); + std::cout << name << " RMSE=" << rmse << '\n'; + } +} + +TEST(AudioDecode, PreservesSampleRate) { + const auto other_rate = clap::decode_audio((fixture_dir / "44100.flac").string()); + ASSERT_EQ(44100, other_rate.sample_rate); + ASSERT_EQ(4410u, other_rate.samples.size()); +} + +TEST(AudioDecode, DetectsFormatFromContents) { + const auto decoded = clap::decode_audio((fixture_dir / "audio.dat").string()); + ASSERT_EQ(4800u, decoded.samples.size()); +} + +TEST(AudioDecode, MixesSurroundChannelsWithEqualWeights) { + const auto surround = clap::decode_audio((fixture_dir / "surround.wav").string()); + ASSERT_EQ(4800u, surround.samples.size()); + for (float value : surround.samples) + ASSERT_LT(std::abs(value - 10500.0f / 32768), 1e-6f); +} + +TEST(AudioDecode, RejectsInvalidFilesAndMemoryBuffers) { + for (const auto &path : + {fixture_dir / "truncated.wav", fixture_dir / "truncated.flac", fixture_dir / "nonfinite.wav", + fixture_dir / "missing.wav", fixture_dir / "invalid.bin", fixture_dir / "empty.wav"}) { + SCOPED_TRACE(path.string()); + ASSERT_THROW(clap::decode_audio(path.string()), std::exception); + if (std::filesystem::exists(path)) { + std::ifstream file(path, std::ios::binary); + const std::vector bytes((std::istreambuf_iterator(file)), {}); + ASSERT_THROW(clap::decode_audio(bytes.data(), bytes.size()), std::exception); } - return 0; - } catch (const std::exception &error) { - std::cerr << error.what() << '\n'; + } +} + +TEST(AudioDecode, RejectsNullBuffers) { + for (size_t size : {size_t{0}, size_t{4}}) { + SCOPED_TRACE(size); + ASSERT_THROW(clap::decode_audio(nullptr, size), std::exception); + } +} +} // namespace + +int main(int argc, char **argv) { + testing::InitGoogleTest(&argc, argv); + if (argc != 2 && !GTEST_FLAG_GET(list_tests)) { + std::cerr << "usage: clap-audio-test FIXTURE_DIR\n"; return 1; } + if (argc == 2) + fixture_dir = argv[1]; + return RUN_ALL_TESTS(); } diff --git a/tests/c-api-allocation-test.cpp b/tests/c-api-allocation-test.cpp deleted file mode 100644 index cbb420c..0000000 --- a/tests/c-api-allocation-test.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "clap.h" - -#include -#include -#include - -namespace { -bool fail_allocations = false; -} -void *operator new(std::size_t size) { - if (fail_allocations) - throw std::bad_alloc(); - if (void *p = std::malloc(size ? size : 1)) - return p; - throw std::bad_alloc(); -} -void *operator new[](std::size_t size) { return ::operator new(size); } -void operator delete(void *p) noexcept { std::free(p); } -void operator delete[](void *p) noexcept { std::free(p); } -void operator delete(void *p, std::size_t) noexcept { std::free(p); } -void operator delete[](void *p, std::size_t) noexcept { std::free(p); } - -int main() { - CLAP *model = nullptr; - fail_allocations = true; - const auto status = clap_create("unused.gguf", nullptr, &model); - // Keep allocations disabled while reading the error: reporting must itself - // be allocation-free, not replace bad_alloc with an escaping exception. - const bool valid = - status == CLAP_OUT_OF_MEMORY && model == nullptr && std::strstr(clap_last_error(), "out of memory") != nullptr; - clap_destroy(nullptr); - clap_free(nullptr); - fail_allocations = false; - return valid ? 0 : 1; -} diff --git a/tests/clap-test.cpp b/tests/clap-test.cpp new file mode 100644 index 0000000..d1886f7 --- /dev/null +++ b/tests/clap-test.cpp @@ -0,0 +1,46 @@ +#include "clap.h" + +#include +#include +#include +#include + +namespace { +bool fail_allocations = false; + +struct AllocationFailureScope { + AllocationFailureScope() { fail_allocations = true; } + ~AllocationFailureScope() { fail_allocations = false; } +}; +} // namespace +void *operator new(std::size_t size) { + if (fail_allocations) + throw std::bad_alloc(); + if (void *p = std::malloc(size ? size : 1)) + return p; + throw std::bad_alloc(); +} +void *operator new[](std::size_t size) { return ::operator new(size); } +void operator delete(void *p) noexcept { std::free(p); } +void operator delete[](void *p) noexcept { std::free(p); } +void operator delete(void *p, std::size_t) noexcept { std::free(p); } +void operator delete[](void *p, std::size_t) noexcept { std::free(p); } + +TEST(CApiAllocation, ReportsOutOfMemoryWithoutAllocating) { + CLAP *model = nullptr; + clap_status status; + bool valid_error; + { + AllocationFailureScope failure_scope; + status = clap_create("unused.gguf", nullptr, &model); + // Keep allocations disabled while reading the error: reporting must itself + // be allocation-free, not replace bad_alloc with an escaping exception. + valid_error = std::strstr(clap_last_error(), "out of memory") != nullptr; + clap_destroy(nullptr); + clap_free(nullptr); + } + // GoogleTest may allocate while reporting assertions, so restore allocations first. + EXPECT_EQ(CLAP_OUT_OF_MEMORY, status); + EXPECT_EQ(nullptr, model); + EXPECT_TRUE(valid_error); +} diff --git a/tests/resample-test.cpp b/tests/resample-test.cpp deleted file mode 100644 index eca39f4..0000000 --- a/tests/resample-test.cpp +++ /dev/null @@ -1,132 +0,0 @@ -#include "audio.h" -#include -#include -#include -#include -#include -#include - -namespace { -constexpr double pi = 3.14159265358979323846; -void require(bool condition, const char *message) { - if (!condition) - throw std::runtime_error(message); -} -clap::Audio tone(int rate, double frequency) { - clap::Audio audio; - audio.sample_rate = rate; - audio.samples.resize(rate / 10); - for (size_t i = 0; i < audio.samples.size(); ++i) - audio.samples[i] = static_cast(0.5 * std::sin(2 * pi * frequency * i / rate)); - return audio; -} -void rejected(const clap::Audio &audio, int rate) { - bool failed = false; - try { - clap::resample_audio(audio, rate); - } catch (const std::exception &) { - failed = true; - } - require(failed, "invalid resampling input must be rejected"); -} -} // namespace - -int main() { - try { - for (const auto rates : {std::pair{8000, 48000}, - {16000, 48000}, - {22050, 48000}, - {44100, 48000}, - {96000, 48000}, - {48000, 24000}, - {44100, 24000}, - {16000, 24000}}) { - const int source = rates.first, target = rates.second; - const auto output = clap::resample_audio(tone(source, 1000), target); - require(output.sample_rate == target, "target sample rate"); - require(output.samples.size() == static_cast(target / 10), "duration must be preserved"); - // Linear interpolation + causal LPF is not a zero-phase sinc. - // Fit sine/cosine coefficients: check frequency, gain and bounded delay - // independently, without requiring the previous algorithm's phase. - double sine = 0, cosine = 0; - const size_t begin = target / 100, end = target * 9 / 100; // whole periods, after startup - for (size_t i = begin; i < end; ++i) { - const double phase = 2 * pi * 1000 * i / target; - sine += output.samples[i] * std::sin(phase); - cosine += output.samples[i] * std::cos(phase); - } - sine *= 2.0 / (end - begin); - cosine *= 2.0 / (end - begin); - const double gain = std::hypot(sine, cosine) / 0.5; - const double phase = std::atan2(cosine, sine); - double error = 0; - for (size_t i = begin; i < end; ++i) { - const double angle = 2 * pi * 1000 * i / target; - error += std::pow(output.samples[i] - sine * std::sin(angle) - cosine * std::cos(angle), 2); - } - const double rmse = std::sqrt(error / (end - begin)); - std::cout << source << " -> " << target << " gain=" << gain << " phase=" << phase << " residual=" << rmse - << '\n'; - require(gain > 0.94 && gain < 1.01, "resampled tone amplitude mismatch"); - require(phase <= 0.01 && phase > -pi, "resampled tone delay out of bounds"); - require(rmse < 0.002, "resampled tone frequency/distortion mismatch"); - } - for (const auto rates : {std::pair{48000, 16000}, {96000, 48000}, {48000, 24000}}) { - const auto output = clap::resample_audio(tone(rates.first, rates.second * 0.75), rates.second); - double energy = 0; - for (size_t i = 128; i < output.samples.size() - 128; ++i) - energy += output.samples[i] * output.samples[i]; - const double rms = std::sqrt(energy / (output.samples.size() - 256)); - std::cout << rates.first << " -> " << rates.second << " alias RMS=" << rms << '\n'; - // At least 35 dB attenuation relative to the 0.5-amplitude input tone. - require(rms < 0.006, "downsampling must suppress frequencies above target Nyquist"); - } - const auto original = tone(48000, 1000); - require(clap::resample_audio(original).samples == original.samples, "same-rate output must be exact"); - for (int source : {8000, 44100, 96000}) { - const clap::Audio constant{std::vector(source / 10 + 1, 0.25f), source}; - const auto output = clap::resample_audio(constant); - const auto expected = - (static_cast(constant.samples.size()) * 48000 + source - 1) / source; - require(output.samples.size() == expected, "fractional duration rounds up"); - require(std::abs(output.samples.front()) < 1e-6f, "causal DC startup"); - require(std::all_of(output.samples.begin() + 480, output.samples.end(), - [](float x) { return std::abs(x - 0.25f) < 1e-6f; }), - "settled DC gain must remain unity through end of clip"); - } - for (int source : {8000, 44100, 96000}) { - const auto short_clip = clap::resample_audio({{0.5f}, source}); - require(short_clip.samples.size() == (48000ULL + source - 1) / source, "single-sample duration"); - for (float x : short_clip.samples) - require(std::isfinite(x), "finite single-sample output"); - } - clap::Audio impulse{std::vector(4800, 0), 16000}; - impulse.samples[100] = 1; - const auto response = clap::resample_audio(impulse); - const auto peak = std::max_element(response.samples.begin(), response.samples.end()); - const auto delay = peak - response.samples.begin() - 300; - std::cout << "16000 -> 48000 impulse peak delay=" << delay << " samples\n"; - require(delay > 0 && delay < 64, "causal resampler impulse latency"); - for (const auto rates : {std::pair{44100, 48000}, {48000, 16000}, {96000, 44100}}) { - for (size_t count : {size_t{1}, size_t{2}, size_t{17}, size_t{4097}}) { - const auto out = clap::resample_audio({std::vector(count, 0), rates.first}, rates.second); - require(out.samples.size() == (count * rates.second + rates.first - 1) / rates.first, - "short/fractional duration"); - for (float x : out.samples) - require(x == 0, "silence stays silent"); - } - } - rejected({{}, 48000}, 48000); - rejected({{0}, 0}, 48000); - rejected({{0}, -1}, 48000); - rejected({{0}, 48000}, 0); - rejected({{0}, 48000}, -1); - rejected({{std::numeric_limits::quiet_NaN()}, 48000}, 48000); - rejected({{std::numeric_limits::infinity()}, 44100}, 48000); - rejected({{0}, 1}, std::numeric_limits::max()); - return 0; - } catch (const std::exception &error) { - std::cerr << error.what() << '\n'; - return 1; - } -} diff --git a/thirdparty/googletest b/thirdparty/googletest new file mode 160000 index 0000000..52eb810 --- /dev/null +++ b/thirdparty/googletest @@ -0,0 +1 @@ +Subproject commit 52eb8108c5bdec04579160ae17225d66034bd723