Browse Source

Reduce radio VAD prefetch to 600 milliseconds

master
evgeny 1 week ago
parent
commit
ffd26c56b4
  1. 2
      doc/tasks.md
  2. 7
      src/radio/radio_audio.c
  3. 2
      src/radio/radio_audio.h
  4. 24
      tools/chatgui/tests/test_voice_tx.cpp

2
doc/tasks.md

@ -184,7 +184,7 @@
512, фильтр-гистерезис + машина состояний (`src/radio/radio_vad.h`, чистый, 512, фильтр-гистерезис + машина состояний (`src/radio/radio_vad.h`, чистый,
юнит-тестируемый): старт при подтверждении 2 окон (~64мс) выше порога юнит-тестируемый): старт при подтверждении 2 окон (~64мс) выше порога
`radio_vad_threshold`, стоп при тишине `radio_vad_hangover_ms` (200мс) или чужом `radio_vad_threshold`, стоп при тишине `radio_vad_hangover_ms` (200мс) или чужом
разговоре (busy = RX-кадр <200мс). Перед BEGIN сохраняется до 1с непереданного PCM; разговоре (busy = RX-кадр <200мс). Перед BEGIN сохраняется до 600мс непереданного PCM;
prefetch отправляется одной задачей перед live без повторов и задержки на стороне отправителя. prefetch отправляется одной задачей перед live без повторов и задержки на стороне отправителя.
VAD использует отдельный компрессор с собственной историей усиления. Desktop `radio_audio_engine.cpp` и Android VAD использует отдельный компрессор с собственной историей усиления. Desktop `radio_audio_engine.cpp` и Android
`RadioAudioEngine.kt` фидят PCM постоянно в VAD-режиме; JNI `radioVadMode()`. `RadioAudioEngine.kt` фидят PCM постоянно в VAD-режиме; JNI `radioVadMode()`.

7
src/radio/radio_audio.c

@ -49,7 +49,8 @@
/* ── VAD авто-PTT (отдельный режим, ручной PTT не меняется) ── */ /* ── VAD авто-PTT (отдельный режим, ручной PTT не меняется) ── */
#define RADIO_VAD_CONFIRM_WINDOWS 2 /* окон (32мс) выше порога для старта (~64мс) */ #define RADIO_VAD_CONFIRM_WINDOWS 2 /* окон (32мс) выше порога для старта (~64мс) */
#define RADIO_VAD_BUSY_TB 2000 /* 200мс: канал занят после последнего RX-кадра */ #define RADIO_VAD_BUSY_TB 2000 /* 200мс: канал занят после последнего RX-кадра */
#define RADIO_PREFETCH_FRAMES RADIO_AUDIO_SAMPLE_RATE /* 1с PCM на канал, только до начала передачи */ #define RADIO_PREFETCH_MS 600 /* PCM только до начала передачи */
#define RADIO_PREFETCH_FRAMES (RADIO_AUDIO_SAMPLE_RATE * RADIO_PREFETCH_MS / 1000)
#define RADIO_PREFETCH_PACKETS (RADIO_PREFETCH_FRAMES / RADIO_AUDIO_FRAME_SAMPLES) #define RADIO_PREFETCH_PACKETS (RADIO_PREFETCH_FRAMES / RADIO_AUDIO_FRAME_SAMPLES)
#define RADIO_TX_PENDING_LIMIT 8 #define RADIO_TX_PENDING_LIMIT 8
@ -141,7 +142,7 @@ struct radio_audio_tx {
struct UTUN_INSTANCE* inst; struct UTUN_INSTANCE* inst;
uint64_t group_id, generation, listen_generation, instance_generation, burst, created_tb; uint64_t group_id, generation, listen_generation, instance_generation, burst, created_tb;
int frame_count; int frame_count;
struct radio_audio_packet frames[]; /* один live-кадр либо до 50 кадров prefetch */ struct radio_audio_packet frames[]; /* один live-кадр либо до 30 кадров prefetch */
}; };
static void radio_audio_send(void* arg) { static void radio_audio_send(void* arg) {
@ -509,7 +510,7 @@ int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_e
"vad=%d threshold=%.2f hangover=%dms prefetch=%dms", "vad=%d threshold=%.2f hangover=%dms prefetch=%dms",
RADIO_AUDIO_ID, (unsigned long long)group_id, capture_channels, opus_mode ? "AUDIO" : "VOIP", opus_bitrate_kbps, RADIO_AUDIO_ID, (unsigned long long)group_id, capture_channels, opus_mode ? "AUDIO" : "VOIP", opus_bitrate_kbps,
ac ? (audio_compressor_is_enabled(ac) ? "on" : "off") : "unavailable", g_vad_mode, g_vad_threshold, ac ? (audio_compressor_is_enabled(ac) ? "on" : "off") : "unavailable", g_vad_mode, g_vad_threshold,
g_vad_hangover_ms, g_vad_mode ? 1000 : 0); g_vad_hangover_ms, g_vad_mode ? RADIO_PREFETCH_MS : 0);
radio_tx_unlock(); radio_tx_unlock();
return 0; return 0;
} }

2
src/radio/radio_audio.h

@ -57,7 +57,7 @@ void radio_audio_talk_end(uint64_t group_id);
/* Единственный TX-writer: очищенные interleaved отсчёты произвольной длины (кратно числу каналов). /* Единственный TX-writer: очищенные interleaved отсчёты произвольной длины (кратно числу каналов).
* VAD использует свою копию PCM и отдельный AGC с общими настройками TX, кроме radio_vad_compressor_max_gain_db. * VAD использует свою копию PCM и отдельный AGC с общими настройками TX, кроме radio_vad_compressor_max_gain_db.
* Включённый AGC VAD накапливает 20мс; выключенный пропускает PCM сразу. TX не использует обработанную VAD-копию. * Включённый AGC VAD накапливает 20мс; выключенный пропускает PCM сразу. TX не использует обработанную VAD-копию.
* При включённом VAD до BEGIN хранится до 1с непереданного PCM и отправляется перед live. * При включённом VAD до BEGIN хранится до 600мс непереданного PCM и отправляется перед live.
* Один автомат manual/VAD; хвост каждой серии дополняется тишиной до FIN. */ * Один автомат manual/VAD; хвост каждой серии дополняется тишиной до FIN. */
int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count); int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count);

24
tools/chatgui/tests/test_voice_tx.cpp

@ -122,11 +122,11 @@ static void test_vad_compressor(UTUN_INSTANCE* instance, int channels, int max_g
check_vad_pcm(pcm.data(), idle + live, channels, max_gain); 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); REQUIRE(*std::max_element(vad_input.begin(), vad_input.end()) > 240.0f / 32768.0f * 1.5f);
uasync_poll(instance->ua, 0); uasync_poll(instance->ua, 0);
REQUIRE(packets.size() == 54 && events.front() == 'B' && events.back() == 'F'); REQUIRE(packets.size() == 34 && events.front() == 'B' && events.back() == 'F');
auto* encoder = opus_codec_encoder_create(48000, channels); REQUIRE(encoder); auto* encoder = opus_codec_encoder_create(48000, channels); REQUIRE(encoder);
opus_codec_encoder_bitrate_set(encoder, 32000); opus_codec_encoder_bitrate_set(encoder, 32000);
auto* tx = reference_compressor(channels, 6); 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); for (int i = 0; i < 34; ++i) check_packet(encoder, pcm.data() + (idle - 28800 + i * 960) * channels, i, tx, channels);
audio_compressor_destroy(tx); opus_codec_encoder_destroy(encoder); audio_compressor_destroy(tx); opus_codec_encoder_destroy(encoder);
// Разрыв и новый захват должны убрать и неполный блок 20мс, и историю усиления. // Разрыв и новый захват должны убрать и неполный блок 20мс, и историю усиления.
feed_chunks(pcm.data(), 800); feed_chunks(pcm.data(), 800);
@ -143,7 +143,7 @@ static void test_vad_compressor(UTUN_INSTANCE* instance, int channels, int max_g
debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_NONE); debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_NONE);
} }
/* Полная секунда кольца, граница VAD внутри кадра и live должны совпасть с непрерывным Opus-потоком. */ /* Полные 600мс кольца, граница VAD внутри кадра и live должны совпасть с непрерывным Opus-потоком. */
static void test_prefetch(UTUN_INSTANCE* instance, int channels, int compressor_enabled = 0) { static void test_prefetch(UTUN_INSTANCE* instance, int channels, int compressor_enabled = 0) {
debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_INFO); debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_INFO);
packets.clear(); events.clear(); probability = 0; radio_compressor_enabled = compressor_enabled; packets.clear(); events.clear(); probability = 0; radio_compressor_enabled = compressor_enabled;
@ -163,14 +163,14 @@ static void test_prefetch(UTUN_INSTANCE* instance, int channels, int compressor_
REQUIRE(radio_audio_feed_pcm(7, pcm.data() + (trigger + 1) * channels, live * channels) == 0); REQUIRE(radio_audio_feed_pcm(7, pcm.data() + (trigger + 1) * channels, live * channels) == 0);
int pending = 0; int pending = 0;
for (auto* task = instance->ua->posted_tasks_head; task; task = task->next) ++pending; for (auto* task = instance->ua->posted_tasks_head; task; task = task->next) ++pending;
REQUIRE(pending == 3); // BEGIN + одна задача на 50 кадров prefetch + один live-кадр. REQUIRE(pending == 3); // BEGIN + одна задача на 30 кадров prefetch + один live-кадр.
radio_audio_capture_stop(); uasync_poll(instance->ua, 0); radio_audio_capture_stop(); uasync_poll(instance->ua, 0);
REQUIRE(packets.size() == 51 && events.size() == 53 && events.front() == 'B' && events.back() == 'F'); REQUIRE(packets.size() == 31 && events.size() == 33 && events.front() == 'B' && events.back() == 'F');
auto* encoder = opus_codec_encoder_create(48000, channels); REQUIRE(encoder); auto* encoder = opus_codec_encoder_create(48000, channels); REQUIRE(encoder);
opus_codec_encoder_bitrate_set(encoder, 32000); opus_codec_encoder_bitrate_set(encoder, 32000);
auto* tx = compressor_enabled ? reference_compressor(channels, 15) : nullptr; auto* tx = compressor_enabled ? reference_compressor(channels, 15) : nullptr;
const int first = trigger - 48000; const int first = trigger - 28800;
for (int i = 0; i < 51; ++i) check_packet(encoder, pcm.data() + (first + i * 960) * channels, i, tx, channels); for (int i = 0; i < 31; ++i) check_packet(encoder, pcm.data() + (first + i * 960) * channels, i, tx, channels);
audio_compressor_destroy(tx); audio_compressor_destroy(tx);
opus_codec_encoder_destroy(encoder); opus_codec_encoder_destroy(encoder);
radio_compressor_enabled = 0; radio_compressor_enabled = 0;
@ -208,7 +208,7 @@ static void test_prefetch(UTUN_INSTANCE* instance, int channels, int compressor_
check_packet(encoder, tail.data(), 0); check_packet(encoder, tail.data(), 0);
opus_codec_encoder_destroy(encoder); opus_codec_encoder_destroy(encoder);
radio_audio_stop(); uasync_poll(instance->ua, 0); 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); printf("voice TX: prefetch 600ms, exact ring/live order, short history, discontinuity, no replay ch=%d PASS\n", channels);
debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_NONE); debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_NONE);
} }
@ -233,12 +233,12 @@ static void test_prefetch_deferred(UTUN_INSTANCE* instance) {
packets.clear(); events.clear(); packets.clear(); events.clear();
REQUIRE(radio_audio_feed_pcm(7, pcm.data() + idle + confirm, 1) == 0 && radio_audio_transmitting()); REQUIRE(radio_audio_feed_pcm(7, pcm.data() + idle + confirm, 1) == 0 && radio_audio_transmitting());
radio_audio_capture_stop(); uasync_poll(instance->ua, 0); radio_audio_capture_stop(); uasync_poll(instance->ua, 0);
REQUIRE(packets.size() == 51 && events.front() == 'B' && events.back() == 'F'); REQUIRE(packets.size() == 31 && events.front() == 'B' && events.back() == 'F');
auto* encoder = opus_codec_encoder_create(48000, 1); REQUIRE(encoder); auto* encoder = opus_codec_encoder_create(48000, 1); REQUIRE(encoder);
opus_codec_encoder_bitrate_set(encoder, 32000); opus_codec_encoder_bitrate_set(encoder, 32000);
const int first = idle + confirm - 48000; const int first = idle + confirm - 28800;
for (int i = 0; i < 50; ++i) check_packet(encoder, pcm.data() + first + i * 960, i); for (int i = 0; i < 30; ++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); std::array<int16_t, 960> tail{}; tail[0] = pcm.back(); check_packet(encoder, tail.data(), 30);
opus_codec_encoder_destroy(encoder); opus_codec_encoder_destroy(encoder);
radio_audio_stop(); uasync_poll(instance->ua, 0); radio_audio_stop(); uasync_poll(instance->ua, 0);
puts("voice TX: full media queue defers BEGIN and preserves complete prefetch PASS"); puts("voice TX: full media queue defers BEGIN and preserves complete prefetch PASS");

Loading…
Cancel
Save