diff --git a/doc/chat_call_arch.md b/doc/chat_call_arch.md index ada60166..9c82cbb0 100644 --- a/doc/chat_call_arch.md +++ b/doc/chat_call_arch.md @@ -140,6 +140,27 @@ INFO-логи Android показывают длительности настро (на desktop 60–80мс). Лог `playback buffering` показывает принятый порог и ёмкость; ежесекундный `clock` — queuedUs, текущий порог и underruns, прирост underruns имеет уровень WARN. Это очередь AudioTrack, а не полная сквозная задержка и не сетевой jitter reserve. +Повторный замер: startFrames=1920 принят, но written-played остался 140мс; одного стартового порога недостаточно. + +Диагностика Android при `call=debug`: playback сохраняет один числовой снимок на блок (20мс) +в кольце 512 элементов, без форматирования/вывода. Отдельный reporter раз в 200мс забирает записи; +раз в секунду выводит `audio summary` с min/avg/max очередей, задержек pull/write и опоздания потока, +целевым запасом/разбросом доставки JB, темпом, недостающим PCM и отдельными счётчиками JB/AudioTrack. +Снимок JB не сбрасывает события для CALL_STATS. encodedMs — очередь Opus после pull, +processingMs — оставшийся PCM SoundTouch в длительности исходной речи; reserveMs зависит только от доставки. +AudioTimestamp опрашивается раз в 200мс, его frame/time и время опроса печатаются отдельно; +при недоступности стоит stampOk=0. Времена trace — MONOTONIC; очередь устройства — written-played. +Эти показатели нельзя складывать с возрастом AudioTimestamp как независимые задержки. + +`audio detail` сохраняет 2с до и 3с после старта, фактической смены routedDevice или новой аномалии: +очередь устройства >100мс в течение секунды, отсутствие продвижения head 100мс при наличии данных, +либо новый episode underrun (затухает через 3с без повторений). Порог очереди — диагностический, +не настройка JB и не изменение воспроизведения. События внутри окна объединяются, между выгрузками +не менее 10с; продолжающаяся аномалия не создаёт повторных дампов. Восстановление логируется INFO. +Подробные строки содержат по четыре снимка и заголовок с порядком колонок; при stop окно может быть неполным. +Переполнение кольца при задержке reporter имеет отдельный WARN lost=N. Аудиоданные не сохраняются. +Выключение call=debug прекращает сбор/выгрузку; буфер и reporter освобождаются с playback worker. +Сравнивать сводки следует по call ID и monoUs; рост сетевого запаса сам по себе не является ошибкой. ## 7. Буфер звонка и восстановление diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioTrace.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioTrace.kt index 49270f8a..472a3995 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioTrace.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioTrace.kt @@ -29,6 +29,8 @@ internal class CallAudioTrace(private val id: Long, private val emit: (String, S private var stoppedSince = -1L private var underrunAt = -10_000_000L private var faults = 0 + private var reportedDeviceUnderruns = 0L + private var reportedJbUnderruns = 0L @Synchronized fun record(row: LongArray) { if (!enabled) return @@ -51,6 +53,8 @@ internal class CallAudioTrace(private val id: Long, private val emit: (String, S if (!enabled) { history.clear(); summary.clear(); previous = null; eventAt = -1 faults = 0; highSince = -1; stoppedSince = -1; underrunAt = -10_000_000 + lastDump = -10_000_000; suppressed = 0 + reportedDeviceUnderruns = 0; reportedJbUnderruns = 0 return } if (overflow > 0) emit("WARN", "audio trace call=$id reporter overrun lost=$overflow") @@ -108,10 +112,12 @@ internal class CallAudioTrace(private val id: Long, private val emit: (String, S "route=${last[1]}/${last[2]} min/avg/max queueUs=${range(::queued)} " + "jbMs=${range { it[16] + it[17] }} reserveMs=${range { it[18] }} spreadMs=${range { it[19] }} " + "pullUs=${range { it[5] }} writeUs=${range { it[6] }} wakeLateUs=${range { it[7] }} " + - "tempo=${last[21]} deviceUnderruns=${last[9]} jbUnderruns=${last[22]} " + + "tempo=${last[21]} deviceUnderruns=${last[9]} deviceUnderDelta=${last[9] - reportedDeviceUnderruns} " + + "jbUnderruns=${last[22]} jbUnderDelta=${last[22] - reportedJbUnderruns} " + "zeroWrites=${summary.sumOf { it[8] }} missingPcm=${summary.sumOf { (960 - it[20]).coerceAtLeast(0) }} " + "stamp=${last[12]}@${last[13]} readUs=${last[14]} ok=${last[15]} " + "buffer=${last[10]} start=${last[11]} fault=$faults suppressed=$suppressed") + reportedDeviceUnderruns = last[9]; reportedJbUnderruns = last[22] summary.clear() } diff --git a/tools/chatgui/tests/test_call_jitter.cpp b/tools/chatgui/tests/test_call_jitter.cpp index 208c51f2..972f1d74 100644 --- a/tools/chatgui/tests/test_call_jitter.cpp +++ b/tools/chatgui/tests/test_call_jitter.cpp @@ -122,6 +122,16 @@ int main() { CHECK(call_jitter_pull(j, pcm, 480) == 0); push(j, 2); 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.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. + call_jitter_get_diagnostics(j, &diagnostic, 1); + CHECK(diagnostic.resumed == 1); + call_jitter_get_diagnostics(j, &diagnostic, 0); + CHECK(diagnostic.resumed == 0); call_jitter_range range; for (int i = 3; i < 503; i++) push(j, i, 10060); // 10с сетевой задержки, затем пачка call_jitter_get_stats(j, nullptr, nullptr, nullptr, nullptr, &range);