Browse Source

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

proxy
evgeny 2 weeks ago
parent
commit
6a0373ee73
  1. 29
      src/call/call_audio.c
  2. 48
      src/call/voice_jitter.cpp
  3. 3
      src/call/voice_jitter.h
  4. 18
      tools/chatgui/src/call_audio_engine.cpp
  5. 5
      tools/chatgui/src/call_audio_engine.h

29
src/call/call_audio.c

@ -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
src/call/voice_jitter.cpp

@ -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
src/call/voice_jitter.h

@ -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

18
tools/chatgui/src/call_audio_engine.cpp

@ -7,6 +7,7 @@
extern "C" {
#include "call/call_audio.h"
#include "../../lib/debug_config.h"
#include "../../lib/u_async.h"
}
#include <QTimer>
@ -164,6 +165,23 @@ void CallAudioEngine::dataCallback(const void* in, void* out, unsigned int frame
const int16_t* in16 = (const int16_t*)in;
int16_t* out16 = (int16_t*)out;
/* диагностика (DEBUG_CATEGORY_DEBUG): фактические параметры miniaudio-коллбэка */
if (m_dbgFrames == 0) {
m_dbgFrames = frames;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "CallAudio: callback frames=%u in=%s out=%s",
frames, in16 ? "yes" : "no", out16 ? "yes" : "no");
}
m_dbgCbCount++;
{
uint64_t now = get_time_tb();
if (m_dbgLastTb == 0) m_dbgLastTb = now;
else if (now - m_dbgLastTb >= 10000) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "CallAudio: cb=%u/s frames=%u", m_dbgCbCount, frames);
m_dbgCbCount = 0;
m_dbgLastTb = now;
}
}
/* захват: копим 960 samples → encode+send (сервисный слой). mute → тишина. */
if (in16) {
bool muted = m_muted.load();

5
tools/chatgui/src/call_audio_engine.h

@ -59,4 +59,9 @@ private:
std::vector<int16_t> m_captureBuf; /* 960 samples, аудио-поток */
int m_capturePos = 0;
/* диагностика (DEBUG_CATEGORY_DEBUG): фактические параметры miniaudio-коллбэка */
unsigned int m_dbgFrames = 0;
uint32_t m_dbgCbCount = 0;
uint64_t m_dbgLastTb = 0;
};

Loading…
Cancel
Save