Раздели аудиопотоки и привяжи AEC к фактическому общему выходу
This commit is contained in:
1 parent
d7fb0c83f9
commit
bd082a26bf
30 files changed
+1839
-412
No files matched your search
@@ -0,0 +1,143 @@
|
||||
/* Реальные 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");
|
||||
}
|
||||
Reference in new issue
Block a user