Browse Source

Fix call gap tone to follow 500ms of missing jitter PCM

master
evgeny 19 hours ago
parent
commit
1c51df039f
  1. 50
      src/call/call_audio.c
  2. 52
      tools/chatgui/tests/test_call_jitter.cpp

50
src/call/call_audio.c

@ -25,7 +25,7 @@
#define CALL_AUDIO_ID "call_audio" #define CALL_AUDIO_ID "call_audio"
#define CALL_AUDIO_BITRATE 32000 #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_FILTER_SAMPLES 14400 /* хвост эха 300мс @48кГц */
#define CALL_AEC_DELAY_FRAMES 2 /* desktop: минимальная задержка duplex */ #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_glitch_started = 0; /* только аудиопоток */
static int g_ended_started = 0; /* только аудио-поток: тон завершения запущен */ static int g_ended_started = 0; /* только аудио-поток: тон завершения запущен */
static uint64_t g_call_id = 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_encoder_t* g_encoder = NULL;
static opus_codec_decoder_t* g_decoder = NULL; static opus_codec_decoder_t* g_decoder = NULL;
static struct call_jitter* g_vj = 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); pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id && g_vj && opus && len > 0) { if (g_active && call_id == g_call_id && g_vj && opus && len > 0) {
call_jitter_push(g_vj, opus, len); call_jitter_push(g_vj, opus, len);
g_last_media_tb = get_time_tb();
} }
pthread_mutex_unlock(&g_mtx); pthread_mutex_unlock(&g_mtx);
} }
@ -185,7 +184,7 @@ int call_audio_start(uint64_t call_id) {
g_ending = 0; g_ending = 0;
g_ended_started = 0; g_ended_started = 0;
g_glitch_started = 0; g_glitch_started = 0;
g_last_media_tb = 0; g_gap_samples = 0;
pthread_mutex_unlock(&g_mtx); 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, 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_ending = 0;
g_ended_started = 0; g_ended_started = 0;
g_call_id = 0; g_call_id = 0;
g_last_media_tb = 0; g_gap_samples = 0;
enc = g_encoder; g_encoder = NULL; enc = g_encoder; g_encoder = NULL;
dec = g_decoder; g_decoder = NULL; dec = g_decoder; g_decoder = NULL;
j = g_vj; g_vj = 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; int active = 0, ending = 0;
struct call_jitter* j = NULL; struct call_jitter* j = NULL;
struct call_tones* tones = NULL; struct call_tones* tones = NULL;
uint64_t last_media = 0;
pthread_mutex_lock(&g_mtx); pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id) { 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; ending = g_ending;
j = g_vj; j = g_vj;
tones = g_tones; tones = g_tones;
last_media = g_last_media_tb;
} }
pthread_mutex_unlock(&g_mtx); 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); n = call_tones_render(tones, out, max_samples);
} else { } else {
n = call_jitter_pull(j, out, max_samples); n = call_jitter_pull(j, out, max_samples);
uint64_t now = get_time_tb(); int previous_gap = g_gap_samples;
int stalled = n < max_samples && last_media != 0 && now - last_media >= CALL_STALL_TB; if (n > 0) g_gap_samples = 0;
if (stalled != g_glitch_started) { int missing = max_samples - n;
g_glitch_started = stalled; int quiet = CALL_STALL_SAMPLES - g_gap_samples;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: gap tone %s call=%016llx media_gap=%llums pcm=%d/%d", if (quiet > missing) quiet = missing;
CALL_AUDIO_ID, stalled ? "started" : "stopped", (unsigned long long)call_id, int tone_samples = missing - quiet;
(unsigned long long)((now - last_media) / 10), n, max_samples); 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); call_tones_glitch_start(tones);
n += call_tones_render(tones, out + n, max_samples - n); call_tones_render(tones, out + n + quiet, tone_samples);
} else {
call_tones_glitch_stop(tones);
} }
for (int i = n; i < max_samples; i++) out[i] = 0;
n = max_samples; n = max_samples;
} }

52
tools/chatgui/tests/test_call_jitter.cpp

@ -8,8 +8,6 @@
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <vector> #include <vector>
#include <chrono>
#include <thread>
static int failures; static int failures;
#define CHECK(c) do { if (!(c)) { std::printf("FAIL line %d: %s\n", __LINE__, #c); failures++; } } while (0) #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]; uint8_t encoded[512];
int len = opus_codec_encode(encoder, voice, 960, encoded, sizeof(encoded)); int len = opus_codec_encode(encoder, voice, 960, encoded, sizeof(encoded));
CHECK(len > 0); CHECK(len > 0);
for (int i = 0; i < 3; i++) call_audio_on_media(nullptr, id, i, 0, encoded, len); // Без входных пакетов первые 500мс выходного аудио — строго тишина.
for (int i = 0; i < 30; i++) call_audio_pull_pcm(id, pcm, 480); for (int tick = 0; tick < 50; tick++) {
// Аудиоустройства и uasync в тесте нет: проверяем реальный срок включения сигнала. CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
std::this_thread::sleep_for(std::chrono::milliseconds(100)); for (auto value : pcm) CHECK(value == 0);
call_audio_pull_pcm(id, pcm, 480); }
bool silent = true; CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
for (auto value : pcm) if (value) silent = false; bool audible = false;
CHECK(silent); for (auto value : pcm) if (value) audible = true;
std::this_thread::sleep_for(std::chrono::milliseconds(500)); CHECK(audible);
call_audio_pull_pcm(id, pcm, 480); // Один пакет не возобновляет PCM: писк продолжается при ожидании запаса.
silent = true; call_audio_on_media(nullptr, id, 0, 0, encoded, len);
for (auto value : pcm) if (value) silent = false; CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
CHECK(!silent); audible = false;
for (int i = 0; i < 6; i++) call_audio_on_media(nullptr, id, i+3, 0, encoded, len); 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); CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
call_audio_begin_end(id); // явное завершение прекращает догон и включает финальный тон call_audio_begin_end(id); // явное завершение прекращает догон и включает финальный тон
CHECK(call_audio_pull_pcm(id, pcm, 480) > 0); CHECK(call_audio_pull_pcm(id, pcm, 480) > 0);
call_audio_release(id); call_audio_release(id);
opus_codec_encoder_destroy(encoder); opus_codec_encoder_destroy(encoder);
} }
{
// Порог проходит внутри блока: сначала ровно 500мс тишины, затем тон.
CHECK(call_audio_start(124) == 0);
std::vector<int16_t> 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"); std::printf("call_jitter: %s\n", failures ? "FAILED" : "PASSED");
return failures ? 1 : 0; return failures ? 1 : 0;
} }

Loading…
Cancel
Save