diff --git a/.gitmodules b/.gitmodules index 525dc6a4..c3f22321 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "external/simde"] path = external/simde url = https://github.com/simd-everywhere/simde.git +[submodule "external/st_audiofile/thirdparty/wavpack"] + path = external/st_audiofile/thirdparty/wavpack + url = https://github.com/dbry/WavPack.git diff --git a/common.mk b/common.mk index ebcdf76b..27576171 100644 --- a/common.mk +++ b/common.mk @@ -171,6 +171,45 @@ SFIZZ_CXX_FLAGS += \ -I$(SFIZZ_DIR)/external/st_audiofile/thirdparty/dr_libs \ -I$(SFIZZ_DIR)/external/st_audiofile/thirdparty/stb_vorbis +# Wavpack support +SFIZZ_SOURCES += \ + external/st_audiofile/thirdparty/wavpack/src/common_utils.c \ + external/st_audiofile/thirdparty/wavpack/src/decorr_utils.c \ + external/st_audiofile/thirdparty/wavpack/src/entropy_utils.c \ + external/st_audiofile/thirdparty/wavpack/src/extra1.c \ + external/st_audiofile/thirdparty/wavpack/src/extra2.c \ + external/st_audiofile/thirdparty/wavpack/src/open_utils.c \ + external/st_audiofile/thirdparty/wavpack/src/open_filename.c \ + external/st_audiofile/thirdparty/wavpack/src/open_legacy.c \ + external/st_audiofile/thirdparty/wavpack/src/open_raw.c \ + external/st_audiofile/thirdparty/wavpack/src/pack.c \ + external/st_audiofile/thirdparty/wavpack/src/pack_dns.c \ + external/st_audiofile/thirdparty/wavpack/src/pack_floats.c \ + external/st_audiofile/thirdparty/wavpack/src/pack_utils.c \ + external/st_audiofile/thirdparty/wavpack/src/read_words.c \ + external/st_audiofile/thirdparty/wavpack/src/tags.c \ + external/st_audiofile/thirdparty/wavpack/src/tag_utils.c \ + external/st_audiofile/thirdparty/wavpack/src/unpack.c \ + external/st_audiofile/thirdparty/wavpack/src/unpack_floats.c \ + external/st_audiofile/thirdparty/wavpack/src/unpack_seek.c \ + external/st_audiofile/thirdparty/wavpack/src/unpack_utils.c \ + external/st_audiofile/thirdparty/wavpack/src/write_words.c \ + external/st_audiofile/thirdparty/wavpack/src/pack_dsd.c \ + external/st_audiofile/thirdparty/wavpack/src/unpack_dsd.c \ + external/st_audiofile/thirdparty/wavpack/src/unpack3.c \ + external/st_audiofile/thirdparty/wavpack/src/unpack3_open.c \ + external/st_audiofile/thirdparty/wavpack/src/unpack3_seek.c + +SFIZZ_C_FLAGS += \ + -I$(SFIZZ_DIR)/external/st_audiofile/thirdparty/wavpack/include \ + -I$(SFIZZ_DIR)/external/st_audiofile/thirdparty/wavpack/src \ + -DENABLE_DSD -DENABLE_LEGACY + +SFIZZ_CXX_FLAGS += \ + -I$(SFIZZ_DIR)/external/st_audiofile/thirdparty/wavpack/include \ + -I$(SFIZZ_DIR)/external/st_audiofile/thirdparty/wavpack/src \ + -DENABLE_DSD -DENABLE_LEGACY + ifeq ($(SFIZZ_USE_SNDFILE),1) SFIZZ_C_FLAGS += $(SFIZZ_SNDFILE_C_FLAGS) -DST_AUDIO_FILE_USE_SNDFILE=1 SFIZZ_CXX_FLAGS += $(SFIZZ_SNDFILE_CXX_FLAGS) -DST_AUDIO_FILE_USE_SNDFILE=1 diff --git a/external/st_audiofile/CMakeLists.txt b/external/st_audiofile/CMakeLists.txt index 06b5251b..4fcceabd 100644 --- a/external/st_audiofile/CMakeLists.txt +++ b/external/st_audiofile/CMakeLists.txt @@ -22,6 +22,9 @@ target_include_directories(st_audiofile_formats add_subdirectory("thirdparty/libaiff" EXCLUDE_FROM_ALL) target_link_libraries(st_audiofile_formats PUBLIC aiff::aiff) +add_subdirectory("thirdparty/wavpack" EXCLUDE_FROM_ALL) +target_link_libraries(st_audiofile_formats PUBLIC wavpack) + ### if(NOT ST_AUDIO_FILE_USE_SNDFILE) target_link_libraries(st_audiofile PRIVATE st_audiofile_formats) diff --git a/external/st_audiofile/src/st_audiofile.c b/external/st_audiofile/src/st_audiofile.c index 0659c3e7..1abc51da 100644 --- a/external/st_audiofile/src/st_audiofile.c +++ b/external/st_audiofile/src/st_audiofile.c @@ -9,6 +9,8 @@ #include "st_audiofile_libs.h" #include +#define WAVPACK_MEMORY_ASSUMED_VERSION 5 + struct st_audio_file { int type; union { @@ -17,12 +19,14 @@ struct st_audio_file { AIFF_Ref aiff; drmp3 *mp3; stb_vorbis* ogg; + WavpackContext* wv; }; union { struct { uint32_t channels; float sample_rate; uint64_t frames; } aiff; struct { uint64_t frames; } mp3; struct { uint32_t channels; float sample_rate; uint64_t frames; } ogg; + struct { uint32_t channels; float sample_rate; uint64_t frames; int bitrate; int mode; } wv; } cache; union { @@ -173,6 +177,25 @@ static st_audio_file* st_generic_open_file(const void* filename, int widepath) } } + // Try WV + { + af->wv = +#if defined(_WIN32) + WavpackOpenFileInput((const char*)filename, NULL, OPEN_FILE_UTF8, 0); +#else + WavpackOpenFileInput((const char*)filename, NULL, 0, 0); +#endif + if (af->wv) { + af->cache.wv.channels = (uint32_t)WavpackGetNumChannels(af->wv); + af->cache.wv.sample_rate = (float)WavpackGetSampleRate(af->wv); + af->cache.wv.frames = (uint64_t)WavpackGetNumSamples64(af->wv); + af->cache.wv.bitrate = WavpackGetBitsPerSample(af->wv); + af->cache.wv.mode = WavpackGetMode(af->wv); + af->type = st_audio_file_wv; + return af; + } + } + free(af); return NULL; } @@ -278,6 +301,17 @@ st_audio_file* st_open_memory(const void* memory, size_t length) } } + // Try WV + { + af->wv = + WavpackOpenRawDecoder((void*)memory, (int32_t)length, NULL, 0, + WAVPACK_MEMORY_ASSUMED_VERSION, NULL, 0, 0); + if (af->wv) { + af->type = st_audio_file_wv; + return af; + } + } + free(af); return NULL; } @@ -315,6 +349,9 @@ void st_close(st_audio_file* af) drmp3_uninit(af->mp3); free(af->mp3); break; + case st_audio_file_wv: + WavpackCloseFile(af->wv); + break; } free(af); @@ -345,6 +382,9 @@ uint32_t st_get_channels(st_audio_file* af) case st_audio_file_mp3: channels = af->mp3->channels; break; + case st_audio_file_wv: + channels = af->cache.wv.channels; + break; } return channels; @@ -370,6 +410,9 @@ float st_get_sample_rate(st_audio_file* af) case st_audio_file_mp3: sample_rate = af->mp3->sampleRate; break; + case st_audio_file_wv: + sample_rate = af->cache.wv.sample_rate; + break; } return sample_rate; @@ -395,6 +438,9 @@ uint64_t st_get_frame_count(st_audio_file* af) case st_audio_file_mp3: frames = af->cache.mp3.frames; break; + case st_audio_file_wv: + frames = af->cache.wv.frames; + break; } return frames; @@ -420,6 +466,9 @@ bool st_seek(st_audio_file* af, uint64_t frame) case st_audio_file_mp3: success = drmp3_seek_to_pcm_frame(af->mp3, frame); break; + case st_audio_file_wv: + success = WavpackSeekSample64(af->wv, (int64_t)frame); + break; } return success; @@ -449,6 +498,25 @@ uint64_t st_read_s16(st_audio_file* af, int16_t* buffer, uint64_t count) case st_audio_file_mp3: count = drmp3_read_pcm_frames_s16(af->mp3, count, buffer); break; + case st_audio_file_wv: + { + uint32_t channels = af->cache.wv.channels; + int32_t* buf_i32 = (int32_t*)malloc(4 * channels * count); + if (!buf_i32) { + return 0; + } + count = channels * WavpackUnpackSamples(af->wv, buf_i32, (uint32_t)count); + if (af->cache.wv.mode & MODE_FLOAT) { + drwav_f32_to_s16((drwav_int16*)buffer, (float*)buf_i32, (size_t)count); + } else { + int d = af->cache.wv.bitrate - 16; + for (uint64_t i = 0; i < count; i++) { + buffer[i] = (int16_t)(buf_i32[i] >> d); + } + } + free(buf_i32); + } + break; } return count; @@ -478,6 +546,28 @@ uint64_t st_read_f32(st_audio_file* af, float* buffer, uint64_t count) case st_audio_file_mp3: count = drmp3_read_pcm_frames_f32(af->mp3, count, buffer); break; + case st_audio_file_wv: + if (af->cache.wv.mode & MODE_FLOAT) { + count = WavpackUnpackSamples(af->wv, (int32_t*)buffer, (uint32_t)count); + } else { + uint32_t channels = af->cache.wv.channels; + int32_t* buf_i32 = (int32_t*)malloc(4 * channels * count); + if (!buf_i32) { + return 0; + } + count = channels * WavpackUnpackSamples(af->wv, buf_i32, (uint32_t)count); + if (!(af->cache.wv.mode & MODE_FLOAT)) { + if (af->cache.wv.bitrate < 32) { + int d = 32 - af->cache.wv.bitrate; + for (uint64_t i = 0; i < count; i++) { + buf_i32[i] <<= d; + } + } + drwav_s32_to_f32(buffer, (drwav_int32*)buf_i32, (size_t)count); + } + free(buf_i32); + } + break; } return count; diff --git a/external/st_audiofile/src/st_audiofile.h b/external/st_audiofile/src/st_audiofile.h index f4f7d252..787cc332 100644 --- a/external/st_audiofile/src/st_audiofile.h +++ b/external/st_audiofile/src/st_audiofile.h @@ -31,6 +31,7 @@ typedef enum st_audio_file_type { st_audio_file_aiff, st_audio_file_ogg, st_audio_file_mp3, + st_audio_file_wv, st_audio_file_other, } st_audio_file_type; diff --git a/external/st_audiofile/src/st_audiofile_common.c b/external/st_audiofile/src/st_audiofile_common.c index c3e14297..0ab77bc4 100644 --- a/external/st_audiofile/src/st_audiofile_common.c +++ b/external/st_audiofile/src/st_audiofile_common.c @@ -32,6 +32,9 @@ const char* st_type_string(int type) case st_audio_file_mp3: type_string = "MP3"; break; + case st_audio_file_wv: + type_string = "WV"; + break; case st_audio_file_other: type_string = "other"; break; diff --git a/external/st_audiofile/src/st_audiofile_libs.h b/external/st_audiofile/src/st_audiofile_libs.h index 67aacaba..f60b9f6b 100644 --- a/external/st_audiofile/src/st_audiofile_libs.h +++ b/external/st_audiofile/src/st_audiofile_libs.h @@ -15,6 +15,7 @@ #endif #include "stb_vorbis.c" #include "libaiff/libaiff.h" +#include "wavpack.h" #if defined(_WIN32) #include diff --git a/external/st_audiofile/thirdparty/wavpack b/external/st_audiofile/thirdparty/wavpack new file mode 160000 index 00000000..36b08dbc --- /dev/null +++ b/external/st_audiofile/thirdparty/wavpack @@ -0,0 +1 @@ +Subproject commit 36b08dbcb1de136e9ae477e9f1e2b57c958fff18 diff --git a/src/sfizz/import/foreign_instruments/AudioFile.cpp b/src/sfizz/import/foreign_instruments/AudioFile.cpp index 4fa897ea..65457cb1 100644 --- a/src/sfizz/import/foreign_instruments/AudioFile.cpp +++ b/src/sfizz/import/foreign_instruments/AudioFile.cpp @@ -14,7 +14,7 @@ namespace sfz { static const char* kRecognizedAudioExtensions[] = { - ".wav", ".flac", ".ogg", ".mp3", ".aif", ".aiff", ".aifc", + ".wav", ".flac", ".ogg", ".mp3", ".aif", ".aiff", ".aifc", ".wv", }; /// diff --git a/tests/AudioFilesT.cpp b/tests/AudioFilesT.cpp new file mode 100644 index 00000000..f888c650 --- /dev/null +++ b/tests/AudioFilesT.cpp @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "AudioSpan.h" +#include "absl/types/span.h" +#include "sfizz/Synth.h" +#include "TestHelpers.h" +#include "catch2/catch.hpp" +#include +#include +using namespace Catch::literals; + +TEST_CASE("[AudioFiles] No leakage on right") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, static_cast(synth.getSamplesPerBlock()) }; + std::vector zeros(synth.getSamplesPerBlock(), 0); + sfz::AudioSpan span { buffer }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/wavpack.sfz", R"( + sample=kick.wav key=60 pan=-100 + )"); + synth.noteOn(0, 60, 127); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + REQUIRE( approxEqual(span.getConstSpan(1), absl::MakeConstSpan(zeros), 1e-2f) ); + while (numPlayingVoices(synth) > 0) { + synth.renderBlock(buffer); + REQUIRE( approxEqual(span.getConstSpan(1), absl::MakeConstSpan(zeros), 1e-2f) ); + } +} + +TEST_CASE("[AudioFiles] WavPack file") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, static_cast(synth.getSamplesPerBlock()) }; + sfz::AudioSpan span { buffer }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/wavpack.sfz", R"( + sample=kick.wav key=60 pan=-100 + sample=kick.wv key=60 pan=100 + )"); + synth.noteOn(0, 60, 127); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); + REQUIRE( approxEqual(span.getConstSpan(0), span.getConstSpan(1)) ); + while (numPlayingVoices(synth) > 0) { + synth.renderBlock(buffer); + REQUIRE( approxEqual(span.getConstSpan(0), span.getConstSpan(1)) ); + } +} + +TEST_CASE("[AudioFiles] Flac file") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, static_cast(synth.getSamplesPerBlock()) }; + sfz::AudioSpan span { buffer }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/wavpack.sfz", R"( + sample=kick.wav key=60 pan=-100 + sample=kick.flac key=60 pan=100 + )"); + synth.noteOn(0, 60, 127); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); + REQUIRE( approxEqual(span.getConstSpan(0), span.getConstSpan(1)) ); + while (numPlayingVoices(synth) > 0) { + synth.renderBlock(buffer); + REQUIRE( approxEqual(span.getConstSpan(0), span.getConstSpan(1)) ); + } +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8553aca9..db17ba1d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -57,6 +57,7 @@ set(SFIZZ_TEST_SOURCES MessagingT.cpp OversamplerT.cpp MemoryT.cpp + AudioFilesT.cpp DataHelpers.h DataHelpers.cpp ) diff --git a/tests/EventEnvelopesT.cpp b/tests/EventEnvelopesT.cpp index 01287d3e..60bd9830 100644 --- a/tests/EventEnvelopesT.cpp +++ b/tests/EventEnvelopesT.cpp @@ -7,6 +7,7 @@ #include "sfizz/ModifierHelpers.h" #include "sfizz/Buffer.h" #include "catch2/catch.hpp" +#include "TestHelpers.h" #include #include #include @@ -14,20 +15,6 @@ #include using namespace Catch::literals; -template -inline bool approxEqual(absl::Span lhs, absl::Span rhs, Type eps = 1e-3) -{ - if (lhs.size() != rhs.size()) - return false; - - for (size_t i = 0; i < rhs.size(); ++i) - if (rhs[i] != Approx(lhs[i]).epsilon(eps)) { - std::cerr << lhs[i] << " != " << rhs[i] << " at index " << i << '\n'; - return false; - } - - return true; -} const auto idModifier = [](float x) { return x; }; const auto twiceModifier = [](float x) { return 2 * x; }; diff --git a/tests/SIMDHelpersT.cpp b/tests/SIMDHelpersT.cpp index 6bdf8cf6..4bac07b1 100644 --- a/tests/SIMDHelpersT.cpp +++ b/tests/SIMDHelpersT.cpp @@ -7,6 +7,7 @@ #include "sfizz/simd/Common.h" #include "sfizz/SIMDHelpers.h" #include "sfizz/Panning.h" +#include "TestHelpers.h" #include "catch2/catch.hpp" #include #include @@ -20,39 +21,9 @@ template using aligned_vector = std::vector>; constexpr int bigBufferSize { 4095 }; -constexpr int medBufferSize { 127 }; +constexpr int medBufferSize { 1023 }; constexpr float fillValue { 1.3f }; -template -inline bool approxEqualMargin(absl::Span lhs, absl::Span rhs, Type eps = 1e-3) -{ - if (lhs.size() != rhs.size()) - return false; - - for (size_t i = 0; i < rhs.size(); ++i) - if (rhs[i] != Approx(lhs[i]).margin(eps)) { - std::cerr << lhs[i] << " != " << rhs[i] << " at index " << i << '\n'; - return false; - } - - return true; -} - -template -inline bool approxEqual(absl::Span lhs, absl::Span rhs, Type eps = 1e-3) -{ - if (lhs.size() != rhs.size()) - return false; - - for (size_t i = 0; i < rhs.size(); ++i) - if (rhs[i] != Approx(lhs[i]).epsilon(eps)) { - std::cerr << lhs[i] << " != " << rhs[i] << " at index " << i << '\n'; - return false; - } - - return true; -} - TEST_CASE("[Helpers] willAlign, prevAligned and unaligned tests") { aligned_vector array(16); @@ -430,8 +401,8 @@ TEST_CASE("[Helpers] Linear Ramp (SIMD)") TEST_CASE("[Helpers] Linear Ramp (SIMD vs scalar)") { const float start { 0.0f }; - std::vector outputScalar(bigBufferSize); - std::vector outputSIMD(bigBufferSize); + std::vector outputScalar(medBufferSize); + std::vector outputSIMD(medBufferSize); sfz::setSIMDOpStatus(sfz::SIMDOps::linearRamp, false); sfz::linearRamp(absl::MakeSpan(outputScalar), start, fillValue); sfz::setSIMDOpStatus(sfz::SIMDOps::linearRamp, true); @@ -442,8 +413,8 @@ TEST_CASE("[Helpers] Linear Ramp (SIMD vs scalar)") TEST_CASE("[Helpers] Linear Ramp unaligned (SIMD vs scalar)") { const float start { 0.0f }; - std::vector outputScalar(bigBufferSize); - std::vector outputSIMD(bigBufferSize); + std::vector outputScalar(medBufferSize); + std::vector outputSIMD(medBufferSize); sfz::setSIMDOpStatus(sfz::SIMDOps::linearRamp, false); sfz::linearRamp(absl::MakeSpan(outputScalar).subspan(1), start, fillValue); sfz::setSIMDOpStatus(sfz::SIMDOps::linearRamp, true); diff --git a/tests/SmoothersT.cpp b/tests/SmoothersT.cpp index 63c05141..342da8e3 100644 --- a/tests/SmoothersT.cpp +++ b/tests/SmoothersT.cpp @@ -7,6 +7,7 @@ #include "sfizz/OnePoleFilter.h" #include "sfizz/Smoothers.h" #include "catch2/catch.hpp" +#include "TestHelpers.h" // #include "cnpy.h" // #include "ghc/fs_std.hpp" #include @@ -16,21 +17,6 @@ #include using namespace Catch::literals; -template -inline bool approxEqual(const std::array& lhs, const std::array& rhs, Type epsilon = 1e-3) -{ - if (lhs.size() != rhs.size()) - return false; - - for (size_t i = 0; i < rhs.size(); ++i) - if (lhs[i] != Approx(rhs[i]).epsilon(epsilon)) { - std::cerr << lhs[i] << " != " << rhs[i] << " at index " << i << '\n'; - return false; - } - - return true; -} - template void testFilter(const std::array& input, const std::array& expectedLow, const std::array& expectedHigh, Type gain) { @@ -43,19 +29,19 @@ void testFilter(const std::array& input, const std::array& exp sfz::OnePoleFilter filter; filter.setGain(gain); filter.processLowpass(input, outputSpan); - REQUIRE(approxEqual(output, expectedLow)); + REQUIRE(approxEqual(absl::MakeConstSpan(output), absl::MakeConstSpan(expectedLow))); filter.reset(); filter.processLowpass(input, outputSpan, gains); - REQUIRE(approxEqual(output, expectedLow)); + REQUIRE(approxEqual(absl::MakeConstSpan(output), absl::MakeConstSpan(expectedLow))); filter.reset(); filter.processHighpass(input, outputSpan); - REQUIRE(approxEqual(output, expectedHigh)); + REQUIRE(approxEqual(absl::MakeConstSpan(output), absl::MakeConstSpan(expectedHigh))); filter.reset(); filter.processHighpass(input, outputSpan, gains); - REQUIRE(approxEqual(output, expectedHigh)); + REQUIRE(approxEqual(absl::MakeConstSpan(output), absl::MakeConstSpan(expectedHigh))); } constexpr std::array floatInput01 = { diff --git a/tests/TestFiles/kick.flac b/tests/TestFiles/kick.flac new file mode 100755 index 00000000..79a2f4e6 Binary files /dev/null and b/tests/TestFiles/kick.flac differ diff --git a/tests/TestFiles/kick.wv b/tests/TestFiles/kick.wv new file mode 100644 index 00000000..a00e5d9f Binary files /dev/null and b/tests/TestFiles/kick.wv differ diff --git a/tests/TestHelpers.h b/tests/TestHelpers.h index 8faa28b5..37d10de1 100644 --- a/tests/TestHelpers.h +++ b/tests/TestHelpers.h @@ -12,6 +12,8 @@ #include "sfizz/Messaging.h" #include "catch2/catch.hpp" #include "sfizz/modulations/ModKey.h" +#include +#include class RegionCCView { public: @@ -148,18 +150,52 @@ std::string createDefaultGraph(std::vector lines, int numRegions = std::string createModulationDotGraph(std::vector lines); template -inline bool approxEqual(absl::Span lhs, absl::Span rhs, Type eps = 1e-3) +bool approxEqual(absl::Span lhs, absl::Span rhs, Type eps = 1e-3) { if (lhs.size() != rhs.size()) return false; + bool different = false; for (size_t i = 0; i < rhs.size(); ++i) - if (rhs[i] != Approx(lhs[i]).epsilon(eps)) { - std::cerr << lhs[i] << " != " << rhs[i] << " at index " << i << '\n'; - return false; + if (rhs[i] != Approx(lhs[i]).margin(eps)) { + std::cerr << lhs[i] << " != " << rhs[i] << " (delta " << std::abs(rhs[i] - lhs[i]) << ") at index " << i << '\n'; + different = true; + break; } - return true; + auto print_span = [](absl::Span span) { + if (span.empty()) { + std::cerr << "{ }" << '\n'; + return; + } + + std::cerr << "{ "; + if (span.size() < 16) { + for (unsigned i = 0; i < span.size() - 1; ++i) { + std::cerr << span[i] << ", "; + } + } else { + for (unsigned i = 0; i < 8; ++i) { + std::cerr << span[i] << ", "; + } + std::cerr << "..., "; + for (unsigned i = span.size() - 8; i < span.size() - 1; ++i) { + std::cerr << span[i] << ", "; + } + } + std::cerr << span.back() << " }" << '\n'; + }; + + if (different) { + std::cerr << std::setprecision(3) << std::fixed; + std::cerr << "Differences between spans" << '\n'; + std::cerr << "lhs: "; + print_span(lhs); + std::cerr << "rhs: "; + print_span(rhs); + } + + return !different; } /**