Browse Source

Reduce radio VAD prefetch to 600 milliseconds

master
evgeny 17 hours 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`, чистый,
юнит-тестируемый): старт при подтверждении 2 окон (~64мс) выше порога
`radio_vad_threshold`, стоп при тишине `radio_vad_hangover_ms` (200мс) или чужом
разговоре (busy = RX-кадр <200мс). Перед BEGIN сохраняется до 1с непереданного PCM;
разговоре (busy = RX-кадр <200мс). Перед BEGIN сохраняется до 600мс непереданного PCM;
prefetch отправляется одной задачей перед live без повторов и задержки на стороне отправителя.
VAD использует отдельный компрессор с собственной историей усиления. Desktop `radio_audio_engine.cpp` и Android
`RadioAudioEngine.kt` фидят PCM постоянно в VAD-режиме; JNI `radioVadMode()`.

7
src/radio/radio_audio.c

@ -49,7 +49,8 @@
/* ── VAD авто-PTT (отдельный режим, ручной PTT не меняется) ── */
#define RADIO_VAD_CONFIRM_WINDOWS 2 /* окон (32мс) выше порога для старта (~64мс) */
#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_TX_PENDING_LIMIT 8
@ -141,7 +142,7 @@ struct radio_audio_tx {
struct UTUN_INSTANCE* inst;
uint64_t group_id, generation, listen_generation, instance_generation, burst, created_tb;
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) {
@ -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",
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,
g_vad_hangover_ms, g_vad_mode ? 1000 : 0);
g_vad_hangover_ms, g_vad_mode ? RADIO_PREFETCH_MS : 0);
radio_tx_unlock();
return 0;
}

2
src/radio/radio_audio.h

@ -57,7 +57,7 @@ void radio_audio_talk_end(uint64_t group_id);
/* Единственный TX-writer: очищенные interleaved отсчёты произвольной длины (кратно числу каналов).
* VAD использует свою копию PCM и отдельный AGC с общими настройками TX, кроме radio_vad_compressor_max_gain_db.
* Включённый AGC VAD накапливает 20мс; выключенный пропускает PCM сразу. TX не использует обработанную VAD-копию.
* При включённом VAD до BEGIN хранится до 1с непереданного PCM и отправляется перед live.
* При включённом VAD до BEGIN хранится до 600мс непереданного PCM и отправляется перед live.
* Один автомат manual/VAD; хвост каждой серии дополняется тишиной до FIN. */
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);
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');
REQUIRE(packets.size() == 34 && 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);
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);
// Разрыв и новый захват должны убрать и неполный блок 20мс, и историю усиления.
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);
}
/* Полная секунда кольца, граница VAD внутри кадра и live должны совпасть с непрерывным Opus-потоком. */
/* Полные 600мс кольца, граница 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;
@ -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);
int pending = 0;
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);
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);
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);
const int first = trigger - 28800;
for (int i = 0; i < 31; ++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;
@ -208,7 +208,7 @@ static void test_prefetch(UTUN_INSTANCE* instance, int channels, int compressor_
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);
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);
}
@ -233,12 +233,12 @@ static void test_prefetch_deferred(UTUN_INSTANCE* instance) {
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');
REQUIRE(packets.size() == 31 && 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);
const int first = idle + confirm - 28800;
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(), 30);
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");

Loading…
Cancel
Save