From ff16adda1751bbb18df0c237afbec8ddd440ebd5 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 15 Oct 2026 12:39:00 +0200 Subject: [PATCH] Expose SoundTouch starvation with a 20 ms jitter reserve --- doc/tasks.md | 11 ++++++ tools/chatgui/tests/test_call_jitter.cpp | 47 ++++++++++++++++-------- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/doc/tasks.md b/doc/tasks.md index 3fbcf452..49fc0e1f 100644 --- a/doc/tasks.md +++ b/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с. diff --git a/tools/chatgui/tests/test_call_jitter.cpp b/tools/chatgui/tests/test_call_jitter.cpp index 972f1d74..78c29d1d 100644 --- a/tools/chatgui/tests/test_call_jitter.cpp +++ b/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);