You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

143 lines
9.1 KiB

/* Реальные 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 <vector>
#include <array>
#include <cstring>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <atomic>
#include <thread>
#include <chrono>
#define REQUIRE(x) do { if (!(x)) { fprintf(stderr, "voice-tx:%d %s\n", __LINE__, #x); abort(); } } while (0)
static std::vector<char> events;
static std::vector<std::vector<uint8_t>> packets;
static float probability;
static int vad_windows;
static std::atomic<bool> stall_vad{false}, vad_stalled{false};
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*) {}
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<int16_t, 1920> 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<char>({'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<int16_t, 960> 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<char>({'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<char>({'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<bool> 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<uint8_t, 960> 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");
}