You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
429 lines
17 KiB
429 lines
17 KiB
// 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 <pthread.h> |
|
#include <string.h> |
|
|
|
#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; |
|
}
|
|
|