call: диагностика ставок TX/RX и детект сталла аудио-девайса с хард-ресинком

This commit is contained in:
evgeny committed 2026-09-21 16:54:14 +03:00
1 parent 570b421ba0
commit 6a0373ee73
5 files changed
+103

No files matched your search

+29
View File
@@ -37,6 +37,10 @@ static opus_codec_decoder_t* g_decoder = NULL;
static struct vj* g_vj = NULL;
static struct call_tones* g_tones = NULL;
/* диагностика ставок (DEBUG_CATEGORY_DEBUG): под g_mtx */
static uint32_t g_tx_feed = 0, g_tx_encode_fail = 0, g_rx_push = 0, g_rx_pull = 0;
static uint64_t g_rate_last_tb = 0;
static const struct call_audio_ops g_ops = {
.on_media = call_audio_on_media,
.get_stats = call_audio_get_stats,
@@ -74,6 +78,7 @@ void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
if (g_active && call_id == g_call_id && g_vj && opus && len > 0) {
vj_push(g_vj, opus, len);
g_last_media_tb = get_time_tb();
g_rx_push++;
}
pthread_mutex_unlock(&g_mtx);
}
@@ -199,6 +204,20 @@ void call_audio_stop(void) {
/* ── аудио-поток: TX / RX ── */
/* Раз в ~1с логируем ставки TX/RX — диагностика «звонок не проходит» (DEBUG_CATEGORY_DEBUG).
* Вызывается из аудио-потока (pull_pcm); счётчики под g_mtx. */
static void call_audio_rate_tick(uint64_t now_tb) {
uint32_t feed = 0, encfail = 0, push = 0, pull = 0, resync = 0;
pthread_mutex_lock(&g_mtx);
feed = g_tx_feed; encfail = g_tx_encode_fail; push = g_rx_push; pull = g_rx_pull;
g_tx_feed = g_tx_encode_fail = g_rx_push = g_rx_pull = 0;
if (g_vj) resync = vj_resyncs(g_vj);
g_rate_last_tb = now_tb;
pthread_mutex_unlock(&g_mtx);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "%s: rates feed=%u/s encfail=%u push=%u/s pull=%u/s resync=%u",
CALL_AUDIO_ID, feed, encfail, push, pull, resync);
}
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) {
if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES) return -1;
@@ -213,6 +232,8 @@ int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) {
return -1;
}
len = opus_codec_encode(g_encoder, pcm, count, opus, (int)sizeof(opus));
g_tx_feed++;
if (len <= 0) g_tx_encode_fail++;
ua = g_inst ? g_inst->ua : NULL;
inst = g_inst;
pthread_mutex_unlock(&g_mtx);
@@ -248,11 +269,19 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
j = g_vj;
tones = g_tones;
last_media = g_last_media_tb;
g_rx_pull++;
}
pthread_mutex_unlock(&g_mtx);
if (!active || !j || !tones) return 0;
/* диагностика ставок: раз в ~1с (аудио-поток) */
{
uint64_t now_tb = get_time_tb();
if (g_rate_last_tb == 0) g_rate_last_tb = now_tb;
else if (now_tb - g_rate_last_tb >= 10000) call_audio_rate_tick(now_tb);
}
/* завершение: доигрываем тон (450мс), затем тишина. Тон стартует лениво
* здесь (аудио-поток) — begin_end лишь выставляет флаг, чтобы не гонять
* генератор тонов с GUI-потоком. */
+48
View File
@@ -27,6 +27,8 @@ static const double kMaxDepthMs = 1000.0; /* при этой глубин
static const double kEmaAlpha = 0.2; /* сглаживание глубины */
static const double kTempoStep = 0.01; /* макс. изменение темпа за один pull (без кликов) */
static const double kPrimeTimeoutMs = 200.0; /* pre-roll: принудительный prime через это время */
static const double kStallGapMs = 80.0; /* gap между pull > это → сталь аудио-девайса */
static const double kResyncDepthMs = 400.0; /* бэклог > это после сталла → хард-ресинк к live */
struct vj {
int sr = 0;
@@ -45,6 +47,12 @@ struct vj {
int count = 0;
uint32_t dropped = 0;
uint32_t underruns = 0;
uint32_t resyncs = 0;
/* детект сталла аудио-девайса (gap между pull) */
std::chrono::steady_clock::time_point last_pull_tp; /* только аудио-поток */
int first_pull = 1; /* только аудио-поток */
int stalled = 0; /* только аудио-поток */
soundtouch::SoundTouch st; /* только аудио-поток */
uint8_t enc_buf[VJ_MAX_ENC]; /* scratch: вытащенный кодированный кадр */
@@ -168,9 +176,43 @@ void vj_push(struct vj* j, const uint8_t* enc, int len) {
j->count++;
}
/* Хард-ресинк к live: сбросить бэклог (кольцо + SoundTouch) и темп к 1.0.
* Применяется после сталла аудио-девайса, когда бэклог > kResyncDepthMs — один
* чистый срез вместо долгого догона темпом (chipmunk) + дропов по одному. */
static void vj_hard_resync(struct vj* j) {
{ std::lock_guard<std::mutex> lk(j->mtx); j->count = 0; j->head = 0; }
j->st.clear();
j->tempo = 1.0;
j->tempo_x100.store(100);
j->ema_depth_ms = j->target_ms;
j->resyncs++;
}
int vj_pull(struct vj* j, int16_t* out, int max_samples) {
if (!j || !out || max_samples <= 0) return 0;
/* ── детект сталла аудио-девайса (gap между pull) + ресинк по его окончании ── */
{
auto now = std::chrono::steady_clock::now();
if (j->first_pull) {
j->first_pull = 0;
j->last_pull_tp = now;
} else {
double gap_ms = std::chrono::duration<double, std::milli>(now - j->last_pull_tp).count();
j->last_pull_tp = now;
if (gap_ms > kStallGapMs) {
j->stalled = 1; /* сталь начался — просто флаг */
} else if (j->stalled) {
j->stalled = 0; /* сталь кончился */
int c;
{ std::lock_guard<std::mutex> lk(j->mtx); c = j->count; }
double depth_ms = (double)c * j->frame_ms
+ (double)j->st.numSamples() * 1000.0 / (double)j->sr;
if (depth_ms > kResyncDepthMs) vj_hard_resync(j);
}
}
}
/* 0) pre-roll: не отдаём звук, пока глубина кольца < target_ms (или не истёк таймаут).
* Состояние — только аудио-поток; кадры копятся в кольце продюсером (uasync). */
int cnt;
@@ -256,4 +298,10 @@ uint32_t vj_dropped(struct vj* j) {
return j->dropped;
}
uint32_t vj_resyncs(struct vj* j) {
if (!j) return 0;
std::lock_guard<std::mutex> lk(j->mtx);
return j->resyncs;
}
} // extern "C"
+3
View File
@@ -69,6 +69,9 @@ void vj_get_stats(struct vj* j, int* depth_ms, int* tempo_x100, uint32_t* droppe
/* Число дропнутых кадров при переполнении (для диагностики/тестов). */
uint32_t vj_dropped(struct vj* j);
/* Число хард-ресинков к live (после сталла аудио-девайса) — диагностика. */
uint32_t vj_resyncs(struct vj* j);
#ifdef __cplusplus
}
#endif