diff --git a/doc/tasks.md b/doc/tasks.md index 02a01ffa..650ee653 100644 --- a/doc/tasks.md +++ b/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()`. diff --git a/src/radio/radio_audio.c b/src/radio/radio_audio.c index 6fcf5d1a..93fd6e90 100644 --- a/src/radio/radio_audio.c +++ b/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; } diff --git a/src/radio/radio_audio.h b/src/radio/radio_audio.h index cc006766..97ed33f1 100644 --- a/src/radio/radio_audio.h +++ b/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); diff --git a/tools/chatgui/tests/test_voice_tx.cpp b/tools/chatgui/tests/test_voice_tx.cpp index 61c2eecf..47ca8b8b 100644 --- a/tools/chatgui/tests/test_voice_tx.cpp +++ b/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 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 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");