|
|
|
|
@ -28,6 +28,7 @@
|
|
|
|
|
|
|
|
|
|
#include <pthread.h> |
|
|
|
|
#include <string.h> |
|
|
|
|
#include <math.h> |
|
|
|
|
|
|
|
|
|
#define RADIO_AUDIO_ID "radio_audio" |
|
|
|
|
#define RADIO_AUDIO_BITRATE 32000 |
|
|
|
|
@ -48,6 +49,9 @@
|
|
|
|
|
/* ── 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_PACKETS (RADIO_PREFETCH_FRAMES / RADIO_AUDIO_FRAME_SAMPLES) |
|
|
|
|
#define RADIO_TX_PENDING_LIMIT 8 |
|
|
|
|
|
|
|
|
|
struct radio_pending { |
|
|
|
|
uint8_t used; |
|
|
|
|
@ -90,6 +94,11 @@ static uint64_t g_last_stats_tb = 0;
|
|
|
|
|
/* ── VAD авто-PTT: состояние (под g_mtx) ── */ |
|
|
|
|
static int g_vad_mode = 0; /* режим включён для текущей группы */ |
|
|
|
|
static silero_vad_t* g_vad = NULL; /* детектор (16кГц), создаётся в start */ |
|
|
|
|
static struct audio_compressor* g_vad_compressor = NULL; /* отдельная история и gain, только для детектора */ |
|
|
|
|
static int16_t g_vad_pcm[RADIO_AUDIO_MIX_SAMPLES]; /* копия 20мс PCM до TX-компрессора */ |
|
|
|
|
static int g_vad_pcm_frames = 0; |
|
|
|
|
static double g_vad_input_sum_sq = 0; |
|
|
|
|
static float g_vad_input_rms = 0, g_vad_output_rms = 0; |
|
|
|
|
static float g_vad_threshold = 0.5f; |
|
|
|
|
static int g_vad_hangover_ms = 200; |
|
|
|
|
static int g_vad_manual_ptt = 0; /* PTT override: зажата — VAD не вмешивается */ |
|
|
|
|
@ -101,6 +110,7 @@ static uint64_t g_last_rx_tb = 0; /* последний п
|
|
|
|
|
/* TX-статистика (кадрирование/кодирование), выводится раз в секунду пока идёт PTT. */ |
|
|
|
|
static uint64_t g_tx_stats_tb = 0; |
|
|
|
|
static uint64_t g_vad_stats_tb = 0; |
|
|
|
|
static float g_vad_peak_probability = 0; /* максимум между логами, чтобы не пропускать короткую речь */ |
|
|
|
|
static uint32_t g_tx_frames = 0; /* кадров за интервал */ |
|
|
|
|
static uint32_t g_tx_bytes = 0; /* суммарный размер opus за интервал */ |
|
|
|
|
static uint32_t g_tx_tail = 0; /* нулевых отсчётов дополнения хвоста за интервал */ |
|
|
|
|
@ -108,6 +118,8 @@ static uint32_t g_tx_tail = 0; /* нулевых отсчётов доп
|
|
|
|
|
/* Один TX-автомат и отдельный накопитель сетевого кадра; AEC выполняется до этого API. */ |
|
|
|
|
static int g_transmitting, g_tx_pcm_frames, g_vad_decimation; |
|
|
|
|
static int16_t g_tx_pcm[RADIO_AUDIO_MIX_SAMPLES]; |
|
|
|
|
static int16_t g_prefetch_pcm[RADIO_PREFETCH_FRAMES * RADIO_AUDIO_MIX_CHANNELS]; |
|
|
|
|
static int g_prefetch_write, g_prefetch_frames; |
|
|
|
|
static float g_vad_sum; |
|
|
|
|
static uint64_t g_tx_burst; |
|
|
|
|
/* Только uasync: идентичность открытой сетевой серии, включая поколение capture. */ |
|
|
|
|
@ -119,12 +131,17 @@ static unsigned int g_control_pending;
|
|
|
|
|
static int g_control_warned; |
|
|
|
|
static struct radio_audio_talk* g_reserved_fin; |
|
|
|
|
static unsigned int g_tx_pending, g_tx_dropped, g_tx_reported; |
|
|
|
|
static int g_tx_warned; |
|
|
|
|
struct radio_audio_packet { |
|
|
|
|
int len; |
|
|
|
|
uint8_t opus[RADIO_MAX_OPUS]; |
|
|
|
|
}; |
|
|
|
|
struct radio_audio_tx { |
|
|
|
|
struct posted_task task; |
|
|
|
|
struct UTUN_INSTANCE* inst; |
|
|
|
|
uint64_t group_id, generation, listen_generation, instance_generation, burst, created_tb; |
|
|
|
|
int len; |
|
|
|
|
uint8_t opus[RADIO_MAX_OPUS]; |
|
|
|
|
int frame_count; |
|
|
|
|
struct radio_audio_packet frames[]; /* один live-кадр либо до 50 кадров prefetch */ |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
static void radio_audio_send(void* arg) { |
|
|
|
|
@ -135,7 +152,7 @@ static void radio_audio_send(void* arg) {
|
|
|
|
|
if (owner && g_tx_pending) --g_tx_pending; |
|
|
|
|
int send = owner && tx->listen_generation == g_listen_generation && g_active && g_group_id == tx->group_id && |
|
|
|
|
g_net_generation == tx->generation && g_net_burst == tx->burst && g_net_group == tx->group_id; |
|
|
|
|
if (get_time_tb() - tx->created_tb > 2000) { send = 0; if (current) ++g_tx_dropped; } |
|
|
|
|
if (get_time_tb() - tx->created_tb > 2000) { send = 0; if (current) g_tx_dropped += tx->frame_count; } |
|
|
|
|
unsigned int dropped = g_tx_dropped, pending = g_tx_pending; |
|
|
|
|
int changed = dropped != g_tx_reported; |
|
|
|
|
g_tx_reported = dropped; |
|
|
|
|
@ -147,27 +164,55 @@ static void radio_audio_send(void* arg) {
|
|
|
|
|
} |
|
|
|
|
pthread_mutex_unlock(&g_mtx); |
|
|
|
|
if (changed) { |
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: TX dropped=%u pending=%u limit=8 max_age=200ms", RADIO_AUDIO_ID, |
|
|
|
|
dropped, pending); |
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: TX dropped=%u pending_tasks=%u limit=%d max_age=200ms", RADIO_AUDIO_ID, |
|
|
|
|
dropped, pending, RADIO_TX_PENDING_LIMIT); |
|
|
|
|
} |
|
|
|
|
if (frames) DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: TX frames=%u bytes=%u tail_padding_samples=%u", RADIO_AUDIO_ID, frames, bytes, tail); |
|
|
|
|
if (send) radio_talk_send(tx->inst, tx->group_id, tx->opus, tx->len); |
|
|
|
|
if (tx->frame_count > 1) DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: TX prefetch frames=%d send=%d", RADIO_AUDIO_ID, tx->frame_count, send); |
|
|
|
|
if (send) { |
|
|
|
|
for (int i = 0; i < tx->frame_count; i++) { |
|
|
|
|
int rc = radio_talk_send(tx->inst, tx->group_id, tx->frames[i].opus, tx->frames[i].len); |
|
|
|
|
if (rc != 0) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: TX send failed frame=%d/%d rc=%d", RADIO_AUDIO_ID, i, tx->frame_count, rc); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Вызывается под g_mtx; post и FIN упорядочены тем же mutex, задачи не накапливаются бесконечно. */ |
|
|
|
|
static int radio_audio_post_frame(struct UTUN_INSTANCE* inst, uint64_t group_id, const uint8_t* opus, int len) { |
|
|
|
|
if (!inst || !inst->ua || g_tx_pending >= 8) { ++g_tx_dropped; return -1; } |
|
|
|
|
struct radio_audio_tx* task = u_calloc(1, sizeof(*task)); |
|
|
|
|
if (!task) { ++g_tx_dropped; return -1; } |
|
|
|
|
/* Под g_mtx: prefetch резервирует одну media-задачу до BEGIN, при перегрузке история остаётся в кольце. */ |
|
|
|
|
static struct radio_audio_tx* radio_audio_tx_alloc(struct UTUN_INSTANCE* inst, int frames) { |
|
|
|
|
if (!inst || !inst->ua) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: TX without instance/uasync", RADIO_AUDIO_ID); return NULL; } |
|
|
|
|
if (frames < 1 || frames > RADIO_PREFETCH_PACKETS) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: TX invalid frame count=%d limit=%d", RADIO_AUDIO_ID, frames, RADIO_PREFETCH_PACKETS); |
|
|
|
|
return NULL; |
|
|
|
|
} |
|
|
|
|
if (g_tx_pending >= RADIO_TX_PENDING_LIMIT) { |
|
|
|
|
if (!g_tx_warned) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: TX queue full pending_tasks=%u limit=%d", RADIO_AUDIO_ID, |
|
|
|
|
g_tx_pending, RADIO_TX_PENDING_LIMIT); |
|
|
|
|
g_tx_warned = 1; return NULL; |
|
|
|
|
} |
|
|
|
|
struct radio_audio_tx* task = u_calloc(1, sizeof(*task) + (size_t)frames * sizeof(task->frames[0])); |
|
|
|
|
if (!task) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: TX allocation failed frames=%d", RADIO_AUDIO_ID, frames); return NULL; } |
|
|
|
|
g_tx_warned = 0; |
|
|
|
|
return task; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Вызывается под g_mtx; один BEGIN, пакет prefetch, live и FIN сохраняют порядок uasync. */ |
|
|
|
|
static void radio_audio_post_tx(struct UTUN_INSTANCE* inst, uint64_t group_id, struct radio_audio_tx* task) { |
|
|
|
|
task->inst = inst; task->group_id = group_id; |
|
|
|
|
task->generation = g_tx_generation; task->burst = g_tx_burst; task->created_tb = get_time_tb(); |
|
|
|
|
task->listen_generation = g_listen_generation; |
|
|
|
|
task->instance_generation = g_instance_generation; |
|
|
|
|
task->len = len; |
|
|
|
|
memcpy(task->opus, opus, (size_t)len); |
|
|
|
|
for (int i = 0; i < task->frame_count; i++) { ++g_tx_frames; g_tx_bytes += (uint32_t)task->frames[i].len; } |
|
|
|
|
task->task.callback = radio_audio_send; task->task.arg = task; |
|
|
|
|
++g_tx_pending; |
|
|
|
|
uasync_post_reserved(inst->ua, &task->task); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static int radio_audio_post_frame(struct UTUN_INSTANCE* inst, uint64_t group_id, const uint8_t* opus, int len) { |
|
|
|
|
struct radio_audio_tx* task = radio_audio_tx_alloc(inst, 1); |
|
|
|
|
if (!task) { ++g_tx_dropped; return -1; } |
|
|
|
|
task->frame_count = 1; task->frames[0].len = len; |
|
|
|
|
memcpy(task->frames[0].opus, opus, (size_t)len); |
|
|
|
|
radio_audio_post_tx(inst, group_id, task); |
|
|
|
|
return 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -392,51 +437,79 @@ int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_e
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
|
/* Общие параметры TX/VAD; экземпляры разделяют только конфигурацию, не историю и gain. */ |
|
|
|
|
audio_compressor_config_t accfg = {0}; |
|
|
|
|
accfg.sample_rate = RADIO_AUDIO_SAMPLE_RATE; |
|
|
|
|
accfg.channels = capture_channels; |
|
|
|
|
accfg.block_duration_ms = RADIO_AUDIO_FRAME_MS; |
|
|
|
|
accfg.lookback_ms = 200; |
|
|
|
|
accfg.lookahead_ms = 0; |
|
|
|
|
accfg.max_gain_db = (float)chat_setting_get_int(g_inst, "radio_compressor_max_gain_db", 15); |
|
|
|
|
accfg.rise_rate_per_sec = (float)chat_setting_get_int(g_inst, "compressor_rise_rate", 2); |
|
|
|
|
accfg.release_rate_per_sec = accfg.rise_rate_per_sec * 5.0f; |
|
|
|
|
accfg.target_level = 0.25f; |
|
|
|
|
int compressor_enabled = chat_setting_get_int(g_inst, "radio_compressor_enabled", 1); |
|
|
|
|
/* Компрессор TX: lookahead=0, без задержки и хвоста. */ |
|
|
|
|
struct audio_compressor* ac = audio_compressor_create(); |
|
|
|
|
if (ac) { |
|
|
|
|
audio_compressor_config_t accfg = {0}; |
|
|
|
|
accfg.sample_rate = RADIO_AUDIO_SAMPLE_RATE; |
|
|
|
|
accfg.channels = capture_channels; |
|
|
|
|
accfg.block_duration_ms = RADIO_AUDIO_FRAME_MS; |
|
|
|
|
accfg.lookback_ms = 200; |
|
|
|
|
accfg.lookahead_ms = 0; |
|
|
|
|
accfg.max_gain_db = (float)chat_setting_get_int(g_inst, "radio_compressor_max_gain_db", 15); |
|
|
|
|
accfg.rise_rate_per_sec = (float)chat_setting_get_int(g_inst, "compressor_rise_rate", 2); |
|
|
|
|
accfg.target_level = 0.25f; /* -12 dBFS — как у голосовых сообщений */ |
|
|
|
|
if (audio_compressor_configure(ac, &accfg) != 0) { audio_compressor_destroy(ac); ac = NULL; } |
|
|
|
|
if (ac) audio_compressor_set_enabled(ac, chat_setting_get_int(g_inst, "radio_compressor_enabled", 1)); |
|
|
|
|
if (ac) audio_compressor_set_enabled(ac, compressor_enabled); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!ac) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: compressor unavailable", RADIO_AUDIO_ID); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: compressor TX=%s max_gain=%.0fdB rate=%d ch=%d block=%dms " |
|
|
|
|
"lookback=%dms lookahead=%dms target=%.3f rise=%.1f release=%.1f", RADIO_AUDIO_ID, |
|
|
|
|
ac ? (compressor_enabled ? "on" : "off") : "unavailable", accfg.max_gain_db, accfg.sample_rate, |
|
|
|
|
accfg.channels, accfg.block_duration_ms, accfg.lookback_ms, accfg.lookahead_ms, accfg.target_level, |
|
|
|
|
accfg.rise_rate_per_sec, accfg.release_rate_per_sec); |
|
|
|
|
|
|
|
|
|
/* VAD анализирует текущие кадры без истории захвата. */ |
|
|
|
|
/* VAD анализирует текущие кадры; prefetch сохраняет предшествующий непереданный PCM. */ |
|
|
|
|
g_vad_mode = 0; |
|
|
|
|
if (vad_enabled) { |
|
|
|
|
g_vad_threshold = (float)chat_setting_get_int(g_inst, "radio_vad_threshold", 50) / 100.0f; |
|
|
|
|
g_vad_hangover_ms = chat_setting_get_int(g_inst, "radio_vad_hangover_ms", 200); |
|
|
|
|
pthread_mutex_unlock(&g_mtx); |
|
|
|
|
silero_vad_t* vad = silero_vad_create_default(); |
|
|
|
|
pthread_mutex_lock(&g_mtx); |
|
|
|
|
g_vad = vad; g_vad_mode = vad != NULL; |
|
|
|
|
if (!g_vad) { |
|
|
|
|
accfg.max_gain_db = (float)chat_setting_get_int(g_inst, "radio_vad_compressor_max_gain_db", 15); |
|
|
|
|
if (compressor_enabled) { |
|
|
|
|
g_vad_compressor = audio_compressor_create(); |
|
|
|
|
if (g_vad_compressor && audio_compressor_configure(g_vad_compressor, &accfg) != 0) { |
|
|
|
|
audio_compressor_destroy(g_vad_compressor); g_vad_compressor = NULL; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
if (!compressor_enabled || g_vad_compressor) { |
|
|
|
|
pthread_mutex_unlock(&g_mtx); |
|
|
|
|
silero_vad_t* vad = silero_vad_create_default(); |
|
|
|
|
pthread_mutex_lock(&g_mtx); |
|
|
|
|
g_vad = vad; g_vad_mode = vad != NULL; |
|
|
|
|
} else DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: VAD compressor create failed — auto-PTT disabled", RADIO_AUDIO_ID); |
|
|
|
|
if (!g_vad && (!compressor_enabled || g_vad_compressor)) { |
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: silero_vad_create_default failed — VAD auto-PTT disabled", RADIO_AUDIO_ID); |
|
|
|
|
} |
|
|
|
|
if (!g_vad_mode && g_vad_compressor) { audio_compressor_destroy(g_vad_compressor); g_vad_compressor = NULL; } |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: VAD compressor=%s max_gain=%.0fdB shared_with_TX=enabled,target,history,rise,release " |
|
|
|
|
"block=%dms buffering=%dms vad=%d", RADIO_AUDIO_ID, |
|
|
|
|
g_vad_compressor ? "on" : (compressor_enabled ? "unavailable" : "off"), accfg.max_gain_db, |
|
|
|
|
accfg.block_duration_ms, g_vad_compressor ? accfg.block_duration_ms : 0, g_vad_mode); |
|
|
|
|
} |
|
|
|
|
g_vad_pcm_frames = 0; g_vad_input_sum_sq = 0; |
|
|
|
|
g_vad_input_rms = g_vad_output_rms = 0; |
|
|
|
|
if (g_vad_mode) { |
|
|
|
|
g_vad_win_len = 0; |
|
|
|
|
radio_vad_fsm_reset(&g_vad_fsm); |
|
|
|
|
g_vad_stats_tb = 0; g_vad_peak_probability = 0; |
|
|
|
|
} |
|
|
|
|
g_vad_manual_ptt = g_transmitting = g_tx_pcm_frames = g_vad_decimation = 0; |
|
|
|
|
g_vad_sum = 0; g_tx_burst = 0; |
|
|
|
|
g_prefetch_write = g_prefetch_frames = g_tx_warned = 0; |
|
|
|
|
|
|
|
|
|
++g_tx_generation; g_tx_dropped = g_tx_reported = 0; |
|
|
|
|
g_encoder = enc; |
|
|
|
|
g_compressor = ac; |
|
|
|
|
g_capture_channels = capture_channels; |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: capture started grp=%016llx ch=%d opus=%s bitrate=%dkbps compressor=%s vad=%d threshold=%.2f hangover=%dms", |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: capture started grp=%016llx ch=%d opus=%s bitrate=%dkbps compressor=%s " |
|
|
|
|
"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); |
|
|
|
|
ac ? (audio_compressor_is_enabled(ac) ? "on" : "off") : "unavailable", g_vad_mode, g_vad_threshold, |
|
|
|
|
g_vad_hangover_ms, g_vad_mode ? 1000 : 0); |
|
|
|
|
radio_tx_unlock(); |
|
|
|
|
return 0; |
|
|
|
|
} |
|
|
|
|
@ -452,10 +525,18 @@ void radio_audio_capture_stop(void) {
|
|
|
|
|
g_transmitting = g_tx_pcm_frames = 0; |
|
|
|
|
opus_codec_encoder_destroy(g_encoder); g_encoder = NULL; |
|
|
|
|
if (g_compressor) { audio_compressor_destroy(g_compressor); g_compressor = NULL; } |
|
|
|
|
if (g_vad_compressor) { |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: VAD compressor stopped gain=%.1fdB clips=%u tail=%d", RADIO_AUDIO_ID, |
|
|
|
|
audio_compressor_gain_db(g_vad_compressor), audio_compressor_clip_count(g_vad_compressor), g_vad_pcm_frames); |
|
|
|
|
audio_compressor_destroy(g_vad_compressor); g_vad_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; |
|
|
|
|
g_vad_pcm_frames = 0; g_vad_input_sum_sq = 0; |
|
|
|
|
g_vad_input_rms = g_vad_output_rms = 0; |
|
|
|
|
g_prefetch_write = g_prefetch_frames = 0; |
|
|
|
|
radio_vad_fsm_reset(&g_vad_fsm); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: capture stopped", RADIO_AUDIO_ID); |
|
|
|
|
} |
|
|
|
|
@ -598,24 +679,26 @@ void radio_audio_talk_end(uint64_t group_id) {
|
|
|
|
|
|
|
|
|
|
/* ── VAD авто-PTT: helpers (под g_mtx) ── */ |
|
|
|
|
|
|
|
|
|
/* Закодировать один 20мс кадр и запостить radio_talk_send в uasync.
|
|
|
|
|
* Вызывается при удержании 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) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: encode without instance", RADIO_AUDIO_ID); return; } |
|
|
|
|
/* Один 20мс кадр, общий путь AGC/Opus для prefetch и live; под g_mtx. */ |
|
|
|
|
static int radio_audio_encode_frame(const int16_t* frame, uint8_t* opus) { |
|
|
|
|
int16_t amplified[RADIO_AUDIO_MIX_SAMPLES]; |
|
|
|
|
if (g_compressor) { |
|
|
|
|
if (audio_compressor_process_frame(g_compressor, frame, amplified, |
|
|
|
|
(size_t)RADIO_AUDIO_FRAME_SAMPLES * g_capture_channels) != 0) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: compressor frame failed", RADIO_AUDIO_ID); |
|
|
|
|
return; |
|
|
|
|
return -1; |
|
|
|
|
} |
|
|
|
|
frame = amplified; |
|
|
|
|
} |
|
|
|
|
int len = opus_codec_encode(g_encoder, frame, RADIO_AUDIO_FRAME_SAMPLES, opus, RADIO_MAX_OPUS); |
|
|
|
|
if (len <= 0) DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: opus encode failed rc=%d", RADIO_AUDIO_ID, len); |
|
|
|
|
return len; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static void radio_audio_encode_post(uint64_t group_id, const int16_t* frame, struct UTUN_INSTANCE* inst) { |
|
|
|
|
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) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: opus encode failed rc=%d", RADIO_AUDIO_ID, l); return; } |
|
|
|
|
if (radio_audio_post_frame(inst, group_id, opus, l) == 0) { ++g_tx_frames; g_tx_bytes += (uint32_t)l; } |
|
|
|
|
int len = radio_audio_encode_frame(frame, opus); |
|
|
|
|
if (len > 0) radio_audio_post_frame(inst, group_id, opus, len); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Хвост относится только к своей серии. Тишина добавляется исключительно перед Opus. */ |
|
|
|
|
@ -624,38 +707,82 @@ static void radio_audio_flush_tx(void) {
|
|
|
|
|
int count = g_tx_pcm_frames * g_capture_channels; |
|
|
|
|
g_tx_tail += RADIO_AUDIO_FRAME_SAMPLES * g_capture_channels - count; |
|
|
|
|
memset(g_tx_pcm + count, 0, (size_t)(RADIO_AUDIO_FRAME_SAMPLES * g_capture_channels - count) * sizeof(int16_t)); |
|
|
|
|
radio_audio_encode_post(g_group_id, g_tx_pcm, g_inst ? g_inst->ua : NULL, g_inst); |
|
|
|
|
radio_audio_encode_post(g_group_id, g_tx_pcm, g_inst); |
|
|
|
|
g_tx_pcm_frames = 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Перед BEGIN история зарезервирована одной задачей; неполный кадр продолжается текущим PCM без padding. */ |
|
|
|
|
static void radio_audio_send_prefetch(struct radio_audio_tx* task) { |
|
|
|
|
int frames = g_prefetch_frames; |
|
|
|
|
int offset = (g_prefetch_write + RADIO_PREFETCH_FRAMES - frames) % RADIO_PREFETCH_FRAMES; |
|
|
|
|
int remaining = frames; |
|
|
|
|
while (remaining) { |
|
|
|
|
int count = remaining < RADIO_AUDIO_FRAME_SAMPLES ? remaining : RADIO_AUDIO_FRAME_SAMPLES; |
|
|
|
|
int contiguous = RADIO_PREFETCH_FRAMES - offset; |
|
|
|
|
if (contiguous > count) contiguous = count; |
|
|
|
|
memcpy(g_tx_pcm, g_prefetch_pcm + offset * g_capture_channels, (size_t)contiguous * g_capture_channels * sizeof(int16_t)); |
|
|
|
|
if (contiguous < count) { |
|
|
|
|
memcpy(g_tx_pcm + contiguous * g_capture_channels, g_prefetch_pcm, |
|
|
|
|
(size_t)(count - contiguous) * g_capture_channels * sizeof(int16_t)); |
|
|
|
|
} |
|
|
|
|
if (count == RADIO_AUDIO_FRAME_SAMPLES) { |
|
|
|
|
int len = radio_audio_encode_frame(g_tx_pcm, task->frames[task->frame_count].opus); |
|
|
|
|
if (len > 0) task->frames[task->frame_count++].len = len; |
|
|
|
|
else ++g_tx_dropped; |
|
|
|
|
} else g_tx_pcm_frames = count; |
|
|
|
|
offset = (offset + count) % RADIO_PREFETCH_FRAMES; |
|
|
|
|
remaining -= count; |
|
|
|
|
} |
|
|
|
|
int packets = task ? task->frame_count : 0; |
|
|
|
|
if (packets) radio_audio_post_tx(g_inst, g_group_id, task); |
|
|
|
|
else u_free(task); |
|
|
|
|
g_prefetch_write = g_prefetch_frames = 0; |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: TX prefetch samples_per_channel=%d duration=%dms frames=%d tail=%d ch=%d", RADIO_AUDIO_ID, |
|
|
|
|
frames, frames * 1000 / RADIO_AUDIO_SAMPLE_RATE, packets, g_tx_pcm_frames, g_capture_channels); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Ручной PTT имеет приоритет; смена manual/VAD при открытой серии не создаёт второй BEGIN. */ |
|
|
|
|
static void radio_audio_update_transmission(void) { |
|
|
|
|
int desired = g_vad_manual_ptt || (g_vad_mode && g_vad_fsm.talking); |
|
|
|
|
if (desired == g_transmitting) return; |
|
|
|
|
struct radio_audio_tx* prefetch = NULL; |
|
|
|
|
if (!desired) radio_audio_flush_tx(); |
|
|
|
|
else { ++g_tx_burst; g_tx_pcm_frames = 0; } |
|
|
|
|
if (radio_audio_post_talk(g_inst ? g_inst->ua : NULL, g_group_id, desired) != 0) return; |
|
|
|
|
else { |
|
|
|
|
int packets = g_vad_mode ? g_prefetch_frames / RADIO_AUDIO_FRAME_SAMPLES : 0; |
|
|
|
|
if (packets) { |
|
|
|
|
prefetch = radio_audio_tx_alloc(g_inst, packets); |
|
|
|
|
if (!prefetch) return; |
|
|
|
|
} |
|
|
|
|
++g_tx_burst; g_tx_pcm_frames = 0; |
|
|
|
|
} |
|
|
|
|
if (radio_audio_post_talk(g_inst ? g_inst->ua : NULL, g_group_id, desired) != 0) { u_free(prefetch); return; } |
|
|
|
|
g_transmitting = desired; |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: TX %s generation=%llu burst=%llu manual=%d vad=%d", RADIO_AUDIO_ID, |
|
|
|
|
desired ? "BEGIN" : "FIN", (unsigned long long)g_tx_generation, (unsigned long long)g_tx_burst, |
|
|
|
|
g_vad_manual_ptt, g_vad_fsm.talking); |
|
|
|
|
if (desired && g_vad_mode) radio_audio_send_prefetch(prefetch); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void radio_audio_capture_discontinuity(uint64_t group_id) { |
|
|
|
|
radio_tx_lock(); |
|
|
|
|
if (g_active && g_group_id == group_id && g_encoder) { |
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: capture discontinuity tail=%d transmitting=%d", RADIO_AUDIO_ID, |
|
|
|
|
g_tx_pcm_frames, g_transmitting); |
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: capture discontinuity tail=%d transmitting=%d vad_tail=%d vad_gain=%.1fdB", |
|
|
|
|
RADIO_AUDIO_ID, g_tx_pcm_frames, g_transmitting, g_vad_pcm_frames, audio_compressor_gain_db(g_vad_compressor)); |
|
|
|
|
radio_audio_flush_tx(); |
|
|
|
|
g_vad_win_len = g_vad_decimation = 0; g_vad_sum = 0; |
|
|
|
|
g_vad_pcm_frames = 0; g_vad_input_sum_sq = 0; |
|
|
|
|
g_vad_input_rms = g_vad_output_rms = 0; |
|
|
|
|
if (g_vad_compressor) audio_compressor_reset(g_vad_compressor); |
|
|
|
|
g_prefetch_write = g_prefetch_frames = 0; |
|
|
|
|
if (g_vad) silero_vad_reset(g_vad); |
|
|
|
|
radio_vad_fsm_reset(&g_vad_fsm); |
|
|
|
|
g_vad_stats_tb = 0; g_vad_peak_probability = 0; |
|
|
|
|
radio_audio_update_transmission(); |
|
|
|
|
} |
|
|
|
|
radio_tx_unlock(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* VAD читает очищенный сигнал до AGC, накопление 48k→16k непрерывно между PCM-чанками. */ |
|
|
|
|
/* VAD читает свою ветку PCM; накопление 48k→16k непрерывно между PCM-чанками и блоками AGC. */ |
|
|
|
|
static void radio_audio_vad_sample(const int16_t* frame) { |
|
|
|
|
float mono = g_capture_channels == 2 ? ((float)frame[0] + frame[1]) * 0.5f : frame[0]; |
|
|
|
|
g_vad_sum += mono; |
|
|
|
|
@ -663,6 +790,11 @@ static void radio_audio_vad_sample(const int16_t* frame) {
|
|
|
|
|
g_vad_win[g_vad_win_len++] = g_vad_sum / (3.0f * 32768.0f); |
|
|
|
|
g_vad_decimation = 0; g_vad_sum = 0; |
|
|
|
|
if (g_vad_win_len != RADIO_VAD_WINDOW_16K) return; |
|
|
|
|
if (!g_vad_compressor) { |
|
|
|
|
double sum_sq = 0; |
|
|
|
|
for (int i = 0; i < RADIO_VAD_WINDOW_16K; ++i) sum_sq += (double)g_vad_win[i] * g_vad_win[i]; |
|
|
|
|
g_vad_input_rms = g_vad_output_rms = sqrtf((float)(sum_sq / RADIO_VAD_WINDOW_16K)); |
|
|
|
|
} |
|
|
|
|
float probability = 0; |
|
|
|
|
pthread_mutex_unlock(&g_mtx); |
|
|
|
|
int result = silero_vad_process(g_vad, g_vad_win, &probability); |
|
|
|
|
@ -672,16 +804,55 @@ static void radio_audio_vad_sample(const int16_t* frame) {
|
|
|
|
|
g_vad_win_len = 0; |
|
|
|
|
uint64_t now = get_time_tb(); |
|
|
|
|
int busy = g_last_rx_tb && now - g_last_rx_tb < RADIO_VAD_BUSY_TB; |
|
|
|
|
radio_vad_fsm_update(&g_vad_fsm, probability, g_vad_threshold, RADIO_VAD_CONFIRM_WINDOWS, |
|
|
|
|
(uint64_t)g_vad_hangover_ms * 10, busy, now); |
|
|
|
|
int transition = radio_vad_fsm_update(&g_vad_fsm, probability, g_vad_threshold, RADIO_VAD_CONFIRM_WINDOWS, |
|
|
|
|
(uint64_t)g_vad_hangover_ms * 10, busy, now); |
|
|
|
|
if (transition) { |
|
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: VAD %s probability=%.3f threshold=%.2f busy=%d hangover=%dms", RADIO_AUDIO_ID, |
|
|
|
|
transition > 0 ? "START" : "STOP", probability, g_vad_threshold, busy, g_vad_hangover_ms); |
|
|
|
|
} |
|
|
|
|
radio_audio_update_transmission(); |
|
|
|
|
if (probability > g_vad_peak_probability) g_vad_peak_probability = probability; |
|
|
|
|
if (!g_vad_stats_tb || now - g_vad_stats_tb >= RADIO_AUDIO_STATS_TB) { |
|
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: VAD probability=%.3f busy=%d manual=%d transmitting=%d", |
|
|
|
|
RADIO_AUDIO_ID, probability, busy, g_vad_manual_ptt, g_transmitting); |
|
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: VAD probability=%.3f peak=%.3f threshold=%.2f confirm=%d/%d busy=%d manual=%d transmitting=%d " |
|
|
|
|
"compressor=%s input_rms=%.6f output_rms=%.6f gain=%.1fdB clips=%u", |
|
|
|
|
RADIO_AUDIO_ID, probability, g_vad_peak_probability, g_vad_threshold, g_vad_fsm.confirm_count, |
|
|
|
|
RADIO_VAD_CONFIRM_WINDOWS, busy, g_vad_manual_ptt, g_transmitting, g_vad_compressor ? "on" : "off", |
|
|
|
|
g_vad_input_rms, g_vad_output_rms, audio_compressor_gain_db(g_vad_compressor), |
|
|
|
|
audio_compressor_clip_count(g_vad_compressor)); |
|
|
|
|
g_vad_stats_tb = now; |
|
|
|
|
g_vad_peak_probability = 0; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Сжимается только копия для VAD. Выключенный AGC не добавляет ожидания 20мс. */ |
|
|
|
|
static void radio_audio_vad_feed(const int16_t* frame) { |
|
|
|
|
if (!g_vad_compressor) { radio_audio_vad_sample(frame); return; } |
|
|
|
|
memcpy(g_vad_pcm + g_vad_pcm_frames * g_capture_channels, frame, g_capture_channels * sizeof(int16_t)); |
|
|
|
|
for (int ch = 0; ch < g_capture_channels; ++ch) { |
|
|
|
|
double sample = (double)frame[ch] / 32768.0; |
|
|
|
|
g_vad_input_sum_sq += sample * sample; |
|
|
|
|
} |
|
|
|
|
if (++g_vad_pcm_frames != RADIO_AUDIO_FRAME_SAMPLES) return; |
|
|
|
|
int count = RADIO_AUDIO_FRAME_SAMPLES * g_capture_channels; |
|
|
|
|
g_vad_input_rms = sqrtf((float)(g_vad_input_sum_sq / count)); |
|
|
|
|
g_vad_pcm_frames = 0; g_vad_input_sum_sq = 0; |
|
|
|
|
if (audio_compressor_process_frame(g_vad_compressor, g_vad_pcm, g_vad_pcm, count) != 0) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: VAD compressor frame failed count=%d — auto-PTT disabled until capture restart", |
|
|
|
|
RADIO_AUDIO_ID, count); |
|
|
|
|
g_vad_mode = 0; g_prefetch_write = g_prefetch_frames = 0; |
|
|
|
|
radio_vad_fsm_reset(&g_vad_fsm); |
|
|
|
|
radio_audio_update_transmission(); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
double sum_sq = 0; |
|
|
|
|
for (int i = 0; i < count; ++i) { |
|
|
|
|
double sample = (double)g_vad_pcm[i] / 32768.0; |
|
|
|
|
sum_sq += sample * sample; |
|
|
|
|
} |
|
|
|
|
g_vad_output_rms = sqrtf((float)(sum_sq / count)); |
|
|
|
|
for (int i = 0; i < count; i += g_capture_channels) radio_audio_vad_sample(g_vad_pcm + i); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Очищенные interleaved отсчёты произвольной длины; границы PTT ставит единственный TX-writer. */ |
|
|
|
|
int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count) { |
|
|
|
|
radio_tx_lock(); |
|
|
|
|
@ -692,8 +863,15 @@ int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count) {
|
|
|
|
|
} |
|
|
|
|
radio_audio_update_transmission(); // Повторить BEGIN после освобождения bounded control-очереди.
|
|
|
|
|
for (int offset = 0; offset < count; offset += g_capture_channels) { |
|
|
|
|
if (g_vad_mode) radio_audio_vad_sample(pcm + offset); |
|
|
|
|
if (!g_transmitting) continue; |
|
|
|
|
if (g_vad_mode) radio_audio_vad_feed(pcm + offset); |
|
|
|
|
if (!g_transmitting) { |
|
|
|
|
if (g_vad_mode) { |
|
|
|
|
memcpy(g_prefetch_pcm + g_prefetch_write * g_capture_channels, pcm + offset, g_capture_channels * sizeof(int16_t)); |
|
|
|
|
g_prefetch_write = (g_prefetch_write + 1) % RADIO_PREFETCH_FRAMES; |
|
|
|
|
if (g_prefetch_frames < RADIO_PREFETCH_FRAMES) ++g_prefetch_frames; |
|
|
|
|
} |
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
memcpy(g_tx_pcm + g_tx_pcm_frames * g_capture_channels, pcm + offset, g_capture_channels * sizeof(int16_t)); |
|
|
|
|
if (++g_tx_pcm_frames == RADIO_AUDIO_FRAME_SAMPLES) radio_audio_flush_tx(); |
|
|
|
|
} |
|
|
|
|
|