Improve Android radio capture lifecycle, VAD and PTT controls

This commit is contained in:
evgeny committed 2026-09-30 12:25:42 +03:00
1 parent 52ffb91101
commit 0d44ada922
14 files changed
+839 -385

No files matched your search

-1
View File
@@ -44,7 +44,6 @@ static const struct chat_setting_def g_setting_defs[] = {
{"radio_compressor_max_gain_db",CHAT_SETTING_INT, 15, 1, 100},
{"radio_vad_enabled", CHAT_SETTING_BOOL, 0, 0, 1},
{"radio_vad_threshold", CHAT_SETTING_INT, 50, 10, 95}, /* порог вероятности речи, % */
{"radio_vad_pre_roll_ms", CHAT_SETTING_INT, 200, 0, 500}, /* pre-roll перед срабатыванием */
{"radio_vad_hangover_ms", CHAT_SETTING_INT, 200, 50, 2000},/* тишина до авто-отбоя */
{"media_download_max_peers", CHAT_SETTING_INT, 3, 1, 16},
{"storage_backfill_days", CHAT_SETTING_INT, 7, 1, 365},
+90 -100
View File
@@ -48,7 +48,6 @@
/* ── VAD авто-PTT (отдельный режим, ручной PTT не меняется) ── */
#define RADIO_VAD_CONFIRM_WINDOWS 2 /* окон (32мс) выше порога для старта (~64мс) */
#define RADIO_VAD_BUSY_TB 2000 /* 200мс: канал занят после последнего RX-кадра */
#define RADIO_VAD_MAX_PREROLL_FRAMES 25 /* максимум pre-roll 500мс @ 20мс */
struct radio_pending {
uint8_t used;
@@ -88,15 +87,10 @@ static uint64_t g_last_stats_tb = 0;
static int g_vad_mode = 0; /* режим включён для текущей группы */
static silero_vad_t* g_vad = NULL; /* детектор (16кГц), создаётся в start */
static float g_vad_threshold = 0.5f;
static int g_vad_pre_roll_ms = 200;
static int g_vad_hangover_ms = 200;
static int g_vad_manual_ptt = 0; /* PTT override: зажата — VAD не вмешивается */
static float g_vad_win[RADIO_VAD_WINDOW_16K]; /* накопитель окон 16кГц */
static int g_vad_win_len = 0;
static int16_t* g_vad_preroll = NULL; /* кольцевой pre-roll (сырой PCM) */
static int g_vad_preroll_cap = 0; /* ёмкость в сэмплах (interleaved) */
static int g_vad_preroll_len = 0; /* заполнено сэмплов */
static int g_vad_preroll_pos = 0; /* позиция записи */
static struct radio_vad_fsm g_vad_fsm;
static uint64_t g_last_rx_tb = 0; /* последний принятый аудио-кадр (busy) */
@@ -262,6 +256,8 @@ static struct radio_source* radio_src_acquire(uint64_t src, uint16_t stream) {
return NULL;
}
static void radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, void (*fn)(void*));
/* ── lifecycle ── */
int radio_audio_init(struct UTUN_INSTANCE* inst) {
@@ -281,13 +277,35 @@ void radio_audio_destroy(struct UTUN_INSTANCE* inst) {
}
int radio_audio_start(uint64_t group_id, int capture_channels) {
if (group_id == 0) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: start bad group_id=0", RADIO_AUDIO_ID); return -1; }
if (capture_channels != 1 && capture_channels != 2) capture_channels = 1;
if (!group_id) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: start bad group_id=0", RADIO_AUDIO_ID); return -1; }
pthread_mutex_lock(&g_mtx);
if (g_active && g_group_id == group_id) { pthread_mutex_unlock(&g_mtx); return 0; }
if (g_active) { pthread_mutex_unlock(&g_mtx); radio_audio_stop(); pthread_mutex_lock(&g_mtx); }
pthread_mutex_unlock(&g_mtx);
radio_audio_stop();
pthread_mutex_lock(&g_mtx);
g_group_id = group_id;
g_active = 1;
g_last_rx_tb = 0;
memset(g_sources, 0, sizeof(g_sources));
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: listening grp=%016llx", RADIO_AUDIO_ID, (unsigned long long)group_id);
if (!capture_channels) return 0;
int rc = radio_audio_capture_start(group_id, capture_channels, chat_setting_get_int(g_inst, "radio_vad_enabled", 0));
if (rc != 0) radio_audio_stop();
return rc;
}
int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_enabled) {
if (capture_channels != 1 && capture_channels != 2) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: capture invalid channels=%d", RADIO_AUDIO_ID, capture_channels);
return -1;
}
pthread_mutex_lock(&g_mtx);
if (!g_active || g_group_id != group_id || g_encoder) {
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: capture start invalid state grp=%016llx", RADIO_AUDIO_ID, (unsigned long long)group_id);
return -1;
}
opus_codec_encoder_t* enc = opus_codec_encoder_create(RADIO_AUDIO_SAMPLE_RATE, capture_channels);
if (!enc) { pthread_mutex_unlock(&g_mtx); DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: encoder create failed", RADIO_AUDIO_ID); return -1; }
int opus_mode = chat_setting_get_int(g_inst, "radio_opus_mode", 0);
@@ -295,8 +313,7 @@ int radio_audio_start(uint64_t group_id, int capture_channels) {
opus_codec_encoder_application_set(enc, opus_mode ? OPUS_CODEC_APP_AUDIO : OPUS_CODEC_APP_VOIP);
opus_codec_encoder_bitrate_set(enc, opus_bitrate_kbps * 1000);
/* компрессор микрофона (AGC): конфиг из [chatserver], lookahead=0 — без задержки и хвоста.
* gain_smoothed не сбрасывается между PTT — адаптивная громкость запоминается. */
/* Компрессор TX: lookahead=0, без задержки и хвоста. */
struct audio_compressor* ac = audio_compressor_create();
if (ac) {
audio_compressor_config_t accfg = {0};
@@ -312,59 +329,54 @@ int radio_audio_start(uint64_t group_id, int capture_channels) {
audio_compressor_set_enabled(ac, chat_setting_get_int(g_inst, "radio_compressor_enabled", 1));
}
/* VAD авто-PTT: отдельный режим (ручной PTT не меняется). Компрессор в VAD-режиме
* байпасится — pre-roll передаётся как сырой PCM, кодируемый по срабатыванию. */
g_vad_mode = chat_setting_get_int(g_inst, "radio_vad_enabled", 0) ? 1 : 0;
if (!ac) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: compressor unavailable", RADIO_AUDIO_ID);
/* VAD анализирует текущие кадры без истории захвата. */
g_vad_mode = vad_enabled ? 1 : 0;
if (g_vad_mode) {
g_vad_threshold = (float)chat_setting_get_int(g_inst, "radio_vad_threshold", 50) / 100.0f;
g_vad_pre_roll_ms = chat_setting_get_int(g_inst, "radio_vad_pre_roll_ms", 200);
g_vad_hangover_ms = chat_setting_get_int(g_inst, "radio_vad_hangover_ms", 200);
g_vad = silero_vad_create_default();
if (!g_vad) {
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: silero_vad_create_default failed — VAD auto-PTT disabled", RADIO_AUDIO_ID);
g_vad_mode = 0;
} else {
int frames = g_vad_pre_roll_ms / RADIO_AUDIO_FRAME_MS;
if (frames < 1) frames = 1;
if (frames > RADIO_VAD_MAX_PREROLL_FRAMES) frames = RADIO_VAD_MAX_PREROLL_FRAMES;
g_vad_preroll_cap = frames * RADIO_AUDIO_FRAME_SAMPLES * capture_channels;
g_vad_preroll = (int16_t*)u_malloc((size_t)g_vad_preroll_cap * sizeof(int16_t));
if (!g_vad_preroll) {
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: pre-roll alloc failed — VAD auto-PTT disabled", RADIO_AUDIO_ID);
silero_vad_destroy(g_vad); g_vad = NULL;
g_vad_mode = 0; g_vad_preroll_cap = 0;
}
}
}
if (g_vad_mode) {
g_vad_preroll_len = 0;
g_vad_preroll_pos = 0;
g_vad_win_len = 0;
radio_vad_fsm_reset(&g_vad_fsm);
}
g_vad_manual_ptt = 0;
g_last_rx_tb = 0;
g_encoder = enc;
g_compressor = ac;
g_capture_channels = capture_channels;
g_group_id = group_id;
g_active = 1;
memset(g_sources, 0, sizeof(g_sources));
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: capture started grp=%016llx ch=%d opus=%s bitrate=%dkbps compressor=%s vad=%d threshold=%.2f hangover=%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);
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: started grp=%016llx opus_mode=%s bitrate=%dkbps capture_ch=%d compressor=%s vad=%s", RADIO_AUDIO_ID,
(unsigned long long)group_id, opus_mode ? "AUDIO" : "VOIP", opus_bitrate_kbps, capture_channels,
ac ? (audio_compressor_is_enabled(ac) ? "on" : "off") : "unavailable",
g_vad_mode ? "on" : "off");
return 0;
}
void radio_audio_capture_stop(void) {
pthread_mutex_lock(&g_mtx);
if (g_encoder) {
if (g_vad_manual_ptt || g_vad_fsm.talking)
radio_audio_post_talk(g_inst ? g_inst->ua : NULL, g_group_id, radio_talk_end_trampoline);
opus_codec_encoder_destroy(g_encoder); g_encoder = NULL;
if (g_compressor) { audio_compressor_destroy(g_compressor); g_compressor = NULL; }
if (g_vad) { silero_vad_destroy(g_vad); g_vad = NULL; }
g_vad_mode = 0;
g_vad_manual_ptt = 0;
g_vad_win_len = 0;
radio_vad_fsm_reset(&g_vad_fsm);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: capture stopped", RADIO_AUDIO_ID);
}
pthread_mutex_unlock(&g_mtx);
}
void radio_audio_stop(void) {
opus_codec_encoder_t* enc = NULL;
struct audio_compressor* ac = NULL;
silero_vad_t* vad = NULL;
int16_t* preroll = NULL;
radio_audio_capture_stop();
int was_active = 0;
pthread_mutex_lock(&g_mtx);
@@ -372,27 +384,12 @@ void radio_audio_stop(void) {
was_active = 1;
g_active = 0;
g_group_id = 0;
enc = g_encoder; g_encoder = NULL;
ac = g_compressor; g_compressor = NULL;
vad = g_vad; g_vad = NULL;
preroll = g_vad_preroll; g_vad_preroll = NULL;
g_vad_mode = 0;
g_vad_manual_ptt = 0;
g_vad_preroll_cap = 0;
g_vad_preroll_len = 0;
g_vad_preroll_pos = 0;
g_vad_win_len = 0;
radio_vad_fsm_reset(&g_vad_fsm);
for (int i = 0; i < RADIO_AUDIO_MAX_SOURCES; i++)
if (g_sources[i].used) radio_src_free(&g_sources[i]);
}
pthread_mutex_unlock(&g_mtx);
if (was_active) {
if (enc) opus_codec_encoder_destroy(enc);
if (ac) audio_compressor_destroy(ac);
if (vad) silero_vad_destroy(vad);
if (preroll) u_free(preroll);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: stopped", RADIO_AUDIO_ID);
}
}
@@ -413,11 +410,19 @@ int radio_audio_vad_mode(void) {
return m;
}
int radio_audio_transmitting(void) {
pthread_mutex_lock(&g_mtx);
int talking = g_active && (g_vad_manual_ptt || g_vad_fsm.talking);
pthread_mutex_unlock(&g_mtx);
return talking;
}
/* ── PTT (GUI → uasync) ── */
static void radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, void (*fn)(void*)) {
if (!ua) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: talk post without uasync", RADIO_AUDIO_ID); return; }
struct radio_talk_arg* a = (struct radio_talk_arg*)u_malloc(sizeof(*a));
if (!a) return;
if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: talk post allocation failed", RADIO_AUDIO_ID); return; }
a->inst = g_inst;
a->group_id = group_id;
uasync_post(ua, fn, a);
@@ -426,22 +431,33 @@ static void radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, void (*f
void radio_audio_talk_begin(uint64_t group_id) {
if (!g_inst || !g_inst->ua) return;
pthread_mutex_lock(&g_mtx);
if (!g_active || group_id != g_group_id || !g_encoder) {
pthread_mutex_unlock(&g_mtx);
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: talk_begin without capture grp=%016llx", RADIO_AUDIO_ID, (unsigned long long)group_id);
return;
}
g_vad_manual_ptt = 1;
if (g_vad_fsm.talking) {
/* VAD вела передачу — закрываем её FIN, затем открываем ручной burst */
radio_audio_post_talk(g_inst->ua, group_id, radio_talk_end_trampoline);
radio_vad_fsm_reset(&g_vad_fsm);
}
pthread_mutex_unlock(&g_mtx);
radio_audio_post_talk(g_inst->ua, group_id, radio_talk_begin_trampoline);
pthread_mutex_unlock(&g_mtx);
}
void radio_audio_talk_end(uint64_t group_id) {
if (!g_inst || !g_inst->ua) return;
pthread_mutex_lock(&g_mtx);
if (!g_active || group_id != g_group_id || !g_vad_manual_ptt) {
pthread_mutex_unlock(&g_mtx);
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: talk_end without manual transmission grp=%016llx", RADIO_AUDIO_ID,
(unsigned long long)group_id);
return;
}
g_vad_manual_ptt = 0;
pthread_mutex_unlock(&g_mtx);
radio_audio_post_talk(g_inst->ua, group_id, radio_talk_end_trampoline);
pthread_mutex_unlock(&g_mtx);
}
/* ── VAD авто-PTT: helpers (под g_mtx) ── */
@@ -450,12 +466,12 @@ void radio_audio_talk_end(uint64_t group_id) {
* Вызывается при удержании g_mtx (использует g_encoder/g_capture_channels). */
static void radio_audio_encode_post(uint64_t group_id, const int16_t* frame,
struct UASYNC* ua, struct UTUN_INSTANCE* inst) {
if (!ua || !inst) return;
if (!ua || !inst) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: encode without instance", RADIO_AUDIO_ID); return; }
uint8_t opus[RADIO_MAX_OPUS];
int l = opus_codec_encode(g_encoder, frame, RADIO_AUDIO_FRAME_SAMPLES, opus, (int)sizeof(opus));
if (l <= 0) return;
if (l <= 0) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: opus encode failed rc=%d", RADIO_AUDIO_ID, l); return; }
struct radio_media_arg* a = (struct radio_media_arg*)u_malloc(sizeof(*a));
if (!a) return;
if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: media allocation failed", RADIO_AUDIO_ID); return; }
a->inst = inst;
a->group_id = group_id;
a->len = (uint16_t)l;
@@ -463,34 +479,6 @@ static void radio_audio_encode_post(uint64_t group_id, const int16_t* frame,
uasync_post(ua, radio_talk_send_trampoline, a);
}
/* Записать кадр сырого PCM в кольцевой pre-roll (старые сэмплы вытесняются). */
static void radio_vad_preroll_put(const int16_t* pcm, int count) {
if (!g_vad_preroll || count <= 0 || g_vad_preroll_cap <= 0) return;
if (count > g_vad_preroll_cap) { pcm += (count - g_vad_preroll_cap); count = g_vad_preroll_cap; }
for (int i = 0; i < count; i++) {
g_vad_preroll[g_vad_preroll_pos] = pcm[i];
g_vad_preroll_pos = (g_vad_preroll_pos + 1) % g_vad_preroll_cap;
if (g_vad_preroll_len < g_vad_preroll_cap) g_vad_preroll_len++;
}
}
/* Выдать все полные кадры из pre-roll (oldest→newest) через encode_post, затем очистить. */
static void radio_vad_preroll_flush(uint64_t group_id, struct UASYNC* ua, struct UTUN_INSTANCE* inst) {
if (!g_vad_preroll || g_vad_preroll_cap <= 0) return;
int frame_total = RADIO_AUDIO_FRAME_SAMPLES * g_capture_channels;
if (frame_total <= 0) return;
int n_frames = g_vad_preroll_len / frame_total;
int16_t tmp[RADIO_AUDIO_FRAME_SAMPLES * 2]; /* frame_total макс. 1920 */
for (int f = 0; f < n_frames; f++) {
int start = g_vad_preroll_pos - g_vad_preroll_len + f * frame_total;
start = ((start % g_vad_preroll_cap) + g_vad_preroll_cap) % g_vad_preroll_cap;
for (int i = 0; i < frame_total; i++)
tmp[i] = g_vad_preroll[(start + i) % g_vad_preroll_cap];
radio_audio_encode_post(group_id, tmp, ua, inst);
}
g_vad_preroll_len = 0;
}
/* VAD-ветка feed_pcm: ресемпл → Silero → фильтр → старт/стоп/передача (держит g_mtx). */
static int radio_vad_feed(uint64_t group_id, const int16_t* pcm, int count) {
float out16k[RADIO_VAD_FRAME_SAMPLES_16K];
@@ -506,16 +494,12 @@ static int radio_vad_feed(uint64_t group_id, const int16_t* pcm, int count) {
inst = g_inst;
if (g_vad_manual_ptt) {
/* PTT override: ведём pre-roll и просто передаём кадр (VAD не вмешивается) */
radio_vad_preroll_put(pcm, count);
/* Ручной PTT: передаём текущий кадр, VAD не вмешивается. */
radio_audio_encode_post(group_id, pcm, ua, inst);
pthread_mutex_unlock(&g_mtx);
return 0;
}
radio_vad_preroll_put(pcm, count);
int started_this_frame = 0;
radio_vad_resample(pcm, g_capture_channels, out16k);
int off = 0;
while (off < RADIO_VAD_FRAME_SAMPLES_16K) {
@@ -526,10 +510,18 @@ static int radio_vad_feed(uint64_t group_id, const int16_t* pcm, int count) {
off += take;
if (g_vad_win_len == RADIO_VAD_WINDOW_16K) {
float prob = 0.0f;
if (silero_vad_process(g_vad, g_vad_win, &prob) != 0) prob = 0.0f;
if (silero_vad_process(g_vad, g_vad_win, &prob) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: VAD processing failed", RADIO_AUDIO_ID);
prob = 0.0f;
}
g_vad_win_len = 0;
uint64_t now = get_time_tb();
int busy = (g_last_rx_tb != 0 && (now - g_last_rx_tb) < RADIO_VAD_BUSY_TB) ? 1 : 0;
if (!g_tx_stats_tb || now - g_tx_stats_tb >= RADIO_AUDIO_STATS_TB) {
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: vad prob=%.3f threshold=%.2f busy=%d talking=%d confirm=%d",
RADIO_AUDIO_ID, prob, g_vad_threshold, busy, g_vad_fsm.talking, g_vad_fsm.confirm_count);
g_tx_stats_tb = now;
}
int action = radio_vad_fsm_update(&g_vad_fsm, prob, g_vad_threshold,
RADIO_VAD_CONFIRM_WINDOWS,
(uint64_t)g_vad_hangover_ms * 10, busy, now);
@@ -537,8 +529,6 @@ static int radio_vad_feed(uint64_t group_id, const int16_t* pcm, int count) {
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: vad START prob=%.2f grp=%016llx",
RADIO_AUDIO_ID, prob, (unsigned long long)group_id);
radio_audio_post_talk(ua, group_id, radio_talk_begin_trampoline);
radio_vad_preroll_flush(group_id, ua, inst); /* флашит ring (включая текущий кадр) */
started_this_frame = 1;
} else if (action == -1) {
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: vad STOP grp=%016llx",
RADIO_AUDIO_ID, (unsigned long long)group_id);
@@ -547,7 +537,7 @@ static int radio_vad_feed(uint64_t group_id, const int16_t* pcm, int count) {
}
}
if (g_vad_fsm.talking && !started_this_frame) {
if (g_vad_fsm.talking) {
radio_audio_encode_post(group_id, pcm, ua, inst);
}
+9
View File
@@ -33,10 +33,16 @@ int radio_audio_init(struct UTUN_INSTANCE* inst);
void radio_audio_destroy(struct UTUN_INSTANCE* inst);
/* GUI-поток: начать прослушивание канала (encoder + реестр источников).
* capture_channels=0 — только RX; 1/2 сохраняют запуск RX+TX для desktop.
* capture_channels — каналы захвата (1=моно, 2=стерео): задаёт GUI по фактической
* возможности устройства (стерео недоступно → откат на моно). RX всегда стерео. */
int radio_audio_start(uint64_t group_id, int capture_channels);
/* Независимый TX: start(..., 0) включает только RX, захват готовится по фактическому формату микрофона.
* capture_stop отправляет FIN активной передачи и освобождает TX, сохраняя RX. */
int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_enabled);
void radio_audio_capture_stop(void);
/* GUI-поток: прекратить прослушивание (освободить всё). */
void radio_audio_stop(void);
@@ -58,6 +64,9 @@ int radio_audio_active(void);
* и не гасить захват по PTT; ручной PTT остаётся override). */
int radio_audio_vad_mode(void);
/* Текущее состояние передачи: ручной PTT либо сработавший VAD. */
int radio_audio_transmitting(void);
/* uasync-поток: принятый Opus-кадр источника (декодируем + кладём в ring). */
void radio_audio_on_frame(struct UTUN_INSTANCE* inst, uint64_t group_id,
uint64_t src_node_id, uint16_t stream_id, uint16_t seq,