|
|
|
|
@ -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 не обновляет уже запущенный процесс. |
|
|
|
|
|
|
|
|
|
## Реализация |
|
|
|
|
|
|
|
|
|
`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. |
|
|
|
|
## Реализация (2026-10-02) |
|
|
|
|
|
|
|
|
|
`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. |
|
|
|
|
|
|
|
|
|
Изолированная Debug-сборка в `/tmp/utun-audio-asan`: четыре теста |
|
|
|
|
`audio_recovery/linux_audio/call_jitter/radio_audio` проходят с ASAN/UBSAN и |
|
|
|
|
LeakSanitizer. Запуск вне песочницы нужен LeakSanitizer для проверки потоков. |
|
|
|
|
|
|
|
|
|
Реальный перезапуск сервера: |
|
|
|
|
Добавлены отдельные проверки 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. |
|
|
|
|
|
|
|
|
|
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. Системный звуковой сервер не перезапускается. |
|
|
|
|
|
|
|
|
|
Полный `check.sh` до исключения async-тестов: 119 passed, 0 failed, 1 skipped; |
|
|
|
|
proxy/burst/load интеграции прошли. По последующему указанию пользователя async |
|
|
|
|
тесты исключены из дальнейших запусков. `./gradlew --offline assembleDebug` — |
|
|
|
|
успешно, APK не установлен на телефон. |
|
|
|
|
|
|
|
|
|
Физический USB/Bluetooth hotplug, сон ОС и качество длительного реального звонка |
|
|
|
|
этими проверками не подтверждены. AEC reference отдельных уведомлений и компенсация |
|
|
|
|
дрейфа часов звонка остаются ограничениями исходного DSP. Граница 1 с относится |
|
|
|
|
к отдельной PA-операции: последовательность init/uninit нескольких устройств |
|
|
|
|
может задержать GUI дольше. Дополнительный owner-thread/DSP-worker не добавлен. |
|
|
|
|
Итоговый прогон 2026-10-02: сборка `vibechat` успешна, CTest — 11/11, |
|
|
|
|
ASAN/UBSAN/LeakSanitizer — 5/5, компрессор — 479/479, Speex — PASS, |
|
|
|
|
MPSC под ThreadSanitizer — PASS. Timing-стенд прошёл ещё 100 последовательных |
|
|
|
|
повторов. Пауза PCM проверяется также серией pause/resume при работающем соседнем |
|
|
|
|
проигрывателе; она ждёт чтение узла, а не только меняет его флаг. |
|
|
|
|
|
|
|
|
|
Тест создаёт собственные PipeWire/PulseAudio/WirePlumber в `/tmp`, отключает |
|
|
|
|
аппаратные monitors и использует два виртуальных sink. Проверяет объединение |
|
|
|
|
voice с общим выходом после `move-sink-input`, сохранение desired выхода звонка |
|
|
|
|
при recovery общего выхода (включая server stream-restore), затем восстановление capture/PCM |
|
|
|
|
после остановки и запуска своего PulseAudio. Звонок и рация сохраняют идентичность; |
|
|
|
|
recorder и медиа продолжают работу. Системный аудиосервер не затрагивается. |
|
|
|
|
|
|
|
|
|
Исторические проверки исходного recovery: полный `check.sh` — 119 passed, |
|
|
|
|
0 failed, 1 skipped; proxy/burst/load интеграции прошли. По указанию пользователя |
|
|
|
|
async-тесты исключены из дальнейших запусков. APK ранее собран offline; установка |
|
|
|
|
на телефон не выполнялась. |
|
|
|
|
|
|
|
|
|
Физический USB/Bluetooth hotplug, сон ОС, внешние звуки других приложений и качество |
|
|
|
|
AEC в длительном реальном звонке этими проверками не подтверждены. Reference |
|
|
|
|
охватывает звук этого приложения на том же выходе, согласно выбранному контракту. |
|
|
|
|
|
|
|
|
|
## Референсы |
|
|
|
|
|
|
|
|
|
|