From f054f9c8d99e789a0984c8ca9b10e46061a1c2d7 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 8 Oct 2026 14:30:21 +0200 Subject: [PATCH] test: cover radio receive guard timing and playback progress --- tools/chatgui/CMakeLists.txt | 2 +- tools/chatgui/tests/test_voice_audio_io.cpp | 15 ++++ tools/chatgui/tests/test_voice_tx.cpp | 81 +++++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 440a265e..d83b9b8c 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -456,7 +456,7 @@ if(UNIX AND NOT APPLE) target_link_options(test_voice_tx PRIVATE "-Wl,--wrap=radio_talk_begin" "-Wl,--wrap=radio_talk_end" "-Wl,--wrap=radio_talk_send" "-Wl,--wrap=call_send_media" "-Wl,--wrap=chat_setting_get_int" "-Wl,--wrap=silero_vad_create_default" "-Wl,--wrap=silero_vad_destroy" "-Wl,--wrap=silero_vad_reset" - "-Wl,--wrap=silero_vad_process") + "-Wl,--wrap=silero_vad_process" "-Wl,--wrap=get_time_tb") add_test(NAME test_voice_tx COMMAND test_voice_tx) endif() diff --git a/tools/chatgui/tests/test_voice_audio_io.cpp b/tools/chatgui/tests/test_voice_audio_io.cpp index cad83e53..f7af6719 100644 --- a/tools/chatgui/tests/test_voice_audio_io.cpp +++ b/tools/chatgui/tests/test_voice_audio_io.cpp @@ -21,6 +21,10 @@ static std::vector masks; static bool talking; static std::atomic pulls{0}, blocks{0}; static std::atomic stall{false}, stalled{false}; +static std::atomic output_position{0}, output_delay{0}; +extern "C" void radio_audio_playback_progress(uint64_t, uint64_t position, uint64_t delay) { + output_delay = delay; output_position = position; +} extern "C" int call_audio_pull_pcm(uint64_t, int16_t* out, int count) { ++pulls; std::fill_n(out, count, 1000); return count; } extern "C" int radio_audio_pull_pcm(uint64_t, int16_t* out, int count) { ++pulls; for (int i = 0; i < count; ++i) out[i] = i % 2 ? 2000 : 1000; return count; @@ -54,6 +58,17 @@ int main() { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); VoiceAudioIo voice; std::array pcm{}; pcm.fill(1234); + // Выдача RX worker не подтверждает вывод; частичный callback сохраняет позицию и backend delay. + REQUIRE(voice.start(VoiceAudioIo::Kind::Radio, 7)); + await([] { return pulls.load() >= 2; }); + REQUIRE(output_position == 0); + float partial[1920]{}; + voice.mix(partial, 240, now_us() + 500000); + await([] { return output_position.load() == 240; }); + REQUIRE(output_delay > 4500); + voice.mix(partial, 720, now_us()); + await([] { return output_position.load() == 960; }); + voice.stop(); voice.start(VoiceAudioIo::Kind::Radio, 7); REQUIRE(voice.startCapture(1, false)); voice.capture(pcm.data(), 240, 0); voice.setPtt(true); voice.capture(pcm.data(), 360, 0); voice.setPtt(false); voice.capture(pcm.data(), 360, 0); diff --git a/tools/chatgui/tests/test_voice_tx.cpp b/tools/chatgui/tests/test_voice_tx.cpp index 9a48fa75..6e9bd2a3 100644 --- a/tools/chatgui/tests/test_voice_tx.cpp +++ b/tools/chatgui/tests/test_voice_tx.cpp @@ -30,6 +30,9 @@ static int radio_compressor_enabled; static bool observe_vad; static std::vector vad_input; static std::atomic stall_vad{false}, vad_stalled{false}; +static std::atomic test_time{0}; +extern "C" uint64_t __real_get_time_tb(void); +extern "C" uint64_t __wrap_get_time_tb(void) { uint64_t now = test_time.load(); return now ? now : __real_get_time_tb(); } extern "C" silero_vad_t* __wrap_silero_vad_create_default() { return reinterpret_cast(1); } extern "C" void __wrap_silero_vad_destroy(silero_vad_t*) {} extern "C" void __wrap_silero_vad_reset(silero_vad_t*) {} @@ -291,6 +294,83 @@ static void test_vad_diagnostics(UTUN_INSTANCE* instance) { puts("voice TX: VAD/radio logs identify confirmation reset, speech start/stop, RX blocking and manual override PASS"); } +/* Настоящие RX/jitter/TX, управляемые часы: FIN не означает окончание слышимого голоса. */ +static void test_vad_receive_guard(UTUN_INSTANCE* instance) { + debug_set_category_level(DEBUG_CATEGORY_VAD, DEBUG_LEVEL_DEBUG); + debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_DEBUG); + test_time = __real_get_time_tb(); + auto* encoder = opus_codec_encoder_create(48000, 1); REQUIRE(encoder); + std::array speech; speech.fill(1200); + uint8_t opus[256]; int len = opus_codec_encode(encoder, speech.data(), 960, opus, sizeof(opus)); REQUIRE(len > 0); + opus_codec_encoder_destroy(encoder); + auto receive = [&](uint64_t source, int fin) { + radio_audio_on_frame(instance, 7, source, 1, fin ? 1 : 0, fin, fin ? nullptr : opus, fin ? 0 : len, nullptr); + }; + auto feed = [&] { REQUIRE(radio_audio_feed_pcm(7, speech.data(), speech.size()) == 0); }; + uint64_t pulled = 0; + auto drain = [&] { + int16_t out[1920]; + for (int i = 0; i < 40; ++i) { REQUIRE(radio_audio_pull_pcm(7, out, 1920) == 1920); pulled += 960; } + }; + auto start = [&] { + events.clear(); packets.clear(); pulled = 0; probability = 0.9f; + REQUIRE(radio_audio_start(7, 0) == 0 && radio_audio_capture_start(7, 1, 1) == 0); + radio_audio_playback_progress(7, 0, 0); + }; + start(); + // Один старый подтверждённый кадр и PCM до RX не должны попасть в следующую передачу. + REQUIRE(radio_audio_feed_pcm(7, speech.data(), 1536) == 0 && !radio_audio_transmitting()); + receive(42, 0); receive(43, 0); + int before = vad_windows; + test_time += 3000; feed(); REQUIRE(!radio_audio_transmitting() && vad_windows == before); + receive(42, 1); drain(); radio_audio_playback_progress(7, pulled, 0); + test_time += 3000; feed(); REQUIRE(!radio_audio_transmitting()); // Второй пир всё ещё говорит. + receive(43, 1); drain(); + test_time += 10000; feed(); REQUIRE(!radio_audio_transmitting()); // PCM ещё не дошёл до выхода. + radio_audio_playback_progress(7, pulled, 500); // Desktop: последние 50мс ещё в устройстве. + test_time += 2499; feed(); REQUIRE(!radio_audio_transmitting() && vad_windows == before); + test_time += 1; + speech.fill(3100); + REQUIRE(radio_audio_feed_pcm(7, speech.data(), 1536) == 0 && !radio_audio_transmitting()); + REQUIRE(radio_audio_feed_pcm(7, speech.data(), 1536) == 0 && radio_audio_transmitting()); + radio_audio_capture_stop(); uasync_poll(instance->ua, 0); + REQUIRE(events.front() == 'B' && events.back() == 'F' && packets.size() == 4); + encoder = opus_codec_encoder_create(48000, 1); REQUIRE(encoder); + opus_codec_encoder_bitrate_set(encoder, 32000); + check_packet(encoder, speech.data(), 0); // Нет PCM чужого разговора или старого prefetch. + opus_codec_encoder_destroy(encoder); radio_audio_stop(); + + start(); feed(); REQUIRE(radio_audio_transmitting()); uasync_poll(instance->ua, 0); + receive(42, 0); feed(); REQUIRE(!radio_audio_transmitting()); uasync_poll(instance->ua, 0); + REQUIRE(events.back() == 'F'); + radio_audio_talk_begin(7); feed(); REQUIRE(radio_audio_transmitting()); // Ручной PTT работает при занятом эфире. + radio_audio_talk_end(7); REQUIRE(!radio_audio_transmitting()); uasync_poll(instance->ua, 0); + drain(); test_time += 20001; drain(); radio_audio_playback_progress(7, pulled, 0); // FIN потерян. + test_time += 1999; feed(); REQUIRE(!radio_audio_transmitting()); + receive(43, 0); receive(43, 1); drain(); radio_audio_playback_progress(7, pulled, 0); // Новый голос продлевает паузу. + test_time += 1999; feed(); REQUIRE(!radio_audio_transmitting()); + test_time += 1; feed(); REQUIRE(radio_audio_transmitting()); + radio_audio_stop(); uasync_poll(instance->ua, 0); + + start(); + REQUIRE(radio_audio_feed_pcm(7, speech.data(), 1536) == 0); + stall_vad = true; vad_stalled = false; + std::thread tx([&] { REQUIRE(radio_audio_feed_pcm(7, speech.data(), 1536) == 0); }); + for (int i = 0; i < 200 && !vad_stalled; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + REQUIRE(vad_stalled); receive(42, 0); stall_vad = false; tx.join(); + REQUIRE(!radio_audio_transmitting()); // RX во время второго inference отменяет готовый START. + radio_audio_reset_playback(7); + test_time += 1999; feed(); REQUIRE(!radio_audio_transmitting()); + test_time += 1; feed(); REQUIRE(radio_audio_transmitting()); + radio_audio_stop(); uasync_poll(instance->ua, 0); + start(); feed(); REQUIRE(radio_audio_transmitting()); // Новая сессия не наследует RX-запрет. + radio_audio_stop(); uasync_poll(instance->ua, 0); + test_time = 0; + debug_set_category_level(DEBUG_CATEGORY_VAD, DEBUG_LEVEL_NONE); + debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_NONE); + puts("voice TX: RX/FIN/drain/device delay + exact 200ms, multiple speakers, timeout, prefetch, manual PTT, inference race PASS"); +} + int main() { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); UTUN_INSTANCE instance{}; instance.ua = uasync_create(); REQUIRE(instance.ua); @@ -368,6 +448,7 @@ int main() { test_prefetch(&instance, 2, 1); test_prefetch_deferred(&instance); test_vad_diagnostics(&instance); + test_vad_receive_guard(&instance); for (int channels : {1, 2}) for (int gain : {6, 30}) test_vad_compressor(&instance, channels, gain); // Старый FIN не обращается к новому ядру, даже когда allocator повторно использовал тот же адрес. REQUIRE(radio_audio_start(7, 1) == 0);