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

// 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;
}