Раздели аудиопотоки и привяжи AEC к фактическому общему выходу

This commit is contained in:
evgeny committed 2026-10-02 10:22:34 +03:00
1 parent d7fb0c83f9
commit bd082a26bf
30 files changed
+1839 -412

No files matched your search

+38 -11
View File
@@ -44,6 +44,7 @@ static struct call_jitter* g_vj = NULL;
static struct call_tones* g_tones = NULL;
static speex_aec_t* g_aec = NULL;
static int g_aec_delay_frames = CALL_AEC_DELAY_FRAMES;
static int g_prepared_capture = 0;
static int g_aec_enabled = 1; /* по умолчанию: настройка aec_enabled */
static struct audio_compressor* g_compressor = NULL;
static uint64_t g_tx_generation;
@@ -124,7 +125,7 @@ void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
/* ── GUI-поток: lifecycle ── */
int call_audio_start(uint64_t call_id) {
static int call_audio_start_impl(uint64_t call_id, int prepared) {
if (call_id == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: start bad call_id=0", CALL_AUDIO_ID);
return -1;
@@ -132,8 +133,10 @@ int call_audio_start(uint64_t call_id) {
pthread_mutex_lock(&g_mtx);
if (g_active && g_call_id == call_id) {
int same_mode = g_prepared_capture == prepared;
pthread_mutex_unlock(&g_mtx);
return 0;
if (!same_mode) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: capture policy cannot change within active call", CALL_AUDIO_ID);
return same_mode ? 0 : -1;
}
if (g_active) {
pthread_mutex_unlock(&g_mtx);
@@ -179,7 +182,7 @@ int call_audio_start(uint64_t call_id) {
g_aec_enabled = g_inst ? chat_setting_get_int(g_inst, "aec_enabled", 1) : 1;
speex_aec_t* aec = NULL;
if (g_aec_enabled) {
if (g_aec_enabled && !prepared) {
aec = call_audio_aec_create();
if (!aec) {
g_aec_enabled = 0;
@@ -210,7 +213,7 @@ int call_audio_start(uint64_t call_id) {
g_decoder = dec;
g_vj = j;
g_tones = tones;
g_aec = aec;
g_aec = aec; g_prepared_capture = prepared;
g_compressor = ac;
g_call_id = call_id;
g_active = 1;
@@ -231,6 +234,10 @@ int call_audio_start(uint64_t call_id) {
return 0;
}
/* Платформа с собственным голосовым захватом явно передаёт PCM после AEC. */
int call_audio_start_prepared(uint64_t call_id) { return call_audio_start_impl(call_id, 1); }
int call_audio_start(uint64_t call_id) { return call_audio_start_impl(call_id, 0); }
void call_audio_begin_end(uint64_t call_id) {
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id && !g_ending) {
@@ -292,7 +299,7 @@ void call_audio_reset_io(uint64_t call_id, int playback) {
pthread_mutex_lock(&g_mtx);
if (g_active && g_call_id == call_id) {
if (g_aec) speex_aec_reset(g_aec);
if (g_compressor) audio_compressor_reset(g_compressor);
if (!playback && g_compressor) audio_compressor_reset(g_compressor);
if (playback) {
call_jitter_reset(g_vj);
g_gap_samples = 0;
@@ -317,7 +324,7 @@ static speex_aec_t* call_audio_aec_create(void) {
void call_audio_set_aec_enabled(int enabled) {
pthread_mutex_lock(&g_mtx);
g_aec_enabled = enabled ? 1 : 0;
if (g_active) {
if (g_active && !g_prepared_capture) {
if (g_aec_enabled && !g_aec) {
g_aec = call_audio_aec_create();
if (!g_aec) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: AEC create failed (enabled)", CALL_AUDIO_ID);
@@ -335,7 +342,7 @@ void call_audio_set_aec_delay_frames(int frames) {
if (frames < 1) frames = 1;
pthread_mutex_lock(&g_mtx);
g_aec_delay_frames = frames;
if (g_active && g_aec_enabled && g_aec) {
if (g_active && !g_prepared_capture && g_aec_enabled && g_aec) {
speex_aec_destroy(g_aec);
g_aec = call_audio_aec_create();
if (!g_aec) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: AEC recreate failed (delay=%d)", CALL_AUDIO_ID, frames);
@@ -346,8 +353,11 @@ void call_audio_set_aec_delay_frames(int frames) {
/* ── аудио-поток: TX / RX ── */
int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted) {
if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES) return -1;
static int call_audio_feed_impl(uint64_t call_id, const int16_t* pcm, int count, int muted, const uint8_t* mute_mask, int prepared) {
if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES || (prepared && !mute_mask)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: invalid capture frame count=%d prepared=%d", CALL_AUDIO_ID, count, prepared);
return -1;
}
uint8_t opus[256];
int16_t cleaned[CALL_AUDIO_FRAME_SAMPLES];
@@ -360,6 +370,11 @@ int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, i
pthread_mutex_unlock(&g_mtx);
return -1;
}
if (g_prepared_capture != prepared) {
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: capture policy mismatch prepared=%d", CALL_AUDIO_ID, prepared);
return -1;
}
const int16_t* tx = pcm;
if (g_aec) {
if (speex_aec_process_capture(g_aec, pcm, count, cleaned) == count)
@@ -374,7 +389,11 @@ int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, i
}
tx = amplified;
}
if (muted) { memset(amplified, 0, sizeof(amplified)); tx = amplified; }
if (mute_mask) {
if (tx != amplified) memcpy(amplified, tx, sizeof(amplified));
for (int i = 0; i < count; ++i) if (mute_mask[i]) amplified[i] = 0;
tx = amplified;
} else if (muted) { memset(amplified, 0, sizeof(amplified)); tx = amplified; }
len = opus_codec_encode(g_encoder, tx, count, opus, (int)sizeof(opus));
ua = g_inst ? g_inst->ua : NULL;
inst = g_inst;
@@ -405,9 +424,17 @@ int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, i
return 0;
}
/* Маска соответствует границам mute в том же упорядоченном потоке, что и PCM. */
int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask) {
return call_audio_feed_impl(call_id, pcm, CALL_AUDIO_FRAME_SAMPLES, 0, mute_mask, 1);
}
int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted) {
return call_audio_feed_impl(call_id, pcm, count, muted, NULL, 0);
}
/* Незамьюченный захват для платформ без программного mute. */
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) {
return call_audio_feed_pcm_muted(call_id, pcm, count, 0);
return call_audio_feed_impl(call_id, pcm, count, 0, NULL, 0);
}
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
+9 -4
View File
@@ -35,8 +35,11 @@ extern "C" {
int call_audio_init(struct UTUN_INSTANCE* inst);
void call_audio_destroy(struct UTUN_INSTANCE* inst);
/* GUI-поток: создать codec + jitter + tones для звонка. 0 = успех. */
/* GUI-поток: создать codec + jitter + tones. Старый API принимает raw PCM и использует core AEC.
* Для desktop нужен start_prepared: AEC и stereo reference принадлежат VoiceAudioIo. 0 = успех. */
int call_audio_start(uint64_t call_id);
/* AEC принадлежит платформенному захвату; pull больше не подаёт собственный референс. */
int call_audio_start_prepared(uint64_t call_id);
/* GUI-поток: по CALL_ENDED — начать тон завершения (pull_pcm выдаст 450мс тона). */
void call_audio_begin_end(uint64_t call_id);
@@ -50,6 +53,8 @@ void call_audio_stop(void);
/* аудио-поток: закодировать PCM-кадр (ровно CALL_AUDIO_FRAME_SAMPLES) и отправить пиру. */
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count);
/* Mute после AEC/AGC: фильтр получает настоящий микрофон, Opus — нулевой PCM. */
/* Подготовленный PCM и маска mute (960 элементов); AEC уже выполнен до этого API. */
int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask);
int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted);
/* аудио-поток: выдать до max_samples финального PCM (jitter + stretch + тоны).
@@ -70,12 +75,12 @@ int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100,
* Release допустим только после остановки обоих. */
void call_audio_reset_io(uint64_t call_id, int playback);
/* Включить/выключить AEC (SpeexDSP) в рантайме. desktop — по настройке GUI,
* Android — по спикерфону (route == SPEAKER). Работает и в активном звонке. */
/* Core AEC для raw capture: Android — по спикерфону (route == SPEAKER).
* Prepared capture не создаёт core AEC; desktop передаёт настройку в VoiceAudioIo. */
void call_audio_set_aec_enabled(int enabled);
/* Задать задержку рендер→захват AEC в кадрах (20 мс). Android измеряет через
* AudioTrack/AudioRecord getTimestamp, desktop использует дефолт. Если AEC уже
* AudioTrack/AudioRecord getTimestamp. Prepared desktop выравнивает референс по времени. Если core AEC уже
* активен — пересоздаётся с новой задержкой. */
void call_audio_set_aec_delay_frames(int frames);
+172 -116
View File
@@ -73,6 +73,10 @@ struct radio_source {
};
static pthread_mutex_t g_mtx = PTHREAD_MUTEX_INITIALIZER;
/* TX → общий mutex. На время VAD общий mutex отпускается; TX сохраняет lifetime и порядок PCM/PTT. */
static pthread_mutex_t g_tx_mtx = PTHREAD_MUTEX_INITIALIZER;
static void radio_tx_lock(void) { pthread_mutex_lock(&g_tx_mtx); pthread_mutex_lock(&g_mtx); }
static void radio_tx_unlock(void) { pthread_mutex_unlock(&g_mtx); pthread_mutex_unlock(&g_tx_mtx); }
static struct UTUN_INSTANCE* g_inst = NULL;
static int g_active = 0;
static uint64_t g_group_id = 0;
@@ -99,14 +103,26 @@ static uint64_t g_tx_stats_tb = 0;
static uint64_t g_vad_stats_tb = 0;
static uint32_t g_tx_frames = 0; /* кадров за интервал */
static uint32_t g_tx_bytes = 0; /* суммарный размер opus за интервал */
static uint32_t g_tx_tail = 0; /* дропнутых хвостовых сэмплов за интервал */
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 float g_vad_sum;
static uint64_t g_tx_burst;
/* Только uasync: идентичность открытой сетевой серии, включая поколение capture. */
static uint64_t g_net_generation, g_net_burst, g_net_group;
static uint64_t g_tx_generation;
static uint64_t g_listen_generation;
static uint64_t g_instance_generation;
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;
struct radio_audio_tx {
struct posted_task task;
struct UTUN_INSTANCE* inst;
uint64_t group_id, generation, created_tb;
uint64_t group_id, generation, listen_generation, instance_generation, burst, created_tb;
int len;
uint8_t opus[RADIO_MAX_OPUS];
};
@@ -115,8 +131,10 @@ static void radio_audio_send(void* arg) {
struct radio_audio_tx* tx = arg;
pthread_mutex_lock(&g_mtx);
int current = tx->generation == g_tx_generation;
if (g_tx_pending) --g_tx_pending;
int send = current && g_active && g_inst == tx->inst && g_group_id == tx->group_id;
int owner = tx->instance_generation == g_instance_generation && g_inst == tx->inst;
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; }
unsigned int dropped = g_tx_dropped, pending = g_tx_pending;
int changed = dropped != g_tx_reported;
@@ -132,7 +150,7 @@ static void radio_audio_send(void* arg) {
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: TX dropped=%u pending=%u limit=8 max_age=200ms", RADIO_AUDIO_ID,
dropped, pending);
}
if (frames) DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: TX frames=%u bytes=%u tail_samples=%u", RADIO_AUDIO_ID, frames, bytes, tail);
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);
}
@@ -142,7 +160,9 @@ static int radio_audio_post_frame(struct UTUN_INSTANCE* inst, uint64_t group_id,
struct radio_audio_tx* task = u_calloc(1, sizeof(*task));
if (!task) { ++g_tx_dropped; return -1; }
task->inst = inst; task->group_id = group_id;
task->generation = g_tx_generation; task->created_tb = get_time_tb();
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);
task->task.callback = radio_audio_send; task->task.arg = task;
@@ -307,7 +327,9 @@ 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, int begin);
static int radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, int begin);
static void radio_audio_update_transmission(void);
static void radio_audio_flush_tx(void);
/* ── lifecycle ── */
@@ -315,7 +337,7 @@ int radio_audio_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
pthread_mutex_lock(&g_mtx);
if (g_inst == inst) { pthread_mutex_unlock(&g_mtx); return 0; }
g_inst = inst; g_tx_pending = 0;
g_inst = inst; g_tx_pending = g_control_pending = 0; ++g_listen_generation; ++g_instance_generation;
pthread_mutex_unlock(&g_mtx);
radio_set_frame_cb(inst, radio_audio_on_frame, NULL);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: initialized", RADIO_AUDIO_ID);
@@ -327,6 +349,7 @@ void radio_audio_destroy(struct UTUN_INSTANCE* inst) {
radio_set_frame_cb(inst, NULL, NULL);
pthread_mutex_lock(&g_mtx);
g_inst = NULL;
++g_instance_generation;
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: destroyed", RADIO_AUDIO_ID);
}
@@ -339,6 +362,7 @@ int radio_audio_start(uint64_t group_id, int capture_channels) {
radio_audio_stop();
pthread_mutex_lock(&g_mtx);
g_group_id = group_id;
++g_listen_generation;
g_active = 1;
g_last_rx_tb = 0;
memset(g_sources, 0, sizeof(g_sources));
@@ -355,14 +379,14 @@ int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_e
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: capture invalid channels=%d", RADIO_AUDIO_ID, capture_channels);
return -1;
}
pthread_mutex_lock(&g_mtx);
radio_tx_lock();
if (!g_active || g_group_id != group_id || g_encoder) {
pthread_mutex_unlock(&g_mtx);
radio_tx_unlock();
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; }
if (!enc) { radio_tx_unlock(); 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);
int opus_bitrate_kbps = chat_setting_get_int(g_inst, "radio_opus_bitrate", RADIO_AUDIO_BITRATE / 1000);
opus_codec_encoder_application_set(enc, opus_mode ? OPUS_CODEC_APP_AUDIO : OPUS_CODEC_APP_VOIP);
@@ -387,21 +411,24 @@ int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_e
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_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);
g_vad = silero_vad_create_default();
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) {
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: silero_vad_create_default failed — VAD auto-PTT disabled", RADIO_AUDIO_ID);
g_vad_mode = 0;
}
}
if (g_vad_mode) {
g_vad_win_len = 0;
radio_vad_fsm_reset(&g_vad_fsm);
}
g_vad_manual_ptt = 0;
g_vad_manual_ptt = g_transmitting = g_tx_pcm_frames = g_vad_decimation = 0;
g_vad_sum = 0; g_tx_burst = 0;
++g_tx_generation; g_tx_dropped = g_tx_reported = 0;
g_encoder = enc;
@@ -410,16 +437,19 @@ int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_e
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);
radio_tx_unlock();
return 0;
}
void radio_audio_capture_stop(void) {
pthread_mutex_lock(&g_mtx);
radio_tx_lock();
if (g_encoder) {
++g_tx_generation;
if (g_vad_manual_ptt || g_vad_fsm.talking)
if (g_transmitting) {
radio_audio_flush_tx();
radio_audio_post_talk(g_inst ? g_inst->ua : NULL, g_group_id, 0);
}
++g_tx_generation;
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) { silero_vad_destroy(g_vad); g_vad = NULL; }
@@ -429,7 +459,7 @@ void radio_audio_capture_stop(void) {
radio_vad_fsm_reset(&g_vad_fsm);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: capture stopped", RADIO_AUDIO_ID);
}
pthread_mutex_unlock(&g_mtx);
radio_tx_unlock();
}
void radio_audio_stop(void) {
@@ -478,7 +508,7 @@ int radio_audio_vad_mode(void) {
int radio_audio_transmitting(void) {
pthread_mutex_lock(&g_mtx);
int talking = g_active && (g_vad_manual_ptt || g_vad_fsm.talking);
int talking = g_active && g_transmitting;
pthread_mutex_unlock(&g_mtx);
return talking;
}
@@ -488,55 +518,82 @@ int radio_audio_transmitting(void) {
struct radio_audio_talk {
struct posted_task task;
struct UTUN_INSTANCE* inst;
uint64_t group_id;
uint64_t group_id, generation, listen_generation, instance_generation, burst, created_tb;
int begin;
};
static void radio_audio_talk(void* arg) {
struct radio_audio_talk* talk = arg;
if (talk->begin) radio_talk_begin(talk->inst, talk->group_id);
else radio_talk_end(talk->inst, talk->group_id);
pthread_mutex_lock(&g_mtx);
int owner = g_inst == talk->inst && g_instance_generation == talk->instance_generation;
if (owner && g_control_pending) --g_control_pending;
int current = owner && talk->listen_generation == g_listen_generation && g_active && g_group_id == talk->group_id;
pthread_mutex_unlock(&g_mtx);
if (talk->begin) {
if (current && get_time_tb() - talk->created_tb <= 2000 && radio_talk_begin(talk->inst, talk->group_id) == 0) {
g_net_generation = talk->generation; g_net_burst = talk->burst; g_net_group = talk->group_id;
}
} else if (g_net_generation == talk->generation && g_net_burst == talk->burst && g_net_group == talk->group_id) {
if (owner) radio_talk_end(talk->inst, talk->group_id);
g_net_generation = g_net_burst = g_net_group = 0;
}
}
static void radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, int begin) {
if (!ua) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: talk post without uasync", RADIO_AUDIO_ID); return; }
struct radio_audio_talk* talk = u_calloc(1, sizeof(*talk));
if (!talk) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: talk post allocation failed", RADIO_AUDIO_ID); return; }
/* Не более 16 control-задач, включая заранее выделенный FIN активной серии. */
static int radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, int begin) {
if (!ua) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: talk post without uasync", RADIO_AUDIO_ID); return -1; }
struct radio_audio_talk* talk;
if (begin) {
if (g_control_pending > 14) {
if (!g_control_warned) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: TX BEGIN deferred control_pending=%u limit=16",
RADIO_AUDIO_ID, g_control_pending);
g_control_warned = 1; return -1;
}
talk = u_calloc(1, sizeof(*talk));
struct radio_audio_talk* fin = u_calloc(1, sizeof(*fin));
if (!talk || !fin) {
u_free(talk); u_free(fin);
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: BEGIN/FIN allocation failed", RADIO_AUDIO_ID); return -1;
}
g_reserved_fin = fin; g_control_pending += 2; g_control_warned = 0;
} else {
talk = g_reserved_fin; g_reserved_fin = NULL;
if (!talk) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: FIN without reserved task", RADIO_AUDIO_ID); return -1; }
}
talk->inst = g_inst; talk->group_id = group_id; talk->begin = begin;
talk->generation = g_tx_generation; talk->burst = g_tx_burst;
talk->listen_generation = g_listen_generation; talk->created_tb = get_time_tb();
talk->instance_generation = g_instance_generation;
talk->task.callback = radio_audio_talk; talk->task.arg = talk;
uasync_post_reserved(ua, &talk->task);
return 0;
}
void radio_audio_talk_begin(uint64_t group_id) {
if (!g_inst || !g_inst->ua) return;
pthread_mutex_lock(&g_mtx);
radio_tx_lock();
if (!g_active || group_id != g_group_id || !g_encoder) {
pthread_mutex_unlock(&g_mtx);
radio_tx_unlock();
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, 0);
radio_vad_fsm_reset(&g_vad_fsm);
}
radio_audio_post_talk(g_inst->ua, group_id, 1);
pthread_mutex_unlock(&g_mtx);
radio_audio_update_transmission();
radio_tx_unlock();
}
void radio_audio_talk_end(uint64_t group_id) {
if (!g_inst || !g_inst->ua) return;
pthread_mutex_lock(&g_mtx);
radio_tx_lock();
if (!g_active || group_id != g_group_id || !g_vad_manual_ptt) {
pthread_mutex_unlock(&g_mtx);
radio_tx_unlock();
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;
radio_audio_post_talk(g_inst->ua, group_id, 0);
pthread_mutex_unlock(&g_mtx);
radio_audio_update_transmission();
radio_tx_unlock();
}
/* ── VAD авто-PTT: helpers (под g_mtx) ── */
@@ -561,87 +618,86 @@ static void radio_audio_encode_post(uint64_t group_id, const int16_t* frame,
if (radio_audio_post_frame(inst, group_id, opus, l) == 0) { ++g_tx_frames; g_tx_bytes += (uint32_t)l; }
}
/* 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];
struct UASYNC* ua;
struct UTUN_INSTANCE* inst;
pthread_mutex_lock(&g_mtx);
if (!g_active || group_id != g_group_id || !g_encoder || !g_vad) {
pthread_mutex_unlock(&g_mtx);
return -1;
}
ua = g_inst ? g_inst->ua : NULL;
inst = g_inst;
if (g_vad_manual_ptt) {
/* Ручной PTT: передаём текущий кадр, VAD не вмешивается. */
radio_audio_encode_post(group_id, pcm, ua, inst);
pthread_mutex_unlock(&g_mtx);
return 0;
}
radio_vad_resample(pcm, g_capture_channels, out16k);
int off = 0;
while (off < RADIO_VAD_FRAME_SAMPLES_16K) {
int space = RADIO_VAD_WINDOW_16K - g_vad_win_len;
int take = (RADIO_VAD_FRAME_SAMPLES_16K - off < space) ? (RADIO_VAD_FRAME_SAMPLES_16K - off) : space;
memcpy(g_vad_win + g_vad_win_len, out16k + off, (size_t)take * sizeof(float));
g_vad_win_len += take;
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) {
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_vad_stats_tb || now - g_vad_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_vad_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);
if (action == 1) {
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, 1);
} else if (action == -1) {
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: vad STOP grp=%016llx",
RADIO_AUDIO_ID, (unsigned long long)group_id);
radio_audio_post_talk(ua, group_id, 0);
}
}
}
if (g_vad_fsm.talking) {
radio_audio_encode_post(group_id, pcm, ua, inst);
}
pthread_mutex_unlock(&g_mtx);
return 0;
/* Хвост относится только к своей серии. Тишина добавляется исключительно перед Opus. */
static void radio_audio_flush_tx(void) {
if (!g_tx_pcm_frames) return;
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);
g_tx_pcm_frames = 0;
}
/* ── TX (аудио-поток) ── */
/* Ручной 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;
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;
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);
}
int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count) {
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);
radio_audio_flush_tx();
g_vad_win_len = g_vad_decimation = 0; g_vad_sum = 0;
if (g_vad) silero_vad_reset(g_vad);
radio_vad_fsm_reset(&g_vad_fsm);
radio_audio_update_transmission();
}
radio_tx_unlock();
}
/* VAD читает очищенный сигнал до AGC, накопление 48k→16k непрерывно между PCM-чанками. */
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;
if (++g_vad_decimation != 3) return;
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;
float probability = 0;
pthread_mutex_unlock(&g_mtx);
int result = silero_vad_process(g_vad, g_vad_win, &probability);
pthread_mutex_lock(&g_mtx);
int frame_total = RADIO_AUDIO_FRAME_SAMPLES * g_capture_channels;
if (!pcm || count != frame_total) { pthread_mutex_unlock(&g_mtx); return -1; }
if (g_vad_mode) { pthread_mutex_unlock(&g_mtx); return radio_vad_feed(group_id, pcm, count); }
if (result != 0)
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: VAD process failed", RADIO_AUDIO_ID);
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);
radio_audio_update_transmission();
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);
g_vad_stats_tb = now;
}
}
if (!g_active || group_id != g_group_id || !g_encoder) {
pthread_mutex_unlock(&g_mtx);
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: frame without capture", RADIO_AUDIO_ID);
/* Очищенные interleaved отсчёты произвольной длины; границы PTT ставит единственный TX-writer. */
int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count) {
radio_tx_lock();
if (!g_active || group_id != g_group_id || !g_encoder || !pcm || count <= 0 || count % g_capture_channels) {
radio_tx_unlock();
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: invalid capture frame count=%d", RADIO_AUDIO_ID, count);
return -1;
}
if (g_vad_manual_ptt) radio_audio_encode_post(group_id, pcm, g_inst ? g_inst->ua : NULL, g_inst);
pthread_mutex_unlock(&g_mtx);
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;
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();
}
radio_tx_unlock();
return 0;
}
+9 -6
View File
@@ -1,15 +1,15 @@
// radio_audio.h — сервисный аудио-движок рации (единый для desktop/Android/headless).
//
// Владеет Opus encoder (TX), per-source декодерами и микшером (RX). Приём:
// radio.c → radio_set_frame_cb → radio_audio_on_frame (uasync) декодирует Opus-кадр
// и кладёт PCM в ring-буфер источника; аудио-поток radio_audio_pull_pcm микширует
// radio.c → radio_set_frame_cb → radio_audio_on_frame (uasync) сохраняет Opus-кадр
// в jitter источника; декодирование и time-stretch выполняются на pull; аудио-поток radio_audio_pull_pcm микширует
// PCM всех источников. Передача: аудио-поток radio_audio_feed_pcm кодирует PCM и
// постит radio_talk_send в uasync. Микшер избыточных источников — насыщающее
// суммирование (soft-clip). Полудуплекс: радио включено — слушаем, PTT — говорим.
//
// Потоки (как call_audio):
// - radio_audio_start / talk_begin / talk_end / stop — GUI-поток;
// - radio_audio_feed_pcm / pull_pcm — аудио-поток;
// - radio_audio_start / capture_start / capture_stop / stop — GUI после остановки workers;
// - radio_audio_feed_pcm / talk_begin / talk_end — один TX-writer, pull_pcm — отдельный RX-writer;
// - radio_audio_on_frame — uasync-поток (из radio.c).
// Общее состояние — под мьютексом. Синглтон на один активный group_id.
@@ -42,17 +42,20 @@ int radio_audio_start(uint64_t group_id, int capture_channels);
* 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);
/* Единственный TX-writer: завершить старый PCM-хвост, сбросить VAD после потери захвата. */
void radio_audio_capture_discontinuity(uint64_t group_id);
/* GUI-поток: прекратить прослушивание (освободить всё). */
void radio_audio_stop(void);
/* Playback остановлен: удалить устаревшие источники, сохранив TX и подписку. */
void radio_audio_reset_playback(uint64_t group_id);
/* GUI-поток: PTT зажата/отпущена — постит radio_talk_begin/end в uasync. */
/* Единственный TX-writer: PTT упорядочен с PCM; сетевые BEGIN/FIN исполняются в uasync. */
void radio_audio_talk_begin(uint64_t group_id);
void radio_audio_talk_end(uint64_t group_id);
/* аудио-поток: закодировать PCM-кадр (ровно RADIO_AUDIO_FRAME_SAMPLES) и отправить. */
/* Единственный TX-writer: очищенные interleaved отсчёты произвольной длины (кратно числу каналов).
* VAD до AGC; один автомат manual/VAD; хвост каждой серии дополняется тишиной до FIN. */
int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count);
/* аудио-поток: выдать до max_samples микшированного PCM (источники микшируются,