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
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"); |
|
}
|
|
|