Improve Android radio capture lifecycle, VAD and PTT controls
This commit is contained in:
1 parent
52ffb91101
commit
0d44ada922
14 files changed
+839
-385
No files matched your search
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user