Browse Source

Expose SoundTouch starvation with a 20 ms jitter reserve

master
evgeny 3 days ago
parent
commit
ff16adda17
  1. 11
      doc/tasks.md
  2. 47
      tools/chatgui/tests/test_call_jitter.cpp

11
doc/tasks.md

@ -4,6 +4,17 @@
## Текущие задачи
[ ] **Снижение минимального запаса JB звонка до 20мс** — 15.10.2026:
Нижняя граница общего call_jitter изменена с 60 до 20мс; адаптация по 10-секундному окну сохранена.
Изменение не установлено: test_call_jitter обнаружил недостачи PCM даже при ровной доставке после догона.
Диагностика: tick=841, PCM=384/480, depth_before=37мс, encoded=0, processing=30мс, reserve=20мс, spread=0.
Регулятор считает внутреннее накопление SoundTouch доступным запасом и ускоряет до нехватки готового PCM.
Требуется отдельно учитывать сетевой запас 20мс и необходимое накопление обработки при управлении темпом/старте.
Порог SoundTouch зависит от tempo; проверять его по параметрам движка, а не возвращать скрытый минимум 60мс.
Дальше: тест ровной доставки для pull 5/10/20мс, старт/возобновление, рост и спад джиттера, burst и A/V clock.
test_call_video_clock проходит; test_call_jitter намеренно сохраняет строгую проверку отсутствия недостач.
По правилу AGENTS.md реализация остановлена на обнаруженной нестыковке; рабочие приложения остались прежними.
[x] **Завершение звонка и видеопорта после обрыва транспорта** — 15.10.2026:
Трасса test_call доказала потерю HANGUP при смене эпохи сигналинга: router reset удалял старую очередь.
HANGUP/CANCEL/DECLINE теперь подтверждаются END_ACK и повторяются каждые 300мс, максимум 20с.

47
tools/chatgui/tests/test_call_jitter.cpp

@ -86,8 +86,8 @@ int main() {
w.observe(0, 100); w.observe(500, 400);
CHECK(w.low == 100 && w.high == 400 && w.reserve == 300);
w.observe(1000, 2000); CHECK(w.reserve == 1000);
w.observe(10000, 2000); CHECK(w.low == 2000 && w.reserve == 60);
w.observe(30000, 80); CHECK(w.low == 80 && w.high == 80 && w.reserve == 60);
w.observe(10000, 2000); CHECK(w.low == 2000 && w.reserve == 20);
w.observe(30000, 80); CHECK(w.low == 80 && w.high == 80 && w.reserve == 20);
for (int cap : {500, 2000}) {
call_jitter_window limited;
limited.max_reserve = cap;
@ -100,13 +100,18 @@ int main() {
// Разные часы и переполнение uint32 не создают джиттер при ровной доставке.
uint32_t sent = UINT32_MAX - 39;
for (int i = 0; i < 100; i++) arrival.observe_arrival(900000 + i * 20, sent + (uint32_t)(i * 20));
CHECK(arrival.high == 0 && arrival.low == 0 && arrival.reserve == 60);
CHECK(arrival.high == 0 && arrival.low == 0 && arrival.reserve == 20);
arrival.observe_arrival(902200, sent + 2000); // задержка доставки +200мс
CHECK(arrival.reserve == 200);
arrival.observe_arrival(902220, sent + 2220); // очередь пришла пачкой
CHECK(arrival.reserve == 200);
arrival.observe_arrival(920000, sent + 20000); // старый burst ушёл из окна
CHECK(arrival.reserve == 60);
CHECK(arrival.reserve == 20);
// Небольшой джиттер больше минимума тоже учитывается, без прежнего порога 60мс.
arrival.observe_arrival(920055, sent + 20020);
CHECK(arrival.reserve == 35);
arrival.observe_arrival(931055, sent + 31020);
CHECK(arrival.reserve == 20);
}
debug_set_level(DEBUG_LEVEL_WARN);
debug_enable_console(1);
@ -118,13 +123,13 @@ int main() {
decoder d;
auto* j = call_jitter_create(decode, &d, 1000);
CHECK(j);
push(j, 0); push(j, 1);
push(j, 0); // 20мс достаточно для допуска в decoder, но SoundTouch ещё накапливает PCM.
CHECK(call_jitter_pull(j, pcm, 480) == 0);
push(j, 2);
push(j, 1);
CHECK(call_jitter_pull(j, pcm, 480) > 0);
call_jitter_diagnostics diagnostic{};
call_jitter_get_diagnostics(j, &diagnostic, 0);
CHECK(diagnostic.resumed == 1 && diagnostic.pcm_samples == 480 && diagnostic.reserve_ms == 60);
CHECK(diagnostic.resumed == 1 && diagnostic.pcm_samples == 480 && diagnostic.reserve_ms == 20);
CHECK(diagnostic.encoded_ms >= 0 && diagnostic.processing_ms >= 0 && diagnostic.arrival_spread_ms == 0);
call_jitter_get_diagnostics(j, &diagnostic, 0);
CHECK(diagnostic.resumed == 1); // Снимок playback не потребляет события для uasync.
@ -133,6 +138,7 @@ int main() {
call_jitter_get_diagnostics(j, &diagnostic, 0);
CHECK(diagnostic.resumed == 0);
call_jitter_range range;
push(j, 2);
for (int i = 3; i < 503; i++) push(j, i, 10060); // 10с сетевой задержки, затем пачка
call_jitter_get_stats(j, nullptr, nullptr, nullptr, nullptr, &range);
CHECK(range.reserve_ms == 1000);
@ -144,7 +150,7 @@ int main() {
if (tempo > highest) highest = tempo;
}
call_jitter_get_stats(j, nullptr, nullptr, nullptr, nullptr, &range);
CHECK(range.reserve_ms == 60 && range.max_ms >= 10000);
CHECK(range.reserve_ms == 20 && range.max_ms >= 10000);
CHECK(range.min_ms <= range.max_ms);
CHECK(highest >= 150);
CHECK(depth < 200);
@ -153,16 +159,16 @@ int main() {
for (size_t i = 0; i < d.seen.size(); i++) CHECK(d.seen[i] == (int)i);
for (int i = 0; i < 100; i++) call_jitter_pull(j, pcm, 480);
size_t before = d.seen.size();
// Один кадр плюс остаток SoundTouch ниже 60мс; два кадра уже могут заполнить запас.
// После опустошения один кадр дополняет остаток SoundTouch и возобновляет PCM при запасе 20мс.
push(j, next++);
int refill = call_jitter_pull(j, pcm, 480);
if (refill) {
if (refill != 480) {
call_jitter_get_stats(j, &depth, nullptr, nullptr, nullptr, &range);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "refill check: pcm=%d remaining=%dms reserve=%dms decoded=%zu",
refill, depth, range.reserve_ms, d.seen.size() - before);
}
CHECK(refill == 0);
CHECK(d.seen.size() == before);
CHECK(refill == 480);
CHECK(d.seen.size() == before + 1);
for (int i = 0; i < 50; i++) push(j, next++);
CHECK(call_jitter_pull(j, pcm, 480) > 0);
call_jitter_destroy(j);
@ -174,15 +180,24 @@ int main() {
for (int i = 0; i < 60; i++) push(j, i);
call_jitter_range range;
call_jitter_get_stats(j, &depth, nullptr, nullptr, nullptr, &range);
CHECK(depth == 1200 && range.reserve_ms == 60);
CHECK(depth == 1200 && range.reserve_ms == 20);
CHECK(call_jitter_pull(j, pcm, 480) == 480); // первый pull сразу воспроизводит речь
int next = 60;
int next = 60, short_pulls = 0;
for (int tick = 0; tick < 6000; tick++) {
if (tick % 2 == 0) push(j, next++);
CHECK(call_jitter_pull(j, pcm, 480) == 480);
int produced = call_jitter_pull(j, pcm, 480);
if (produced != 480 && ++short_pulls <= 3) {
call_jitter_diagnostics state{};
call_jitter_get_diagnostics(j, &state, 0);
DEBUG_ERROR(DEBUG_CATEGORY_CALL,
"steady delivery short pull tick=%d pcm=%d depth_before=%d encoded=%d processing=%d reserve=%d tempo=%d spread=%lld",
tick, produced, state.depth_ms, state.encoded_ms, state.processing_ms, state.reserve_ms,
state.tempo_x100, (long long)state.arrival_spread_ms);
}
call_jitter_get_stats(j, &depth, &tempo, &dropped, &under, &range);
CHECK(range.reserve_ms == 60); // догон не увеличивает сетевой резерв
CHECK(range.reserve_ms == 20); // догон не увеличивает сетевой резерв
}
CHECK(short_pulls == 0);
CHECK(depth < 150 && tempo <= 110 && dropped == 0 && under == 0);
for (size_t i = 0; i < d.seen.size(); i++) CHECK(d.seen[i] == (int)i);
call_jitter_destroy(j);

Loading…
Cancel
Save