/* Реальные codec/AGC/автоматы/очередь uasync; наблюдаем сетевую границу вместо отправки пакетов. */ extern "C" { #include "utun_instance.h" #include "radio/radio_audio.h" #include "call/call_audio.h" #include "opus_codec.h" #include "debug_config.h" #include "u_async.h" #include "silero_vad.h" } #include #include #include #include #include #include #include #include #include #include #define REQUIRE(x) do { if (!(x)) { fprintf(stderr, "voice-tx:%d %s\n", __LINE__, #x); abort(); } } while (0) static std::vector events; static std::vector> packets; static float probability; static int vad_windows; static std::atomic stall_vad{false}, vad_stalled{false}; 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*) {} extern "C" int __wrap_silero_vad_process(silero_vad_t*, const float*, float* result) { if (stall_vad) { vad_stalled = true; while (stall_vad) std::this_thread::sleep_for(std::chrono::milliseconds(1)); } ++vad_windows; *result = probability; return 0; } extern "C" int __wrap_radio_talk_begin(UTUN_INSTANCE*, uint64_t) { events.push_back('B'); return 0; } extern "C" int __wrap_radio_talk_end(UTUN_INSTANCE*, uint64_t) { events.push_back('F'); return 0; } extern "C" int __wrap_radio_talk_send(UTUN_INSTANCE*, uint64_t, const uint8_t* data, int len) { events.push_back('M'); packets.emplace_back(data, data + len); return 0; } extern "C" int __wrap_call_send_media(UTUN_INSTANCE*, uint64_t, const uint8_t* data, int len) { packets.emplace_back(data, data + len); return 0; } extern "C" int __real_chat_setting_get_int(UTUN_INSTANCE*, const char*, int); extern "C" int __wrap_chat_setting_get_int(UTUN_INSTANCE* inst, const char* key, int fallback) { if (!strcmp(key, "radio_compressor_enabled") || !strcmp(key, "call_compressor_enabled")) return 0; if (!strcmp(key, "radio_vad_hangover_ms")) return 0; return __real_chat_setting_get_int(inst, key, fallback); } static void check_packet(opus_codec_encoder_t* encoder, const int16_t* pcm, size_t index) { uint8_t data[1024]; int size = opus_codec_encode(encoder, pcm, 960, data, sizeof(data)); REQUIRE(size > 0 && index < packets.size()); REQUIRE(packets[index].size() == (size_t)size && !memcmp(packets[index].data(), data, size)); } int main() { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); UTUN_INSTANCE instance{}; instance.ua = uasync_create(); REQUIRE(instance.ua); REQUIRE(radio_audio_init(&instance) == 0 && call_audio_init(&instance) == 0); REQUIRE(radio_audio_start(7, 0) == 0 && radio_audio_capture_start(7, 1, 0) == 0); std::array input{}; for (int i = 0; i < 1920; ++i) input[i] = (int16_t)(12000 * sin(i * 0.1)); radio_audio_talk_begin(7); radio_audio_talk_begin(7); REQUIRE(radio_audio_feed_pcm(7, input.data(), 1200) == 0); radio_audio_talk_end(7); radio_audio_talk_begin(7); REQUIRE(radio_audio_feed_pcm(7, input.data() + 1200, 120) == 0); radio_audio_talk_end(7); uasync_poll(instance.ua, 0); REQUIRE(events == std::vector({'B','M','M','F','B','M','F'})); auto* encoder = opus_codec_encoder_create(48000, 1); REQUIRE(encoder); check_packet(encoder, input.data(), 0); std::array tail{}; memcpy(tail.data(), input.data() + 960, 240 * sizeof(int16_t)); check_packet(encoder, tail.data(), 1); tail.fill(0); memcpy(tail.data(), input.data() + 1200, 120 * sizeof(int16_t)); check_packet(encoder, tail.data(), 2); // Разрыв не склеивает отсчёты до и после потери внутри одного Opus-кадра. packets.clear(); events.clear(); radio_audio_talk_begin(7); radio_audio_feed_pcm(7, input.data(), 100); radio_audio_capture_discontinuity(7); radio_audio_feed_pcm(7, input.data() + 100, 100); radio_audio_talk_end(7); uasync_poll(instance.ua, 0); REQUIRE(events == std::vector({'B','M','M','F'})); tail.fill(0); memcpy(tail.data(), input.data(), 100 * sizeof(int16_t)); check_packet(encoder, tail.data(), 0); tail.fill(0); memcpy(tail.data(), input.data() + 100, 100 * sizeof(int16_t)); check_packet(encoder, tail.data(), 1); opus_codec_encoder_destroy(encoder); // Capture teardown завершает принятую серию до FIN; следующее capture не получает старый хвост. packets.clear(); events.clear(); radio_audio_talk_begin(7); radio_audio_feed_pcm(7, input.data(), 120); radio_audio_capture_stop(); REQUIRE(radio_audio_capture_start(7, 1, 0) == 0); radio_audio_talk_begin(7); radio_audio_feed_pcm(7, input.data() + 120, 120); radio_audio_talk_end(7); uasync_poll(instance.ua, 0); REQUIRE(events == std::vector({'B','M','F','B','M','F'})); // Старое прослушивание того же group ID отменено: старые BEGIN/media не попадают в новую сессию. packets.clear(); events.clear(); radio_audio_talk_begin(7); radio_audio_feed_pcm(7, input.data(), 120); radio_audio_stop(); REQUIRE(radio_audio_start(7, 1) == 0); uasync_poll(instance.ua, 0); REQUIRE(events.empty() && packets.empty()); // У остановленного core ограничены не только media, но и быстрые PTT-команды. for (int i = 0; i < 1000; ++i) { radio_audio_talk_begin(7); radio_audio_feed_pcm(7, input.data(), 120); radio_audio_talk_end(7); } int pending = 0; for (auto* task = instance.ua->posted_tasks_head; task; task = task->next) ++pending; REQUIRE(pending <= 24); // 16 control + 8 media, FIN каждой принятой серии уже зарезервирован. uasync_poll(instance.ua, 0); packets.clear(); events.clear(); radio_audio_capture_stop(); REQUIRE(radio_audio_capture_start(7, 1, 1) == 0 && radio_audio_vad_mode()); auto feed = [&](int frames) { while (frames) { int count = std::min(frames, 1920); REQUIRE(radio_audio_feed_pcm(7, input.data(), count) == 0); frames -= count; } }; probability = 0.9f; radio_audio_talk_begin(7); feed(3072); // Два окна VAD даже во время manual PTT. REQUIRE(vad_windows == 2); radio_audio_talk_end(7); REQUIRE(radio_audio_transmitting()); probability = 0; feed(3072); REQUIRE(!radio_audio_transmitting()); // Первое окно начинает silence, второе завершает hangover. uasync_poll(instance.ua, 0); REQUIRE(std::count(events.begin(), events.end(), 'B') == 1 && std::count(events.begin(), events.end(), 'F') == 1); packets.clear(); events.clear(); probability = 0.9f; feed(3072); REQUIRE(radio_audio_transmitting()); radio_audio_talk_begin(7); probability = 0; feed(3072); REQUIRE(radio_audio_transmitting()); radio_audio_talk_end(7); uasync_poll(instance.ua, 0); REQUIRE(std::count(events.begin(), events.end(), 'B') == 1 && std::count(events.begin(), events.end(), 'F') == 1); stall_vad = true; std::thread tx([&] { feed(1536); }); for (int i = 0; i < 200 && !vad_stalled; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); REQUIRE(vad_stalled); std::atomic rx_finished{false}; std::thread rx([&] { int16_t out[1920]; REQUIRE(radio_audio_pull_pcm(7, out, 1920) == 1920); rx_finished = true; }); for (int i = 0; i < 200 && !rx_finished; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); REQUIRE(rx_finished); stall_vad = false; tx.join(); rx.join(); radio_audio_stop(); uasync_poll(instance.ua, 0); // Старый FIN не обращается к новому ядру, даже когда allocator повторно использовал тот же адрес. REQUIRE(radio_audio_start(7, 1) == 0); radio_audio_talk_begin(7); uasync_poll(instance.ua, 0); radio_audio_talk_end(7); radio_audio_stop(); radio_audio_destroy(&instance); REQUIRE(radio_audio_init(&instance) == 0); events.clear(); uasync_poll(instance.ua, 0); REQUIRE(events.empty()); // Prepared policy исключает повторный core AEC; mute применяется только к нужной части кадра. packets.clear(); REQUIRE(call_audio_start_prepared(42) == 0); REQUIRE(call_audio_start(42) == -1); REQUIRE(call_audio_feed_pcm(42, input.data(), 960) == -1); std::array mask{}; std::fill(mask.begin() + 300, mask.end(), 1); REQUIRE(call_audio_feed_prepared_pcm(42, input.data(), mask.data()) == 0); uasync_poll(instance.ua, 0); encoder = opus_codec_encoder_create(48000, 1); REQUIRE(encoder); tail.fill(0); memcpy(tail.data(), input.data(), 300 * sizeof(int16_t)); check_packet(encoder, tail.data(), 0); call_audio_stop(); opus_codec_encoder_destroy(encoder); call_audio_destroy(&instance); radio_audio_destroy(&instance); uasync_destroy(instance.ua, 0); puts("voice TX: exact Opus tail, rapid bursts, discontinuity, capture policy, final mute PASS"); }