Browse Source

Опиши контракты аудиотракта и результаты проверок

master
evgeny 2 days ago
parent
commit
01bc6e4cf0
  1. 227
      doc/linux_audio_recovery.md

227
doc/linux_audio_recovery.md

@ -1,6 +1,6 @@
# Linux: диагностика и восстановление аудио # Linux: диагностика и восстановление аудио
Проверка 2026-10-01: desktop chatgui, miniaudio/PulseAudio, общий голосовой стек, Проверка 2026-10-02: desktop chatgui, miniaudio/PulseAudio, общий голосовой стек,
звонки, рация и запись сообщений. Исправления обработки PCM, владения устройствами звонки, рация и запись сообщений. Исправления обработки PCM, владения устройствами
и автоматическое восстановление реализованы. Ниже сохранены исходные результаты и автоматическое восстановление реализованы. Ниже сохранены исходные результаты
аудита; раздел «Реализация» описывает действующие контракты и проверки. аудита; раздел «Реализация» описывает действующие контракты и проверки.
@ -238,95 +238,174 @@ uasync вне аудиомьютекса. Часть существующих л
Новый binary собран в `tools/chatgui/build/vibechat`; для нового измерения нужен Новый binary собран в `tools/chatgui/build/vibechat`; для нового измерения нужен
его обычный перезапуск. Изменение файла binary не обновляет уже запущенный процесс. его обычный перезапуск. Изменение файла binary не обновляет уже запущенный процесс.
## Реализация ## Реализация (2026-10-02)
`AudioDevice` — небольшой владелец одного miniaudio device в GUI-потоке. `SoundManager` владеет общим контекстом и конечными выходами. На каждый фактический
Успешный init всегда имеет парный uninit, включая ошибку start. Callback и его выход приходится один микшер: PCM сообщений/видео, заранее декодированные сигналы,
данные готовятся до start. Close останавливает/join-ит worker перед очисткой готовый голос звонка и рации. После суммирования float PCM ограничивается, и один
callback. Прогресс — атомарный счётчик завершённых кадров, независимый от reporter. и тот же stereo буфер отправляется устройству и AEC соответствующего микрофона.
Каждый владелец проверяет свои устройства Qt-таймером 200 мс: stopped либо 1 с Звук другого выхода в референс не входит. Default/явный backend ID объединяются;
без продвижения требуют пересоздания. Suspend обновляет grace; единичный xrun PulseAudio также публикует идентификатор фактического маршрута. После server move
не запускает restart. Backoff: 250/500/1000/2000 мс, сброс после прогресса. совпавшие выходы объединяются в GUI только после подтверждения фактического
маршрута; предположение о default не используется вместо PA snapshot. При временной коллизии дополнительный выход
Capture и playback звонка теперь независимы. Период выбирает backend; аппаратный отдаёт тишину до объединения. Изменение списка источников выполняется после stop
чанк не обязан совпадать с 20-мс Opus-кадром. Рация заполняет большой output callback, с сохранением backend stream и выбранного сервером маршрута.
несколькими pulls по максимум 20 мс. AEC сохраняет весь render, включая хвосты
1200/720 samples, а capture принимает только полный кадр. При аппаратном recovery
после join сбрасываются capture accumulator, AEC и локальный jitter/stretch;
из encoded backlog сохраняется только свежий резерв. Call ID и группа сохраняются.
`SoundManager` отдельно владеет общим контекстом и устройством вывода.
`ma_engine` работает без своего устройства: mixer, звуки и PCM cursor переживают
recovery. При failed context синхронный сигнал сначала закрывает всех клиентов,
затем context освобождается. После повторного init клиенты открываются по desired
state. Stop выключает recovery; таймер завершения звонка проверяет generation.
MainWindow останавливает звонок/рацию/recorder до SoundManager и сетевого ядра.
Регистрация desktop voice ops выполняется в потоке ядра.
PulseAudio control-waits ограничены 1 с на операцию, mainloop iterates неблокирующие.
При timeout pending operation отменяется до возврата из функции, чтобы callback
не получил просроченный stack pointer. Partial init закрывает PA context/mainloop
и события miniaudio. Device worker распознаёт failed context/stream; лог содержит
состояния и PA errno. Capture hole подаётся как тишина той же длительности;
zero writable/zero converter progress возвращают управление. Duplex output заранее
полностью инициализируется, starvation не выключает playback callback.
TX ограничен восемью задачами до помещения в uasync, возраст — максимум 200 мс.
Generation исключает передачу кадров старого звонка/захвата. Ещё не обработанные
старые задачи учитываются и при повторном start, поэтому очередь ограничена
между поколениями. Task и аргументы имеют одну аллокацию; destroy очереди освобождает
их и для media, и для PTT control. Bad Opus в звонке учитывается в бюджете decode
на pull. Ошибки encode/очереди/выделения памяти/возраста различаются в сводке TX.
Устройства сохраняются как backend ID вместо индекса. При init/start failure Устройства сохраняются как backend ID вместо индекса. При init/start failure
явного выбора пробуется default, причина логируется. Выбранный ID сохраняется; явного выбора пробуется default, причина логируется. Выбранный ID сохраняется;
вернуться к нему можно повторным выбором или при следующем recovery, автоматический вернуться к нему можно повторным выбором или при следующем recovery. Автоматический
опрос hotplug не добавлен. Recorder использует pUserData своего устройства, опрос hotplug для возврата на первоначальный выбор не добавлен.
компрессор настраивается до записи, UI получает атомарный уровень и длительность
по записанным кадрам. Настройки компрессора в ходе записи применяются к следующей. `AudioDevice` — владелец одного miniaudio device в GUI. Успешный init всегда имеет
парный uninit, включая ошибку start. Callback и его состояние готовятся до start.
Close останавливает/join-ит device worker перед освобождением callback. Отдельный
атомарный счётчик кадров контролирует прогресс. Таймер 200 мс проверяет stopped
и отсутствие продвижения 1 с; suspend продлевает grace, отдельный xrun не вызывает
restart. Backoff: 250/500/1000/2000 мс, сброс после продвижения.
Miniaudio не добавляет накопитель фиксированного периода: `noFixedSizedCallback`
включён, hardware chunks могут иметь любую длину. В PulseAudio физическое начало
блока определяется по timing snapshot с AUTO_TIMING_UPDATE/INTERPOLATE_TIMING.
Capture: monotonic now минус latency read index, без повторного вычитания длины
блока. Playback: now плюс latency write index. Учитываются задержка converter и
остающийся input cache. Один backend dispatch имеет один snapshot, позиции
последующих client callbacks продвигаются по числу кадров. В отсутствие snapshot
используется оценка по callback; источник времени и physical route логируются.
PA hole отдельно помечен как отсутствующий capture, а не как реальная тишина.
`VoiceAudioIo` создаётся только для звонка и рации. Capture и RX обрабатывают
независимые потоки. Hardware capture только копирует PCM в фиксированную MPSC-очередь;
RX worker выполняет decode/SoundTouch/тоны и поддерживает до 40 мс готового голоса.
В сервисном слое рации вычисление Silero держит только TX mutex: общий mutex
отпущен, поэтому VAD не блокирует RX; lifetime и порядок PCM/PTT сохранены.
Hardware output смешивает готовый PCM. Opus, AEC, VAD, AGC и растущие буферы записи
не исполняются в аппаратных callbacks. Обработка самого backend, PCM/resampler
узлов miniaudio и атомарные уведомления остаются в callback; строгая hard realtime
гарантия не заявляется.
Capture worker — единственный владелец AEC и порядка PCM/PTT/mute. Кадр AEC —
960 отсчётов на канал; 1/2 канала микрофона и 2 независимых канала референса.
Противофазное стерео не усредняется. История рендера содержит до 1,92 с PCM и
физические timestamps. Reference выбирается по времени микрофона; небольшое
различие часов компенсируется интерполяцией только референса. Реальная тишина
сохраняется как валидный PCM; при отсутствующем reference AEC пропускает микрофон
и не адаптируется. Потеря reference, смена маршрута/поколения и скачок часов
сбрасывают фильтр. Возраст capture-очереди ограничен 100 мс от enqueue, отдельно
от собственной задержки backend. Потери и очередь видны в сводках категории `aec`.
Mute применяется после AEC/AGC: фильтр продолжает получать настоящий микрофон,
а Opus получает нули только в отмеченной части кадра. Desktop запускает core
через `call_audio_start_prepared`: повторного core AEC нет. Raw API с core AEC
сохранён для Android/headless; смешивать политики в активном звонке нельзя.
У рации один автомат передачи: manual PTT или VAD. VAD читает непрерывный очищенный
сигнал до AGC, включая время ручного PTT. Передача использует отдельный накопитель
Opus-кадра; границы PTT внутри AEC-кадра сохраняются. При закрытии серии хвост
дополняется нулями только перед Opus и помещается в очередь перед FIN. Короткий
capture tail при stop проходит без AEC, а не дополняет непрерывный фильтр. Разрыв
захвата завершает старый сетевой хвост и сбрасывает VAD, исключая склейку через
потерянные отсчёты. Capture restart завершает принятую серию; новое прослушивание
того же канала отменяет старые BEGIN/media. FIN проверяет токен своего capture/burst
и поколение экземпляра ядра, поэтому не закрывает более новую серию и не обращается
к освобождённому ядру даже при повторном использовании его адреса.
TX ограничен 8 media-задачами, возраст — 200 мс. У рации дополнительно ограничено
16 control-задач; FIN выделяется заранее вместе с BEGIN. Если control-очередь
занята, новый BEGIN откладывается до освобождения; это логируется. Очередь PCM
резервирует 8 слотов для управляющих сообщений; producer не меняет consumer и
публикует слот только после копирования. Capture callback никогда не ждёт слот.
Отказ постановки GUI-команды завершает capture; таймер запускает recovery.
`AudioRecorder` получает raw microphone без AEC. Его worker обрабатывает AGC,
накапливает запись и публикует атомарный уровень/длительность. Переполнение или
ошибка обработки отклоняют запись с логом. Meter-only использует тот же capture,
но не сохраняет PCM и не создаёт голосовое сообщение. Stop: close device, drain
и join worker, затем передача владельца записи задаче attachment. Настройки
компрессора в ходе записи применяются к следующей.
PCM воспроизводится через независимые handles. Сообщение, видео и тест динамика
не останавливают чужой PCM; sample rate принадлежит источнику. Seek пересоздаёт
узел и очищает его read/resampler cache, сохраняя handle и владельца PCM. Cursor
вычисляется по реально обработанным графом кадрам, а не по prefetched datasource.
Pause отсоединяет узел и ждёт начатое чтение графа; resume подключает тот же узел,
сохраняя его PCM и cache. Соседние проигрыватели продолжают работать.
У асинхронного открытия видео/голоса проверяется generation.
Потоковый AGC использует фиксированные кольца истории и PCM, нулевой lookahead
и непосредственный process_frame без растущего output. У записи lookahead явно
100 мс; растёт только сохраняемый результат. Push/flush/configure возвращают
ошибки; flush проверяется перед кодированием вложения.
Порядок остановки голосового направления: close capture → join capture worker →
detach output → join RX worker → release core. При failed context синхронный сигнал
сначала закрывает всех клиентов, затем освобождает context. Mixer, PCM handles,
call ID и группа переживают recovery. Stop выключает recovery; таймер завершения
звонка проверяет generation. MainWindow останавливает клиентов до SoundManager
и сетевого ядра. Сетевые операции и регистрация audio ops выполняются в uasync.
PulseAudio control-waits ограничены 1 с на операцию; при timeout operation
отменяется до возврата, исключая callback с просроченным stack pointer. Partial
init освобождает контекст/mainloop/события. Failed stream/context содержит PA
errno в логе. Zero writable/zero converter progress возвращают управление.
Последовательность нескольких операций init/uninit всё ещё может задержать GUI
дольше секунды; отдельный device owner-thread не добавлен.
## Проверки реализации ## Проверки реализации
`cmake --build tools/chatgui/build -j4` — успешно. `vibechat` и аудиотесты собираются через CMake. Набор проверки:
```sh ```sh
ctest --test-dir tools/chatgui/build --output-on-failure \ ctest --test-dir tools/chatgui/build --output-on-failure \
-R 'test_(linux_audio|audio_diagnostics|audio_recovery|call_jitter|radio_audio|radio_jitter|voice_file)$' -R 'test_(linux_audio|audio_timing|audio_diagnostics|audio_recovery|audio_event_queue|voice_audio_io|voice_tx|call_jitter|radio_audio|radio_jitter|voice_file)$'
make -C tests test_audio_compressor test_speex_aec
./tests/test_audio_compressor
./tests/test_speex_aec
``` ```
7/7 успешно. Проверены целостность MPSC, bounded overflow/restart reporter, Добавлены отдельные проверки MPSC (80000 сообщений, reserve/control, слот ещё не
duplex starvation, PA zero-progress, capture hole, deadline/cancel, сохранение опубликован), точных PCM/PTT/mute границ, короткого хвоста, независимого RX при
AEC render, bad Opus budget, partial start/init, две записи одновременно, задержке capture worker, противофазного stereo AEC, старого физического timestamp
восстановление только playback, потеря общего context и повторные отказы init, при свежем enqueue, совпадения реальных Opus-пакетов с правильно дополненным
stop во время backoff, старый end timer, сохранение PCM cursor, selected→default, хвостом, rapid PTT, capture restart, отмены старого прослушивания и bounded control.
TX limit между поколениями и освобождение pending PTT при destroy. Проверяются manual/VAD handover
без второй серии и продвижение настоящего сервисного RX при заблокированном VAD.
Изолированная Debug-сборка в `/tmp/utun-audio-asan`: четыре теста Timing test проверяет read-index timestamp, несколько converter callbacks одного
`audio_recovery/linux_audio/call_jitter/radio_audio` проходят с ASAN/UBSAN и блока и метку hole. Recorder тестирует raw PCM и отсутствие вложения в meter-only.
LeakSanitizer. Запуск вне песочницы нужен LeakSanitizer для проверки потоков.
Speex проверяет подавление эха и сохранение double-talk также для двух микрофонов
Реальный перезапуск сервера: с двумя независимыми каналами reference. В детерминированном синтетическом тесте
остаточная энергия эха — около 0,009 исходной; корреляция near-end — около 0,95.
Эти величины относятся только к тесту, а не к реальным акустическим условиям.
В `/tmp/utun-audio-asan` проверяются пять тестов нового тракта с ASAN/UBSAN и
LeakSanitizer; LeakSanitizer запускается вне песочницы. MPSC отдельно проверяется
ThreadSanitizer. Изолированный интеграционный запуск:
```sh ```sh
python3 tools/chatgui/tests/run_pulse_recovery.py python3 tools/chatgui/tests/run_pulse_recovery.py
``` ```
Тест создаёт собственные PipeWire, PulseAudio и WirePlumber в `/tmp`, отключает Итоговый прогон 2026-10-02: сборка `vibechat` успешна, CTest — 11/11,
аппаратные monitors, использует виртуальный sink/monitor source. После остановки ASAN/UBSAN/LeakSanitizer — 5/5, компрессор — 479/479, Speex — PASS,
PulseAudio проверяет освобождение контекста; после запуска — новый прогресс MPSC под ThreadSanitizer — PASS. Timing-стенд прошёл ещё 100 последовательных
capture и прежнего PCM sound. Проверка прошла: capture 325→575 мс, playback повторов. Пауза PCM проверяется также серией pause/resume при работающем соседнем
16384→29184 frames. Системный звуковой сервер не перезапускается. проигрывателе; она ждёт чтение узла, а не только меняет его флаг.
Полный `check.sh` до исключения async-тестов: 119 passed, 0 failed, 1 skipped; Тест создаёт собственные PipeWire/PulseAudio/WirePlumber в `/tmp`, отключает
proxy/burst/load интеграции прошли. По последующему указанию пользователя async аппаратные monitors и использует два виртуальных sink. Проверяет объединение
тесты исключены из дальнейших запусков. `./gradlew --offline assembleDebug` — voice с общим выходом после `move-sink-input`, сохранение desired выхода звонка
успешно, APK не установлен на телефон. при recovery общего выхода (включая server stream-restore), затем восстановление capture/PCM
после остановки и запуска своего PulseAudio. Звонок и рация сохраняют идентичность;
Физический USB/Bluetooth hotplug, сон ОС и качество длительного реального звонка recorder и медиа продолжают работу. Системный аудиосервер не затрагивается.
этими проверками не подтверждены. AEC reference отдельных уведомлений и компенсация
дрейфа часов звонка остаются ограничениями исходного DSP. Граница 1 с относится Исторические проверки исходного recovery: полный `check.sh` — 119 passed,
к отдельной PA-операции: последовательность init/uninit нескольких устройств 0 failed, 1 skipped; proxy/burst/load интеграции прошли. По указанию пользователя
может задержать GUI дольше. Дополнительный owner-thread/DSP-worker не добавлен. async-тесты исключены из дальнейших запусков. APK ранее собран offline; установка
на телефон не выполнялась.
Физический USB/Bluetooth hotplug, сон ОС, внешние звуки других приложений и качество
AEC в длительном реальном звонке этими проверками не подтверждены. Reference
охватывает звук этого приложения на том же выходе, согласно выбранному контракту.
## Референсы ## Референсы

Loading…
Cancel
Save