Browse Source

test: cover radio receive guard timing and playback progress

master
evgeny 2 days ago
parent
commit
f054f9c8d9
  1. 2
      tools/chatgui/CMakeLists.txt
  2. 15
      tools/chatgui/tests/test_voice_audio_io.cpp
  3. 81
      tools/chatgui/tests/test_voice_tx.cpp

2
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()

15
tools/chatgui/tests/test_voice_audio_io.cpp

@ -21,6 +21,10 @@ static std::vector<uint8_t> masks;
static bool talking;
static std::atomic<int> pulls{0}, blocks{0};
static std::atomic<bool> stall{false}, stalled{false};
static std::atomic<uint64_t> 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<int16_t, 960> 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);

81
tools/chatgui/tests/test_voice_tx.cpp

@ -30,6 +30,9 @@ static int radio_compressor_enabled;
static bool observe_vad;
static std::vector<float> vad_input;
static std::atomic<bool> stall_vad{false}, vad_stalled{false};
static std::atomic<uint64_t> 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<silero_vad_t*>(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<int16_t, 3072> 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);

Loading…
Cancel
Save