434 lines
45 KiB
Markdown
434 lines
45 KiB
Markdown
# Linux: диагностика и восстановление аудио
|
||
|
||
Проверка 2026-10-02: desktop chatgui, miniaudio/PulseAudio, общий голосовой стек,
|
||
звонки, рация и запись сообщений. Исправления обработки PCM, владения устройствами
|
||
и автоматическое восстановление реализованы. Ниже сохранены исходные результаты
|
||
аудита; раздел «Реализация» описывает действующие контракты и проверки.
|
||
|
||
## Вывод
|
||
|
||
До исправлений тракт был некорректен при сбоях. Самое существенное: воспроизведение
|
||
звонка зависит от наличия микрофонных кадров в duplex-ring. Его опустошение
|
||
останавливает вызов пользовательского callback, оставляя хвост выходного буфера
|
||
незаполненным. Восстановление jitter-буфера не исправляет остановившееся устройство.
|
||
Отдельного управляющего механизма восстановления устройств не было.
|
||
|
||
В реализации восстановлены три гарантии: каждый запрос output полностью заполнен; обработка
|
||
одного callback всегда ограничена; остановка/пересоздание устройства выполняются
|
||
вне аудиопотока и не уничтожают сетевую сессию звонка.
|
||
|
||
## Что установлено по работающим клиентам
|
||
|
||
В локальном старом логе звонка `132e4a9698f47bdf` с Samsung SM_A525F наблюдались
|
||
паузы callback примерно 0,5–2,6 секунды при зарегистрированной обработке 0–2 мс.
|
||
Входящий буфер desktop достигал 15 секунд, росли потери, включалось ускорение 160%.
|
||
На Samsung захват продолжал давать около 50 кадров/с, playback регистрировал
|
||
underrun и тоны разрыва. RTT порядка 1–4 мс не объясняет эти паузы сам по себе.
|
||
|
||
Это признаки проблемы обслуживания локального аудио, но ещё не доказательство
|
||
конкретной причины зависания PulseAudio. Старое измерение исключало время
|
||
синхронных DEBUG-вызовов между концом обработки и обновлением timestamp.
|
||
Новая диагностика охватывает весь callback, включая эти логи.
|
||
Работающий GUI не перезапускался, поэтому новый инструмент ещё не записал этот
|
||
сбой на реальном звонке. Лог Samsung сохранён в `/tmp/sm-audio-current.log`.
|
||
|
||
## Найденные дефекты и ограничения до исправлений
|
||
|
||
| Место | Поведение и последствия | Необходимая доработка |
|
||
| --- | --- | --- |
|
||
| `lib/miniaudio.h`, `ma_device__handle_duplex_callback_playback()` | При пустом capture-ring `acquire_read` возвращает success с нулём кадров. Выполняется `break`, output остаётся частично незаполненным, call callback не вызывается. PulseAudio затем отправляет весь mapped buffer. | Обеспечить полный output и продолжение playback при отсутствии capture. Нельзя делать commit_read для синтетических кадров. |
|
||
| `ma_device_on_write__pulse()` | Успешная запись с нулём обработанных кадров не уменьшает остаток цикла. Возможно бесконечное вращение с блокированием того же PA mainloop, включая capture. | Немедленно выходить при отсутствии продвижения, возвращать управление mainloop. Логировать состояние и запрос, не пытаться записывать до успеха в этом callback. |
|
||
| `ma_device_on_read__pulse()` | PA hole отбрасывается без подачи соответствующего количества нулевых кадров. Из capture timeline исчезает интервал, duplex-ring может опустеть. | Сохранять длительность hole тишиной либо явно обозначать discontinuity независимому capture-тракту. |
|
||
| `lib/speex_aec.c`, `speex_aec_feed_playback()` | Обрабатывает только остаток одного 960-sample кадра, отбрасывая хвост входного блока. Контракт разрешает произвольный count. | Циклом потреблять весь count, складывать все полные кадры, сохранять остаток. |
|
||
| `CallAudioEngine::setPlaybackDeviceIndex()` | После неудачного пересоздания `m_active` остаётся true при отсутствии устройства; `start` того же звонка возвращает успех. | Разделить желание продолжать звонок и работоспособность устройства. Ошибка запуска переводит устройство в recovering. |
|
||
| `RadioAudioEngine::teardownDevice()` | Если playback init успешен, а start нет, uninit пропускается: он зависит от флага started. Объект удаляется вместе с незавершёнными ресурсами. | Разделить initialized и started; каждый успешный init обязательно завершать uninit. |
|
||
| `MainWindow::~MainWindow()` / `SoundManager::shutdown()` | Общий context освобождается вместе с engine, но звонок и рация здесь явно не останавливаются. Их singleton-объекты могут оставлять живые устройства. `m_context` также не обнуляется. | Сначала остановить все зависимые устройства и дождаться callbacks, затем освобождать общий context. |
|
||
| `AudioRecorder` | Callback выбирает глобальный `g_recorder`, игнорируя `pUserData`. Второй recorder, в том числе тест микрофона, заменяет получателя. Флаги/уровень/вектор waveform используются из GUI и аудиопотока без согласованной синхронизации. | Использовать `pUserData`; передавать уровни атомарно, waveform — безопасным snapshot; буфер записи должен иметь однозначного владельца. |
|
||
|
||
До исправлений первые два дефекта воспроизводились стендом без аппаратуры:
|
||
duplex оставлял sentinel в output при starvation, а PA-write без продвижения
|
||
требовал внешней остановки. AEC из 1200 + 720 samples сохранял один кадр вместо
|
||
двух. Теперь эти проверки требуют полного output, самостоятельного возврата
|
||
из callback и двух сохранённых AEC-кадров; результаты приведены ниже.
|
||
|
||
Capture-AEC также обрабатывает не более одного кадра за вызов. В звонке его
|
||
вызывают ровно с 960 samples, поэтому текущий call feed этот случай не нарушает.
|
||
При расширении API нельзя объявлять произвольный count без согласованного размера
|
||
выходного буфера. `speex_aec_reset()` уже существует: новый API для сброса самого
|
||
AEC не нужен, но нужен согласованный сброс всего локального voice-тракта.
|
||
|
||
## Чистота реализации
|
||
|
||
Этот раздел фиксирует исходный аудит; изменения владения и ограничение работы
|
||
callback описаны в разделе «Реализация».
|
||
|
||
Нормальная структура уже есть: GUI владеет устройствами, сервисный слой кодирует
|
||
Opus и обрабатывает jitter, сетевой поток доставляет пакеты. Нет необходимости
|
||
переписывать весь стек или заводить новый backend.
|
||
|
||
Однако границы владения не завершены: context фактически принадлежит engine
|
||
звуковых уведомлений, а используется независимыми устройствами звонка/рации.
|
||
Флаги active/started/initialized местами смешивают три разных состояния.
|
||
Выбор input применяется к recorder, звонок использует default input, рация —
|
||
default устройства. Output звонка и SoundManager тоже выбираются отдельно.
|
||
Индекс перечисления не является устойчивой идентичностью устройства после hotplug.
|
||
В radio capture fallback `useDefault` фактически повторяет ту же попытку:
|
||
`pDeviceID` и так NULL в обеих ветках. Callbacks рации запускаются раньше
|
||
подготовки capture-буфера и установки VAD; порядок запуска нужно привести к
|
||
«сначала состояние и буферы, затем устройства», чтобы исключить гонки при старте.
|
||
|
||
Callback не является строго realtime-safe. Capture удерживает `g_mtx` во время
|
||
AEC/AGC/Opus; отправка выделяет память и вызывает `uasync_post`. Playback вызывает
|
||
SoundTouch и код с возможным расширением контейнеров, mutex и синхронными логами.
|
||
Приём пакетов тоже может держать mutex при работе с jitter и логировании.
|
||
Плохие Opus-кадры могут последовательно выбираться из большой очереди в одном
|
||
pull: нужен предел работы на callback. Новая диагностика покажет, где это
|
||
действительно нарушает deadline. Перенос всего DSP в ещё один worker заранее
|
||
увеличит число очередей и точек синхронизации без установленной необходимости.
|
||
|
||
AEC получает reference только звонка. Звуки отдельного SoundManager в него не
|
||
попадают; delay по умолчанию фиксирован, не вычисляется по фактической задержке
|
||
устройств. Это ограничение качества эхоподавления, а не установленная причина
|
||
текущего зависания.
|
||
|
||
Jitter умеет повторно накопить резерв после сетевого underrun, но диапазон tempo
|
||
ограничен 1,0–1,6. Ускорение убирает избыток, а замедления для компенсации более
|
||
медленных часов отправителя нет. При устойчивом отрицательном дрейфе резерв будет
|
||
снова опустошаться. Это отдельная задача длительных звонков: после восстановления
|
||
устройств проверить наклон глубины буфера и при необходимости добавить небольшую
|
||
компенсацию дрейфа в обе стороны, не смешивая её с быстрым catch-up после паузы.
|
||
|
||
Принудительные 480 frames задают период 10 мс при 48 кГц. Собственный PA default
|
||
miniaudio — 25 мс с комментарием о проблемах PipeWire при периодах меньше ~20 мс.
|
||
Период backend и Opus-кадр 20 мс должны оставаться независимыми. Увеличивать
|
||
период до исправления AEC нельзя: 1200-sample блоки уже воспроизводят потерю хвоста.
|
||
После исправления разумно сначала проверить default backend и измерить задержку.
|
||
|
||
## Предлагаемое восстановление без большого усложнения
|
||
|
||
### 1. Сначала восстановить инварианты обработки и владения
|
||
|
||
Исправить полный output, zero-progress, AEC accumulation и uninit при неудачном
|
||
start. Перед освобождением context явно остановить recorder, звонок, рацию и
|
||
прочие его устройства. До остановки отменять recovery timer; после join можно
|
||
сбрасывать voice-ресурсы. Проверять возвраты feed/start/init и логировать причину.
|
||
|
||
Самый небольшой вариант — оставить один duplex-device: при нехватке capture
|
||
подать в call callback нулевой input, полностью сформировать playback и не
|
||
продвигать реальный capture-ring за синтетические samples. Это чинит непосредственный
|
||
дефект, но сохраняет общий жизненный цикл input/output и синтетический TX по
|
||
playback-clock.
|
||
|
||
Для устойчивости к исчезновению микрофона предпочтительнее два устройства
|
||
capture/playback внутри существующего `CallAudioEngine`, как уже сделано в рации.
|
||
Capture только накапливает 960 samples и вызывает feed; playback независимо
|
||
вызывает pull и всегда заполняет output. Это два handle с одним владельцем,
|
||
без нового DSP-worker или дополнительной PCM-очереди. Потребуется уточнить
|
||
потоковый контракт voice-слоя: один capture-writer и один playback-writer,
|
||
согласованный доступ к AEC, остановка обоих до release. Текущий `g_mtx` уже
|
||
защищает AEC, но остальные общие поля нужно проверить перед разделением.
|
||
|
||
### 2. Разделить краткий underrun и неисправность устройства
|
||
|
||
| Событие | Действие |
|
||
| --- | --- |
|
||
| Нет сетевого PCM, callback продолжает приходить | Тишина/существующий gap tone; jitter автоматически накапливает данные. Устройство не пересоздавать. |
|
||
| Один backend underrun, новые кадры принимаются | Подать следующий полностью заполненный буфер; собрать счётчик. Не сбрасывать AEC и не запускать restart на каждый xrun. |
|
||
| Hole/переполнение capture | Учесть потерянную длительность; краткую потерю пережить, длительную отметить discontinuity для AEC. |
|
||
| Поток failed/stopped или нет продвижения кадров длительное время | Перейти в recovering и пересоздать затронутое устройство вне callback. |
|
||
| Suspend/resume ОС | Учитывать suspend как отдельную причину, восстановление проверять после resume; не запускать частый restart-loop во время сна. |
|
||
| Потерян общий backend/context | Однократно остановить всех его пользователей, восстановить владельца, затем требуемые устройства. |
|
||
|
||
Один owner timer и состояния `Stopped / Running / Recovering` достаточны.
|
||
`wanted` хранит желание пользователя продолжать; generation меняется при stop,
|
||
новом звонке и смене устройства. Каждая отложенная попытка проверяет generation
|
||
и wanted — законченный звонок никогда не возрождается.
|
||
|
||
Callbacks только обновляют атомарные счётчики успешно обработанных кадров и
|
||
признак backend failure. Watchdog проверяет фактический прогресс отдельно по
|
||
направлениям. Тишина в PCM не означает остановку устройства. Reporter диагностической
|
||
очереди не должен становиться watchdog: его события могут теряться.
|
||
|
||
Начальные значения для проверки: timer 200 мс, подозрение при отсутствии прогресса
|
||
500 мс после startup grace; повторные попытки через 250/500/1000 мс с потолком
|
||
2 секунды. Это предлагаемые настройки, не доказанные универсальные пределы.
|
||
Suspend и фактический согласованный backend period должны учитываться отдельно.
|
||
Нельзя обещать bounded recovery, пока init/stop/uninit сами могут зависнуть:
|
||
сначала устранить spin-loop и проверить время backend-операций. Если они реально
|
||
блокируют GUI, вынести только управление устройствами в один owner-thread,
|
||
сохранив тот же автомат состояний.
|
||
|
||
На пересоздании сбросить capture-accumulator, AEC delay/filter и локальный PCM/
|
||
stretch, сохранив call ID и сетевую сессию. После длительной аппаратной паузы
|
||
устаревшую речь нужно явно отбросить до актуального резерва jitter, иначе
|
||
15 секунд накопленного звука при скорости 160% разгружаются примерно 25 секунд.
|
||
Этот сброс применять только при аппаратной discontinuity; обычный сетевой jitter
|
||
не должен автоматически приводить к отбрасыванию очереди.
|
||
|
||
Устройство хранить по backend ID либо как default, не по индексу. При его
|
||
исчезновении попробовать default и явно сообщить о смене; выбранное устройство
|
||
можно вернуть при появлении. Нужны ошибки с ID/именем/направлением и результатом
|
||
попытки. Для PA-ошибок следующий уровень диагностики — stream/context state и
|
||
`pa_context_errno`, потому что один `-1` от API не объясняет причину.
|
||
|
||
### 3. Проверить сценарии, а не только успешный старт
|
||
|
||
После реализации добавить проверки: capture starvation 20/500 мс и возврат;
|
||
hole; AEC блоки, пересекающие границу кадра; zero writable; ошибка записи;
|
||
исчезновение выбранного устройства и появление default; suspend/resume;
|
||
перезапуск звукового сервера; stop во время backoff; закрытие GUI при активном
|
||
звонке/рации. Проверять полный output, bounded очереди и работу callback,
|
||
возобновление прогресса, отсутствие callbacks после release и отсутствие
|
||
повторного старта после пользовательского stop.
|
||
|
||
## Как читать добавленную диагностику
|
||
|
||
В GUI-категориях включить `call=debug` либо задать отдельную строку в INI:
|
||
|
||
```ini
|
||
[debug]
|
||
call=debug
|
||
```
|
||
|
||
Обычные сводки идут на DEBUG, аномалии — WARN/ERROR; miniaudio info перенесён
|
||
на DEBUG, подробный backend log — TRACE. Все устройства используют категорию
|
||
call, включая recorder, radio и sound-engine; role и ID различают их в строках.
|
||
Не включать глобальный trace для всего проекта при измерении аудио.
|
||
|
||
Reporter раз в секунду печатает stage, tid, callbacks/s, requested frames/s,
|
||
максимальный интервал между входами, полное wall-time обработки и thread CPU-time,
|
||
незавершённый вход и время с последнего входа. `work_max` и `cpu_max` — отдельные
|
||
максимумы за окно, они не обязательно относятся к одному вызову.
|
||
|
||
| Stage | Что измеряется |
|
||
| --- | --- |
|
||
| loop | Один `pa_mainloop_iterate`, включая ожидание и вложенные callbacks. Долгое время само по себе не доказывает тяжёлый DSP. |
|
||
| read / write | Полный backend read/write callback. Frames/s здесь — запрошенные, не гарантированно обработанные кадры. |
|
||
| map | `writable_size` и `begin_write`; пустой/ошибочный выход тоже закрывает измерение. |
|
||
| submit | Вызов `pa_stream_write`. |
|
||
| duplex | Перенос capture-ring в duplex-output; detail содержит глубину capture-ring. |
|
||
| callback | Полный miniaudio data callback, включая код приложения и его синхронные логи. |
|
||
| feed / pull | Capture DSP/encode/send и playback jitter/stretch/tone в звонке. |
|
||
|
||
`unfilled_frames` — незаполненный output, `hole_frames` — потеря capture timeline,
|
||
`capture_drop_frames` — переполнение capture-ring, `no_progress` — PA-write без
|
||
продвижения. `backend_underruns` — отдельный callback самого PA-stream; это не
|
||
то же самое, что пустой сетевой jitter. `backend playback started/resumed`
|
||
показывает фактическое начало/возобновление playback. Notification содержит
|
||
числовые miniaudio type/state.
|
||
|
||
Первое событие каждого типа за окно имеет tid и CLOCK_MONOTONIC timestamp;
|
||
повторения суммируются. Для задержавшихся потоков reporter читает `/proc` wchan
|
||
и накопленные schedstat CPU/ready-wait/slices. Сравнивать соседние snapshots:
|
||
сами totals не являются длительностью текущей паузы. CPU-time намного меньше
|
||
wall-time означает ожидание/вытеснение; wchan помогает отличить poll от mutex/
|
||
I/O wait. Это снимок в момент отчёта, не исторический стек зависшего вызова.
|
||
|
||
Producer не выделяет память, не ждёт mutex reporter и не пишет диагностические
|
||
логи: ограниченная MPSC-очередь 8192 события, не более четырёх CAS-попыток.
|
||
Reporter не разыменовывает ma_device/pa_stream. Переполнение логируется как
|
||
`diagnostic events lost`; такое окно нельзя считать полным. Инструментация
|
||
добавляет получение часов/TID и атомарные операции, поэтому overhead ненулевой.
|
||
Частые события call jitter/AEC/TX теперь публикуются счётчиками и выводятся из
|
||
uasync вне аудиомьютекса. Часть существующих логов рации остаётся синхронной;
|
||
тракт не объявляется строго realtime-safe.
|
||
После close устройство удаляется из reporter: для устройств короче секунды
|
||
доступны lifecycle и первые аномалии, но не полная секундная сводка.
|
||
|
||
Новый binary собран в `tools/chatgui/build/vibechat`; для нового измерения нужен
|
||
его обычный перезапуск. Изменение файла binary не обновляет уже запущенный процесс.
|
||
|
||
## Реализация (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 для возврата на первоначальный выбор не добавлен.
|
||
|
||
`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 не добавлен.
|
||
|
||
## Проверки реализации
|
||
|
||
`vibechat` и аудиотесты собираются через CMake. Набор проверки:
|
||
|
||
```sh
|
||
ctest --test-dir tools/chatgui/build --output-on-failure \
|
||
-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
|
||
```
|
||
|
||
Добавлены отдельные проверки 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
|
||
```
|
||
|
||
Итоговый прогон 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
|
||
охватывает звук этого приложения на том же выходе, согласно выбранному контракту.
|
||
|
||
## Референсы
|
||
|
||
Первичные исходники и документация просмотрены 2026-10-01; ссылки на master могут
|
||
изменяться. Используем принципы, а не копируем целиком архитектуру библиотеки.
|
||
|
||
- [PipeWire audio-src-ring.c](https://docs.pipewire.org/audio-src-ring_8c-example.html):
|
||
callback читает доступные кадры, заполняет остаток тишиной и возвращает полный
|
||
буфер; пополнение инициирует через событие управляющего потока. Хороший минимальный
|
||
пример поведения при пустой PCM-очереди.
|
||
- [PulseAudio stream API](https://github.com/pulseaudio/pulseaudio/blob/master/src/pulse/stream.h):
|
||
отдельные underflow/overflow/started/suspended/moved callbacks; peek различает
|
||
пустую очередь и hole с ненулевой длиной. Нужно разделять эти состояния в логах.
|
||
- [PulseAudio pacat.c](https://github.com/pulseaudio/pulseaudio/blob/master/src/utils/pacat.c):
|
||
underflow callback сообщает событие без пересоздания stream. При failed context
|
||
утилита завершает работу — это пример диагностики, не готовая self-healing система.
|
||
- [Mozilla cubeb PulseAudio backend](https://github.com/mozilla/cubeb/blob/master/src/cubeb_pulse.c):
|
||
явные context/stream states, пересоздание ошибочного context при инициализации,
|
||
согласованное отключение callbacks и доступ под mainloop lock; minimum latency
|
||
25 мс. Это полезный референс владения и границ управления, а не основание
|
||
автоматически заимствовать всю библиотеку.
|
||
- [miniaudio](https://github.com/mackron/miniaudio/blob/master/miniaudio.h):
|
||
init/start/stop/uninit запрещены внутри data callback из-за deadlock. Просмотренный
|
||
upstream тоже содержит break при пустом duplex-ring: обновление библиотеки само
|
||
по себе не решает этот найденный дефект.
|