From 811b980883fb36ac68161cec75b2251f50b47fde Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 14 Oct 2026 19:17:46 +0200 Subject: [PATCH] Verify exact SoundTouch provenance and unchanged PCM output --- .../include/soundtouch/SourceSpan.h | 9 +- tools/chatgui/tests/test_soundtouch_clock.cpp | 86 +++++++++++++++++++ 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/lib/soundtouch-master/include/soundtouch/SourceSpan.h b/lib/soundtouch-master/include/soundtouch/SourceSpan.h index a3a78bb2..b6f54341 100644 --- a/lib/soundtouch-master/include/soundtouch/SourceSpan.h +++ b/lib/soundtouch-master/include/soundtouch/SourceSpan.h @@ -13,9 +13,12 @@ enum class SourceKind { Reset, Copy, Mix, Invalid }; enum class SourceError { None, Profile, Reconfigure, Flush, LateAttach }; struct SourceSpan { SourceKind kind; - SourceError error = SourceError::None; - uint64_t output = 0, input0 = 0, input1 = 0; - uint32_t count = 0; + SourceError error; + uint64_t output, input0, input1; + uint32_t count; + SourceSpan(SourceKind k, SourceError e = SourceError::None, uint64_t out = 0, + uint64_t first = 0, uint64_t second = 0, uint32_t length = 0) + : kind(k), error(e), output(out), input0(first), input1(second), count(length) {} }; using SourceCallback = void (*)(void*, const SourceSpan&); } diff --git a/tools/chatgui/tests/test_soundtouch_clock.cpp b/tools/chatgui/tests/test_soundtouch_clock.cpp index dc4adbbd..9b024627 100644 --- a/tools/chatgui/tests/test_soundtouch_clock.cpp +++ b/tools/chatgui/tests/test_soundtouch_clock.cpp @@ -13,6 +13,91 @@ #define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "clock test line=%d: %s", __LINE__, #x); exit(1); } } while (0) struct Span { double first, last, sourceFirst, sourceLast; }; +struct Trace { + std::vector spans; + unsigned resets = 0, invalid = 0; + soundtouch::SourceError reason = soundtouch::SourceError::None; +}; +static void source_event(void* context, const soundtouch::SourceSpan& s) { + auto& trace = *static_cast(context); + if (s.kind == soundtouch::SourceKind::Reset) { trace.spans.clear(); trace.resets++; return; } + if (s.kind == soundtouch::SourceKind::Invalid) { trace.invalid++; trace.reason = s.error; return; } + CHECK(!trace.invalid && s.count); + CHECK(s.output == (trace.spans.empty() ? 0 : trace.spans.back().output + trace.spans.back().count)); + trace.spans.push_back(s); +} + +static void configure(soundtouch::SoundTouch& st, unsigned channels) { + st.setSampleRate(48000); st.setChannels(channels); + st.setSetting(SETTING_USE_QUICKSEEK, 1); st.setSetting(SETTING_SEQUENCE_MS, 20); + st.setSetting(SETTING_SEEKWINDOW_MS, 10); st.setSetting(SETTING_OVERLAP_MS, 4); + st.setSetting(SETTING_USE_AA_FILTER, 0); +} + +/* Независимая проверка: восстановить каждый сэмпл по карте и сравнить с реальным DSP. + * Второй SoundTouch получает тот же вход без callback: выход обязан совпадать побитово. */ +static void check_exact(unsigned channels, unsigned block) { + soundtouch::SoundTouch tracked, plain; + configure(tracked, channels); configure(plain, channels); + Trace trace; + CHECK(tracked.setSourceCallback(source_event, &trace)); + std::vector source, output; + uint32_t random = 17; + for (unsigned tick = 0; tick < 3000; tick++) { + size_t start = source.size(); + for (unsigned i = 0; i < block * channels; i++) { + random ^= random << 13; random ^= random >> 17; random ^= random << 5; + source.push_back((int32_t)(random & 0xffffff) / 16777216.0f - 0.5f); + } + double tempo = tick % 2 ? 1.0 : 1.6; + tracked.setTempo(tempo); plain.setTempo(tempo); + tracked.putSamples(source.data() + start, block); plain.putSamples(source.data() + start, block); + // Оставляем готовый выход до следующего put, затем читаем произвольными частями. + if (tick % 3 == 0) continue; + for (;;) { + float a[1554], b[1554]; + unsigned count = tracked.receiveSamples(a, 777), other = plain.receiveSamples(b, 777); + CHECK(count == other && !std::memcmp(a, b, count * channels * sizeof(float))); + if (!count) break; + output.insert(output.end(), a, a + count * channels); + } + } + unsigned mixed = 0, copied = 0; + for (const auto& s : trace.spans) { + for (unsigned i = 0; i < s.count && (s.output + i) * channels < output.size(); i++) { + CHECK((s.input0 + i + 1) * channels <= source.size()); + for (unsigned ch = 0; ch < channels; ch++) { + float expected = source[(s.input0 + i) * channels + ch]; + if (s.kind == soundtouch::SourceKind::Mix) { + CHECK((s.input1 + i + 1) * channels <= source.size()); + float weight = (float)i / s.count; + expected = expected * (1.0f - weight) + source[(s.input1 + i) * channels + ch] * weight; + } + float actual = output[(s.output + i) * channels + ch]; + CHECK(std::abs(actual - expected) < 0.000002f); + if (s.kind == soundtouch::SourceKind::Copy) CHECK(actual == expected); + } + } + if (s.kind == soundtouch::SourceKind::Mix) mixed++; else copied++; + } + CHECK(mixed > 100 && copied > 100 && trace.resets == 1 && !trace.invalid); + tracked.clear(); plain.clear(); + CHECK(trace.resets == 2 && trace.spans.empty()); + tracked.putSamples(source.data(), 4000); plain.putSamples(source.data(), 4000); + CHECK(!trace.spans.empty() && trace.spans.front().output == 0 && trace.spans.front().input0 == 1); + CHECK(!tracked.setSourceCallback(source_event, &trace) && trace.reason == soundtouch::SourceError::LateAttach); + trace.invalid = 0; + tracked.setRate(1.01); + CHECK(trace.invalid == 1 && trace.reason == soundtouch::SourceError::Profile); + tracked.setRate(1.0); tracked.flush(); + CHECK(trace.invalid == 1); // Invalid однократный, восстановление только через clear. + tracked.clear(); trace.invalid = 0; + tracked.putSamples(source.data(), 4000); + tracked.flush(); + CHECK(trace.invalid == 1 && trace.reason == soundtouch::SourceError::Flush); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS exact source spans channels=%u block=%u copy=%u mix=%u PCM bit-identical", + channels, block, copied, mixed); +} static double position(const std::vector& spans, size_t& index, double output) { while (index + 1 < spans.size() && spans[index].last <= output) index++; const auto& s = spans[index]; @@ -82,5 +167,6 @@ static void check_clock(unsigned mode) { int main() { debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); check_clock(0); check_clock(1); check_clock(2); + check_exact(1, 240); check_exact(1, 960); check_exact(2, 777); return 0; }