Browse Source

Test VAD compression, quiet speech and independent prefetch audio

master
evgeny 1 week ago
parent
commit
9c8f635e63
  1. 142
      tests/test_silero_vad.c
  2. 207
      tools/chatgui/tests/test_voice_tx.cpp

142
tests/test_silero_vad.c

@ -6,6 +6,7 @@
* - что на тишине вероятность речи близка к нулю (реальный инференс, не мусор);
* - что путь pcm16 даёт тот же результат, что и float (конвертация /32768);
* - что вероятность всегда лежит в [0;1].
* - что речь запускает авто-PTT, тишина останавливает, reset воспроизводит тот же поток.
*
* Модель ищется в argv[1], по умолчанию "../lib/silero_vad.onnx" (запуск из tests/).
*/
@ -14,7 +15,9 @@
#include <math.h>
#include "../lib/silero_vad.h"
#include "../lib/audio_compressor.h"
#include "../lib/debug_config.h"
#include "../src/radio/radio_vad.h"
#define PI 3.14159265358979323846
@ -111,6 +114,7 @@ static int test_pcm16_matches_float(const char* model_path) {
gen_vowel(samples, SILERO_VAD_WINDOW_SAMPLES, 0);
for (int i = 0; i < SILERO_VAD_WINDOW_SAMPLES; i++) {
pcm[i] = (int16_t)(samples[i] * 32768.0f);
samples[i] = (float)pcm[i] / 32768.0f; /* Сравниваем один сигнал после квантования. */
}
rc = silero_vad_process(vad, samples, &prob_f);
@ -172,6 +176,139 @@ static int test_null_args(const char* model_path) {
return 0;
}
/* Речевая запись обязательна: тесты на тишине не замечают неверный вход модели. */
static void test_speech(const char* model_path) {
enum { SPEECH_WINDOWS = 32, SILENCE_WINDOWS = 64 };
uint8_t bytes[SPEECH_WINDOWS * SILERO_VAD_WINDOW_SAMPLES * 2];
float reference[SPEECH_WINDOWS];
int16_t pcm[SILERO_VAD_WINDOW_SAMPLES];
float silence[SILERO_VAD_WINDOW_SAMPLES] = {0};
printf("[silero_vad] speech -> auto-PTT -> silence -> reset (%s)\n", model_path ? "file" : "embedded");
FILE* file = fopen(TEST_SILERO_SPEECH_PATH, "rb");
CHECK(file != NULL, "cannot open speech fixture: %s", TEST_SILERO_SPEECH_PATH);
if (!file) return;
size_t size = fread(bytes, 1, sizeof(bytes), file);
int extra = fgetc(file);
fclose(file);
CHECK(size == sizeof(bytes) && extra == EOF, "invalid speech fixture size: %zu", size);
if (size != sizeof(bytes) || extra != EOF) return;
silero_vad_t* vad = model_path ? silero_vad_create(model_path) : silero_vad_create_default();
CHECK(vad != NULL, "speech detector create failed");
if (!vad) return;
for (int pass = 0; pass < 2; pass++) {
silero_vad_reset(vad);
struct radio_vad_fsm fsm;
radio_vad_fsm_reset(&fsm);
uint64_t now = 1;
float max_prob = 0;
int starts = 0, stops = 0;
for (int window = 0; window < SPEECH_WINDOWS + SILENCE_WINDOWS; window++) {
float probability = -1;
int rc;
if (window < SPEECH_WINDOWS) {
for (int i = 0; i < SILERO_VAD_WINDOW_SAMPLES; i++) {
size_t offset = ((size_t)window * SILERO_VAD_WINDOW_SAMPLES + i) * 2;
int value = bytes[offset] | ((int)bytes[offset + 1] << 8);
pcm[i] = (int16_t)(value >= 32768 ? value - 65536 : value);
}
rc = silero_vad_process_pcm16(vad, pcm, &probability);
} else rc = silero_vad_process(vad, silence, &probability);
CHECK(rc == 0, "speech process pass=%d window=%d rc=%d", pass, window, rc);
if (rc != 0) { silero_vad_destroy(vad); return; }
CHECK(probability >= 0 && probability <= 1, "speech probability out of range: %f", probability);
if (window < SPEECH_WINDOWS) {
if (!pass) reference[window] = probability;
else CHECK(fabsf(reference[window] - probability) < 1e-5f, "reset changed speech window=%d", window);
if (probability > max_prob) max_prob = probability;
}
now += 320;
int transition = radio_vad_fsm_update(&fsm, probability, 0.5f, 2, 2000, 0, now);
if (transition == 1 && window < SPEECH_WINDOWS) starts++;
if (transition == -1 && window >= SPEECH_WINDOWS) stops++;
}
printf(" pass=%d speech max_prob=%.5f starts=%d silence_stops=%d talking=%d\n", pass, max_prob, starts, stops, fsm.talking);
CHECK(max_prob > 0.8f && starts > 0, "speech did not activate auto-PTT");
CHECK(stops > 0 && !fsm.talking, "silence did not stop auto-PTT");
}
silero_vad_destroy(vad);
}
/* Реальная цепочка 48кГц/20мс AGC → mono 16кГц/512 → Silero → авто-PTT; фон не должен включать передачу. */
static void test_compressed_speech(float amplitude, int max_gain_db) {
enum { SPEECH_SAMPLES = 16384, BLOCKS = 200, NOISE_FRAMES = 48000 };
uint8_t bytes[SPEECH_SAMPLES * 2];
FILE* file = fopen(TEST_SILERO_SPEECH_PATH, "rb");
CHECK(file != NULL, "cannot open compressor speech fixture");
if (!file) return;
size_t size = fread(bytes, 1, sizeof(bytes), file);
fclose(file);
CHECK(size == sizeof(bytes), "invalid compressor speech fixture size=%zu", size);
if (size != sizeof(bytes)) return;
struct audio_compressor* ac = audio_compressor_create();
silero_vad_t* vad = silero_vad_create_default();
CHECK(ac && vad, "compressed speech create failed");
if (!ac || !vad) { audio_compressor_destroy(ac); silero_vad_destroy(vad); return; }
audio_compressor_config_t cfg = {0};
cfg.sample_rate = 48000; cfg.channels = 1; cfg.block_duration_ms = 20;
cfg.lookback_ms = 200; cfg.max_gain_db = max_gain_db; cfg.rise_rate_per_sec = 2; cfg.target_level = 0.25f;
int rc = audio_compressor_configure(ac, &cfg);
CHECK(rc == 0, "compressed speech configure failed");
if (rc != 0) { audio_compressor_destroy(ac); silero_vad_destroy(vad); return; }
float reference[BLOCKS], window[SILERO_VAD_WINDOW_SAMPLES];
int16_t pcm[960];
printf("[silero_vad] independent AGC speech amplitude=%.3f max_gain=%ddB + low noise\n", amplitude, max_gain_db);
for (int pass = 0; pass < 2; ++pass) {
audio_compressor_reset(ac); silero_vad_reset(vad);
struct radio_vad_fsm fsm;
radio_vad_fsm_reset(&fsm);
uint32_t noise = 1;
float peak = 0, noise_peak = 0;
int window_len = 0, window_idx = 0, starts = 0, stops = 0;
for (int block = 0; block < BLOCKS; ++block) {
for (int i = 0; i < 960; ++i) {
int offset = block * 960 + i - NOISE_FRAMES;
noise = noise * 1664525u + 1013904223u;
int sample = (int)(noise >> 27) - 16;
if (offset >= 0 && offset < SPEECH_SAMPLES * 3) {
int index = offset / 3 * 2;
int value = bytes[index] | ((int)bytes[index + 1] << 8);
sample += (int)((value >= 32768 ? value - 65536 : value) * amplitude);
}
if (sample > 32767) sample = 32767;
if (sample < -32768) sample = -32768;
pcm[i] = (int16_t)sample;
}
rc = audio_compressor_process_frame(ac, pcm, pcm, 960);
CHECK(rc == 0, "compressed speech frame failed block=%d", block);
for (int i = 0; i < 960; i += 3) {
window[window_len++] = ((float)pcm[i] + pcm[i + 1] + pcm[i + 2]) / (3.0f * 32768.0f);
if (window_len != SILERO_VAD_WINDOW_SAMPLES) continue;
float probability = -1;
rc = silero_vad_process(vad, window, &probability);
CHECK(rc == 0 && probability >= 0 && probability <= 1, "compressed speech process failed window=%d", window_idx);
if (!pass) reference[window_idx] = probability;
else CHECK(fabsf(reference[window_idx] - probability) < 1e-5f, "compressed reset changed window=%d", window_idx);
int end_frame = block * 960 + i + 3;
int speech = end_frame > NOISE_FRAMES && end_frame <= NOISE_FRAMES + SPEECH_SAMPLES * 3;
if (speech && probability > peak) peak = probability;
if (end_frame <= NOISE_FRAMES && probability > noise_peak) noise_peak = probability;
int transition = radio_vad_fsm_update(&fsm, probability, 0.5f, 2, 2000, 0, (uint64_t)(window_idx + 1) * 320);
if (transition > 0) starts++;
if (transition < 0) stops++;
window_len = 0; ++window_idx;
}
}
printf(" pass=%d speech_peak=%.5f noise_peak=%.5f starts=%d stops=%d gain=%.1fdB clips=%u\n", pass,
peak, noise_peak, starts, stops, audio_compressor_gain_db(ac), audio_compressor_clip_count(ac));
CHECK(noise_peak < 0.5f, "background triggered compressed VAD");
CHECK(peak > 0.8f && starts > 0, "compressed speech did not activate auto-PTT");
CHECK(stops > 0 && !fsm.talking, "compressed background did not stop auto-PTT");
CHECK(audio_compressor_gain_db(ac) <= max_gain_db + 0.001f, "compressor exceeded max gain");
}
audio_compressor_destroy(ac); silero_vad_destroy(vad);
}
int main(int argc, char** argv) {
const char* model_path = (argc > 1) ? argv[1] : "../lib/silero_vad.onnx";
@ -186,6 +323,11 @@ int main(int argc, char** argv) {
test_pcm16_matches_float(model_path);
test_null_args(model_path);
test_default_model();
test_speech(model_path);
test_speech(NULL);
test_compressed_speech(1.0f, 15);
test_compressed_speech(0.03f, 15);
test_compressed_speech(0.03f, 30);
if (g_failures == 0) {
printf("TEST PASSED\n");

207
tools/chatgui/tests/test_voice_tx.cpp

@ -7,8 +7,11 @@ extern "C" {
#include "debug_config.h"
#include "u_async.h"
#include "silero_vad.h"
#include "audio_compressor.h"
#include "chat/chat_setting.h"
}
#include <vector>
#include <string>
#include <array>
#include <cstring>
#include <cmath>
@ -23,12 +26,16 @@ static std::vector<char> events;
static std::vector<std::vector<uint8_t>> packets;
static float probability;
static int vad_windows;
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};
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) {
extern "C" int __wrap_silero_vad_process(silero_vad_t*, const float* samples, float* result) {
if (stall_vad) { vad_stalled = true; while (stall_vad) std::this_thread::sleep_for(std::chrono::milliseconds(1)); }
if (observe_vad) vad_input.insert(vad_input.end(), samples, samples + SILERO_VAD_WINDOW_SAMPLES);
++vad_windows; *result = probability; return 0;
}
extern "C" int __wrap_radio_talk_begin(UTUN_INSTANCE*, uint64_t) { events.push_back('B'); return 0; }
@ -41,15 +48,203 @@ extern "C" int __wrap_call_send_media(UTUN_INSTANCE*, uint64_t, const uint8_t* d
}
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_compressor_enabled")) return radio_compressor_enabled;
if (!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) {
static void check_packet(opus_codec_encoder_t* encoder, const int16_t* pcm, size_t index,
struct audio_compressor* compressor = nullptr, int channels = 1) {
std::array<int16_t, 1920> amplified;
if (compressor) {
REQUIRE(audio_compressor_process_frame(compressor, pcm, amplified.data(), 960 * channels) == 0);
pcm = amplified.data();
}
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));
}
static struct audio_compressor* reference_compressor(int channels, int max_gain) {
auto* ac = audio_compressor_create(); REQUIRE(ac);
audio_compressor_config_t cfg{};
cfg.sample_rate = 48000; cfg.channels = channels; cfg.block_duration_ms = 20;
cfg.lookback_ms = 200; cfg.max_gain_db = max_gain; cfg.rise_rate_per_sec = 2; cfg.target_level = 0.25f;
REQUIRE(audio_compressor_configure(ac, &cfg) == 0);
return ac;
}
/* Проверяем фактический вход Silero; разбиение callback не должно менять AGC или ресемплер. */
static void check_vad_pcm(const int16_t* pcm, int frames, int channels, int max_gain) {
auto* ac = reference_compressor(channels, max_gain);
std::array<int16_t, 1920> amplified;
std::vector<float> expected;
for (int offset = 0; offset + 960 <= frames; offset += 960) {
REQUIRE(audio_compressor_process_frame(ac, pcm + offset * channels, amplified.data(), 960 * channels) == 0);
for (int i = 0; i < 960; i += 3) {
float sum = 0;
for (int j = 0; j < 3; ++j) {
const int16_t* sample = amplified.data() + (i + j) * channels;
sum += channels == 2 ? ((float)sample[0] + sample[1]) * 0.5f : sample[0];
}
expected.push_back(sum / (3.0f * 32768.0f));
}
}
expected.resize(expected.size() / 512 * 512);
REQUIRE(vad_input == expected);
REQUIRE(audio_compressor_gain_db(ac) <= max_gain + 0.001f);
audio_compressor_destroy(ac);
}
static void test_vad_compressor(UTUN_INSTANCE* instance, int channels, int max_gain) {
debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_DEBUG);
REQUIRE(chat_setting_set(instance, "radio_compressor_max_gain_db", "6") == 0);
REQUIRE(chat_setting_set(instance, "radio_vad_compressor_max_gain_db", std::to_string(max_gain).c_str()) == 0);
REQUIRE(chat_setting_set(instance, "radio_vad_compressor_max_gain_db", "61") == -2);
REQUIRE(chat_setting_set(instance, "radio_compressor_max_gain_db", "61") == -2);
radio_compressor_enabled = 1; observe_vad = true; probability = 0;
packets.clear(); events.clear(); vad_input.clear();
REQUIRE(radio_audio_start(7, 0) == 0 && radio_audio_capture_start(7, channels, 1) == 0);
const int idle = 55 * 960, live = 4 * 960;
std::vector<int16_t> pcm((idle + live) * channels);
for (size_t i = 0; i < pcm.size(); ++i) pcm[i] = (int16_t)(240 * sin(i * 0.07));
auto feed_chunks = [&](const int16_t* samples, int frames) {
const int chunks[] = {1, 7, 65, 997, 1536, 11, 333};
for (int offset = 0, chunk = 0; offset < frames; ++chunk) {
int count = std::min(chunks[chunk % 7], frames - offset);
REQUIRE(radio_audio_feed_pcm(7, samples + offset * channels, count * channels) == 0);
offset += count;
}
};
feed_chunks(pcm.data(), idle);
REQUIRE(!radio_audio_transmitting());
radio_audio_talk_begin(7); feed_chunks(pcm.data() + idle * channels, live); radio_audio_talk_end(7);
check_vad_pcm(pcm.data(), idle + live, channels, max_gain);
REQUIRE(*std::max_element(vad_input.begin(), vad_input.end()) > 240.0f / 32768.0f * 1.5f);
uasync_poll(instance->ua, 0);
REQUIRE(packets.size() == 54 && events.front() == 'B' && events.back() == 'F');
auto* encoder = opus_codec_encoder_create(48000, channels); REQUIRE(encoder);
opus_codec_encoder_bitrate_set(encoder, 32000);
auto* tx = reference_compressor(channels, 6);
for (int i = 0; i < 54; ++i) check_packet(encoder, pcm.data() + (idle - 48000 + i * 960) * channels, i, tx, channels);
audio_compressor_destroy(tx); opus_codec_encoder_destroy(encoder);
// Разрыв и новый захват должны убрать и неполный блок 20мс, и историю усиления.
feed_chunks(pcm.data(), 800);
radio_audio_capture_discontinuity(7); vad_input.clear();
feed_chunks(pcm.data(), 2897); check_vad_pcm(pcm.data(), 2897, channels, max_gain);
auto reset_input = vad_input;
radio_audio_capture_stop(); REQUIRE(radio_audio_capture_start(7, channels, 1) == 0); vad_input.clear();
feed_chunks(pcm.data(), 2897); REQUIRE(vad_input == reset_input);
radio_audio_stop(); uasync_poll(instance->ua, 0);
observe_vad = false; radio_compressor_enabled = 0;
REQUIRE(chat_setting_set(instance, "radio_compressor_max_gain_db", "15") == 0);
REQUIRE(chat_setting_set(instance, "radio_vad_compressor_max_gain_db", "15") == 0);
printf("voice TX: separate VAD gain=%ddB ch=%d, exact Silero input, independent TX/prefetch, chunking/reset PASS\n", max_gain, channels);
debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_NONE);
}
/* Полная секунда кольца, граница VAD внутри кадра и live должны совпасть с непрерывным Opus-потоком. */
static void test_prefetch(UTUN_INSTANCE* instance, int channels, int compressor_enabled = 0) {
debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_INFO);
packets.clear(); events.clear(); probability = 0; radio_compressor_enabled = compressor_enabled;
REQUIRE(radio_audio_start(7, 0) == 0 && radio_audio_capture_start(7, channels, 1) == 0);
const int idle = 33 * 1536, confirm = 2 * 1536, live = 959;
std::vector<int16_t> pcm((idle + confirm + live) * channels);
for (size_t i = 0; i < pcm.size(); ++i) pcm[i] = (int16_t)(10000 * sin(i * 0.07));
REQUIRE(radio_audio_feed_pcm(7, pcm.data(), idle * channels) == 0);
REQUIRE(!radio_audio_transmitting() && !instance->ua->posted_tasks_head);
probability = 0.9f;
int trigger = idle;
for (; trigger < idle + confirm; ++trigger) {
REQUIRE(radio_audio_feed_pcm(7, pcm.data() + trigger * channels, channels) == 0);
if (radio_audio_transmitting()) break;
}
REQUIRE(radio_audio_transmitting());
REQUIRE(radio_audio_feed_pcm(7, pcm.data() + (trigger + 1) * channels, live * channels) == 0);
int pending = 0;
for (auto* task = instance->ua->posted_tasks_head; task; task = task->next) ++pending;
REQUIRE(pending == 3); // BEGIN + одна задача на 50 кадров prefetch + один live-кадр.
radio_audio_capture_stop(); uasync_poll(instance->ua, 0);
REQUIRE(packets.size() == 51 && events.size() == 53 && events.front() == 'B' && events.back() == 'F');
auto* encoder = opus_codec_encoder_create(48000, channels); REQUIRE(encoder);
opus_codec_encoder_bitrate_set(encoder, 32000);
auto* tx = compressor_enabled ? reference_compressor(channels, 15) : nullptr;
const int first = trigger - 48000;
for (int i = 0; i < 51; ++i) check_packet(encoder, pcm.data() + (first + i * 960) * channels, i, tx, channels);
audio_compressor_destroy(tx);
opus_codec_encoder_destroy(encoder);
radio_compressor_enabled = 0;
// Разрыв очищает историю; короткий prefetch не дополняется тишиной на границе с live.
packets.clear(); events.clear(); probability = 0;
REQUIRE(radio_audio_capture_start(7, channels, 1) == 0);
REQUIRE(radio_audio_feed_pcm(7, pcm.data(), 100 * channels) == 0);
radio_audio_capture_discontinuity(7);
REQUIRE(radio_audio_feed_pcm(7, pcm.data() + 100 * channels, 240 * channels) == 0);
radio_audio_talk_begin(7);
REQUIRE(radio_audio_feed_pcm(7, pcm.data() + 340 * channels, 120 * channels) == 0);
radio_audio_talk_end(7); uasync_poll(instance->ua, 0);
REQUIRE(events == std::vector<char>({'B','M','F'}));
encoder = opus_codec_encoder_create(48000, channels); REQUIRE(encoder);
opus_codec_encoder_bitrate_set(encoder, 32000);
std::vector<int16_t> tail(960 * channels);
memcpy(tail.data(), pcm.data() + 100 * channels, 360 * channels * sizeof(int16_t));
check_packet(encoder, tail.data(), 0);
opus_codec_encoder_destroy(encoder);
// Следующая передача получает только новую паузу, а не уже переданный prefetch/хвост.
packets.clear(); events.clear();
REQUIRE(radio_audio_feed_pcm(7, pcm.data() + 460 * channels, 100 * channels) == 0);
radio_audio_talk_begin(7);
REQUIRE(radio_audio_feed_pcm(7, pcm.data() + 560 * channels, 100 * channels) == 0);
radio_audio_talk_end(7); uasync_poll(instance->ua, 0);
REQUIRE(events == std::vector<char>({'B','M','F'}));
// Эталонный encoder сохраняет ту же историю предыдущего пакета.
encoder = opus_codec_encoder_create(48000, channels); REQUIRE(encoder);
opus_codec_encoder_bitrate_set(encoder, 32000);
uint8_t unused[1024]; REQUIRE(opus_codec_encode(encoder, tail.data(), 960, unused, sizeof(unused)) > 0);
std::fill(tail.begin(), tail.end(), 0);
memcpy(tail.data(), pcm.data() + 460 * channels, 200 * channels * sizeof(int16_t));
check_packet(encoder, tail.data(), 0);
opus_codec_encoder_destroy(encoder);
radio_audio_stop(); uasync_poll(instance->ua, 0);
printf("voice TX: prefetch 1000ms, exact ring/live order, short history, discontinuity, no replay ch=%d PASS\n", channels);
debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_NONE);
}
static void test_prefetch_deferred(UTUN_INSTANCE* instance) {
debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_INFO);
packets.clear(); events.clear(); probability = 0;
REQUIRE(radio_audio_start(7, 0) == 0 && radio_audio_capture_start(7, 1, 0) == 0);
std::array<int16_t, 8 * 960> queued{};
radio_audio_talk_begin(7); REQUIRE(radio_audio_feed_pcm(7, queued.data(), queued.size()) == 0);
radio_audio_capture_stop(); REQUIRE(radio_audio_capture_start(7, 1, 1) == 0);
const int idle = 33 * 1536, confirm = 2 * 1536;
std::vector<int16_t> pcm(idle + confirm + 1);
for (size_t i = 0; i < pcm.size(); ++i) pcm[i] = (int16_t)(9000 * sin(i * 0.09));
REQUIRE(radio_audio_feed_pcm(7, pcm.data(), idle) == 0);
probability = 0.9f;
REQUIRE(radio_audio_feed_pcm(7, pcm.data() + idle, confirm) == 0);
REQUIRE(!radio_audio_transmitting()); // Media-очередь занята: BEGIN отложен, история продолжает копиться.
int pending = 0;
for (auto* task = instance->ua->posted_tasks_head; task; task = task->next) ++pending;
REQUIRE(pending == 10); // Только старые BEGIN + 8 media + FIN, новых задач пока нет.
uasync_poll(instance->ua, 0); REQUIRE(packets.size() == 8);
packets.clear(); events.clear();
REQUIRE(radio_audio_feed_pcm(7, pcm.data() + idle + confirm, 1) == 0 && radio_audio_transmitting());
radio_audio_capture_stop(); uasync_poll(instance->ua, 0);
REQUIRE(packets.size() == 51 && events.front() == 'B' && events.back() == 'F');
auto* encoder = opus_codec_encoder_create(48000, 1); REQUIRE(encoder);
opus_codec_encoder_bitrate_set(encoder, 32000);
const int first = idle + confirm - 48000;
for (int i = 0; i < 50; ++i) check_packet(encoder, pcm.data() + first + i * 960, i);
std::array<int16_t, 960> tail{}; tail[0] = pcm.back(); check_packet(encoder, tail.data(), 50);
opus_codec_encoder_destroy(encoder);
radio_audio_stop(); uasync_poll(instance->ua, 0);
puts("voice TX: full media queue defers BEGIN and preserves complete prefetch PASS");
debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_NONE);
}
int main() {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
UTUN_INSTANCE instance{}; instance.ua = uasync_create(); REQUIRE(instance.ua);
@ -121,6 +316,12 @@ int main() {
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);
test_prefetch(&instance, 1);
test_prefetch(&instance, 2);
test_prefetch(&instance, 1, 1);
test_prefetch(&instance, 2, 1);
test_prefetch_deferred(&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);
radio_audio_talk_begin(7); uasync_poll(instance.ua, 0);

Loading…
Cancel
Save