// call_audio.c — сервисный аудио-движок P2P-звонка (Opus + джиттер + тоны). // // Один активный звонок: encoder (аудио-поток), decoder + адаптивный джиттер-буфер // vj (кодированные кадры + SoundTouch stretch на pull) и генератор тонов. // // RX: call.c → call_audio_on_media (uasync) → call_jitter_push (кодированный кадр); // аудио-поток call_audio_pull_pcm → call_jitter_pull (декод+stretch) (+ глитч/завершение). // TX: аудио-поток call_audio_feed_pcm → encode → uasync_post(call_send_media). #include "call_audio.h" #include "call.h" #include "call_jitter.h" #include "call_tones.h" #include "../chat/chat_setting.h" #include "../utun_instance.h" #include "../../lib/opus_codec.h" #include "../../lib/debug_config.h" #include "../../lib/mem.h" #include "../../lib/u_async.h" #include "../../lib/speex_aec.h" #include "../../lib/audio_compressor.h" #include #include #define CALL_AUDIO_ID "call_audio" #define CALL_AUDIO_BITRATE 32000 #define CALL_STALL_SAMPLES (CALL_AUDIO_SAMPLE_RATE / 2) /* 500мс без PCM из jitter → глитч-тон */ #define CALL_AEC_FILTER_SAMPLES 14400 /* хвост эха 300мс @48кГц */ #define CALL_AEC_DELAY_FRAMES 2 /* desktop: минимальная задержка duplex */ static pthread_mutex_t g_mtx = PTHREAD_MUTEX_INITIALIZER; static struct UTUN_INSTANCE* g_inst = NULL; static int g_active = 0; static int g_ending = 0; static int g_glitch_started = 0; /* только аудиопоток */ static int g_ended_started = 0; /* только аудио-поток: тон завершения запущен */ static uint64_t g_call_id = 0; static int g_gap_samples = 0; /* только аудиопоток; насыщение на пороге */ static opus_codec_encoder_t* g_encoder = NULL; static opus_codec_decoder_t* g_decoder = NULL; 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_aec_enabled = 1; /* по умолчанию: настройка aec_enabled */ static struct audio_compressor* g_compressor = NULL; static const struct call_audio_ops g_ops = { .on_media = call_audio_on_media, .get_stats = call_audio_get_stats, }; /* Создать AEC с текущей задержкой (объявление; определение ниже). */ static speex_aec_t* call_audio_aec_create(void); /* vj decode-коллбэк: дёргается только из аудио-потока (call_jitter_pull). */ static int call_audio_decode_cb(void* arg, const uint8_t* enc, int len, int16_t* pcm, int max_samples) { opus_codec_decoder_t* dec = (opus_codec_decoder_t*)arg; return opus_codec_decode(dec, enc, len, pcm, max_samples); } int call_audio_init(struct UTUN_INSTANCE* inst) { if (!inst) return -1; if (g_inst == inst) return 0; g_inst = inst; call_set_audio_ops(inst, &g_ops); DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized", CALL_AUDIO_ID); return 0; } void call_audio_destroy(struct UTUN_INSTANCE* inst) { if (!inst || g_inst != inst) return; call_set_audio_ops(inst, NULL); g_inst = NULL; DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: destroyed", CALL_AUDIO_ID); } /* ── uasync-поток: приём медиа (кладём кодированный кадр в jitter-буфер) ── */ void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id, uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len) { (void)inst; (void)seq; (void)ts_ms; pthread_mutex_lock(&g_mtx); if (g_active && call_id == g_call_id && g_vj && opus && len > 0) { call_jitter_push(g_vj, opus, len); } pthread_mutex_unlock(&g_mtx); } /* ── GUI-поток: lifecycle ── */ int call_audio_start(uint64_t call_id) { if (call_id == 0) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: start bad call_id=0", CALL_AUDIO_ID); return -1; } pthread_mutex_lock(&g_mtx); if (g_active && g_call_id == call_id) { pthread_mutex_unlock(&g_mtx); return 0; } if (g_active) { pthread_mutex_unlock(&g_mtx); call_audio_release(g_call_id); pthread_mutex_lock(&g_mtx); } opus_codec_encoder_t* enc = opus_codec_encoder_create(CALL_AUDIO_SAMPLE_RATE, 1); if (!enc) { pthread_mutex_unlock(&g_mtx); DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: encoder create failed", CALL_AUDIO_ID); return -1; } opus_codec_encoder_bitrate_set(enc, CALL_AUDIO_BITRATE); opus_codec_decoder_t* dec = opus_codec_decoder_create(CALL_AUDIO_SAMPLE_RATE, 1); if (!dec) { opus_codec_encoder_destroy(enc); pthread_mutex_unlock(&g_mtx); DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: decoder create failed", CALL_AUDIO_ID); return -1; } int max_reserve_ms = chat_setting_get_int(g_inst, "call_jitter_max_reserve_ms", 1000); struct call_jitter* j = call_jitter_create(call_audio_decode_cb, dec, max_reserve_ms); if (!j) { opus_codec_decoder_destroy(dec); opus_codec_encoder_destroy(enc); pthread_mutex_unlock(&g_mtx); DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: vj create failed", CALL_AUDIO_ID); return -1; } struct call_tones* tones = call_tones_create(); if (!tones) { call_jitter_destroy(j); opus_codec_decoder_destroy(dec); opus_codec_encoder_destroy(enc); pthread_mutex_unlock(&g_mtx); DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: tones create failed", CALL_AUDIO_ID); return -1; } g_aec_enabled = g_inst ? chat_setting_get_int(g_inst, "aec_enabled", 1) : 1; speex_aec_t* aec = NULL; if (g_aec_enabled) { aec = call_audio_aec_create(); if (!aec) { g_aec_enabled = 0; DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: AEC create failed — call without echo cancellation", CALL_AUDIO_ID); } } /* компрессор микрофона (AGC) — как в рации: выравнивает тихий проводной микрофон * (HAL не применяет AGC на проводном входе). lookahead=0 — без задержки. */ struct audio_compressor* ac = audio_compressor_create(); if (ac) { audio_compressor_config_t accfg = {0}; accfg.sample_rate = CALL_AUDIO_SAMPLE_RATE; accfg.channels = 1; accfg.block_duration_ms = CALL_AUDIO_FRAME_MS; accfg.lookback_ms = 200; accfg.lookahead_ms = 0; accfg.max_gain_db = (float)chat_setting_get_int(g_inst, "call_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 — как у голосовых сообщений */ audio_compressor_configure(ac, &accfg); audio_compressor_set_enabled(ac, chat_setting_get_int(g_inst, "call_compressor_enabled", 1)); } else { DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: compressor create failed — call without AGC", CALL_AUDIO_ID); } g_encoder = enc; g_decoder = dec; g_vj = j; g_tones = tones; g_aec = aec; g_compressor = ac; g_call_id = call_id; g_active = 1; g_ending = 0; g_ended_started = 0; g_glitch_started = 0; g_gap_samples = 0; pthread_mutex_unlock(&g_mtx); DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: started call=0x%016llx aec=%d delay=%d compressor=%s gain=%d", CALL_AUDIO_ID, (unsigned long long)call_id, g_aec ? 1 : 0, g_aec_delay_frames, ac ? (audio_compressor_is_enabled(ac) ? "on" : "off") : "unavailable", chat_setting_get_int(g_inst, "call_compressor_max_gain_db", 15)); return 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) { g_ending = 1; DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: begin_end call=0x%016llx (ended tone)", CALL_AUDIO_ID, (unsigned long long)call_id); } pthread_mutex_unlock(&g_mtx); } void call_audio_release(uint64_t call_id) { opus_codec_encoder_t* enc = NULL; opus_codec_decoder_t* dec = NULL; struct call_jitter* j = NULL; struct call_tones* tones = NULL; speex_aec_t* aec = NULL; struct audio_compressor* ac = NULL; int was_active = 0; pthread_mutex_lock(&g_mtx); if (g_active && g_call_id == call_id) { was_active = 1; g_active = 0; g_ending = 0; g_ended_started = 0; g_call_id = 0; g_gap_samples = 0; enc = g_encoder; g_encoder = NULL; dec = g_decoder; g_decoder = NULL; j = g_vj; g_vj = NULL; tones = g_tones; g_tones = NULL; aec = g_aec; g_aec = NULL; ac = g_compressor; g_compressor = NULL; } pthread_mutex_unlock(&g_mtx); if (!was_active) return; /* дальше on_media не тронет объекты (active=0, указатели NULL) */ if (enc) opus_codec_encoder_destroy(enc); if (dec) opus_codec_decoder_destroy(dec); if (j) call_jitter_destroy(j); if (tones) call_tones_destroy(tones); if (aec) speex_aec_destroy(aec); if (ac) audio_compressor_destroy(ac); DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: released call=0x%016llx", CALL_AUDIO_ID, (unsigned long long)call_id); } void call_audio_stop(void) { uint64_t cid = 0; pthread_mutex_lock(&g_mtx); if (g_active) cid = g_call_id; pthread_mutex_unlock(&g_mtx); if (cid) call_audio_release(cid); } /* ── AEC (SpeexDSP): рантайм-управление ── */ /* Создать AEC с текущей задержкой. Вызывается под g_mtx. */ static speex_aec_t* call_audio_aec_create(void) { return speex_aec_create(CALL_AUDIO_SAMPLE_RATE, CALL_AUDIO_FRAME_SAMPLES, CALL_AEC_FILTER_SAMPLES, g_aec_delay_frames); } void call_audio_set_aec_enabled(int enabled) { pthread_mutex_lock(&g_mtx); g_aec_enabled = enabled ? 1 : 0; if (g_active) { 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); else DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: AEC enabled (delay=%d frames)", CALL_AUDIO_ID, g_aec_delay_frames); } else if (!g_aec_enabled && g_aec) { speex_aec_destroy(g_aec); g_aec = NULL; DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: AEC disabled", CALL_AUDIO_ID); } } pthread_mutex_unlock(&g_mtx); } 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) { 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); else DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: AEC delay -> %d frames", CALL_AUDIO_ID, frames); } pthread_mutex_unlock(&g_mtx); } /* ── аудио-поток: TX / RX ── */ int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) { if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES) return -1; uint8_t opus[256]; int16_t cleaned[CALL_AUDIO_FRAME_SAMPLES]; struct UASYNC* ua = NULL; struct UTUN_INSTANCE* inst = NULL; int len = -1; pthread_mutex_lock(&g_mtx); if (!g_active || call_id != g_call_id || !g_encoder) { pthread_mutex_unlock(&g_mtx); return -1; } const int16_t* tx = pcm; if (g_aec) { if (speex_aec_process_capture(g_aec, pcm, count, cleaned) == count) tx = cleaned; } /* компрессор (AGC): push → кодируем прямо из выхода до clear_output. * lookahead=0 ⇒ выход ровно count сэмплов. */ if (g_compressor) { audio_compressor_push(g_compressor, tx, (size_t)count); const int16_t* out = audio_compressor_output(g_compressor); if (out && audio_compressor_output_size(g_compressor) >= (size_t)count) tx = out; len = opus_codec_encode(g_encoder, tx, count, opus, (int)sizeof(opus)); audio_compressor_clear_output(g_compressor); } else { len = opus_codec_encode(g_encoder, tx, count, opus, (int)sizeof(opus)); } ua = g_inst ? g_inst->ua : NULL; inst = g_inst; pthread_mutex_unlock(&g_mtx); if (len <= 0) return -1; if (!ua || !inst) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: feed_pcm: instance not set (call_audio_init not called)", CALL_AUDIO_ID); return -1; } struct call_media_arg* a = (struct call_media_arg*)u_malloc(sizeof(*a)); if (!a) return -1; a->inst = inst; a->call_id = call_id; a->len = (uint16_t)len; memcpy(a->opus, opus, (size_t)len); uasync_post(ua, call_send_media_trampoline, a); return 0; } int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { if (!out || max_samples <= 0) return 0; int active = 0, ending = 0; struct call_jitter* j = NULL; struct call_tones* tones = NULL; pthread_mutex_lock(&g_mtx); if (g_active && call_id == g_call_id) { active = 1; ending = g_ending; j = g_vj; tones = g_tones; } pthread_mutex_unlock(&g_mtx); if (!active || !j || !tones) return 0; int n = 0; /* завершение: доигрываем тон (450мс), затем тишина. Тон стартует лениво * здесь (аудио-поток) — begin_end лишь выставляет флаг, чтобы не гонять * генератор тонов с GUI-потоком. */ if (ending) { if (!g_ended_started) { call_tones_ended_start(tones); g_ended_started = 1; } if (call_tones_ended_pending(tones)) n = call_tones_render(tones, out, max_samples); } else { n = call_jitter_pull(j, out, max_samples); int previous_gap = g_gap_samples; if (n > 0) g_gap_samples = 0; int missing = max_samples - n; int quiet = CALL_STALL_SAMPLES - g_gap_samples; if (quiet > missing) quiet = missing; int tone_samples = missing - quiet; if (n > 0 && previous_gap > 0) DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: PCM resumed call=%016llx gap=%dms pcm=%d/%d", CALL_AUDIO_ID, (unsigned long long)call_id, previous_gap * 1000 / CALL_AUDIO_SAMPLE_RATE, n, max_samples); if (missing > 0 && g_gap_samples == 0) DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: PCM gap started call=%016llx pcm=%d/%d", CALL_AUDIO_ID, (unsigned long long)call_id, n, max_samples); g_gap_samples += quiet; /* Новый PCM прерывает предыдущий цикл тона, даже если хвост блока снова пуст. */ if (g_glitch_started && (n > 0 || tone_samples == 0)) { g_glitch_started = 0; call_tones_glitch_stop(tones); DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: gap tone stopped call=%016llx pcm=%d/%d", CALL_AUDIO_ID, (unsigned long long)call_id, n, max_samples); } memset(out + n, 0, (size_t)quiet * sizeof(*out)); if (tone_samples > 0) { if (!g_glitch_started) { g_glitch_started = 1; DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: gap tone started call=%016llx pcm_gap=500ms pcm=%d/%d", CALL_AUDIO_ID, (unsigned long long)call_id, n, max_samples); } call_tones_glitch_start(tones); call_tones_render(tones, out + n + quiet, tone_samples); } n = max_samples; } /* рендер-референс для AEC: то, что реально пошло в динамик */ if (n > 0) { pthread_mutex_lock(&g_mtx); if (g_aec) speex_aec_feed_playback(g_aec, out, n); pthread_mutex_unlock(&g_mtx); } return n; } /* ── uasync-поток: метрики ── */ int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100, uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range) { int ok = 0; pthread_mutex_lock(&g_mtx); if (g_active && call_id == g_call_id && g_vj) { call_jitter_get_stats(g_vj, buffer_ms, tempo_x100, dropped, underruns, range); ok = 1; } pthread_mutex_unlock(&g_mtx); return ok ? 0 : -1; }