Опиши контракты аудиотракта и результаты проверок
This commit is contained in:
1 parent
bd082a26bf
commit
01bc6e4cf0
1 file changed
+148
-69
+148
-69
@@ -1,6 +1,6 @@
|
||||
# Linux: диагностика и восстановление аудио
|
||||
|
||||
Проверка 2026-10-01: desktop chatgui, miniaudio/PulseAudio, общий голосовой стек,
|
||||
Проверка 2026-10-02: desktop chatgui, miniaudio/PulseAudio, общий голосовой стек,
|
||||
звонки, рация и запись сообщений. Исправления обработки PCM, владения устройствами
|
||||
и автоматическое восстановление реализованы. Ниже сохранены исходные результаты
|
||||
аудита; раздел «Реализация» описывает действующие контракты и проверки.
|
||||
@@ -238,95 +238,174 @@ uasync вне аудиомьютекса. Часть существующих л
|
||||
Новый binary собран в `tools/chatgui/build/vibechat`; для нового измерения нужен
|
||||
его обычный перезапуск. Изменение файла binary не обновляет уже запущенный процесс.
|
||||
|
||||
## Реализация
|
||||
## Реализация (2026-10-02)
|
||||
|
||||
`AudioDevice` — небольшой владелец одного miniaudio device в GUI-потоке.
|
||||
Успешный init всегда имеет парный uninit, включая ошибку start. Callback и его
|
||||
данные готовятся до start. Close останавливает/join-ит worker перед очисткой
|
||||
callback. Прогресс — атомарный счётчик завершённых кадров, независимый от reporter.
|
||||
Каждый владелец проверяет свои устройства Qt-таймером 200 мс: stopped либо 1 с
|
||||
без продвижения требуют пересоздания. Suspend обновляет grace; единичный xrun
|
||||
не запускает restart. Backoff: 250/500/1000/2000 мс, сброс после прогресса.
|
||||
|
||||
Capture и playback звонка теперь независимы. Период выбирает backend; аппаратный
|
||||
чанк не обязан совпадать с 20-мс Opus-кадром. Рация заполняет большой output
|
||||
несколькими 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.
|
||||
`SoundManager` владеет общим контекстом и конечными выходами. На каждый фактический
|
||||
выход приходится один микшер: PCM сообщений/видео, заранее декодированные сигналы,
|
||||
готовый голос звонка и рации. После суммирования float PCM ограничивается, и один
|
||||
и тот же stereo буфер отправляется устройству и AEC соответствующего микрофона.
|
||||
Звук другого выхода в референс не входит. Default/явный backend ID объединяются;
|
||||
PulseAudio также публикует идентификатор фактического маршрута. После server move
|
||||
совпавшие выходы объединяются в GUI только после подтверждения фактического
|
||||
маршрута; предположение о default не используется вместо PA snapshot. При временной коллизии дополнительный выход
|
||||
отдаёт тишину до объединения. Изменение списка источников выполняется после stop
|
||||
callback, с сохранением backend stream и выбранного сервером маршрута.
|
||||
|
||||
Устройства сохраняются как backend ID вместо индекса. При init/start failure
|
||||
явного выбора пробуется default, причина логируется. Выбранный ID сохраняется;
|
||||
вернуться к нему можно повторным выбором или при следующем recovery, автоматический
|
||||
опрос hotplug не добавлен. Recorder использует pUserData своего устройства,
|
||||
компрессор настраивается до записи, UI получает атомарный уровень и длительность
|
||||
по записанным кадрам. Настройки компрессора в ходе записи применяются к следующей.
|
||||
вернуться к нему можно повторным выбором или при следующем recovery. Автоматический
|
||||
опрос hotplug для возврата на первоначальный выбор не добавлен.
|
||||
|
||||
`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
|
||||
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,
|
||||
duplex starvation, PA zero-progress, capture hole, deadline/cancel, сохранение
|
||||
AEC render, bad Opus budget, partial start/init, две записи одновременно,
|
||||
восстановление только playback, потеря общего context и повторные отказы init,
|
||||
stop во время backoff, старый end timer, сохранение PCM cursor, selected→default,
|
||||
TX limit между поколениями и освобождение pending PTT при destroy.
|
||||
Добавлены отдельные проверки MPSC (80000 сообщений, reserve/control, слот ещё не
|
||||
опубликован), точных PCM/PTT/mute границ, короткого хвоста, независимого RX при
|
||||
задержке capture worker, противофазного stereo AEC, старого физического timestamp
|
||||
при свежем enqueue, совпадения реальных Opus-пакетов с правильно дополненным
|
||||
хвостом, rapid PTT, capture restart, отмены старого прослушивания и bounded control.
|
||||
Проверяются manual/VAD handover
|
||||
без второй серии и продвижение настоящего сервисного RX при заблокированном VAD.
|
||||
Timing test проверяет read-index timestamp, несколько converter callbacks одного
|
||||
блока и метку hole. Recorder тестирует raw PCM и отсутствие вложения в meter-only.
|
||||
|
||||
Изолированная Debug-сборка в `/tmp/utun-audio-asan`: четыре теста
|
||||
`audio_recovery/linux_audio/call_jitter/radio_audio` проходят с ASAN/UBSAN и
|
||||
LeakSanitizer. Запуск вне песочницы нужен LeakSanitizer для проверки потоков.
|
||||
Speex проверяет подавление эха и сохранение double-talk также для двух микрофонов
|
||||
с двумя независимыми каналами reference. В детерминированном синтетическом тесте
|
||||
остаточная энергия эха — около 0,009 исходной; корреляция near-end — около 0,95.
|
||||
Эти величины относятся только к тесту, а не к реальным акустическим условиям.
|
||||
|
||||
Реальный перезапуск сервера:
|
||||
В `/tmp/utun-audio-asan` проверяются пять тестов нового тракта с ASAN/UBSAN и
|
||||
LeakSanitizer; LeakSanitizer запускается вне песочницы. MPSC отдельно проверяется
|
||||
ThreadSanitizer. Изолированный интеграционный запуск:
|
||||
|
||||
```sh
|
||||
python3 tools/chatgui/tests/run_pulse_recovery.py
|
||||
```
|
||||
|
||||
Тест создаёт собственные PipeWire, PulseAudio и WirePlumber в `/tmp`, отключает
|
||||
аппаратные monitors, использует виртуальный sink/monitor source. После остановки
|
||||
PulseAudio проверяет освобождение контекста; после запуска — новый прогресс
|
||||
capture и прежнего PCM sound. Проверка прошла: capture 325→575 мс, playback
|
||||
16384→29184 frames. Системный звуковой сервер не перезапускается.
|
||||
Итоговый прогон 2026-10-02: сборка `vibechat` успешна, CTest — 11/11,
|
||||
ASAN/UBSAN/LeakSanitizer — 5/5, компрессор — 479/479, Speex — PASS,
|
||||
MPSC под ThreadSanitizer — PASS. Timing-стенд прошёл ещё 100 последовательных
|
||||
повторов. Пауза PCM проверяется также серией pause/resume при работающем соседнем
|
||||
проигрывателе; она ждёт чтение узла, а не только меняет его флаг.
|
||||
|
||||
Полный `check.sh` до исключения async-тестов: 119 passed, 0 failed, 1 skipped;
|
||||
proxy/burst/load интеграции прошли. По последующему указанию пользователя async
|
||||
тесты исключены из дальнейших запусков. `./gradlew --offline assembleDebug` —
|
||||
успешно, APK не установлен на телефон.
|
||||
Тест создаёт собственные PipeWire/PulseAudio/WirePlumber в `/tmp`, отключает
|
||||
аппаратные monitors и использует два виртуальных sink. Проверяет объединение
|
||||
voice с общим выходом после `move-sink-input`, сохранение desired выхода звонка
|
||||
при recovery общего выхода (включая server stream-restore), затем восстановление capture/PCM
|
||||
после остановки и запуска своего PulseAudio. Звонок и рация сохраняют идентичность;
|
||||
recorder и медиа продолжают работу. Системный аудиосервер не затрагивается.
|
||||
|
||||
Физический USB/Bluetooth hotplug, сон ОС и качество длительного реального звонка
|
||||
этими проверками не подтверждены. AEC reference отдельных уведомлений и компенсация
|
||||
дрейфа часов звонка остаются ограничениями исходного DSP. Граница 1 с относится
|
||||
к отдельной PA-операции: последовательность init/uninit нескольких устройств
|
||||
может задержать GUI дольше. Дополнительный owner-thread/DSP-worker не добавлен.
|
||||
Исторические проверки исходного recovery: полный `check.sh` — 119 passed,
|
||||
0 failed, 1 skipped; proxy/burst/load интеграции прошли. По указанию пользователя
|
||||
async-тесты исключены из дальнейших запусков. APK ранее собран offline; установка
|
||||
на телефон не выполнялась.
|
||||
|
||||
Физический USB/Bluetooth hotplug, сон ОС, внешние звуки других приложений и качество
|
||||
AEC в длительном реальном звонке этими проверками не подтверждены. Reference
|
||||
охватывает звук этого приложения на том же выходе, согласно выбранному контракту.
|
||||
|
||||
## Референсы
|
||||
|
||||
|
||||
Reference in new issue
Block a user