Раздели аудиопотоки и привяжи AEC к фактическому общему выходу
This commit is contained in:
1 parent
d7fb0c83f9
commit
bd082a26bf
30 files changed
+1839
-412
No files matched your search
+38
-11
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (источники микшируются,
|
||||
|
||||
Reference in new issue
Block a user