|
|
|
|
@ -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); |
|
|
|
|
|