From 91030de5ea8fdad01437e9f93ea221b7ddd5c003 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 15 Oct 2026 11:11:45 +0200 Subject: [PATCH] Cover immediate buffer probe failures and document tuning --- doc/chat_call_arch.md | 31 +++++++++++++------ .../utun/chat/data/CallPlaybackBufferTest.kt | 26 +++++++++++++++- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/doc/chat_call_arch.md b/doc/chat_call_arch.md index 9cceebdb..81dba835 100644 --- a/doc/chat_call_arch.md +++ b/doc/chat_call_arch.md @@ -141,8 +141,8 @@ ACTIVE запрашивается до создания аудиоустройс INFO-логи Android показывают длительности настройки режима, регистрации callback, BT-профиля, выбора маршрута, создания/старта устройств, первого чтения микрофона и первой записи в AudioTrack, а также готовность другого направления. Общий `call_audio` логирует время ожидания первого PCM; запись в AudioTrack может содержать начальную тишину. -Эффективный рабочий буфер AudioTrack на всех поддерживаемых API запрашивается размером -два блока PCM (40мс); на API 31+ отдельно задаётся такой же стартовый порог. +Эффективный рабочий буфер AudioTrack на всех поддерживаемых API при старте запрашивается размером +два блока PCM (40мс); на API 31+ стартовый порог следует фактически принятому размеру. Ёмкость выделенной памяти может быть больше. Ограничение повторяется по callback фактической смены выхода на main, под trackLock; callback старого AudioTrack игнорируется и снимается при stop. Подачу PCM задаёт свободное место в AudioTrack, без дополнительного таймера 20мс. @@ -155,7 +155,9 @@ INFO-логи Android показывают длительности настро written-played оставался 120–160мс; одного стартового порога недостаточно. Замер Samsung подтвердил постоянную очередь 40мс. При прежнем параллельном управлении маршрутом AudioTrack пересоздавался, очередь временно достигала 160мс, а ожидание свободного места — 328–336мс. -При старте AudioTrack.play занимал 1080мс. Новое владение маршрутом нужно проверить тем же сценарием. +При старте AudioTrack.play занимал 1080мс. После перехода к единственному владельцу Telecom +в проверенном звонке старт занял 62мс, очередь оставалась не более 40мс и при смене выхода. +Переключение всё ещё останавливало потребление на 293–328мс; этот всплеск принимает сетевой JB. На desktop `AudioDiag: capture clock` при call=debug выводит секундную сводку исходных backend-блоков: число блоков/сэмплов, накопленную последовательность, диапазон ошибки относительно конца предыдущего блока, @@ -172,12 +174,23 @@ Backend публикует только числовой снимок в огр Счётчик sequence описывает полученные backend-сэмплы; отсутствие потерь очереди само по себе не доказывает непрерывность физического захвата. Проверять вместе с hole/overflow и route-событиями. -Дальнейший подбор минимальной очереди аудиоустройства (ещё не реализован) отделён от JB/SoundTouch: -старт с проверенного размера, редкие попытки уменьшения после стабильного участка, увеличение при -подтверждённом backend underrun, пауза обучения при смене маршрута/запуске. Размер запрашивается -аппаратными шагами; сохраняется фактически принятый размер, а не только запрос. -Ожидание полного буфера и недостача сети не являются поводом увеличить очередь устройства. -На Android регулируется AudioTrack без flush/recreate; на desktop выход принадлежит SoundManager, +На Android `CallPlaybackBuffer` подбирает рабочую очередь отдельно от JB/SoundTouch: +после 30с наблюдаемого продвижения playback head без underrun пробует уменьшить её на один +блок PCM (960 сэмплов, 20мс), до одного блока или фактического минимума устройства. +При росте AudioTrack.underrunCount добавляет один блок в пределах выделенной ёмкости. +Интервал следующей пробы удваивается (60/120/240/300с), чтобы не повторять частые неудачные уменьшения. +После старта, запроса/подтверждения маршрута и увеличения даются 2с на стабилизацию; +после уменьшения первый underrun учитывается немедленно. Отсутствие продвижения head или +пропуск наблюдений >1с обнуляет стабильный участок. Сброс системного счётчика учитывается отдельно. +Фактическая смена выхода начинает подбор с 40мс заново. Непринятая меньшая заявка фиксирует +нижнюю границу до следующего выхода; ошибки API выключают подбор с WARN/ERROR, сохраняя воспроизведение. +Reporter управляет размером под trackLock независимо от фильтра debug; playback не берёт этот lock. +Нет flush/recreate, частичная запись PCM сохраняется. После смены размера/выхода через секунду +повторно измеряется задержка для AEC. Ожидание полного буфера и недостача сети не увеличивают его размер. +Лог настройки содержит reason, previous/requested/actualFrames и прирост underruns; +достижение ёмкости предупреждает не чаще раза в 10с. Принятие и стабильность 20мс ещё требуют +проверки на Samsung; буферы HAL и Bluetooth могут добавлять собственную задержку. +На desktop адаптация устройства ещё не реализована: выход принадлежит SoundManager, изменение backend-буфера должно выполняться его владельцем, а не отдельным звонком. Диагностика Android при `call=debug`: playback сохраняет один числовой снимок на блок (20мс) diff --git a/tools/chatgui-android/app/src/test/java/com/utun/chat/data/CallPlaybackBufferTest.kt b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/CallPlaybackBufferTest.kt index dee6b6d5..aae4d2ad 100644 --- a/tools/chatgui-android/app/src/test/java/com/utun/chat/data/CallPlaybackBufferTest.kt +++ b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/CallPlaybackBufferTest.kt @@ -63,7 +63,7 @@ class CallPlaybackBufferTest { assertEquals("stable-probe", d.tick()?.reason) } - @Test fun AcceptedHardwareMinimumIsNotProbedForever() { + @Test fun acceptedHardwareMinimumIsNotProbedForever() { val d = Device(minimum = 1440) d.run(32_000) assertEquals(1440, d.frames) @@ -113,4 +113,28 @@ class CallPlaybackBufferTest { d.run(32_000) assertEquals(960, d.frames) } + + @Test fun firstUnderrunImmediatelyAfterShrinkRejectsProbe() { + val d = Device() + d.run(32_000) + assertEquals(960, d.frames) + d.underruns++ + assertEquals("underrun", d.tick()?.reason) + assertEquals(1920, d.frames) + } + + @Test fun repeatedFailuresBackOffToFiveMinutesButStillAllowRecovery() { + val d = Device() + d.run(32_000) + val delays = listOf(60_000L, 120_000L, 240_000L, 300_000L, 300_000L) + for (delay in delays) { + d.underruns++ + assertEquals("underrun", d.tick()?.reason) + assertEquals(1920, d.frames) + d.run(delay + 1_800) + assertEquals(1920, d.frames) + assertEquals("stable-probe", d.tick()?.reason) + assertEquals(960, d.frames) + } + } }