From fe2fa73cc02aa04b3cd42dda65840fae8fa11156 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 14 Oct 2026 19:23:40 +0200 Subject: [PATCH] Test audio timeline boundaries and sustained SoundTouch playback --- src/call/voice_sources.cmake | 1 + tools/chatgui/CMakeLists.txt | 4 + tools/chatgui/tests/test_call_timeline.cpp | 90 ++++++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 tools/chatgui/tests/test_call_timeline.cpp diff --git a/src/call/voice_sources.cmake b/src/call/voice_sources.cmake index 89c24bc8..195fee34 100644 --- a/src/call/voice_sources.cmake +++ b/src/call/voice_sources.cmake @@ -20,6 +20,7 @@ function(utun_setup_voice LIB_DIR CALL_DIR) ${CALL_DIR}/call_audio.c ${CALL_DIR}/call_tones.c ${CALL_DIR}/call_jitter.cpp + ${CALL_DIR}/call_timeline.cpp ${_src_dir}/radio/radio_jitter.cpp ${_src_dir}/radio/radio_audio.c ${_src_dir}/stream/stream_playback_audio.cpp diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index ea550b9c..eab15ac5 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -449,6 +449,10 @@ add_executable(test_soundtouch_clock tests/test_soundtouch_clock.cpp) target_link_libraries(test_soundtouch_clock PRIVATE utun_voice utun pthread) add_test(NAME test_soundtouch_clock COMMAND test_soundtouch_clock) +add_executable(test_call_timeline tests/test_call_timeline.cpp) +target_link_libraries(test_call_timeline PRIVATE utun_voice utun pthread) +add_test(NAME test_call_timeline COMMAND test_call_timeline) + # тест аудио-движка рации (per-source jitter + микшер + soft-clip) add_executable(test_radio_audio tests/test_radio_audio.cpp diff --git a/tools/chatgui/tests/test_call_timeline.cpp b/tools/chatgui/tests/test_call_timeline.cpp new file mode 100644 index 00000000..684d27f5 --- /dev/null +++ b/tools/chatgui/tests/test_call_timeline.cpp @@ -0,0 +1,90 @@ +#include "call/call_timeline.h" +#include "soundtouch/SoundTouch.h" +#include "../../../lib/debug_config.h" +#include +#include + +#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "timeline test line=%d: %s", __LINE__, #x); exit(1); } } while (0) +using soundtouch::SourceKind; +using soundtouch::SourceError; + +static void test_boundaries() { + CallTimeline map; + map.reset(1); + map.silence(480); + map.progress(1, 240, 5000, 5000, true); + CHECK(!map.clock(5000).valid); + map.feed(1000000, 960, 1); + map.source({SourceKind::Copy, SourceError::None, 0, 0, 0, 960}); + map.read(480); map.silence(480); map.read(480); + map.progress(1, 960, 20000, 20000, true); + auto clock = map.clock(20000); + CHECK(clock.valid && clock.pts_us == 1010000 && clock.written_samples == 1920); + map.progress(1, 1200, 25000, 25000, true); + CHECK(map.clock(25000).pts_us == 1010000); // Локальная тишина. + map.progress(1, 1920, 40000, 30000, true); + clock = map.clock(35000); + CHECK(clock.valid && clock.pts_us == 1015000 && clock.played_samples == 1680); + CHECK(map.clock(40000).pts_us == 1020000); + CHECK(!map.clock(300001).valid); + map.reset(2); + map.progress(1, 1920, 40000, 40000, true); + CHECK(!map.clock(40000).valid); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS timeline: partial PCM, silence, device lead, stale and old epoch"); +} + +static void test_mix() { + CallTimeline map; + map.reset(1); + map.feed(1000000, 960, 7); map.feed(1020000, 960, 7); + map.source({SourceKind::Mix, SourceError::None, 0, 900, 1100, 192}); + map.read(192); + map.progress(1, 96, 10000, 10000, true); + auto clock = map.clock(10000); + CHECK(clock.valid && std::abs(clock.pts_us - (1000000 + 1096 * 1000000 / 48000)) <= 1); + // Ровно на границе двух input spans метаданные сохраняют оба источника. + map.progress(1, 192, 12000, 12000, true); + CHECK(map.clock(12000).pts_us == 1000000 + (1292 * 1000000ll + 24000) / 48000); + map.reset(2); + map.feed(1000000, 960, 7); map.feed(2000000, 960, 8); + map.source({SourceKind::Mix, SourceError::None, 0, 700, 1000, 192}); + map.read(192); map.progress(2, 96, 10000, 10000, true); + CHECK(!map.clock(10000).valid); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS timeline: weighted overlap, input boundary, source discontinuity"); +} + +static void test_long_run() { + CallTimeline map; + map.reset(1); + soundtouch::SoundTouch st; + st.setSampleRate(48000); st.setChannels(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); + CHECK(st.setSourceCallback([](void* p, const soundtouch::SourceSpan& s) { static_cast(p)->source(s); }, &map)); + uint64_t written = 0; + int64_t previous = -1; + float pcm[960], out[777]; + for (unsigned tick = 0; tick < 10000; tick++) { + for (unsigned i = 0; i < 960; i++) pcm[i] = (float)std::sin((tick * 960 + i) * 0.06); + st.setTempo(tick % 2 ? 1.0 : 1.6); + map.feed(1000000 + tick * 20000ll, 960, 1); st.putSamples(pcm, 960); + for (;;) { + unsigned count = st.receiveSamples(out, 777); + if (!count) break; + map.read(count); written += count; + int64_t now = (int64_t)written * 1000000 / 48000; + map.progress(1, written, now, now, true); + auto clock = map.clock(now); + CHECK(clock.valid && clock.pts_us >= previous); + CHECK(std::abs(clock.pts_us - (1000000 + tick * 20000ll)) < 100000); + previous = clock.pts_us; + } + } + DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS timeline: 200s actual SoundTouch, tempo jumps, bounded rings, monotonic clock"); +} + +int main() { + debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); + test_boundaries(); test_mix(); test_long_run(); + return 0; +}