From 1c51df039fe64c4ada40cbd236e18031a60dfeca Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 30 Sep 2026 21:14:37 +0300 Subject: [PATCH] Fix call gap tone to follow 500ms of missing jitter PCM --- src/call/call_audio.c | 50 ++++++++++++++--------- tools/chatgui/tests/test_call_jitter.cpp | 52 ++++++++++++++++-------- 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/src/call/call_audio.c b/src/call/call_audio.c index d45f1689..076f4c5c 100644 --- a/src/call/call_audio.c +++ b/src/call/call_audio.c @@ -25,7 +25,7 @@ #define CALL_AUDIO_ID "call_audio" #define CALL_AUDIO_BITRATE 32000 -#define CALL_STALL_TB 5000 /* 500мс без медиа и недобор PCM → глитч-тон */ +#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 */ @@ -36,7 +36,7 @@ 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 uint64_t g_last_media_tb = 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; @@ -85,7 +85,6 @@ void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id, 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); - g_last_media_tb = get_time_tb(); } pthread_mutex_unlock(&g_mtx); } @@ -185,7 +184,7 @@ int call_audio_start(uint64_t call_id) { g_ending = 0; g_ended_started = 0; g_glitch_started = 0; - g_last_media_tb = 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, @@ -220,7 +219,7 @@ void call_audio_release(uint64_t call_id) { g_ending = 0; g_ended_started = 0; g_call_id = 0; - g_last_media_tb = 0; + g_gap_samples = 0; enc = g_encoder; g_encoder = NULL; dec = g_decoder; g_decoder = NULL; j = g_vj; g_vj = NULL; @@ -347,7 +346,6 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { int active = 0, ending = 0; struct call_jitter* j = NULL; struct call_tones* tones = NULL; - uint64_t last_media = 0; pthread_mutex_lock(&g_mtx); if (g_active && call_id == g_call_id) { @@ -355,7 +353,6 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { ending = g_ending; j = g_vj; tones = g_tones; - last_media = g_last_media_tb; } pthread_mutex_unlock(&g_mtx); @@ -375,21 +372,36 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { n = call_tones_render(tones, out, max_samples); } else { n = call_jitter_pull(j, out, max_samples); - uint64_t now = get_time_tb(); - int stalled = n < max_samples && last_media != 0 && now - last_media >= CALL_STALL_TB; - if (stalled != g_glitch_started) { - g_glitch_started = stalled; - DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: gap tone %s call=%016llx media_gap=%llums pcm=%d/%d", - CALL_AUDIO_ID, stalled ? "started" : "stopped", (unsigned long long)call_id, - (unsigned long long)((now - last_media) / 10), n, 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); } - if (stalled) { + 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); - n += call_tones_render(tones, out + n, max_samples - n); - } else { - call_tones_glitch_stop(tones); + call_tones_render(tones, out + n + quiet, tone_samples); } - for (int i = n; i < max_samples; i++) out[i] = 0; n = max_samples; } diff --git a/tools/chatgui/tests/test_call_jitter.cpp b/tools/chatgui/tests/test_call_jitter.cpp index db91521b..716de92a 100644 --- a/tools/chatgui/tests/test_call_jitter.cpp +++ b/tools/chatgui/tests/test_call_jitter.cpp @@ -8,8 +8,6 @@ #include #include #include -#include -#include static int failures; #define CHECK(c) do { if (!(c)) { std::printf("FAIL line %d: %s\n", __LINE__, #c); failures++; } } while (0) @@ -126,26 +124,48 @@ int main() { uint8_t encoded[512]; int len = opus_codec_encode(encoder, voice, 960, encoded, sizeof(encoded)); CHECK(len > 0); - for (int i = 0; i < 3; i++) call_audio_on_media(nullptr, id, i, 0, encoded, len); - for (int i = 0; i < 30; i++) call_audio_pull_pcm(id, pcm, 480); - // Аудиоустройства и uasync в тесте нет: проверяем реальный срок включения сигнала. - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - call_audio_pull_pcm(id, pcm, 480); - bool silent = true; - for (auto value : pcm) if (value) silent = false; - CHECK(silent); - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - call_audio_pull_pcm(id, pcm, 480); - silent = true; - for (auto value : pcm) if (value) silent = false; - CHECK(!silent); - for (int i = 0; i < 6; i++) call_audio_on_media(nullptr, id, i+3, 0, encoded, len); + // Без входных пакетов первые 500мс выходного аудио — строго тишина. + for (int tick = 0; tick < 50; tick++) { + CHECK(call_audio_pull_pcm(id, pcm, 480) == 480); + for (auto value : pcm) CHECK(value == 0); + } + CHECK(call_audio_pull_pcm(id, pcm, 480) == 480); + bool audible = false; + for (auto value : pcm) if (value) audible = true; + CHECK(audible); + // Один пакет не возобновляет PCM: писк продолжается при ожидании запаса. + call_audio_on_media(nullptr, id, 0, 0, encoded, len); + CHECK(call_audio_pull_pcm(id, pcm, 480) == 480); + audible = false; + for (auto value : pcm) if (value) audible = true; + CHECK(audible); + for (int i = 1; i < 6; i++) call_audio_on_media(nullptr, id, i, 0, encoded, len); CHECK(call_audio_pull_pcm(id, pcm, 480) == 480); call_audio_begin_end(id); // явное завершение прекращает догон и включает финальный тон CHECK(call_audio_pull_pcm(id, pcm, 480) > 0); call_audio_release(id); opus_codec_encoder_destroy(encoder); } + { + // Порог проходит внутри блока: сначала ровно 500мс тишины, затем тон. + CHECK(call_audio_start(124) == 0); + std::vector output(777); + for (int tick = 0; tick < 30; tick++) { + CHECK(call_audio_pull_pcm(124, output.data(), 777) == 777); + for (auto value : output) CHECK(value == 0); + } + CHECK(call_audio_pull_pcm(124, output.data(), 777) == 777); + for (int i = 0; i < 690; i++) CHECK(output[i] == 0); + bool audible = false; + for (int i = 690; i < 777; i++) if (output[i]) audible = true; + CHECK(audible); + call_audio_release(124); + // Новый звонок не наследует включённый тон и отсчёт предыдущего. + CHECK(call_audio_start(125) == 0); + CHECK(call_audio_pull_pcm(125, output.data(), 777) == 777); + for (auto value : output) CHECK(value == 0); + call_audio_release(125); + } std::printf("call_jitter: %s\n", failures ? "FAILED" : "PASSED"); return failures ? 1 : 0; }