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.
554 lines
23 KiB
554 lines
23 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_playback_started = 0; /* первый PCM получен; только аудиопоток */ |
|
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_prepared_capture = 0; |
|
static int g_aec_enabled = 1; /* по умолчанию: настройка aec_enabled */ |
|
static struct audio_compressor* g_compressor = NULL; |
|
static uint64_t g_tx_generation; |
|
static unsigned int g_tx_pending, g_tx_dropped, g_tx_reported; |
|
static unsigned int g_tx_reasons; |
|
static int g_tx_encode_error; |
|
static uint32_t g_aec_reported_over, g_aec_reported_under, g_pcm_events; |
|
|
|
/* Task и данные — одна аллокация; uasync освобождает task после callback и при destroy. */ |
|
struct call_audio_tx { |
|
struct posted_task task; |
|
struct UTUN_INSTANCE* inst; |
|
uint64_t call_id, generation, created_tb; |
|
int len; |
|
uint8_t opus[256]; |
|
}; |
|
|
|
static void call_audio_send(void* arg) { |
|
struct call_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_ending && g_inst == tx->inst && g_call_id == tx->call_id; |
|
if (get_time_tb() - tx->created_tb > 2000) { |
|
send = 0; |
|
if (current) { ++g_tx_dropped; g_tx_reasons |= 16; } |
|
} |
|
pthread_mutex_unlock(&g_mtx); |
|
if (send) call_send_media(tx->inst, tx->call_id, tx->opus, tx->len); |
|
} |
|
|
|
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; |
|
pthread_mutex_lock(&g_mtx); |
|
if (g_inst == inst) { pthread_mutex_unlock(&g_mtx); return 0; } |
|
g_inst = inst; g_tx_pending = 0; |
|
pthread_mutex_unlock(&g_mtx); |
|
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); |
|
pthread_mutex_lock(&g_mtx); |
|
g_inst = NULL; |
|
pthread_mutex_unlock(&g_mtx); |
|
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; |
|
|
|
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, ts_ms, (int64_t)(get_time_tb() / 10)); |
|
} |
|
pthread_mutex_unlock(&g_mtx); |
|
} |
|
|
|
/* ── GUI-поток: lifecycle ── */ |
|
|
|
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; |
|
} |
|
|
|
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); |
|
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); |
|
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 && !prepared) { |
|
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 — как у голосовых сообщений */ |
|
if (audio_compressor_configure(ac, &accfg) != 0) { audio_compressor_destroy(ac); ac = NULL; } |
|
if (ac) audio_compressor_set_enabled(ac, chat_setting_get_int(g_inst, "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_prepared_capture = prepared; |
|
g_compressor = ac; |
|
g_call_id = call_id; |
|
g_active = 1; |
|
++g_tx_generation; g_tx_dropped = g_tx_reported = 0; |
|
g_tx_reasons = 0; g_tx_encode_error = 0; |
|
g_ending = 0; |
|
g_ended_started = 0; |
|
g_glitch_started = 0; |
|
g_playback_started = 0; |
|
g_gap_samples = 0; |
|
g_aec_reported_over = g_aec_reported_under = g_pcm_events = 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; |
|
} |
|
|
|
/* Платформа с собственным голосовым захватом явно передаёт 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) { |
|
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_tx_generation; |
|
g_ending = 0; |
|
g_ended_started = 0; |
|
g_playback_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); |
|
} |
|
|
|
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 (!playback && g_compressor) audio_compressor_reset(g_compressor); |
|
if (playback) { |
|
call_jitter_reset(g_vj); |
|
g_gap_samples = 0; |
|
g_glitch_started = 0; |
|
g_playback_started = 0; |
|
call_tones_glitch_stop(g_tones); |
|
} |
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: I/O reset call=%016llx direction=%s", CALL_AUDIO_ID, |
|
(unsigned long long)call_id, playback ? "playback" : "capture"); |
|
} |
|
pthread_mutex_unlock(&g_mtx); |
|
} |
|
|
|
/* ── 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 && !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); |
|
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_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); |
|
else DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: AEC delay -> %d frames", CALL_AUDIO_ID, frames); |
|
} |
|
pthread_mutex_unlock(&g_mtx); |
|
} |
|
|
|
/* ── аудио-поток: TX / RX ── */ |
|
|
|
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]; |
|
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; |
|
} |
|
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) |
|
tx = cleaned; |
|
} |
|
int16_t amplified[CALL_AUDIO_FRAME_SAMPLES]; |
|
if (g_compressor) { |
|
if (audio_compressor_process_frame(g_compressor, tx, amplified, count) != 0) { |
|
pthread_mutex_unlock(&g_mtx); |
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: compressor frame failed", CALL_AUDIO_ID); |
|
return -1; |
|
} |
|
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; |
|
if (len <= 0 || !ua || !inst || g_ending || g_tx_pending >= 8) { |
|
++g_tx_dropped; |
|
if (g_tx_pending >= 8) g_tx_reasons |= 1; |
|
if (!ua || !inst) g_tx_reasons |= 4; |
|
if (len <= 0) { g_tx_reasons |= 8; g_tx_encode_error = len; } |
|
if (g_ending) g_tx_reasons |= 32; |
|
pthread_mutex_unlock(&g_mtx); |
|
return -1; |
|
} |
|
struct call_audio_tx* task = u_calloc(1, sizeof(*task)); |
|
if (!task) { |
|
++g_tx_dropped; |
|
g_tx_reasons |= 2; |
|
pthread_mutex_unlock(&g_mtx); |
|
return -1; |
|
} |
|
task->inst = inst; task->call_id = call_id; |
|
task->generation = g_tx_generation; task->created_tb = get_time_tb(); |
|
task->len = len; |
|
memcpy(task->opus, opus, (size_t)len); |
|
task->task.callback = call_audio_send; task->task.arg = task; |
|
++g_tx_pending; |
|
uasync_post_reserved(ua, &task->task); |
|
pthread_mutex_unlock(&g_mtx); |
|
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_impl(call_id, pcm, count, 0, NULL, 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; |
|
uint32_t events = 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) { |
|
if (!g_playback_started) events |= 1; |
|
g_playback_started = 1; |
|
g_gap_samples = 0; |
|
} |
|
int missing = max_samples - n; |
|
/* Подготовка устройств и начальное накопление PCM не являются потерей звука. */ |
|
int quiet = g_playback_started ? CALL_STALL_SAMPLES - g_gap_samples : missing; |
|
if (quiet > missing) quiet = missing; |
|
int tone_samples = missing - quiet; |
|
if (n > 0 && previous_gap > 0) events |= 2; |
|
if (missing > 0 && g_playback_started && g_gap_samples == 0) events |= 4; |
|
if (g_playback_started) g_gap_samples += quiet; |
|
/* Новый PCM прерывает предыдущий цикл тона, даже если хвост блока снова пуст. */ |
|
if (g_glitch_started && (n > 0 || tone_samples == 0)) { |
|
g_glitch_started = 0; |
|
call_tones_glitch_stop(tones); |
|
events |= 8; |
|
} |
|
memset(out + n, 0, (size_t)quiet * sizeof(*out)); |
|
if (tone_samples > 0) { |
|
if (!g_glitch_started) { |
|
g_glitch_started = 1; |
|
events |= 16; |
|
} |
|
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); |
|
g_pcm_events |= events; |
|
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; |
|
struct call_jitter_diagnostics diagnostics = {0}; |
|
uint32_t aec_over = 0, aec_under = 0, events = 0; |
|
unsigned int tx_dropped = 0, tx_pending = 0; |
|
unsigned int reasons = 0; |
|
int encode_error = 0; |
|
int tx_changed = 0, aec_changed = 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); |
|
call_jitter_take_diagnostics(g_vj, &diagnostics); |
|
speex_aec_get_stats(g_aec, &aec_over, &aec_under); |
|
aec_changed = aec_over != g_aec_reported_over || aec_under != g_aec_reported_under; |
|
g_aec_reported_over = aec_over; g_aec_reported_under = aec_under; |
|
tx_dropped = g_tx_dropped; tx_pending = g_tx_pending; |
|
tx_changed = tx_dropped != g_tx_reported; g_tx_reported = tx_dropped; |
|
reasons = g_tx_reasons; g_tx_reasons = 0; encode_error = g_tx_encode_error; |
|
events = g_pcm_events; g_pcm_events = 0; |
|
ok = 1; |
|
} |
|
pthread_mutex_unlock(&g_mtx); |
|
if (tx_changed) DEBUG_WARN(DEBUG_CATEGORY_CALL, |
|
"%s: TX dropped=%u pending=%u limit=8 max_age=200ms reasons=queue:%d alloc:%d core:%d encode:%d age:%d ending:%d encode_result=%d", |
|
CALL_AUDIO_ID, tx_dropped, tx_pending, !!(reasons & 1), !!(reasons & 2), !!(reasons & 4), |
|
!!(reasons & 8), !!(reasons & 16), !!(reasons & 32), encode_error); |
|
if (aec_changed) DEBUG_WARN(DEBUG_CATEGORY_AEC, "%s: AEC render overflow=%u capture passthrough=%u", CALL_AUDIO_ID, aec_over, aec_under); |
|
if (diagnostics.decode_errors || diagnostics.budget_hits) |
|
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: decode_errors=%u last_result=%d len=%d budget_hits=%u depth=%dms", |
|
diagnostics.decode_errors, diagnostics.error_result, diagnostics.error_len, diagnostics.budget_hits, diagnostics.depth_ms); |
|
if (events) DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: PCM first=%d resumed=%d gap=%d tone_started=%d tone_stopped=%d", CALL_AUDIO_ID, |
|
!!(events & 1), !!(events & 2), !!(events & 4), !!(events & 16), !!(events & 8)); |
|
if (ok) DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call_jitter: depth=%dms reserve=%dms tempo=%d waiting=%d catching_up=%d resumed=%d", |
|
diagnostics.depth_ms, diagnostics.reserve_ms, diagnostics.tempo_x100, diagnostics.waiting, |
|
diagnostics.catching_up, diagnostics.resumed); |
|
return ok ? 0 : -1; |
|
}
|
|
|