From bdcf8b5634bc8a4def4fbcebee9e187b880df76a Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 2 Oct 2026 01:49:32 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B8=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=BA=D0=BE=D0=BF=D0=BB=D0=B5=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B0=D1=83=D0=B4=D0=B8=D0=BE,=20async=20=D0=B8?= =?UTF-8?q?=20=D0=B4=D0=B8=D0=B0=D0=B3=D0=BD=D0=BE=D1=81=D1=82=D0=B8=D0=BA?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/linux_audio_recovery.md | 354 +++++++++ lib/ll_queue.c | 130 +++- lib/ll_queue.h | 12 +- lib/memory_pool.c | 26 +- lib/miniaudio.h | 207 ++++- lib/speex_aec.c | 71 +- lib/speex_aec.h | 13 +- lib/u_async.c | 711 +++++++----------- lib/u_async.h | 5 +- lib/u_async_doc.md | 8 +- src/call/call_audio.c | 142 +++- src/call/call_audio.h | 7 +- src/call/call_jitter.cpp | 76 +- src/call/call_jitter.h | 9 + src/chat/chat_core.h | 2 +- src/radio/radio_audio.c | 161 ++-- src/radio/radio_audio.h | 2 + tests/Makefile.am | 19 +- tests/async_regression.c | 169 +++++ tests/async_regression.h | 41 + tests/test_android_udp_log.c | 134 ++++ tests/test_call_headless.c | 8 +- tests/test_ll_queue_regressions.c | 402 ++++++++++ tests/test_pool_corruption_log.c | 95 +++ tests/test_speex_aec.c | 7 +- tests/test_u_async_comprehensive.c | 56 +- tests/test_u_async_timeouts.c | 17 +- tests/test_uasync_regressions.c | 551 ++++++++++++++ tests/test_uasync_socket_race.c | 90 ++- .../com/utun/chat/data/TelecomCallManager.kt | 5 +- .../utun/chat/data/UtunConnectionService.kt | 10 +- .../jni_bridge/android_jni_bridge.c | 8 +- .../jni_bridge/android_udp_log.c | 218 +++--- .../jni_bridge/android_udp_log.h | 15 +- .../libutun_lite/instance_lite.c | 13 +- tools/chatgui/CMakeLists.txt | 26 +- tools/chatgui/src/audio_device.cpp | 96 +++ tools/chatgui/src/audio_device.h | 38 + tools/chatgui/src/audiodevicesettingspage.cpp | 55 +- tools/chatgui/src/audiodevicesettingspage.h | 4 +- tools/chatgui/src/audiorecorder.cpp | 196 ++--- tools/chatgui/src/audiorecorder.h | 32 +- tools/chatgui/src/call_audio_engine.cpp | 296 +++----- tools/chatgui/src/call_audio_engine.h | 41 +- tools/chatgui/src/callwindow.cpp | 12 +- tools/chatgui/src/mainwindow.cpp | 17 +- tools/chatgui/src/radio_audio_engine.cpp | 293 +++----- tools/chatgui/src/radio_audio_engine.h | 26 +- tools/chatgui/src/sound_manager.cpp | 146 +++- tools/chatgui/src/sound_manager.h | 25 +- tools/chatgui/tests/run_pulse_recovery.py | 95 +++ .../chatgui/tests/test_audio_diagnostics.cpp | 42 ++ tools/chatgui/tests/test_audio_recovery.cpp | 222 ++++++ tools/chatgui/tests/test_call_jitter.cpp | 17 + tools/chatgui/tests/test_linux_audio.c | 163 ++++ tools/chatgui/transport/audio_diagnostics.cpp | 253 +++++++ tools/chatgui/transport/audio_diagnostics.h | 30 + tools/chatgui/transport/miniaudio_impl.c | 69 ++ tools/chatgui/transport/utun_node.cpp | 6 + 59 files changed, 4582 insertions(+), 1412 deletions(-) create mode 100644 doc/linux_audio_recovery.md create mode 100644 tests/async_regression.c create mode 100644 tests/async_regression.h create mode 100644 tests/test_android_udp_log.c create mode 100644 tests/test_ll_queue_regressions.c create mode 100644 tests/test_pool_corruption_log.c create mode 100644 tests/test_uasync_regressions.c create mode 100644 tools/chatgui/src/audio_device.cpp create mode 100644 tools/chatgui/src/audio_device.h create mode 100644 tools/chatgui/tests/run_pulse_recovery.py create mode 100644 tools/chatgui/tests/test_audio_diagnostics.cpp create mode 100644 tools/chatgui/tests/test_audio_recovery.cpp create mode 100644 tools/chatgui/tests/test_linux_audio.c create mode 100644 tools/chatgui/transport/audio_diagnostics.cpp create mode 100644 tools/chatgui/transport/audio_diagnostics.h diff --git a/doc/linux_audio_recovery.md b/doc/linux_audio_recovery.md new file mode 100644 index 00000000..88fa4896 --- /dev/null +++ b/doc/linux_audio_recovery.md @@ -0,0 +1,354 @@ +# Linux: диагностика и восстановление аудио + +Проверка 2026-10-01: 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 не обновляет уже запущенный процесс. + +## Реализация + +`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. + +Устройства сохраняются как backend ID вместо индекса. При init/start failure +явного выбора пробуется default, причина логируется. Выбранный ID сохраняется; +вернуться к нему можно повторным выбором или при следующем recovery, автоматический +опрос hotplug не добавлен. Recorder использует pUserData своего устройства, +компрессор настраивается до записи, UI получает атомарный уровень и длительность +по записанным кадрам. Настройки компрессора в ходе записи применяются к следующей. + +## Проверки реализации + +`cmake --build tools/chatgui/build -j4` — успешно. + +```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)$' +``` + +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 для проверки потоков. + +Реальный перезапуск сервера: + +```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-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: обновление библиотеки само + по себе не решает этот найденный дефект. diff --git a/lib/ll_queue.c b/lib/ll_queue.c index dd5a144c..60106bb0 100644 --- a/lib/ll_queue.c +++ b/lib/ll_queue.c @@ -52,15 +52,23 @@ static void on_get_trampoline(void* arg); static void empty_trampoline(void* arg) { struct ll_queue* q = (struct ll_queue*)arg; q->empty_call_soon_id = NULL; + if (q->count != 0) return; queue_callback_fn cb = q->empty_callback; void* cb_arg = q->empty_callback_arg; q->empty_callback = NULL; q->empty_callback_arg = NULL; - if (q->count == 0 && cb) cb(q, cb_arg); + if (cb) cb(q, cb_arg); } static void on_get_trampoline(void* arg) { struct ll_queue* q = (struct ll_queue*)arg; + q->on_get_call_soon_id = NULL; + if (!q->on_get_pending || !q->on_get) return; + q->on_get_pending--; + if (q->on_get_pending) { + q->on_get_call_soon_id = uasync_call_soon(q->ua, q, on_get_trampoline); + if (!q->on_get_call_soon_id) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] on_get scheduling failed: pending=%zu", q->name, q->on_get_pending); + } if (q->on_get) q->on_get(q, q->on_get_arg); } @@ -130,6 +138,7 @@ void queue_free(struct ll_queue* q) { // ВАЖНО: Не освобождаем элементы в очереди - они должны быть извлечены отдельно // Это упрощает архитектуру и предотвращает double-u_free + while (q->deferred_waiters) queue_waiter_cancel(q, q->deferred_waiters); // Освободить список waiters (сбросить handle->internal, отменить отложенные вызовы) struct queue_waiter* w = q->waiter_head; while (w) { @@ -159,6 +168,10 @@ void queue_free(struct ll_queue* q) { q->empty_callback_arg = NULL; q->on_get = NULL; q->on_get_arg = NULL; + if (q->on_get_call_soon_id) uasync_call_soon_cancel(q->ua, q->on_get_call_soon_id); + q->on_get_call_soon_id = NULL; q->on_get_pending = 0; + if (q->head) DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] freeing nonempty queue: count=%d; entries remain caller-owned", q->name, q->count); + for (struct ll_entry* e = q->head; e; e = e->next) e->owner = NULL; u_free(q); } @@ -316,22 +329,42 @@ static uint32_t make_hash(const void* data, uint16_t len) {// алгоритм F return hash; } +static void unlink_deferred_waiter(struct ll_queue* q, struct queue_waiter_handle* h) { + struct queue_waiter_handle** p = &q->deferred_waiters; + while (*p && *p != h) p = &(*p)->defer_next; + if (*p) *p = h->defer_next; + else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter not found: handle=%p", q->name, h); + h->defer_next = NULL; h->defer_q = NULL; +} + static void waiter_defer_cb(void* arg) { struct queue_waiter_handle* h = (struct queue_waiter_handle*)arg; + struct ll_queue* q = h->defer_q; + queue_threshold_callback_fn cb = h->defer_cb; + void* cb_arg = h->defer_arg; h->call_soon_id = NULL; - if (h->defer_cb) h->defer_cb(h->defer_q, h->defer_arg); - else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "waiter_defer_cb: NULL callback (stale?) q=%p", (void*)h->defer_q); + unlink_deferred_waiter(q, h); + h->defer_cb = NULL; h->defer_arg = NULL; + if (cb) cb(q, cb_arg); + else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter has no callback", q->name); } -static inline void queue_waiter_call(struct ll_queue* q, queue_threshold_callback_fn cb, void* arg, +static inline int queue_waiter_call(struct ll_queue* q, queue_threshold_callback_fn cb, void* arg, struct queue_waiter_handle* h) { if (q->waiter_defer) { - if (h->call_soon_id) { DEBUG_WARN(DEBUG_CATEGORY_SYS, "queue_waiter_call: overwriting existing call_soon_id=%p", h->call_soon_id); uasync_call_soon_cancel(q->ua, h->call_soon_id); } + if (h->call_soon_id) queue_waiter_cancel(h->defer_q, h); h->defer_q = q; h->defer_cb = cb; h->defer_arg = arg; h->call_soon_id = uasync_call_soon(q->ua, h, waiter_defer_cb); + if (h->call_soon_id) { h->defer_next = q->deferred_waiters; q->deferred_waiters = h; } + else { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter scheduling failed: handle=%p", q->name, h); + h->defer_q = NULL; h->defer_cb = NULL; h->defer_arg = NULL; + return -1; + } } else { cb(q, arg); } + return 0; } // Проверить и запустить первый ожидающий коллбэк (FIFO / round-robin) @@ -351,13 +384,18 @@ static void check_waiters(struct ll_queue* q) { queue_threshold_callback_fn cb = waiter->callback; void* arg = waiter->callback_arg; - u_free(waiter); - if (cb) { DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "check_waiters: waking head waiter, count=%d<=%d, bytes=%zu<=%zu", q->count, q->threshold_max_packets, q->total_bytes, q->threshold_max_bytes); - queue_waiter_call(q, cb, arg, h); + if (queue_waiter_call(q, cb, arg, h) < 0) { + // Отказ планирования не теряет ожидание: следующий get/remove повторит попытку. + waiter->next = q->waiter_head; q->waiter_head = waiter; + if (!q->waiter_tail) q->waiter_tail = waiter; + h->internal = waiter; + return; + } } + u_free(waiter); } int queue_data_put(struct ll_queue* q, struct ll_entry* entry) { @@ -366,7 +404,7 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) { queue_check_thread(q); #endif - if (entry->prev || entry->next || q->head == entry || q->tail == entry) { + if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put detected! entry=%p prev=%p next=%p len=%u", q->name, (void*)entry, (void*)entry->prev, (void*)entry->next, entry->len); abort(); @@ -380,6 +418,7 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) { } if (q->size_limit >= 0 && q->count >= q->size_limit) { + DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit); queue_dgram_free(entry); queue_entry_free(entry); return -1; @@ -391,17 +430,18 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) { if (q->tail) q->tail->next = entry; else q->head = entry; q->tail = entry; + entry->owner = q; q->count++; entry->int_len = entry->len; q->total_bytes += entry->int_len; // НЕ вызываем add_to_hash - if (q->count == 1) queue_trigger_callback(q); - #ifdef QUEUE_DEBUG queue_check_consistency(q); #endif + // Пользовательский callback может освободить очередь. + if (q->count == 1) queue_trigger_callback(q); return 0; } @@ -411,7 +451,7 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) { queue_check_thread(q); #endif - if (entry->prev || entry->next || q->head == entry || q->tail == entry) { + if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_with_index detected! entry=%p prev=%p next=%p", q->name, (void*)entry, (void*)entry->prev, (void*)entry->next); abort(); @@ -427,6 +467,7 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) { entry->index_hash = make_hash(entry->data + q->index_offset, q->index_size); if (q->size_limit >= 0 && q->count >= q->size_limit) { + DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit); queue_dgram_free(entry); queue_entry_free(entry); return -1; @@ -438,17 +479,18 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) { if (q->tail) q->tail->next = entry; else q->head = entry; q->tail = entry; + entry->owner = q; q->count++; entry->int_len = entry->len; q->total_bytes += entry->int_len; add_to_hash(q, entry); - if (q->count == 1) queue_trigger_callback(q); - #ifdef QUEUE_DEBUG queue_check_consistency(q); #endif + // Пользовательский callback может освободить очередь. + if (q->count == 1) queue_trigger_callback(q); return 0; } @@ -458,7 +500,7 @@ int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry) { queue_check_thread(q); #endif - if (entry->prev || entry->next || q->head == entry || q->tail == entry) { + if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_first detected! entry=%p prev=%p next=%p", q->name, (void*)entry, (void*)entry->prev, (void*)entry->next); abort(); @@ -471,20 +513,25 @@ int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry) { return -1; } + if (q->size_limit >= 0 && q->count >= q->size_limit) { + DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit); + queue_dgram_free(entry); queue_entry_free(entry); return -1; + } entry->next = q->head; entry->prev = NULL; if (q->head) q->head->prev = entry; else q->tail = entry; q->head = entry; + entry->owner = q; q->count++; entry->int_len = entry->len; q->total_bytes += entry->int_len; - if (q->count == 1) queue_trigger_callback(q); - #ifdef QUEUE_DEBUG queue_check_consistency(q); #endif + // Пользовательский callback может освободить очередь. + if (q->count == 1) queue_trigger_callback(q); return 0; } @@ -495,7 +542,7 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry) queue_check_thread(q); #endif - if (entry->prev || entry->next || q->head == entry || q->tail == entry) { + if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_first_with_index detected! entry=%p prev=%p next=%p", q->name, (void*)entry, (void*)entry->prev, (void*)entry->next); abort(); @@ -511,6 +558,7 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry) entry->index_hash = make_hash(entry->data + q->index_offset, q->index_size); if (q->size_limit >= 0 && q->count >= q->size_limit) { + DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit); queue_dgram_free(entry); queue_entry_free(entry); return -1; @@ -521,17 +569,18 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry) if (q->head) q->head->prev = entry; else q->tail = entry; q->head = entry; + entry->owner = q; q->count++; entry->int_len = entry->len; q->total_bytes += entry->int_len; add_to_hash(q, entry); - if (q->count == 1) queue_trigger_callback(q); - #ifdef QUEUE_DEBUG queue_check_consistency(q); #endif + // Пользовательский callback может освободить очередь. + if (q->count == 1) queue_trigger_callback(q); return 0; } @@ -554,6 +603,7 @@ struct ll_entry* queue_data_get(struct ll_queue* q) { entry->next = NULL; entry->prev = NULL; + entry->owner = NULL; remove_from_hash(q, entry); @@ -562,18 +612,21 @@ struct ll_entry* queue_data_get(struct ll_queue* q) { // Приостановить коллбэки для предотвращения рекурсии q->callback_suspended = 1; - // Проверить ожидающие коллбэки - check_waiters(q); - if (q->count == 0 && q->empty_callback && !q->empty_call_soon_id) q->empty_call_soon_id = uasync_call_soon(q->ua, q, (timeout_callback_t)empty_trampoline); - if (q->on_get) - uasync_call_soon(q->ua, q, (timeout_callback_t)on_get_trampoline); + if (q->on_get) { + q->on_get_pending++; + if (!q->on_get_call_soon_id) { + q->on_get_call_soon_id = uasync_call_soon(q->ua, q, on_get_trampoline); + if (!q->on_get_call_soon_id) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] on_get scheduling failed: pending=%zu", q->name, q->on_get_pending); + } + } #ifdef QUEUE_DEBUG queue_check_consistency(q);// !!!! for debug #endif + check_waiters(q); return entry; } @@ -672,18 +725,24 @@ void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg) { if (!q) return; q->on_get = cbk_fn; q->on_get_arg = arg; + if (!cbk_fn) { + if (q->on_get_call_soon_id) uasync_call_soon_cancel(q->ua, q->on_get_call_soon_id); + q->on_get_call_soon_id = NULL; q->on_get_pending = 0; + } } int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h, queue_threshold_callback_fn callback, void* arg) { - if (!q || !h) return -1; + if (!q || !h || !callback) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "queue_waiter_wait: invalid arguments q=%p handle=%p callback=%p", q, h, (void*)callback); + return -1; + } - if (h->internal) queue_waiter_cancel(q, h); + if (h->internal || h->call_soon_id) queue_waiter_cancel(h->call_soon_id ? h->defer_q : q, h); if (q->count <= q->threshold_max_packets && (q->threshold_max_bytes == 0 || q->total_bytes <= q->threshold_max_bytes)) { - queue_waiter_call(q, callback, arg, h); - return 1; + return queue_waiter_call(q, callback, arg, h) < 0 ? -1 : 1; } struct queue_waiter* waiter = u_malloc(sizeof(struct queue_waiter)); @@ -709,7 +768,11 @@ void queue_waiter_cancel(struct ll_queue* q, struct queue_waiter_handle* h) { * queue_waiter_wait по ветке «очередь свободна» планирует call_soon_id, * не регистрируя waiter. Ранний выход по !internal оставлял бы висячий * callback с arg=handle, который после teardown разыменовывал бы freed-память. */ - if (h->call_soon_id) { uasync_call_soon_cancel(q->ua, h->call_soon_id); h->call_soon_id = NULL; } + if (h->call_soon_id) { + struct ll_queue* deferred_q = h->defer_q; + uasync_call_soon_cancel(deferred_q->ua, h->call_soon_id); h->call_soon_id = NULL; + unlink_deferred_waiter(deferred_q, h); h->defer_cb = NULL; h->defer_arg = NULL; + } if (!h->internal) return; struct queue_waiter* waiter = h->internal; @@ -769,6 +832,10 @@ struct ll_entry* queue_find_next_by_index(struct ll_queue* q, const void* index_ int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) { if (!q || !entry) return -1; + if (entry->owner != q) { + DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] cannot remove foreign entry=%p owner=%p", q->name, entry, entry->owner); + return -1; + } #ifdef QUEUE_THREAD_CHECK queue_check_thread(q); @@ -792,6 +859,7 @@ int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) { entry->next = NULL; entry->prev = NULL; + entry->owner = NULL; remove_from_hash(q, entry); @@ -799,6 +867,8 @@ int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) { #ifdef QUEUE_DEBUG queue_check_consistency(q);// !!!! for debug #endif + if (q->count == 0 && q->empty_callback && !q->empty_call_soon_id) + q->empty_call_soon_id = uasync_call_soon(q->ua, q, empty_trampoline); check_waiters(q); return 0; } diff --git a/lib/ll_queue.h b/lib/ll_queue.h index f846cba2..1f581054 100644 --- a/lib/ll_queue.h +++ b/lib/ll_queue.h @@ -61,6 +61,7 @@ struct queue_waiter_handle; */ struct ll_entry { char* name; + struct ll_queue* owner; // Владелец элемента; NULL после извлечения struct ll_entry* next; // Следующий элемент в очереди struct ll_entry* prev; // Предыдущий элемент в очереди uint16_t size; // Размер пользовательского буфера data[] (байт) @@ -123,6 +124,7 @@ struct queue_waiter_handle { queue_threshold_callback_fn defer_cb; void* defer_arg; void* call_soon_id; ///< хендл отложенного вызова (для отмены) + struct queue_waiter_handle* defer_next; ///< список отложенных уведомлений очереди }; /** @@ -146,6 +148,7 @@ struct ll_queue { struct queue_waiter* waiter_head; // Голова списка ожидающих struct queue_waiter* waiter_tail; // Хвост списка ожидающих + struct queue_waiter_handle* deferred_waiters; // Уже запланированные уведомления int threshold_max_packets; // Общий порог: макс. кол-во элементов (по умолчанию 0) size_t threshold_max_bytes; // Общий порог: макс. объём данных (0 = не проверять) int waiter_defer; // 0=immediate callback, 1=defer via uasync_call_soon @@ -157,6 +160,8 @@ struct ll_queue { queue_callback_fn on_get; // коллбэк после каждого queue_data_get (deferred) void* on_get_arg; + void* on_get_call_soon_id; + size_t on_get_pending; struct ll_entry** hash_table; // Хеш-таблица для поиска по id (если hash_size > 0) size_t hash_size; // Размер хеш-таблицы @@ -275,12 +280,13 @@ void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg); /** * @brief Регистрирует ожидание освобождения очереди до общего порога. * @param q очередь - * @param h указатель на handle (создаётся как `struct queue_waiter_handle h = {0}` или `h.internal = NULL`) + * @param h полностью обнулённый handle: `struct queue_waiter_handle h = {0}` * @param callback функция, вызываемая при достижении порога * @param arg аргумент коллбэка - * @return 1 — условие уже выполнено (callback вызван немедленно), 0 — зарегистрирован в списке, -1 — ошибка + * @return 1 — условие выполнено (callback вызван или отложен), 0 — ожидает порог, -1 — ошибка * - * Если handle уже ожидает (h->internal != NULL) — старый waiter отменяется и создаётся новый. + * Если handle уже ожидает или имеет отложенный callback — старое ожидание отменяется. + * При queue_free отменяются также уже запланированные уведомления. */ int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h, queue_threshold_callback_fn callback, void* arg); diff --git a/lib/memory_pool.c b/lib/memory_pool.c index 298b565f..bc0a7def 100644 --- a/lib/memory_pool.c +++ b/lib/memory_pool.c @@ -5,11 +5,12 @@ #include "memory_pool.h" #include "mem.h" -// Layout: [user_data: object_size] [canary: 4 bytes] [alloc_loc: pointer] [counter: 1 byte] +// Layout: [user_data] [canary: 4] [alloc_loc: pointer] [free_loc: pointer] [counter: 1] #define POOL_CANARY_OFF (pool->object_size) #define POOL_LOC_OFF (pool->object_size + 4) -#define POOL_COUNTER_OFF (pool->object_size + 4 + sizeof(const char*)) -#define POOL_ALLOC_SIZE (pool->object_size + 4 + sizeof(const char*) + 1) +#define POOL_FREE_LOC_OFF (pool->object_size + 4 + sizeof(const char*)) +#define POOL_COUNTER_OFF (pool->object_size + 4 + 2 * sizeof(const char*)) +#define POOL_ALLOC_SIZE (pool->object_size + 4 + 2 * sizeof(const char*) + 1) #define POOL_CANARY_VAL 0xDEADBEAF static void pool_init_tags(struct memory_pool* pool, void* obj, const char* location) @@ -19,35 +20,44 @@ static void pool_init_tags(struct memory_pool* pool, void* obj, const char* loca /* Метаданные следуют за payload произвольного размера и могут быть невыровненными. */ memcpy((uint8_t*)obj + POOL_CANARY_OFF, &canary, sizeof(canary)); memcpy((uint8_t*)obj + POOL_LOC_OFF, &location, sizeof(location)); + const char* free_loc = NULL; + memcpy((uint8_t*)obj + POOL_FREE_LOC_OFF, &free_loc, sizeof(free_loc)); *counter = 1; } static void pool_check_and_clear_tags(struct memory_pool* pool, void* obj, const char* location) { uint32_t canary; - const char* loc; + const char *loc, *free_loc; memcpy(&canary, (uint8_t*)obj + POOL_CANARY_OFF, sizeof(canary)); memcpy(&loc, (uint8_t*)obj + POOL_LOC_OFF, sizeof(loc)); + memcpy(&free_loc, (uint8_t*)obj + POOL_FREE_LOC_OFF, sizeof(free_loc)); uint8_t* counter = (uint8_t*)obj + POOL_COUNTER_OFF; if (canary != POOL_CANARY_VAL) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free BUFFER OVERFLOW pool=%p name=%s sz=%zu allocs=%zu reuse=%zu free=%d", pool, pool->name ? pool->name : "?", pool->object_size, pool->allocations, pool->reuse_count, pool->free_count); - DEBUG_ERROR(DEBUG_CATEGORY_SYS, " obj=%p alloc=%s free=%s canary=0x%08x expected=0x%08x", - obj, loc ? loc : "(null)", location, canary, POOL_CANARY_VAL); + /* Повреждённый хвост может содержать битые указатели; их нельзя читать как строки. */ + DEBUG_ERROR(DEBUG_CATEGORY_SYS, " obj=%p alloc_loc_ptr=%p free_loc_ptr=%p free_attempt=%s canary=0x%08x expected=0x%08x", + obj, (void*)loc, (void*)free_loc, location, canary, POOL_CANARY_VAL); if (pool->object_size) log_dump(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_SYS, " user_data", obj, pool->object_size); + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free: corruption logged, stopping thread permanently obj=%p", obj); volatile int _halt = 1; while (_halt) {} } if (*counter == 0) { - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free DOUBLE FREE pool=%p name=%s sz=%zu allocs=%zu reuse=%zu obj=%p alloc=%s free=%s", + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free DOUBLE FREE pool=%p name=%s sz=%zu allocs=%zu reuse=%zu obj=%p", pool, pool->name ? pool->name : "?", pool->object_size, pool->allocations, pool->reuse_count, - obj, loc ? loc : "(null)", location); + obj); + DEBUG_ERROR(DEBUG_CATEGORY_SYS, " alloc=%s first_free=%s repeated_free=%s", + loc ? loc : "(null)", free_loc ? free_loc : "(null)", location); + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free: corruption logged, stopping thread permanently obj=%p", obj); volatile int _halt = 1; while (_halt) {} } *counter = 0; + memcpy((uint8_t*)obj + POOL_FREE_LOC_OFF, &location, sizeof(location)); } static size_t g_total_free = 0; diff --git a/lib/miniaudio.h b/lib/miniaudio.h index c6d493ee..49b8c5e7 100644 --- a/lib/miniaudio.h +++ b/lib/miniaudio.h @@ -7543,11 +7543,13 @@ struct ma_context ma_proc pa_context_disconnect; ma_proc pa_context_set_state_callback; ma_proc pa_context_get_state; + ma_proc pa_context_errno; ma_proc pa_context_get_sink_info_list; ma_proc pa_context_get_source_info_list; ma_proc pa_context_get_sink_info_by_name; ma_proc pa_context_get_source_info_by_name; ma_proc pa_operation_unref; + ma_proc pa_operation_cancel; ma_proc pa_operation_get_state; ma_proc pa_channel_map_init_extend; ma_proc pa_channel_map_valid; @@ -7574,6 +7576,7 @@ struct ma_context ma_proc pa_stream_cork; ma_proc pa_stream_trigger; ma_proc pa_stream_begin_write; + ma_proc pa_stream_cancel_write; ma_proc pa_stream_write; ma_proc pa_stream_peek; ma_proc pa_stream_drop; @@ -11553,6 +11556,17 @@ IMPLEMENTATION #ifndef miniaudio_c #define miniaudio_c +/* uTun: необязательные наблюдатели, без изменения ABI и поведения аудиотракта. */ +#ifndef MA_DIAGNOSTIC_EVENT +#define MA_DIAGNOSTIC_EVENT(device, kind, stage, value, detail) ((void)0) +#endif +#ifndef MA_DIAGNOSTIC_DEVICE_INIT +#define MA_DIAGNOSTIC_DEVICE_INIT(device) ((void)0) +#endif +#ifndef MA_DIAGNOSTIC_LOG +#define MA_DIAGNOSTIC_LOG(log, level, message) ((void)0) +#endif + #include #include /* For INT_MAX */ #include /* sin(), etc. */ @@ -13752,6 +13766,7 @@ MA_API ma_result ma_log_post(ma_log* pLog, ma_uint32 level, const char* pMessage return MA_INVALID_ARGS; } + MA_DIAGNOSTIC_LOG(pLog, level, pMessage); ma_log_lock(pLog); { ma_uint32 iLog; @@ -20200,6 +20215,8 @@ static void ma_device__on_notification(ma_device_notification notification) { MA_ASSERT(notification.pDevice != NULL); + MA_DIAGNOSTIC_EVENT(notification.pDevice, AUDIO_DIAG_NOTIFY, -1, notification.type, ma_device_get_state(notification.pDevice)); + if (notification.pDevice->onNotification != NULL) { notification.pDevice->onNotification(¬ification); } @@ -20360,6 +20377,7 @@ static void ma_device__handle_data_callback(ma_device* pDevice, void* pFramesOut { float masterVolumeFactor; + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_CALLBACK, frameCount, (pFramesIn != NULL) | ((pFramesOut != NULL) << 1)); ma_device_get_master_volume(pDevice, &masterVolumeFactor); /* Use ma_device_get_master_volume() to ensure the volume is loaded atomically. */ if (pDevice->onData) { @@ -20407,6 +20425,7 @@ static void ma_device__handle_data_callback(ma_device* pDevice, void* pFramesOut ma_silence_pcm_frames(pFramesOut, frameCount, pDevice->playback.format, pDevice->playback.channels); } } + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_CALLBACK, frameCount, 0); } @@ -20590,6 +20609,7 @@ static ma_result ma_device__handle_duplex_callback_capture(ma_device* pDevice, m if (framesToProcessInClientFormat == 0) { if (ma_pcm_rb_pointer_distance(pRB) == (ma_int32)ma_pcm_rb_get_subbuffer_size(pRB)) { + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_CAPTURE_DROP, AUDIO_DIAG_READ, framesToProcessInDeviceFormat, ma_pcm_rb_available_read(pRB)); break; /* Overrun. Not enough room in the ring buffer for input frame. Excess frames are dropped. */ } } @@ -20625,6 +20645,7 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice, ma_result result; ma_uint8 silentInputFrames[MA_DATA_CONVERTER_STACK_BUFFER_SIZE]; ma_uint32 totalFramesReadOut = 0; + ma_silence_pcm_frames(pFramesInInternalFormat, frameCount, pDevice->playback.internalFormat, pDevice->playback.internalChannels); MA_ASSERT(pDevice != NULL); MA_ASSERT(frameCount > 0); @@ -20636,6 +20657,7 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice, Sitting in the ring buffer should be captured data from the capture callback in external format. If there's not enough data in there for the whole frameCount frames we just use silence instead for the input data. */ + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_DUPLEX, frameCount, ma_pcm_rb_available_read(pRB)); MA_ZERO_MEMORY(silentInputFrames, sizeof(silentInputFrames)); while (totalFramesReadOut < frameCount && ma_device_is_started(pDevice)) { @@ -20646,8 +20668,12 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice, if (pDevice->playback.inputCacheRemaining > 0) { ma_uint64 framesConvertedIn = pDevice->playback.inputCacheRemaining; ma_uint64 framesConvertedOut = (frameCount - totalFramesReadOut); - ma_data_converter_process_pcm_frames(&pDevice->playback.converter, ma_offset_pcm_frames_ptr(pDevice->playback.pInputCache, pDevice->playback.inputCacheConsumed, pDevice->playback.format, pDevice->playback.channels), &framesConvertedIn, pFramesInInternalFormat, &framesConvertedOut); + result = ma_data_converter_process_pcm_frames(&pDevice->playback.converter, ma_offset_pcm_frames_ptr(pDevice->playback.pInputCache, pDevice->playback.inputCacheConsumed, pDevice->playback.format, pDevice->playback.channels), &framesConvertedIn, pFramesInInternalFormat, &framesConvertedOut); + if (result != MA_SUCCESS || (framesConvertedIn == 0 && framesConvertedOut == 0)) { + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result); + break; + } pDevice->playback.inputCacheConsumed += framesConvertedIn; pDevice->playback.inputCacheRemaining -= framesConvertedIn; @@ -20662,30 +20688,34 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice, inputFrameCount = (ma_uint32)pDevice->playback.inputCacheCap; result = ma_pcm_rb_acquire_read(pRB, &inputFrameCount, &pInputFrames); - if (result == MA_SUCCESS) { - if (inputFrameCount > 0) { - ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, pInputFrames, inputFrameCount); - } else { - if (ma_pcm_rb_pointer_distance(pRB) == 0) { - break; /* Underrun. */ - } + if (result != MA_SUCCESS) { + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result); + break; + } + if (inputFrameCount > 0) { + ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, pInputFrames, inputFrameCount); + result = ma_pcm_rb_commit_read(pRB, inputFrameCount); + if (result != MA_SUCCESS) { + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result); + break; } } else { - /* No capture data available. Feed in silence. */ - inputFrameCount = (ma_uint32)ma_min(pDevice->playback.inputCacheCap, sizeof(silentInputFrames) / ma_get_bytes_per_frame(pDevice->capture.format, pDevice->capture.channels)); + /* Отсутствие микрофонных кадров не останавливает рендер; синтетический ввод не сдвигает capture-ring. */ + inputFrameCount = (ma_uint32)ma_min(pDevice->playback.inputCacheCap, + sizeof(silentInputFrames) / ma_get_bytes_per_frame(pDevice->capture.format, pDevice->capture.channels)); ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, silentInputFrames, inputFrameCount); } pDevice->playback.inputCacheConsumed = 0; pDevice->playback.inputCacheRemaining = inputFrameCount; - result = ma_pcm_rb_commit_read(pRB, inputFrameCount); - if (result != MA_SUCCESS) { - return result; /* Should never happen. */ - } } } + if (totalFramesReadOut < frameCount) { + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_UNFILLED, AUDIO_DIAG_DUPLEX, frameCount - totalFramesReadOut, ma_pcm_rb_available_read(pRB)); + } + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_DUPLEX, totalFramesReadOut, ma_pcm_rb_available_read(pRB)); return MA_SUCCESS; } @@ -30908,11 +30938,13 @@ typedef int (* ma_pa_context_connect_proc) ( typedef void (* ma_pa_context_disconnect_proc) (ma_pa_context* c); typedef void (* ma_pa_context_set_state_callback_proc) (ma_pa_context* c, ma_pa_context_notify_cb_t cb, void* userdata); typedef ma_pa_context_state_t (* ma_pa_context_get_state_proc) (const ma_pa_context* c); +typedef int (* ma_pa_context_errno_proc) (const ma_pa_context* c); typedef ma_pa_operation* (* ma_pa_context_get_sink_info_list_proc) (ma_pa_context* c, ma_pa_sink_info_cb_t cb, void* userdata); typedef ma_pa_operation* (* ma_pa_context_get_source_info_list_proc) (ma_pa_context* c, ma_pa_source_info_cb_t cb, void* userdata); typedef ma_pa_operation* (* ma_pa_context_get_sink_info_by_name_proc) (ma_pa_context* c, const char* name, ma_pa_sink_info_cb_t cb, void* userdata); typedef ma_pa_operation* (* ma_pa_context_get_source_info_by_name_proc)(ma_pa_context* c, const char* name, ma_pa_source_info_cb_t cb, void* userdata); typedef void (* ma_pa_operation_unref_proc) (ma_pa_operation* o); +typedef void (* ma_pa_operation_cancel_proc) (ma_pa_operation* o); typedef ma_pa_operation_state_t (* ma_pa_operation_get_state_proc) (const ma_pa_operation* o); typedef ma_pa_channel_map* (* ma_pa_channel_map_init_extend_proc) (ma_pa_channel_map* m, unsigned channels, ma_pa_channel_map_def_t def); typedef int (* ma_pa_channel_map_valid_proc) (const ma_pa_channel_map* m); @@ -30939,6 +30971,7 @@ typedef int (* ma_pa_stream_is_corked_proc) ( typedef ma_pa_operation* (* ma_pa_stream_cork_proc) (ma_pa_stream* s, int b, ma_pa_stream_success_cb_t cb, void* userdata); typedef ma_pa_operation* (* ma_pa_stream_trigger_proc) (ma_pa_stream* s, ma_pa_stream_success_cb_t cb, void* userdata); typedef int (* ma_pa_stream_begin_write_proc) (ma_pa_stream* s, void** data, size_t* nbytes); +typedef int (* ma_pa_stream_cancel_write_proc) (ma_pa_stream* s); typedef int (* ma_pa_stream_write_proc) (ma_pa_stream* s, const void* data, size_t nbytes, ma_pa_free_cb_t free_cb, int64_t offset, ma_pa_seek_mode_t seek); typedef int (* ma_pa_stream_peek_proc) (ma_pa_stream* s, const void** data, size_t* nbytes); typedef int (* ma_pa_stream_drop_proc) (ma_pa_stream* s); @@ -31126,23 +31159,49 @@ static ma_pa_channel_position_t ma_channel_position_to_pulse(ma_channel position } #endif -static ma_result ma_wait_for_operation__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_pa_operation* pOP) +#ifndef MA_PULSE_WAIT_TIMEOUT_MS +#define MA_PULSE_WAIT_TIMEOUT_MS 1000 +#endif + +static ma_result ma_pulse_wait_step(ma_context* pContext, ma_ptr pMainLoop, ma_timer* pTimer, const char* operation) { int resultPA; + if (ma_timer_get_time_in_seconds(pTimer) * 1000 >= MA_PULSE_WAIT_TIMEOUT_MS) { + ma_log_postf(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR, + "[PulseAudio] %s timed out after %d ms.\n", operation, MA_PULSE_WAIT_TIMEOUT_MS); + return MA_TIMEOUT; + } + resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 0, NULL); + if (resultPA < 0) { + ma_log_postf(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR, "[PulseAudio] %s iterate failed: %d.\n", operation, resultPA); + return ma_result_from_pulse(resultPA); + } + if (resultPA == 0) ma_sleep(1); + return MA_SUCCESS; +} + +static ma_result ma_wait_for_operation__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_pa_operation* pOP) +{ + ma_result result; + ma_timer timer; ma_pa_operation_state_t state; MA_ASSERT(pContext != NULL); MA_ASSERT(pOP != NULL); + ma_timer_init(&timer); for (;;) { state = ((ma_pa_operation_get_state_proc)pContext->pulse.pa_operation_get_state)(pOP); + if (state == MA_PA_OPERATION_DONE) return MA_SUCCESS; if (state != MA_PA_OPERATION_RUNNING) { - break; /* Done. */ + ma_log_post(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR, "[PulseAudio] Operation cancelled.\n"); + return MA_ERROR; } - resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL); - if (resultPA < 0) { - return ma_result_from_pulse(resultPA); + result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "wait"); + if (result != MA_SUCCESS) { + ((ma_pa_operation_cancel_proc)pContext->pulse.pa_operation_cancel)(pOP); + return result; } } @@ -31165,9 +31224,11 @@ static ma_result ma_wait_for_operation_and_unref__pulse(ma_context* pContext, ma static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_ptr pPulseContext) { - int resultPA; + ma_result result; + ma_timer timer; ma_pa_context_state_t state; + ma_timer_init(&timer); for (;;) { state = ((ma_pa_context_get_state_proc)pContext->pulse.pa_context_get_state)((ma_pa_context*)pPulseContext); if (state == MA_PA_CONTEXT_READY) { @@ -31179,10 +31240,8 @@ static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext, return MA_ERROR; } - resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL); - if (resultPA < 0) { - return ma_result_from_pulse(resultPA); - } + result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "context connect"); + if (result != MA_SUCCESS) return result; } /* Should never get here. */ @@ -31191,9 +31250,11 @@ static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext, static ma_result ma_wait_for_pa_stream_to_connect__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_ptr pStream) { - int resultPA; + ma_result result; + ma_timer timer; ma_pa_stream_state_t state; + ma_timer_init(&timer); for (;;) { state = ((ma_pa_stream_get_state_proc)pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pStream); if (state == MA_PA_STREAM_READY) { @@ -31205,10 +31266,8 @@ static ma_result ma_wait_for_pa_stream_to_connect__pulse(ma_context* pContext, m return MA_ERROR; } - resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL); - if (resultPA < 0) { - return ma_result_from_pulse(resultPA); - } + result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "stream connect"); + if (result != MA_SUCCESS) return result; } return MA_SUCCESS; @@ -31382,10 +31441,13 @@ static ma_result ma_context_get_default_device_index__pulse(ma_context* pContext if (deviceType == ma_device_type_playback) { ma_pa_sink_info sinkInfo; + MA_ZERO_OBJECT(&sinkInfo); + sinkInfo.index = (ma_uint32)-1; result = ma_context_get_sink_info__pulse(pContext, NULL, &sinkInfo); if (result != MA_SUCCESS) { return result; } + if (sinkInfo.index == (ma_uint32)-1) return MA_NO_DEVICE; if (pIndex != NULL) { *pIndex = sinkInfo.index; @@ -31394,10 +31456,13 @@ static ma_result ma_context_get_default_device_index__pulse(ma_context* pContext if (deviceType == ma_device_type_capture) { ma_pa_source_info sourceInfo; + MA_ZERO_OBJECT(&sourceInfo); + sourceInfo.index = (ma_uint32)-1; result = ma_context_get_source_info__pulse(pContext, NULL, &sourceInfo); if (result != MA_SUCCESS) { return result; } + if (sourceInfo.index == (ma_uint32)-1) return MA_NO_DEVICE; if (pIndex != NULL) { *pIndex = sourceInfo.index; @@ -31551,7 +31616,7 @@ static void ma_context_get_device_info_sink_callback__pulse(ma_pa_context* pPuls { ma_context_get_device_info_callback_data__pulse* pData = (ma_context_get_device_info_callback_data__pulse*)pUserData; - if (endOfList > 0) { + if (endOfList != 0 || pInfo == NULL) { return; } @@ -31588,7 +31653,7 @@ static void ma_context_get_device_info_source_callback__pulse(ma_pa_context* pPu { ma_context_get_device_info_callback_data__pulse* pData = (ma_context_get_device_info_callback_data__pulse*)pUserData; - if (endOfList > 0) { + if (endOfList != 0 || pInfo == NULL) { return; } @@ -31640,6 +31705,7 @@ static ma_result ma_context_get_device_info__pulse(ma_context* pContext, ma_devi } result = ma_context_get_default_device_index__pulse(pContext, deviceType, &callbackData.defaultDeviceIndex); + if (result != MA_SUCCESS) return result; if (deviceType == ma_device_type_playback) { pOP = ((ma_pa_context_get_sink_info_by_name_proc)pContext->pulse.pa_context_get_sink_info_by_name)((ma_pa_context*)(pContext->pulse.pPulseContext), pDeviceName, ma_context_get_device_info_sink_callback__pulse, &callbackData); @@ -31648,7 +31714,8 @@ static ma_result ma_context_get_device_info__pulse(ma_context* pContext, ma_devi } if (pOP != NULL) { - ma_wait_for_operation_and_unref__pulse(pContext, pContext->pulse.pMainLoop, pOP); + result = ma_wait_for_operation_and_unref__pulse(pContext, pContext->pulse.pMainLoop, pOP); + if (result != MA_SUCCESS) goto done; } else { result = MA_ERROR; goto done; @@ -31747,6 +31814,7 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo MA_ASSERT(bpf > 0); frameCount = byteCount / bpf; + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_READ, frameCount, deviceState); framesProcessed = 0; while (ma_device_get_state(pDevice) == ma_device_state_started && framesProcessed < frameCount) { @@ -31756,6 +31824,7 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo int pulseResult = ((ma_pa_stream_peek_proc)pDevice->pContext->pulse.pa_stream_peek)(pStream, &pMappedPCMFrames, &bytesMapped); if (pulseResult < 0) { + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_READ, 1, pulseResult); break; /* Failed to map. Abort. */ } @@ -31764,12 +31833,21 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo if (pMappedPCMFrames != NULL) { ma_device_handle_backend_data_callback(pDevice, NULL, pMappedPCMFrames, framesMapped); } else { - /* It's a hole. */ - ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[PulseAudio] ma_device_on_read__pulse: Hole.\n"); + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_HOLE, AUDIO_DIAG_READ, framesMapped, 0); + ma_uint8 silence[MA_DATA_CONVERTER_STACK_BUFFER_SIZE]; + ma_uint64 remaining = framesMapped; + ma_uint32 capacity = sizeof(silence) / bpf; + ma_silence_pcm_frames(silence, capacity, pDevice->capture.internalFormat, pDevice->capture.internalChannels); + while (remaining > 0 && ma_device_get_state(pDevice) == ma_device_state_started) { + ma_uint32 n = (ma_uint32)ma_min(remaining, capacity); + ma_device_handle_backend_data_callback(pDevice, NULL, silence, n); + remaining -= n; + } } pulseResult = ((ma_pa_stream_drop_proc)pDevice->pContext->pulse.pa_stream_drop)(pStream); if (pulseResult < 0) { + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_READ, 1, pulseResult); break; /* Failed to drop the buffer. */ } @@ -31780,6 +31858,7 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo break; } } + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_READ, framesProcessed, 0); } static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stream* pStream, ma_uint64* pFramesProcessed) @@ -31798,6 +31877,7 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre deviceState = ma_device_get_state(pDevice); + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_MAP, 0, deviceState); bytesMapped = ((ma_pa_stream_writable_size_proc)pDevice->pContext->pulse.pa_stream_writable_size)(pStream); if (bytesMapped != (size_t)-1) { if (bytesMapped > 0) { @@ -31810,15 +31890,23 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre } framesMapped = bytesMapped / bpf; + if (framesMapped == 0 || pMappedPCMFrames == NULL) { + ((ma_pa_stream_cancel_write_proc)pDevice->pContext->pulse.pa_stream_cancel_write)(pStream); + result = MA_ERROR; + goto done; + } + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_MAP, framesMapped, 0); if (deviceState == ma_device_state_started || deviceState == ma_device_state_starting) { /* Check for starting state just in case this is being used to do the initial fill. */ ma_device_handle_backend_data_callback(pDevice, pMappedPCMFrames, NULL, framesMapped); } else { /* Device is not started. Write silence. */ - ma_silence_pcm_frames(pMappedPCMFrames, framesMapped, pDevice->playback.format, pDevice->playback.channels); + ma_silence_pcm_frames(pMappedPCMFrames, framesMapped, pDevice->playback.internalFormat, pDevice->playback.internalChannels); } + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_SUBMIT, framesMapped, 0); pulseResult = ((ma_pa_stream_write_proc)pDevice->pContext->pulse.pa_stream_write)(pStream, pMappedPCMFrames, bytesMapped, NULL, 0, MA_PA_SEEK_RELATIVE); + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_SUBMIT, framesMapped, pulseResult); if (pulseResult < 0) { result = ma_result_from_pulse(pulseResult); goto done; /* Failed to write data to stream. */ @@ -31835,6 +31923,10 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre } done: + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_MAP, framesProcessed, result); + if (result != MA_SUCCESS) { + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_WRITE, 1, result); + } if (pFramesProcessed != NULL) { *pFramesProcessed = framesProcessed; } @@ -31850,6 +31942,7 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v ma_uint64 framesProcessed; ma_uint32 deviceState; ma_result result; + ma_bool32 diagnosticNoProgress = MA_FALSE; MA_ASSERT(pDevice != NULL); @@ -31866,6 +31959,7 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v MA_ASSERT(bpf > 0); frameCount = byteCount / bpf; + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_WRITE, frameCount, deviceState); framesProcessed = 0; while (framesProcessed < frameCount) { @@ -31882,8 +31976,14 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v break; } + if (framesProcessedThisIteration == 0 && !diagnosticNoProgress) { + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_NO_PROGRESS, AUDIO_DIAG_WRITE, 1, frameCount - framesProcessed); + diagnosticNoProgress = MA_TRUE; + } + if (framesProcessedThisIteration == 0) break; framesProcessed += framesProcessedThisIteration; } + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_WRITE, framesProcessed, 0); } static void ma_device_on_suspended__pulse(ma_pa_stream* pStream, void* pUserData) @@ -31902,10 +32002,10 @@ static void ma_device_on_suspended__pulse(ma_pa_stream* pStream, void* pUserData if (suspended == 1) { ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[Pulse] Device suspended state changed. Suspended.\n"); - ma_device__on_notification_stopped(pDevice); + ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_interruption_began)); } else { ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[Pulse] Device suspended state changed. Resumed.\n"); - ma_device__on_notification_started(pDevice); + ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_interruption_ended)); } } @@ -32375,6 +32475,10 @@ on_error1: ((ma_pa_stream_unref_proc)pDevice->pContext->pulse.pa_stream_unref)((ma_pa_stream*)pDevice->pulse.pStreamCapture); } on_error0: + ((ma_pa_context_disconnect_proc)pDevice->pContext->pulse.pa_context_disconnect)((ma_pa_context*)pDevice->pulse.pPulseContext); + ((ma_pa_context_unref_proc)pDevice->pContext->pulse.pa_context_unref)((ma_pa_context*)pDevice->pulse.pPulseContext); + ((ma_pa_mainloop_free_proc)pDevice->pContext->pulse.pa_mainloop_free)((ma_pa_mainloop*)pDevice->pulse.pMainLoop); + MA_ZERO_OBJECT(&pDevice->pulse); return result; } @@ -32504,8 +32608,24 @@ static ma_result ma_device_data_loop__pulse(ma_device* pDevice) the callbacks deal with it. */ while (ma_device_get_state(pDevice) == ma_device_state_started) { + ma_pa_context_state_t contextState = ((ma_pa_context_get_state_proc)pDevice->pContext->pulse.pa_context_get_state)((ma_pa_context*)pDevice->pulse.pPulseContext); + ma_pa_stream_state_t captureState = pDevice->pulse.pStreamCapture ? + ((ma_pa_stream_get_state_proc)pDevice->pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pDevice->pulse.pStreamCapture) : MA_PA_STREAM_READY; + ma_pa_stream_state_t playbackState = pDevice->pulse.pStreamPlayback ? + ((ma_pa_stream_get_state_proc)pDevice->pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pDevice->pulse.pStreamPlayback) : MA_PA_STREAM_READY; + if (contextState == MA_PA_CONTEXT_FAILED || contextState == MA_PA_CONTEXT_TERMINATED || + captureState == MA_PA_STREAM_FAILED || captureState == MA_PA_STREAM_TERMINATED || + playbackState == MA_PA_STREAM_FAILED || playbackState == MA_PA_STREAM_TERMINATED) { + ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR, + "[PulseAudio] Device failed context=%d capture=%d playback=%d errno=%d.\n", contextState, captureState, + playbackState, ((ma_pa_context_errno_proc)pDevice->pContext->pulse.pa_context_errno)((ma_pa_context*)pDevice->pulse.pPulseContext)); + return MA_ERROR; + } + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_LOOP, 0, 0); resultPA = ((ma_pa_mainloop_iterate_proc)pDevice->pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pDevice->pulse.pMainLoop, 1, NULL); + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_LOOP, 0, resultPA); if (resultPA < 0) { + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_LOOP, 1, resultPA); break; } } @@ -32588,11 +32708,13 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c pContext->pulse.pa_context_disconnect = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_disconnect"); pContext->pulse.pa_context_set_state_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_set_state_callback"); pContext->pulse.pa_context_get_state = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_state"); + pContext->pulse.pa_context_errno = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_errno"); pContext->pulse.pa_context_get_sink_info_list = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_sink_info_list"); pContext->pulse.pa_context_get_source_info_list = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_source_info_list"); pContext->pulse.pa_context_get_sink_info_by_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_sink_info_by_name"); pContext->pulse.pa_context_get_source_info_by_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_source_info_by_name"); pContext->pulse.pa_operation_unref = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_unref"); + pContext->pulse.pa_operation_cancel = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_cancel"); pContext->pulse.pa_operation_get_state = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_get_state"); pContext->pulse.pa_channel_map_init_extend = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_channel_map_init_extend"); pContext->pulse.pa_channel_map_valid = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_channel_map_valid"); @@ -32619,6 +32741,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c pContext->pulse.pa_stream_cork = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_cork"); pContext->pulse.pa_stream_trigger = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_trigger"); pContext->pulse.pa_stream_begin_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_begin_write"); + pContext->pulse.pa_stream_cancel_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_cancel_write"); pContext->pulse.pa_stream_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_write"); pContext->pulse.pa_stream_peek = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_peek"); pContext->pulse.pa_stream_drop = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_drop"); @@ -32651,11 +32774,13 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c ma_pa_context_disconnect_proc _pa_context_disconnect = pa_context_disconnect; ma_pa_context_set_state_callback_proc _pa_context_set_state_callback = pa_context_set_state_callback; ma_pa_context_get_state_proc _pa_context_get_state = pa_context_get_state; + ma_pa_context_errno_proc _pa_context_errno = pa_context_errno; ma_pa_context_get_sink_info_list_proc _pa_context_get_sink_info_list = pa_context_get_sink_info_list; ma_pa_context_get_source_info_list_proc _pa_context_get_source_info_list = pa_context_get_source_info_list; ma_pa_context_get_sink_info_by_name_proc _pa_context_get_sink_info_by_name = pa_context_get_sink_info_by_name; ma_pa_context_get_source_info_by_name_proc _pa_context_get_source_info_by_name= pa_context_get_source_info_by_name; ma_pa_operation_unref_proc _pa_operation_unref = pa_operation_unref; + ma_pa_operation_cancel_proc _pa_operation_cancel = pa_operation_cancel; ma_pa_operation_get_state_proc _pa_operation_get_state = pa_operation_get_state; ma_pa_channel_map_init_extend_proc _pa_channel_map_init_extend = pa_channel_map_init_extend; ma_pa_channel_map_valid_proc _pa_channel_map_valid = pa_channel_map_valid; @@ -32682,6 +32807,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c ma_pa_stream_cork_proc _pa_stream_cork = pa_stream_cork; ma_pa_stream_trigger_proc _pa_stream_trigger = pa_stream_trigger; ma_pa_stream_begin_write_proc _pa_stream_begin_write = pa_stream_begin_write; + ma_pa_stream_cancel_write_proc _pa_stream_cancel_write = pa_stream_cancel_write; ma_pa_stream_write_proc _pa_stream_write = pa_stream_write; ma_pa_stream_peek_proc _pa_stream_peek = pa_stream_peek; ma_pa_stream_drop_proc _pa_stream_drop = pa_stream_drop; @@ -32713,11 +32839,13 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c pContext->pulse.pa_context_disconnect = (ma_proc)_pa_context_disconnect; pContext->pulse.pa_context_set_state_callback = (ma_proc)_pa_context_set_state_callback; pContext->pulse.pa_context_get_state = (ma_proc)_pa_context_get_state; + pContext->pulse.pa_context_errno = (ma_proc)_pa_context_errno; pContext->pulse.pa_context_get_sink_info_list = (ma_proc)_pa_context_get_sink_info_list; pContext->pulse.pa_context_get_source_info_list = (ma_proc)_pa_context_get_source_info_list; pContext->pulse.pa_context_get_sink_info_by_name = (ma_proc)_pa_context_get_sink_info_by_name; pContext->pulse.pa_context_get_source_info_by_name = (ma_proc)_pa_context_get_source_info_by_name; pContext->pulse.pa_operation_unref = (ma_proc)_pa_operation_unref; + pContext->pulse.pa_operation_cancel = (ma_proc)_pa_operation_cancel; pContext->pulse.pa_operation_get_state = (ma_proc)_pa_operation_get_state; pContext->pulse.pa_channel_map_init_extend = (ma_proc)_pa_channel_map_init_extend; pContext->pulse.pa_channel_map_valid = (ma_proc)_pa_channel_map_valid; @@ -32744,6 +32872,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c pContext->pulse.pa_stream_cork = (ma_proc)_pa_stream_cork; pContext->pulse.pa_stream_trigger = (ma_proc)_pa_stream_trigger; pContext->pulse.pa_stream_begin_write = (ma_proc)_pa_stream_begin_write; + pContext->pulse.pa_stream_cancel_write = (ma_proc)_pa_stream_cancel_write; pContext->pulse.pa_stream_write = (ma_proc)_pa_stream_write; pContext->pulse.pa_stream_peek = (ma_proc)_pa_stream_peek; pContext->pulse.pa_stream_drop = (ma_proc)_pa_stream_drop; @@ -43767,6 +43896,7 @@ MA_API ma_result ma_device_init(ma_context* pContext, const ma_device_config* pC result = pContext->callbacks.onDeviceInit(pDevice, pConfig, &descriptorPlayback, &descriptorCapture); if (result != MA_SUCCESS) { + ma_event_uninit(&pDevice->stopEvent); ma_event_uninit(&pDevice->startEvent); ma_event_uninit(&pDevice->wakeupEvent); ma_mutex_uninit(&pDevice->startStopLock); @@ -44008,6 +44138,8 @@ MA_API ma_result ma_device_init(ma_context* pContext, const ma_device_config* pC } } + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_OPEN, -1, pDevice->type, pDevice->sampleRate); + MA_DIAGNOSTIC_DEVICE_INIT(pDevice); MA_ASSERT(ma_device_get_state(pDevice) == ma_device_state_stopped); return MA_SUCCESS; } @@ -44175,6 +44307,7 @@ MA_API void ma_device_uninit(ma_device* pDevice) ma_free(pDevice->pContext, &allocationCallbacks); } + MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_CLOSE, -1, 0, 0); MA_ZERO_OBJECT(pDevice); } diff --git a/lib/speex_aec.c b/lib/speex_aec.c index d5d7ee6b..42e7d7cf 100644 --- a/lib/speex_aec.c +++ b/lib/speex_aec.c @@ -8,14 +8,15 @@ // - дрейф независимых потоков: переполнение линии → отбрасываем старый кадр, // недозаполнение (старт/underflow) → passthrough без канселлера. // -// Захват и рендер приходят чанками разного размера (480/960), поэтому обе -// стороны накапливаются до полного кадра (frame_samples) внутри обёртки. +// Рендер принимает чанки любого размера; захват — ровно один полный кадр. +// Частые аномалии учитываются счётчиками; владелец публикует сводку вне audio callback. #include "speex_aec.h" #include "debug_config.h" #include "mem.h" #include +#include #include @@ -30,8 +31,6 @@ struct speex_aec { int count; /* кадров в линии (0..depth) */ int16_t* play_acc; /* аккумулятор рендера до кадра */ int play_len; - int16_t* cap_acc; /* аккумулятор захвата до кадра */ - int cap_len; uint32_t overruns; /* отброшено рендер-кадров (дрейф: рендер быстрее) */ uint32_t underruns; /* passthrough-кадров (дрейф: рендер медленнее/старт) */ }; @@ -42,10 +41,6 @@ static void aec_enqueue(speex_aec_t* a, const int16_t* frame) { a->head = (a->head + 1) % a->depth; a->count--; a->overruns++; - if (a->overruns == 1 || (a->overruns & 0xff) == 0) { - DEBUG_WARN(DEBUG_CATEGORY_AEC, - "%s: delay line overflow (render faster than capture) overruns=%u", AEC_ID, a->overruns); - } } memcpy(a->line + (size_t)((a->head + a->count) % a->depth) * a->frame_samples, frame, (size_t)a->frame_samples * sizeof(int16_t)); @@ -56,7 +51,8 @@ speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_sam speex_aec_t* a; int rate; - if (sample_rate <= 0 || frame_samples <= 0 || filter_samples < frame_samples) { + if (sample_rate <= 0 || frame_samples <= 0 || filter_samples < frame_samples || + (delay_frames > 0 && delay_frames > INT_MAX / frame_samples)) { DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: bad args rate=%d frame=%d filter=%d delay=%d", AEC_ID, sample_rate, frame_samples, filter_samples, delay_frames); return NULL; @@ -69,6 +65,11 @@ speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_sam } a->st = speex_echo_state_init(frame_samples, filter_samples); + if (!a->st) { + DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: echo state allocation failed", AEC_ID); + u_free(a); + return NULL; + } rate = sample_rate; if (speex_echo_ctl(a->st, SPEEX_ECHO_SET_SAMPLING_RATE, &rate) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: SET_SAMPLING_RATE(%d) failed", AEC_ID, sample_rate); @@ -81,12 +82,10 @@ speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_sam a->depth = delay_frames > 0 ? delay_frames : 1; a->line = (int16_t*)u_calloc((uint32_t)(a->depth * frame_samples), sizeof(int16_t)); a->play_acc = (int16_t*)u_calloc((uint32_t)frame_samples, sizeof(int16_t)); - a->cap_acc = (int16_t*)u_calloc((uint32_t)frame_samples, sizeof(int16_t)); - if (!a->line || !a->play_acc || !a->cap_acc) { + if (!a->line || !a->play_acc) { DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: OOM for buffers", AEC_ID); if (a->line) u_free(a->line); if (a->play_acc) u_free(a->play_acc); - if (a->cap_acc) u_free(a->cap_acc); speex_echo_state_destroy(a->st); u_free(a); return NULL; @@ -102,7 +101,6 @@ void speex_aec_destroy(speex_aec_t* a) { speex_echo_state_destroy(a->st); u_free(a->line); u_free(a->play_acc); - u_free(a->cap_acc); u_free(a); } @@ -112,7 +110,6 @@ void speex_aec_reset(speex_aec_t* a) { a->head = 0; a->count = 0; a->play_len = 0; - a->cap_len = 0; DEBUG_INFO(DEBUG_CATEGORY_AEC, "%s: reset (filter + delay line)", AEC_ID); } @@ -122,48 +119,50 @@ void speex_aec_feed_playback(speex_aec_t* a, const int16_t* pcm, int count) { if (!a || !pcm || count <= 0) return; fs = a->frame_samples; - take = count; - if (a->play_len + count > fs) take = fs - a->play_len; - memcpy(a->play_acc + a->play_len, pcm, (size_t)take * sizeof(int16_t)); - a->play_len += take; - if (a->play_len < fs) return; - - aec_enqueue(a, a->play_acc); - a->play_len = 0; + while (count > 0) { + take = fs - a->play_len; + if (take > count) take = count; + memcpy(a->play_acc + a->play_len, pcm, (size_t)take * sizeof(int16_t)); + a->play_len += take; + pcm += take; + count -= take; + if (a->play_len == fs) { + aec_enqueue(a, a->play_acc); + a->play_len = 0; + } + } } int speex_aec_process_capture(speex_aec_t* a, const int16_t* pcm, int count, int16_t* out) { - int fs, take; + int fs; if (!a || !pcm || !out || count <= 0) return 0; fs = a->frame_samples; - take = count; - if (a->cap_len + count > fs) take = fs - a->cap_len; - memcpy(a->cap_acc + a->cap_len, pcm, (size_t)take * sizeof(int16_t)); - a->cap_len += take; - if (a->cap_len < fs) return 0; + if (count != fs) { + DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: capture requires one frame samples=%d expected=%d", AEC_ID, count, fs); + return 0; + } if (a->count == a->depth) { const int16_t* ref = a->line + (size_t)a->head * fs; - speex_echo_cancellation(a->st, a->cap_acc, ref, out); + speex_echo_cancellation(a->st, pcm, ref, out); a->head = (a->head + 1) % a->depth; a->count--; } else { /* линия не наполнена (старт или рендер отстаёт): passthrough без канселлера */ - if (a->underruns == 0 || (a->underruns & 0xff) == 0) { - DEBUG_WARN(DEBUG_CATEGORY_AEC, - "%s: delay line underflow (render slower than capture) underruns=%u fill=%d/%d", - AEC_ID, a->underruns, a->count, a->depth); - } - memcpy(out, a->cap_acc, (size_t)fs * sizeof(int16_t)); + memcpy(out, pcm, (size_t)fs * sizeof(int16_t)); a->underruns++; } - a->cap_len = 0; return fs; } int speex_aec_delay_fill(const speex_aec_t* a) { return a ? a->count : 0; } + +void speex_aec_get_stats(const speex_aec_t* a, uint32_t* overruns, uint32_t* underruns) { + *overruns = a ? a->overruns : 0; + *underruns = a ? a->underruns : 0; +} diff --git a/lib/speex_aec.h b/lib/speex_aec.h index 26462ed6..10954f99 100644 --- a/lib/speex_aec.h +++ b/lib/speex_aec.h @@ -16,7 +16,7 @@ * * Дрейф двух независимых потоков (capture/play на Android) компенсируется * ограниченной глубиной линии: переполнение → отбрасываем старый рендер-кадр, - * недозаполнение → passthrough без канселлера (с подробным логом категории "aec"). + * недозаполнение → passthrough без канселлера; счётчики публикует владелец вне callback. * * Зависимости: SpeexDSP (mdf.c/fftwrap.c/kiss_fft*) из lib/speexdsp, флаги * FLOATING_POINT + USE_KISS_FFT. Собирается всегда (без внешних зависимостей). @@ -39,7 +39,7 @@ typedef struct speex_aec speex_aec_t; * sample_rate — 48000 (также 8000/16000/32000); * frame_samples — сэмплов в кадре (960 = 20 мс @48 кГц); * filter_samples— длина эхо-хвоста в сэмплах (14400 = 300 мс, кратно кадру); - * delay_frames — задержка рендер→захват в кадрах (>=0; 0 = без линии задержки). + * delay_frames — глубина линии рендер→захват в кадрах (значения <=0 приводятся к 1). * Возвращает NULL при ошибке (лог категории "aec"). */ speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_samples, int delay_frames); @@ -57,15 +57,16 @@ void speex_aec_reset(speex_aec_t* aec); void speex_aec_feed_playback(speex_aec_t* aec, const int16_t* pcm, int count); /** - * Обработать захват (микрофон). Накопление до кадра внутри. Когда кадр собран — - * подавляет эхо (с учётом линии задержки) и пишет результат в out. - * Возвращает число сэмплов, записанных в out (frame_samples, если кадр готов; - * 0 — кадр ещё накапливается). out не должен алиаситься с pcm. + * Обработать ровно один кадр захвата (count == frame_samples). + * Возвращает frame_samples при успехе, 0 при ошибке. out вмещает полный кадр + * и не должен алиаситься с pcm. Накопитель аппаратных чанков принадлежит I/O-движку. */ int speex_aec_process_capture(speex_aec_t* aec, const int16_t* pcm, int count, int16_t* out); /* Актуальная глубина линии задержки (кадров) — для диагностики дрейфа. */ int speex_aec_delay_fill(const speex_aec_t* aec); +/* Под той же блокировкой, что feed/process; выводить снимок вне callback. */ +void speex_aec_get_stats(const speex_aec_t* aec, uint32_t* overruns, uint32_t* underruns); #ifdef __cplusplus } diff --git a/lib/u_async.c b/lib/u_async.c index 520a3a3e..d93bf329 100644 --- a/lib/u_async.c +++ b/lib/u_async.c @@ -63,6 +63,7 @@ struct socket_node { int enable_read; // 1 if read monitoring is enabled int enable_write; // 1 if write monitoring is enabled uint32_t gen; // generation counter for epoll event validation + uint32_t poll_gen; // Поколение на момент poll/select, до пользовательских callbacks }; // Array-based socket management for O(1) operations @@ -139,23 +140,32 @@ static int socket_array_add_internal(struct socket_array* sa, int fd, socket_t s socket_callback_t except_cbk, const char* name, void* user_data) { if (!sa || fd < 0) return -1; if (fd >= sa->capacity) { - // Need to resize - double the capacity - int new_capacity = sa->capacity * 2; - if (fd >= new_capacity) new_capacity = fd + 16; // Ensure enough space - - struct socket_node* new_sockets = u_realloc(sa->sockets, new_capacity * sizeof(struct socket_node)); - int* new_fd_to_index = u_realloc(sa->fd_to_index, new_capacity * sizeof(int)); - int* new_index_to_fd = u_realloc(sa->index_to_fd, new_capacity * sizeof(int)); - int* new_active_indices = u_realloc(sa->active_indices, new_capacity * sizeof(int)); + if (fd > INT_MAX - 16) { + DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "socket array: fd=%d exceeds supported range", fd); + return -1; + } + int new_capacity = sa->capacity <= INT_MAX / 2 ? sa->capacity * 2 : INT_MAX; + if (fd >= new_capacity) new_capacity = fd + 16; + + /* Сначала выделяем новый набор целиком: отказ не затрагивает рабочие массивы. */ + struct socket_node* new_sockets = u_calloc(new_capacity, sizeof(struct socket_node)); + int* new_fd_to_index = u_calloc(new_capacity, sizeof(int)); + int* new_index_to_fd = u_calloc(new_capacity, sizeof(int)); + int* new_active_indices = u_calloc(new_capacity, sizeof(int)); if (!new_sockets || !new_fd_to_index || !new_index_to_fd || !new_active_indices) { - // Allocation failed + DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "socket array growth failed: fd=%d capacity=%d requested=%d", fd, sa->capacity, new_capacity); u_free(new_sockets); u_free(new_fd_to_index); u_free(new_index_to_fd); u_free(new_active_indices); return -1; } + + memcpy(new_sockets, sa->sockets, sa->capacity * sizeof(*new_sockets)); + memcpy(new_fd_to_index, sa->fd_to_index, sa->capacity * sizeof(*new_fd_to_index)); + memcpy(new_index_to_fd, sa->index_to_fd, sa->capacity * sizeof(*new_index_to_fd)); + memcpy(new_active_indices, sa->active_indices, sa->capacity * sizeof(*new_active_indices)); // Initialize new elements for (int i = sa->capacity; i < new_capacity; i++) { @@ -166,6 +176,8 @@ static int socket_array_add_internal(struct socket_array* sa, int fd, socket_t s new_sockets[i].active = 0; } + u_free(sa->sockets); u_free(sa->fd_to_index); u_free(sa->index_to_fd); u_free(sa->active_indices); + DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "socket array grown: capacity=%d->%d fd=%d", sa->capacity, new_capacity, fd); sa->sockets = new_sockets; sa->fd_to_index = new_fd_to_index; sa->index_to_fd = new_index_to_fd; @@ -177,7 +189,10 @@ static int socket_array_add_internal(struct socket_array* sa, int fd, socket_t s int index; int existing = sa->fd_to_index[fd]; if (existing != -1) { - if (sa->sockets[existing].active) return -1; // FD уже занят активной нодой — ошибка + if (sa->sockets[existing].active) { + DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "socket already registered: fd=%d name=%s", fd, sa->sockets[existing].name); + return -1; + } index = existing; // переиспользуем неактивный слот (после socket_array_remove) } else { // Find first free slot @@ -204,6 +219,7 @@ static int socket_array_add_internal(struct socket_array* sa, int fd, socket_t s sa->sockets[index].enable_read = (read_cbk_fd != NULL || read_cbk_sock != NULL) ? 1 : 0; sa->sockets[index].enable_write = (write_cbk_fd != NULL || write_cbk_sock != NULL) ? 1 : 0; sa->sockets[index].gen = ++sa->gen_counter; + sa->sockets[index].poll_gen = 0; sa->index_to_fd[index] = fd; sa->active_indices[sa->count] = index; // Add to active list @@ -256,8 +272,7 @@ static int socket_array_remove(struct socket_array* sa, int fd) { sa->sockets[index].name = NULL; sa->sockets[index].enable_read = 0; sa->sockets[index].enable_write = 0; - // fd_to_index[fd] сохраняем — stale epoll события найдут неактивную ноду и пропустятся. - // При переиспользовании fd, socket_array_add_internal перезапишет этот же слот. + sa->fd_to_index[fd] = -1; sa->index_to_fd[index] = -1; // Remove from active_indices by swapping with last element @@ -328,22 +343,8 @@ static void timeout_node_free_callback(void* user_data, void* data) { // Helper to get current time static void get_current_time(struct timeval* tv) { -#ifdef _WIN32 - // Для Windows используем GetTickCount или другие механизмы - FILETIME ft; - GetSystemTimeAsFileTime(&ft); - - ULARGE_INTEGER ul; - ul.LowPart = ft.dwLowDateTime; - ul.HighPart = ft.dwHighDateTime; - - // Конвертируем 100-наносекундные интервалы в секунды и микросекунды - tv->tv_sec = (long)((ul.QuadPart - 116444736000000000ULL) / 10000000ULL); - tv->tv_usec = (long)((ul.QuadPart % 10000000ULL) / 10); -#else - // Для Linux и других Unix-подобных систем используем gettimeofday - gettimeofday(tv, NULL); -#endif + uint64_t us = get_time_us(); + tv->tv_sec = us / 1000000; tv->tv_usec = us % 1000000; } #ifdef _WIN32 @@ -377,7 +378,8 @@ uint64_t get_time_us(void) { LARGE_INTEGER freq, count; QueryPerformanceFrequency(&freq); QueryPerformanceCounter(&count); - return (uint64_t)(count.QuadPart * 1000000ULL / freq.QuadPart); + uint64_t ticks = (uint64_t)count.QuadPart, hz = (uint64_t)freq.QuadPart; + return ticks / hz * 1000000ULL + ticks % hz * 1000000ULL / hz; } #else uint64_t get_time_us(void) { @@ -433,6 +435,7 @@ static void process_posted_tasks(struct UASYNC* ua) { } } +#ifdef _WIN32 // Unified wakeup handler (drain + execute posted callbacks) static void handle_wakeup(struct UASYNC* ua) { if (!ua || !ua->wakeup_initialized) return; @@ -453,6 +456,8 @@ static void handle_wakeup(struct UASYNC* ua) { } +#endif + // Helper to add timeval: tv += dt (timebase units) static void timeval_add_tb(struct timeval* tv, int dt) { tv->tv_usec += (dt % 10000) * 100; @@ -463,15 +468,14 @@ static void timeval_add_tb(struct timeval* tv, int dt) { // Convert timeval to milliseconds. Sub-ms values (>0, <1ms) round up to 1ms // to avoid 0ms timeout → busy-loop in epoll_wait/select. static uint64_t timeval_to_ms(const struct timeval* tv) { - uint64_t ms = (uint64_t)tv->tv_sec * 1000ULL + (uint64_t)tv->tv_usec / 1000ULL; - if (ms == 0 && tv->tv_usec > 0) ms = 1; - return ms; + return (uint64_t)tv->tv_sec * 1000ULL + (uint64_t)(tv->tv_usec + 999) / 1000ULL; } // Process immediate_queue (deferred callbacks via uasync_call_soon) void process_immediate_queue(struct UASYNC* ua) { + struct timeout_node* batch_tail = ua->immediate_queue_tail; while (ua->immediate_queue_head) { struct timeout_node* node = ua->immediate_queue_head; ua->immediate_queue_head = node->next; @@ -484,11 +488,12 @@ void process_immediate_queue(struct UASYNC* ua) { if (node && node->ua) node->ua->timer_free_count++; memory_pool_free(ua->timeout_pool, node); + if (node == batch_tail) break; } } void uasync_drain_immediate(struct UASYNC* ua) { - if (ua) process_immediate_queue(ua); + if (ua) while (ua->immediate_queue_head) process_immediate_queue(ua); } // Process expired timeouts with safe cancellation @@ -499,9 +504,7 @@ static void process_timeouts(struct UASYNC* ua) { if (!ua->timeout_heap) return; - struct timeval now_tv; - get_current_time(&now_tv); - uint64_t now_ms = timeval_to_ms(&now_tv); + uint64_t now_ms = get_time_us() / 1000; while (1) { TimeoutEntry entry; @@ -509,13 +512,28 @@ static void process_timeouts(struct UASYNC* ua) { if (entry.expiration > now_ms) break; // Pop the expired timeout - timeout_heap_pop(ua->timeout_heap, &entry); + if (timeout_heap_pop(ua->timeout_heap, &entry) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_TIMERS, "expired timer disappeared between peek/pop ua=%p heap_size=%zu", ua, ua->timeout_heap->size); + break; + } struct timeout_node* node = (struct timeout_node*)entry.data; + if (node && memory_pool_is_freed(ua->timeout_pool, node)) { + DEBUG_ERROR(DEBUG_CATEGORY_TIMERS, "expired timer already freed before callback: ua=%p node=%p expiration=%llu", + ua, node, (unsigned long long)entry.expiration); + memory_pool_free(ua->timeout_pool, node); /* Подробный лог пула и остановка на повреждении. */ + } + const char* name = node ? node->name : "?"; + timeout_callback_t callback = node ? node->callback : NULL; + void* arg = node ? node->arg : NULL; if (node && node->callback) { - DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "timer→%s expired", node->name); + DEBUG_DEBUG(DEBUG_CATEGORY_TIMERS, "timer expired: name=%s node=%p callback=%p arg=%p expiration=%llu", + name, node, (void*)callback, arg, (unsigned long long)entry.expiration); node->callback(node->arg); } + if (node && memory_pool_is_freed(ua->timeout_pool, node)) + DEBUG_ERROR(DEBUG_CATEGORY_TIMERS, "timer freed inside callback: ua=%p name=%s node=%p callback=%p arg=%p", + ua, name, node, (void*)callback, arg); // Always u_free the node after processing if (node && node->ua) { @@ -541,9 +559,7 @@ static void get_next_timeout(struct UASYNC* ua, struct timeval* tv) { return; } - struct timeval now_tv; - get_current_time(&now_tv); - uint64_t now_ms = timeval_to_ms(&now_tv); + uint64_t now_ms = get_time_us() / 1000; if (entry.expiration <= now_ms) { struct timeout_node* rn = (struct timeout_node*)entry.data; @@ -591,7 +607,7 @@ void* uasync_set_timeout(struct UASYNC* ua, int timeout_tb, void* arg, timeout_c get_current_time(&now); uint64_t now_tb = (uint64_t)now.tv_sec * 10000ULL + (uint64_t)now.tv_usec / 100ULL; timeval_add_tb(&now, timeout_tb); - node->expiration_ms = timeval_to_ms(&now); + node->expiration_ms = timeout_tb ? timeval_to_ms(&now) : (uint64_t)now.tv_sec * 1000 + now.tv_usec / 1000; if (name && strncmp(name, "ncd_connect", 11) == 0) { uint64_t exp_tb = (uint64_t)now.tv_sec * 10000ULL + (uint64_t)now.tv_usec / 100ULL; DEBUG_DEBUG(DEBUG_CATEGORY_TIMERS, "[uasync] set_timeout: name=%s tb=%d now_tb=%llu exp_tb=%llu exp_ms=%llu delta_tb=%llu", @@ -683,9 +699,39 @@ err_t uasync_cancel_timeout(struct UASYNC* ua, void* t_id) { } +#ifndef _WIN32 +// Память для poll резервируется до регистрации, когда API ещё может вернуть ошибку. +static int reserve_poll_fds(struct UASYNC* ua, int required) { + if (ua->use_epoll) return 0; + const int limit = INT_MAX / sizeof(struct pollfd); + if (required < 0 || required > limit) { + DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "poll array capacity overflow: required=%d limit=%d", required, limit); + return -1; + } + if (required <= ua->poll_fds_capacity) return 0; + int capacity = ua->poll_fds_capacity <= limit / 2 ? ua->poll_fds_capacity * 2 : limit; + if (capacity < 16) capacity = 16; + if (capacity < required) capacity = required; + struct pollfd* fds = u_realloc(ua->poll_fds, (size_t)capacity * sizeof(*fds)); + if (!fds) { + DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "poll array reserve failed: capacity=%d requested=%d required=%d", + ua->poll_fds_capacity, capacity, required); + return -1; + } + DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "poll array reserved: capacity=%d->%d required=%d", + ua->poll_fds_capacity, capacity, required); + ua->poll_fds = fds; + ua->poll_fds_capacity = capacity; + return 0; +} +#endif + // Instance version void* uasync_add_socket(struct UASYNC* ua, int fd, socket_callback_t read_cbk, socket_callback_t write_cbk, socket_callback_t except_cbk, const char* name, void* user_data) { if (!ua || fd < 0) return NULL; +#ifndef _WIN32 + if (reserve_poll_fds(ua, ua->sockets->count + 2) < 0) return NULL; // Новый fd и wakeup. +#endif int index = socket_array_add(ua->sockets, fd, read_cbk, write_cbk, except_cbk, name, user_data); if (index < 0) return NULL; @@ -700,10 +746,12 @@ void* uasync_add_socket(struct UASYNC* ua, int fd, socket_callback_t read_cbk, s ev.events = 0; if (read_cbk) ev.events |= EPOLLIN; if (write_cbk) ev.events |= EPOLLOUT; + if (except_cbk) ev.events |= EPOLLPRI; // Embed gen in upper 32 bits of data.u64 for stale-event detection ev.data.u64 = ((uint64_t)ua->sockets->sockets[index].gen << 32) | (uint32_t)fd; if (epoll_ctl(ua->epoll_fd, EPOLL_CTL_ADD, fd, &ev) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "epoll add failed: fd=%d name=%s error=%s", fd, name ? name : "?", strerror(errno)); // Failed to add to epoll - remove from socket array and return error socket_array_remove(ua->sockets, fd); ua->socket_alloc_count--; @@ -751,7 +799,10 @@ err_t uasync_remove_socket(struct UASYNC* ua, void* s_id) { void* uasync_add_socket_t(struct UASYNC* ua, socket_t sock, socket_t_callback_t read_cbk, socket_t_callback_t write_cbk, socket_t_callback_t except_cbk, const char* name, void* user_data) { if (!ua || sock == SOCKET_INVALID) return NULL; - +#ifndef _WIN32 + if (reserve_poll_fds(ua, ua->sockets->count + 2) < 0) return NULL; +#endif + int index = socket_array_add_socket_t(ua->sockets, sock, read_cbk, write_cbk, (socket_callback_t)except_cbk, name, user_data); if (index < 0) return NULL; @@ -765,6 +816,7 @@ void* uasync_add_socket_t(struct UASYNC* ua, socket_t sock, socket_t_callback_t ev.events = 0; if (read_cbk) ev.events |= EPOLLIN; if (write_cbk) ev.events |= EPOLLOUT; + if (except_cbk) ev.events |= EPOLLPRI; // On Windows, need to cast socket_t to int for epoll_ctl #ifdef _WIN32 int fd = (int)(intptr_t)sock; @@ -773,6 +825,7 @@ void* uasync_add_socket_t(struct UASYNC* ua, socket_t sock, socket_t_callback_t #endif ev.data.u64 = ((uint64_t)ua->sockets->sockets[index].gen << 32) | (uint32_t)fd; if (epoll_ctl(ua->epoll_fd, EPOLL_CTL_ADD, fd, &ev) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "epoll add failed: fd=%d name=%s error=%s", fd, name ? name : "?", strerror(errno)); socket_array_remove(ua->sockets, fd); ua->socket_alloc_count--; return NULL; @@ -820,102 +873,62 @@ err_t uasync_remove_socket_t(struct UASYNC* ua, socket_t sock) { return ERR_FAIL; } -err_t uasync_set_socket_read(struct UASYNC* ua, void* s_id, int enable) { +static err_t socket_set_monitoring(struct UASYNC* ua, void* s_id, int read_flag, int enable) { if (!ua || !s_id) return ERR_FAIL; - - int i = (int)(uintptr_t)s_id - 1; - if (i < 0 || i >= ua->sockets->capacity) return ERR_FAIL; - struct socket_node* node = &ua->sockets->sockets[i]; - if (!node->active || node->fd < 0) return ERR_FAIL; - + uintptr_t handle = (uintptr_t)s_id; + if (handle > (uintptr_t)ua->sockets->capacity) { + DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "socket monitoring: invalid handle=%p", s_id); + return ERR_FAIL; + } + struct socket_node* node = &ua->sockets->sockets[handle - 1]; + if (!node->active || node->fd < 0) { + DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "socket monitoring: inactive handle=%p", s_id); + return ERR_FAIL; + } int val = enable ? 1 : 0; - if (node->enable_read == val) return ERR_OK; - node->enable_read = val; - + int want_read = read_flag ? val : node->enable_read; + int want_write = read_flag ? node->enable_write : val; + if (want_read == node->enable_read && want_write == node->enable_write) return ERR_OK; #if HAS_EPOLL if (ua->use_epoll && ua->epoll_fd >= 0) { - struct epoll_event ev; - ev.events = 0; - if (node->type == SOCKET_NODE_TYPE_SOCK) { - if (node->read_cbk_sock && node->enable_read) ev.events |= EPOLLIN; - if (node->write_cbk_sock && node->enable_write) ev.events |= EPOLLOUT; - } else { - if (node->read_cbk && node->enable_read) ev.events |= EPOLLIN; - if (node->write_cbk && node->enable_write) ev.events |= EPOLLOUT; - } + struct epoll_event ev = {0}; + if ((node->read_cbk || node->read_cbk_sock) && want_read) ev.events |= EPOLLIN; + if ((node->write_cbk || node->write_cbk_sock) && want_write) ev.events |= EPOLLOUT; if (node->except_cbk) ev.events |= EPOLLPRI; - ev.data.u64 = ((uint64_t)node->gen << 32) | (uint32_t)(node->type == SOCKET_NODE_TYPE_SOCK ? (int)node->sock : node->fd); -#ifdef _WIN32 - int efd = (int)(intptr_t)(node->type == SOCKET_NODE_TYPE_SOCK ? node->sock : node->fd); -#else - int efd = (node->type == SOCKET_NODE_TYPE_SOCK ? (int)node->sock : node->fd); -#endif - epoll_ctl(ua->epoll_fd, EPOLL_CTL_MOD, efd, &ev); + ev.data.u64 = ((uint64_t)node->gen << 32) | (uint32_t)node->fd; + if (epoll_ctl(ua->epoll_fd, EPOLL_CTL_MOD, node->fd, &ev) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "epoll modify failed: fd=%d read=%d write=%d error=%s", + node->fd, want_read, want_write, strerror(errno)); + return ERR_FAIL; + } } #endif - + node->enable_read = want_read; node->enable_write = want_write; ua->poll_fds_dirty = 1; + DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "socket monitoring: fd=%d read=%d write=%d", node->fd, want_read, want_write); return ERR_OK; } -err_t uasync_set_socket_write(struct UASYNC* ua, void* s_id, int enable) { - if (!ua || !s_id) return ERR_FAIL; - - int i = (int)(uintptr_t)s_id - 1; - if (i < 0 || i >= ua->sockets->capacity) return ERR_FAIL; - struct socket_node* node = &ua->sockets->sockets[i]; - if (!node->active || node->fd < 0) return ERR_FAIL; - - int val = enable ? 1 : 0; - if (node->enable_write == val) return ERR_OK; - node->enable_write = val; - -#if HAS_EPOLL - if (ua->use_epoll && ua->epoll_fd >= 0) { - struct epoll_event ev; - ev.events = 0; - if (node->type == SOCKET_NODE_TYPE_SOCK) { - if (node->read_cbk_sock && node->enable_read) ev.events |= EPOLLIN; - if (node->write_cbk_sock && node->enable_write) ev.events |= EPOLLOUT; - } else { - if (node->read_cbk && node->enable_read) ev.events |= EPOLLIN; - if (node->write_cbk && node->enable_write) ev.events |= EPOLLOUT; - } - if (node->except_cbk) ev.events |= EPOLLPRI; - ev.data.u64 = ((uint64_t)node->gen << 32) | (uint32_t)(node->type == SOCKET_NODE_TYPE_SOCK ? (int)node->sock : node->fd); -#ifdef _WIN32 - int efd = (int)(intptr_t)(node->type == SOCKET_NODE_TYPE_SOCK ? node->sock : node->fd); -#else - int efd = (node->type == SOCKET_NODE_TYPE_SOCK ? (int)node->sock : node->fd); -#endif - epoll_ctl(ua->epoll_fd, EPOLL_CTL_MOD, efd, &ev); - } -#endif +err_t uasync_set_socket_read(struct UASYNC* ua, void* s_id, int enable) { + return socket_set_monitoring(ua, s_id, 1, enable); +} - ua->poll_fds_dirty = 1; - return ERR_OK; +err_t uasync_set_socket_write(struct UASYNC* ua, void* s_id, int enable) { + return socket_set_monitoring(ua, s_id, 0, enable); } -// Helper function to rebuild cached pollfd array -static void rebuild_poll_fds(struct UASYNC* ua) { - if (!ua || !ua->sockets) return; - +#ifndef _WIN32 +// Заполняет уже зарезервированный массив, не выделяя память в event loop. +static int rebuild_poll_fds(struct UASYNC* ua) { int socket_count = ua->sockets->count; int wakeup_fd_present = ua->wakeup_initialized && ua->wakeup_pipe[0] >= 0; int total_fds = socket_count + wakeup_fd_present; - - // Ensure poll_fds capacity is sufficient - if (total_fds > ua->poll_fds_capacity) { - int new_capacity = total_fds * 2; - if (new_capacity < 16) new_capacity = 16; - - struct pollfd* new_poll_fds = u_realloc(ua->poll_fds, sizeof(struct pollfd) * new_capacity); - if (!new_poll_fds) return; // Keep old allocation on failure - - ua->poll_fds = new_poll_fds; - ua->poll_fds_capacity = new_capacity; + if (!ua->poll_fds || total_fds > ua->poll_fds_capacity) { + DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "poll array invariant violated: fds=%p capacity=%d required=%d", + ua->poll_fds, ua->poll_fds_capacity, total_fds); + return -1; } - + int idx = 0; // Add wakeup fd first if present @@ -931,18 +944,7 @@ static void rebuild_poll_fds(struct UASYNC* ua) { int socket_array_idx = ua->sockets->active_indices[i]; struct socket_node* cur = &ua->sockets->sockets[socket_array_idx]; - // Handle socket_t vs int fd - if (cur->type == SOCKET_NODE_TYPE_SOCK) { - // socket_t - cast to int for pollfd -#ifdef _WIN32 - ua->poll_fds[idx].fd = (int)(intptr_t)cur->sock; -#else - ua->poll_fds[idx].fd = cur->sock; -#endif - } else { - // Regular fd - ua->poll_fds[idx].fd = cur->fd; - } + ua->poll_fds[idx].fd = cur->fd; ua->poll_fds[idx].events = 0; ua->poll_fds[idx].revents = 0; @@ -953,7 +955,6 @@ static void rebuild_poll_fds(struct UASYNC* ua) { if (cur->read_cbk && cur->enable_read) ua->poll_fds[idx].events |= POLLIN; if (cur->write_cbk && cur->enable_write) ua->poll_fds[idx].events |= POLLOUT; } - if (cur->write_cbk && cur->enable_write) ua->poll_fds[idx].events |= POLLOUT; if (cur->except_cbk) ua->poll_fds[idx].events |= POLLPRI; idx++; @@ -961,81 +962,49 @@ static void rebuild_poll_fds(struct UASYNC* ua) { ua->poll_fds_count = total_fds; ua->poll_fds_dirty = 0; + return 0; } +#endif // Process events from epoll (Linux only) #if HAS_EPOLL static void process_epoll_events(struct UASYNC* ua, struct epoll_event* events, int n_events) { for (int i = 0; i < n_events; i++) { - // Wakeup fd detected by data.fd < 0 (lower 32 bits of data.u64 = -1) if (events[i].data.fd < 0) { - DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "wakeup→pipe"); - if (events[i].events & EPOLLIN) { drain_wakeup_pipe(ua); } + if (events[i].events & EPOLLIN) drain_wakeup_pipe(ua); process_posted_tasks(ua); continue; } - int fd = (int)(events[i].data.u64 & 0xFFFFFFFF); - uint32_t ev_gen = (uint32_t)(events[i].data.u64 >> 32); - + uint32_t gen = (uint32_t)(events[i].data.u64 >> 32); + uint32_t flags = events[i].events; struct socket_node* node = socket_array_get(ua->sockets, fd); - if (!node || !node->active) continue; - if (node->gen != ev_gen) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "EPOLL STALE fd=%d ev_gen=%u node_gen=%u — skipped", fd, ev_gen, node->gen); continue; } - int r = (events[i].events & EPOLLIN) ? 1 : 0; - int w = (events[i].events & EPOLLOUT) ? 1 : 0; - int e = (events[i].events & (EPOLLERR | EPOLLHUP)) ? 1 : 0; - DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "epoll→fd=%d r=%d w=%d e=%d", fd, r, w, e); - int local_fd = node->fd; - socket_t local_sock = node->sock; - int local_type = node->type; - void* local_ud = node->user_data; - socket_callback_t local_except = node->except_cbk; - socket_callback_t local_read = node->read_cbk; - socket_callback_t local_write = node->write_cbk; - socket_t_callback_t local_read_sock = node->read_cbk_sock; - socket_t_callback_t local_write_sock = node->write_cbk_sock; - - /* Read readiness BEFORE error — avoid losing data on combined IN+HUP events */ - if (events[i].events & EPOLLIN) { - if (local_type == SOCKET_NODE_TYPE_SOCK) { - if (local_read_sock) { - local_read_sock(local_sock, local_ud); - } - } else { - if (local_read) { - local_read(local_fd, local_ud); - } - } + if (!node || node->gen != gen) { + DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "stale epoll event skipped: fd=%d generation=%u flags=0x%x", fd, gen, flags); + continue; } - - /* Write readiness BEFORE error — flush pending writes before handling HUP */ - if (events[i].events & EPOLLOUT) { - if (local_type == SOCKET_NODE_TYPE_SOCK) { - if (local_write_sock) { - local_write_sock(local_sock, local_ud); - } - } else { - if (local_write) { - local_write(local_fd, local_ud); - } - } + DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "epoll event: fd=%d generation=%u flags=0x%x", fd, gen, flags); + if ((flags & EPOLLIN) && node->enable_read) { + if (node->type == SOCKET_NODE_TYPE_SOCK) { + if (node->read_cbk_sock) node->read_cbk_sock(node->sock, node->user_data); + } else if (node->read_cbk) node->read_cbk(node->fd, node->user_data); } - - /* Check for error conditions LAST — I/O handlers drain/process data first */ - if (events[i].events & (EPOLLERR | EPOLLHUP)) { - if (local_except) { - local_except(local_fd, local_ud); - } else { - struct socket_node* n = socket_array_get(ua->sockets, fd); - if (!n) n = socket_array_get_by_sock(ua->sockets, fd); - if (n && n->active && n->gen == ev_gen) { - DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "socket HUP/ERR without except handler: fd=%d name='%s' type=%s r=%p w=%p — unregister", - fd, n->name ? n->name : "?", - n->type == SOCKET_NODE_TYPE_SOCK ? "SOCK" : "FD", - (void*)(n->type == SOCKET_NODE_TYPE_SOCK ? (void*)n->read_cbk_sock : (void*)n->read_cbk), - (void*)(n->type == SOCKET_NODE_TYPE_SOCK ? (void*)n->write_cbk_sock : (void*)n->write_cbk)); - socket_force_unregister(ua, fd); - } + /* Callback может удалить/заменить сокет, отключить write или переместить массив. */ + node = socket_array_get(ua->sockets, fd); + if (!node || node->gen != gen) continue; + if ((flags & EPOLLOUT) && node->enable_write) { + if (node->type == SOCKET_NODE_TYPE_SOCK) { + if (node->write_cbk_sock) node->write_cbk_sock(node->sock, node->user_data); + } else if (node->write_cbk) node->write_cbk(node->fd, node->user_data); + } + node = socket_array_get(ua->sockets, fd); + if (!node || node->gen != gen) continue; + if (flags & (EPOLLERR | EPOLLHUP | EPOLLPRI)) { + if (node->except_cbk) node->except_cbk(node->fd, node->user_data); + else if (flags & (EPOLLERR | EPOLLHUP)) { + DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "socket HUP/ERR without except handler: fd=%d name=%s flags=0x%x — unregister", + fd, node->name, flags); + socket_force_unregister(ua, fd); } } } @@ -1055,56 +1024,21 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) { (unsigned long long)(now_us_poll_entry - ua->last_poll_exit_us)); } - if (timeout_tb < 0) timeout_tb = -1; - else if (timeout_tb == 0) timeout_tb = 0; - + // После stop ещё разрешён неблокирующий проход для teardown и удаления отменённых таймеров. + if (__atomic_load_n(&ua->stop, __ATOMIC_ACQUIRE)) timeout_tb = 0; struct timeval next_timeout; get_next_timeout(ua, &next_timeout); - - struct timeval req_timeout = {0}; - if (timeout_tb >= 0) { - req_timeout.tv_sec = timeout_tb / 10000; - req_timeout.tv_usec = (timeout_tb % 10000) * 100; - } - - struct timeval poll_timeout; - if (timeout_tb < 0) { - poll_timeout = next_timeout; - } else { - if (next_timeout.tv_sec < req_timeout.tv_sec || - (next_timeout.tv_sec == req_timeout.tv_sec && next_timeout.tv_usec < req_timeout.tv_usec)) { - poll_timeout = next_timeout; - } else { - poll_timeout = req_timeout; - } + int timeout_ms = -1; + if (ua->timeout_heap->size) { + uint64_t next_ms = timeval_to_ms(&next_timeout); + timeout_ms = next_ms > INT_MAX ? INT_MAX : (int)next_ms; } - if (poll_timeout.tv_sec == 0 && poll_timeout.tv_usec == 0 && timeout_tb > 0) poll_timeout = req_timeout; - - int timeout_ms; - if (timeout_tb < 0 && (next_timeout.tv_sec > 0 || next_timeout.tv_usec > 0)) { - timeout_ms = (int)timeval_to_ms(&poll_timeout); - } else if (timeout_tb < 0) { - timeout_ms = -1; - } else { - timeout_ms = (int)timeval_to_ms(&poll_timeout); + if (timeout_tb >= 0) { + int requested_ms = timeout_tb / 10 + (timeout_tb % 10 != 0); + if (timeout_ms < 0 || requested_ms < timeout_ms) timeout_ms = requested_ms; } - + if (ua->immediate_queue_head) timeout_ms = 0; int socket_count = ua->sockets->count; - if (socket_count == 0 && timeout_ms == -1) { - if (ua->timeout_heap->size > 0) { - timeout_ms = (int)timeval_to_ms(&poll_timeout); - } else { - ua->last_poll_exit_us = get_time_us(); - return; - } - } - - /* Если ближайший таймер уже истёк (get_next_timeout вернул 0), но heap не пуст — - * опрашиваем немедленно (timeout_ms=0), иначе epoll_wait(-1) заблокирует навсегда, - * а process_timeouts (вызывается только после epoll_wait) никогда не запустится. */ - if (timeout_ms == -1 && ua->timeout_heap->size > 0) { - timeout_ms = 0; - } DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "poll(%d sockets, %zu timers, timeout=%dms)", socket_count, ua->timeout_heap->size, timeout_ms); @@ -1138,6 +1072,12 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) { } #endif + // Снимок поколений защищает также следующие сокеты в общей порции событий. + for (int i = 0; i < socket_count; i++) { + struct socket_node* node = &ua->sockets->sockets[ua->sockets->active_indices[i]]; + node->poll_gen = node->gen; + } + // Fallback to poll() for non-Linux or if epoll failed // Include wakeup pipe if initialized @@ -1197,7 +1137,7 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) { tv.tv_sec = timeout_ms / 1000; tv.tv_usec = (timeout_ms % 1000) * 1000; - int ret = select((int)max_fd + 1, &read_fds, &write_fds, &except_fds, &tv); + int ret = select((int)max_fd + 1, &read_fds, &write_fds, &except_fds, timeout_ms < 0 ? NULL : &tv); ua->last_poll_exit_us = get_time_us(); if (ret < 0) { @@ -1230,6 +1170,8 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) { /* Коллбэки могут удалить/добавить сокет (realloc массива) — перед * каждым последующим коллбэком пере-валидируем node по индексу. */ + if (node->gen != node->poll_gen) continue; + uint32_t generation = node->gen; int cb_called = 0; if (has_except) { @@ -1242,14 +1184,14 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) { if (has_read) { if (cb_called) { node = &ua->sockets->sockets[idx]; - if (!node->active) continue; + if (!node->active || node->gen != generation) continue; } if (node->type == SOCKET_NODE_TYPE_SOCK) { - if (node->read_cbk_sock) { + if (node->read_cbk_sock && node->enable_read) { node->read_cbk_sock(node->sock, node->user_data); } } else { - if (node->read_cbk) { + if (node->read_cbk && node->enable_read) { node->read_cbk(node->fd, node->user_data); } } @@ -1259,14 +1201,14 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) { if (has_write) { if (cb_called) { node = &ua->sockets->sockets[idx]; - if (!node->active) continue; + if (!node->active || node->gen != generation) continue; } if (node->type == SOCKET_NODE_TYPE_SOCK) { - if (node->write_cbk_sock) { + if (node->write_cbk_sock && node->enable_write) { node->write_cbk_sock(node->sock, node->user_data); } } else { - if (node->write_cbk) { + if (node->write_cbk && node->enable_write) { node->write_cbk(node->fd, node->user_data); } } @@ -1277,16 +1219,15 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) { #else // On non-Windows, use poll() - // Rebuild poll_fds if dirty or not allocated - if (ua->poll_fds_dirty || !ua->poll_fds) { - rebuild_poll_fds(ua); - } - - // Ensure poll_fds_count matches current state (in case sockets changed without dirty flag) - if (ua->poll_fds_count != total_fds) { - rebuild_poll_fds(ua); + if (ua->poll_fds_dirty || ua->poll_fds_count != total_fds || !ua->poll_fds) { + if (rebuild_poll_fds(ua) < 0) { + // Нарушение внутреннего инварианта не должно оставлять поток без wakeup. + process_posted_tasks(ua); + process_timeouts(ua); + return; + } } - + int ret = poll(ua->poll_fds, ua->poll_fds_count, timeout_ms); int saved_errno = errno; ua->last_poll_exit_us = get_time_us(); @@ -1319,20 +1260,21 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) { if (!node) { // Try by socket_t (in case this is a socket) node = socket_array_get_by_sock(ua->sockets, fd); } - if (!node) continue; // Socket may have been removed + if (!node || node->gen != node->poll_gen) continue; // Регистрация изменилась после poll. /* Коллбэки могут удалить/добавить сокет (realloc массива) — перед * каждым последующим коллбэком пере-валидируем node по fd. */ + uint32_t generation = node->gen; int cb_called = 0; /* Read readiness BEFORE error — avoid losing data on combined IN+ERR/HUP events */ if (ua->poll_fds[i].revents & POLLIN) { if (node->type == SOCKET_NODE_TYPE_SOCK) { - if (node->read_cbk_sock) { + if (node->read_cbk_sock && node->enable_read) { node->read_cbk_sock(node->sock, node->user_data); } } else { - if (node->read_cbk) { + if (node->read_cbk && node->enable_read) { node->read_cbk(node->fd, node->user_data); } } @@ -1344,14 +1286,14 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) { if (cb_called) { node = socket_array_get(ua->sockets, fd); if (!node) node = socket_array_get_by_sock(ua->sockets, fd); - if (!node) continue; + if (!node || node->gen != generation) continue; } if (node->type == SOCKET_NODE_TYPE_SOCK) { - if (node->write_cbk_sock) { + if (node->write_cbk_sock && node->enable_write) { node->write_cbk_sock(node->sock, node->user_data); } } else { - if (node->write_cbk) { + if (node->write_cbk && node->enable_write) { node->write_cbk(node->fd, node->user_data); } } @@ -1363,7 +1305,7 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) { if (cb_called) { node = socket_array_get(ua->sockets, fd); if (!node) node = socket_array_get_by_sock(ua->sockets, fd); - if (!node) continue; + if (!node || node->gen != generation) continue; } /* Treat as exceptional condition */ if (node->except_cbk) { @@ -1384,7 +1326,7 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) { if (cb_called) { node = socket_array_get(ua->sockets, fd); if (!node) node = socket_array_get_by_sock(ua->sockets, fd); - if (!node) continue; + if (!node || node->gen != generation) continue; } if (node->except_cbk) { node->except_cbk(node->fd, node->user_data); @@ -1409,11 +1351,7 @@ static void wakeup_read_callback_win(socket_t sock, void* arg) { (void)sock; // не нужен handle_wakeup((struct UASYNC*)arg); } -#else -static void wakeup_read_callback_posix(int fd, void* arg) { - (void)fd; - handle_wakeup((struct UASYNC*)arg); -} + #endif @@ -1424,191 +1362,98 @@ static void wakeup_read_callback_posix(int fd, void* arg) { // This ensures the wakeup is a selectable SOCKET. struct UASYNC* uasync_create(void) { - // Initialize socket platform (Winsock on Windows) - socket_platform_init(); - - struct UASYNC* ua = u_calloc(1, sizeof(struct UASYNC)); - if (!ua) return NULL; - - ua->timer_alloc_count = 0; - ua->timer_free_count = 0; - ua->socket_alloc_count = 0; - ua->socket_free_count = 0; - - ua->poll_fds = NULL; - ua->poll_fds_capacity = 0; - ua->poll_fds_count = 0; - ua->poll_fds_dirty = 1; - - ua->wakeup_pipe[0] = -1; - ua->wakeup_pipe[1] = -1; - ua->wakeup_initialized = 0; - ua->posted_tasks_head = NULL; - ua->immediate_queue_head = NULL; - ua->immediate_queue_tail = NULL; - ua->last_poll_exit_us = 0; - ua->running = 0; - - DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "Creating SA..."); - ua->sockets = socket_array_create(16); - DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "Creating SA1..."); - if (!ua->sockets) { - if (ua->wakeup_initialized) { -#ifdef _WIN32 - closesocket((SOCKET)(intptr_t)ua->wakeup_pipe[0]); - closesocket((SOCKET)(intptr_t)ua->wakeup_pipe[1]); -#else - close(ua->wakeup_pipe[0]); - close(ua->wakeup_pipe[1]); -#endif - } - u_free(ua); + if (socket_platform_init() != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "uasync socket platform initialization failed"); return NULL; } - - ua->timeout_heap = timeout_heap_create(16); - if (!ua->timeout_heap) { - socket_array_destroy(ua->sockets); - if (ua->wakeup_initialized) { -#ifdef _WIN32 - closesocket((SOCKET)(intptr_t)ua->wakeup_pipe[0]); - closesocket((SOCKET)(intptr_t)ua->wakeup_pipe[1]); -#else - close(ua->wakeup_pipe[0]); - close(ua->wakeup_pipe[1]); -#endif - } - u_free(ua); - return NULL; + struct UASYNC* ua = u_calloc(1, sizeof(*ua)); + if (!ua) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "uasync instance allocation failed"); + socket_platform_cleanup(); return NULL; } - - // Initialize timeout pool + ua->epoll_fd = -1; ua->wakeup_pipe[0] = ua->wakeup_pipe[1] = -1; ua->poll_fds_dirty = 1; + ua->sockets = socket_array_create(16); + if (!ua->sockets) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "uasync socket array allocation failed"); goto fail; } + ua->timeout_heap = timeout_heap_create(16); + if (!ua->timeout_heap) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "uasync timeout heap allocation failed"); goto fail; } ua->timeout_pool = memory_pool_init(sizeof(struct timeout_node), "timeout_pool"); - if (!ua->timeout_pool) { - timeout_heap_destroy(ua->timeout_heap); - socket_array_destroy(ua->sockets); - if (ua->wakeup_initialized) { -#ifdef _WIN32 - closesocket((SOCKET)(intptr_t)ua->wakeup_pipe[0]); - closesocket((SOCKET)(intptr_t)ua->wakeup_pipe[1]); -#else - close(ua->wakeup_pipe[0]); - close(ua->wakeup_pipe[1]); -#endif - } - u_free(ua); - return NULL; - } - - // Set callback to u_free timeout nodes and update counters + if (!ua->timeout_pool) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "uasync timeout pool allocation failed"); goto fail; } timeout_heap_set_free_callback(ua->timeout_heap, ua, timeout_node_free_callback); - - DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "Creating TH1..."); - // Initialize epoll on Linux - ua->epoll_fd = -1; - ua->use_epoll = 0; #if HAS_EPOLL ua->epoll_fd = epoll_create1(EPOLL_CLOEXEC); if (ua->epoll_fd >= 0) { ua->use_epoll = 1; DEBUG_INFO(DEBUG_CATEGORY_SYS, "Using epoll for socket monitoring"); - // Add wakeup pipe to epoll - if (ua->wakeup_initialized) { - struct epoll_event ev; - ev.events = EPOLLIN; - ev.data.fd = -1; // -1 fd indicates wakeup - if (epoll_ctl(ua->epoll_fd, EPOLL_CTL_ADD, ua->wakeup_pipe[0], &ev) < 0) { - DEBUG_WARN(DEBUG_CATEGORY_SYS, "Failed to add wakeup pipe to epoll: %s", strerror(errno)); - } - } - } else { - DEBUG_WARN(DEBUG_CATEGORY_SYS, "Failed to create epoll fd, falling back to poll: %s", strerror(errno)); - } + } else DEBUG_WARN(DEBUG_CATEGORY_SYS, "Failed to create epoll, falling back to poll: %s", strerror(errno)); #endif - - DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "Creating TH2..."); - #ifdef _WIN32 - // Windows: self-connected UDP socket for wakeup SOCKET r = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); SOCKET w = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + ua->wakeup_pipe[0] = (int)(intptr_t)r; ua->wakeup_pipe[1] = (int)(intptr_t)w; if (r == INVALID_SOCKET || w == INVALID_SOCKET) { - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Failed to create wakeup sockets"); - u_free(ua); - return NULL; + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Failed to create wakeup sockets: %d", WSAGetLastError()); goto fail; } - struct sockaddr_in addr = {0}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - addr.sin_port = 0; - + addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + int addr_len = sizeof(addr); if (bind(r, (struct sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR || - getsockname(r, (struct sockaddr*)&addr, &(int){sizeof(addr)}) == SOCKET_ERROR || + getsockname(r, (struct sockaddr*)&addr, &addr_len) == SOCKET_ERROR || connect(w, (struct sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { - closesocket(r); - closesocket(w); - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Wakeup socket setup failed: %d", WSAGetLastError()); - u_free(ua); - return NULL; + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Wakeup socket setup failed: %d", WSAGetLastError()); goto fail; } - - ua->wakeup_pipe[0] = (int)(intptr_t)r; - ua->wakeup_pipe[1] = (int)(intptr_t)w; - ua->wakeup_initialized = 1; - u_long mode = 1; - ioctlsocket(r, FIONBIO, &mode); - - // Register the read socket with uasync - uasync_add_socket_t(ua, r, wakeup_read_callback_win, NULL, NULL, "wakeup", ua); // ← ua как user_data -// uasync_add_socket_t(ua, r, wakeup_read_callback_win, NULL, NULL, NULL); + if (ioctlsocket(r, FIONBIO, &mode) || ioctlsocket(w, FIONBIO, &mode)) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Wakeup socket nonblocking setup failed: %d", WSAGetLastError()); goto fail; + } + if (!uasync_add_socket_t(ua, r, wakeup_read_callback_win, NULL, NULL, "wakeup", ua)) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Wakeup socket registration failed"); goto fail; + } InitializeCriticalSection(&ua->posted_lock); - #else #ifdef __linux__ ua->wakeup_pipe[0] = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); - if (ua->wakeup_pipe[0] < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "eventfd() failed: %s", strerror(errno)); - u_free(ua); - return NULL; - } + if (ua->wakeup_pipe[0] < 0) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "eventfd failed: %s", strerror(errno)); goto fail; } ua->wakeup_pipe[1] = ua->wakeup_pipe[0]; #else - if (pipe(ua->wakeup_pipe) < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pipe() failed: %s", strerror(errno)); - u_free(ua); - return NULL; - } - if (fcntl(ua->wakeup_pipe[0], F_SETFL, O_NONBLOCK) < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "fcntl(wakeup_pipe[0], O_NONBLOCK) failed: %s", strerror(errno)); - } - if (fcntl(ua->wakeup_pipe[1], F_SETFL, O_NONBLOCK) < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "fcntl(wakeup_pipe[1], O_NONBLOCK) failed: %s", strerror(errno)); + if (pipe(ua->wakeup_pipe) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "wakeup pipe failed: %s", strerror(errno)); goto fail; } + if (fcntl(ua->wakeup_pipe[0], F_SETFL, O_NONBLOCK) < 0 || fcntl(ua->wakeup_pipe[1], F_SETFL, O_NONBLOCK) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "wakeup pipe nonblocking setup failed: %s", strerror(errno)); goto fail; } #endif - ua->wakeup_initialized = 1; - #if HAS_EPOLL - if (ua->use_epoll && ua->epoll_fd >= 0) { - struct epoll_event ev; - ev.events = EPOLLIN; - ev.data.fd = -1; + if (ua->use_epoll) { + struct epoll_event ev = {0}; ev.events = EPOLLIN; ev.data.fd = -1; if (epoll_ctl(ua->epoll_fd, EPOLL_CTL_ADD, ua->wakeup_pipe[0], &ev) < 0) { - DEBUG_WARN(DEBUG_CATEGORY_SYS, "Failed to add wakeup pipe to epoll: %s", strerror(errno)); + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Wakeup epoll registration failed: %s", strerror(errno)); goto fail; } } #endif - - if (!ua->use_epoll) { - uasync_add_socket(ua, ua->wakeup_pipe[0], wakeup_read_callback_posix, NULL, NULL, "wakeup", ua); // ← ua - } - pthread_mutex_init(&ua->posted_lock, NULL); - + if (reserve_poll_fds(ua, 1) < 0) goto fail; + int lock_error = pthread_mutex_init(&ua->posted_lock, NULL); + if (lock_error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "uasync mutex initialization failed: %s", strerror(lock_error)); goto fail; } #endif - DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "Creating TH3..."); - + ua->wakeup_initialized = 1; + DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "uasync created: ua=%p epoll=%d wakeup=%d", ua, ua->use_epoll, ua->wakeup_pipe[0]); return ua; +fail: + u_free(ua->poll_fds); + timeout_heap_destroy(ua->timeout_heap); memory_pool_destroy(ua->timeout_pool); socket_array_destroy(ua->sockets); + if (ua->epoll_fd >= 0) close(ua->epoll_fd); + if (ua->wakeup_pipe[0] >= 0) { +#ifdef _WIN32 + closesocket((SOCKET)(intptr_t)ua->wakeup_pipe[0]); +#else + close(ua->wakeup_pipe[0]); +#endif + } + if (ua->wakeup_pipe[1] >= 0 && ua->wakeup_pipe[1] != ua->wakeup_pipe[0]) { +#ifdef _WIN32 + closesocket((SOCKET)(intptr_t)ua->wakeup_pipe[1]); +#else + close(ua->wakeup_pipe[1]); +#endif + } + u_free(ua); socket_platform_cleanup(); return NULL; } // Print all resources for debugging @@ -1697,18 +1542,8 @@ void uasync_destroy(struct UASYNC* ua, int close_fds) { // Диагностика ресурсов перед очисткой uasync_print_resources(ua, "BEFORE_DESTROY"); - // Check for potential memory leaks - if (ua->timer_alloc_count != ua->timer_free_count || ua->socket_alloc_count != ua->socket_free_count) { - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Memory leaks detected before cleanup: timers %zu/%zu, sockets %zu/%zu", - ua->timer_alloc_count, ua->timer_free_count, ua->socket_alloc_count, ua->socket_free_count); - DEBUG_ERROR(DEBUG_CATEGORY_TIMERS, "Timer leak: allocated=%zu, u_freed=%zu, diff=%zd", - ua->timer_alloc_count, ua->timer_free_count, - (ssize_t)(ua->timer_alloc_count - ua->timer_free_count)); - DEBUG_ERROR(DEBUG_CATEGORY_TIMERS, "Socket leak: allocated=%zu, u_freed=%zu, diff=%zd", - ua->socket_alloc_count, ua->socket_free_count, - (ssize_t)(ua->socket_alloc_count - ua->socket_free_count)); - // Continue cleanup, will abort after if leaks remain - } + DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "Cleanup pending resources: timers=%zu sockets=%zu", + ua->timer_alloc_count - ua->timer_free_count, ua->socket_alloc_count - ua->socket_free_count); // Free all remaining timeouts @@ -1938,22 +1773,26 @@ int uasync_get_wakeup_fd(struct UASYNC* ua) { return ua->wakeup_pipe[1]; } -/* Lookup socket by file descriptor - returns current pointer even after u_realloc */ +/* Поиск кодированного handle по fd, устойчивого к расширению массива. */ int uasync_lookup_socket(struct UASYNC* ua, int fd, void** socket_id) { - if (!ua || !ua->sockets || !socket_id || fd < 0 || fd >= FD_SETSIZE) { + if (!ua || !ua->sockets || !socket_id || fd < 0 || fd >= ua->sockets->capacity) { return -1; } - *socket_id = socket_array_get(ua->sockets, fd); - return (*socket_id != NULL) ? 0 : -1; + struct socket_node* node = socket_array_get(ua->sockets, fd); + *socket_id = node ? (void*)(uintptr_t)(ua->sockets->fd_to_index[fd] + 1) : NULL; + return node ? 0 : -1; } void uasync_stop(struct UASYNC* ua) { - if (ua) ua->stop = 1; + if (!ua) return; + __atomic_store_n(&ua->stop, 1, __ATOMIC_RELEASE); + if (uasync_wakeup(ua) < 0 && errno != EAGAIN) + DEBUG_WARN(DEBUG_CATEGORY_SYS, "uasync stop wakeup failed: ua=%p", ua); } void uasync_mainloop(struct UASYNC* ua) { - while (!ua->stop) { + while (!__atomic_load_n(&ua->stop, __ATOMIC_ACQUIRE)) { uasync_poll(ua, -1); } } diff --git a/lib/u_async.h b/lib/u_async.h index 145d5982..82414795 100644 --- a/lib/u_async.h +++ b/lib/u_async.h @@ -90,6 +90,7 @@ static inline int uasync_is_running(struct UASYNC* ua) { // Instance API - основной API для работы с uasync struct UASYNC* uasync_create(void); +// Уничтожение только вне callbacks, после возврата из poll/mainloop. void uasync_destroy(struct UASYNC* ua, int close_fds); void uasync_mark_running(struct UASYNC* ua); void uasync_mark_stopped(struct UASYNC* ua); @@ -127,14 +128,14 @@ err_t uasync_set_socket_write(struct UASYNC* ua, void* s_id, int enable); void uasync_poll(struct UASYNC* ua, int timeout_tb); void uasync_drain_immediate(struct UASYNC* ua); -// Mainloop (бесконечный цикл, __noreturn) +// Цикл до uasync_stop (возвращается после остановки). void uasync_mainloop(struct UASYNC* ua); void uasync_stop(struct UASYNC* ua); // Debug statistics void uasync_get_stats(struct UASYNC* ua, size_t* timer_alloc, size_t* timer_free, size_t* socket_alloc, size_t* socket_free); -// Lookup socket by file descriptor - returns current pointer even after realloc +// Handle — кодированный индекс; сохраняется при расширении массива сокетов. int uasync_lookup_socket(struct UASYNC* ua, int fd, void** socket_id); // Print all resources (timers, sockets) for debugging diff --git a/lib/u_async_doc.md b/lib/u_async_doc.md index 5658d1f1..6bdb41e3 100644 --- a/lib/u_async_doc.md +++ b/lib/u_async_doc.md @@ -17,7 +17,7 @@ u_async — центральный планировщик проекта uTun. - **Один UASYNC на поток.** Нельзя создать несколько экземпляров и вызывать их из разных потоков. - **Никаких sleep/usleep.** Если поток заблокирован на sleep, цикл событий стоит. Все ожидания — только через таймеры `uasync_set_timeout`. -- **Закрытие из callback.** Можно вызывать `uasync_destroy` или `uasync_remove_socket` прямо из callback сокета/таймера. Модуль использует локальные копии указателей перед вызовом callback и генерационные счётчики (gen) для защиты от stale epoll-событий. +- **Закрытие из callback.** Можно удалять сокет и освобождать его пользовательский контекст из callback. Перед следующим callback модуль повторно проверяет регистрацию, поколение и разрешённые события. Для завершения цикла вызывайте `uasync_stop`; `uasync_destroy` выполняйте после возврата из `uasync_poll` / `uasync_mainloop`, вне callbacks. - **Указатель `user_arg`** передаётся во все callback и позволяет передать контекстную структуру (например, `UTUN_INSTANCE`). ### Типовой сценарий @@ -141,7 +141,7 @@ uint64_t now_us = get_time_us(); // микросекунды (для burst-и | `uasync_remove_socket_t(ua, sock)` | Удалить по значению `socket_t`. | | `uasync_set_socket_read(ua, s_id, enable)` | Динамически включить/отключить мониторинг чтения (EPOLL_CTL_MOD). | | `uasync_set_socket_write(ua, s_id, enable)` | Динамически включить/отключить мониторинг записи. | -| `uasync_lookup_socket(ua, fd, &s_id)` | Найти дескриптор сокета по fd (возвращает актуальный указатель даже после realloc). | +| `uasync_lookup_socket(ua, fd, &s_id)` | Найти handle сокета по fd (кодированный индекс, устойчивый к расширению массива). | ### Межпоточное взаимодействие @@ -172,3 +172,7 @@ uint64_t now_us = get_time_us(); // микросекунды (для burst-и - **`struct socket_node`** — узел сокета: fd/sock, тип (FD/SOCK), колбэки, user_data, флаги active/enable_read/enable_write, gen (защита от stale epoll-событий после переиспользования fd). - **`struct socket_array`** — массив сокетов: O(1) доступ по fd через `fd_to_index[]`, обход активных через `active_indices[]`, динамическое расширение. - **`struct posted_task`** — задача из другого потока: callback + arg + next, защищена `posted_lock`. + +Таймеры используют монотонное время: коррекция календарных часов не меняет срок срабатывания. Положительные интервалы округляются вверх до миллисекунд; нулевой интервал готов немедленно. Каждый проход обрабатывает исходную порцию `call_soon`, затем проверяет таймеры и сокеты; новые callbacks остаются на следующий проход. `uasync_drain_immediate` явно обрабатывает все порции до опустошения очереди. + +Для fallback `poll` массив дескрипторов резервируется при создании экземпляра и до регистрации нового сокета. Отказ памяти возвращает `NULL` из `uasync_create` / `uasync_add_socket*`; существующие регистрации сохраняются. Перестроение массива в `uasync_poll` не выделяет память. Расширение из callback сохраняет события текущего прохода, новый сокет опрашивается со следующего прохода. diff --git a/src/call/call_audio.c b/src/call/call_audio.c index 2efdf9b7..2ee7d4d9 100644 --- a/src/call/call_audio.c +++ b/src/call/call_audio.c @@ -35,7 +35,6 @@ static int g_active = 0; static int g_ending = 0; static int g_glitch_started = 0; /* только аудиопоток */ static int g_playback_started = 0; /* первый PCM получен; только аудиопоток */ -static uint64_t g_start_tb = 0; /* для диагностики ожидания первого PCM */ static int g_ended_started = 0; /* только аудио-поток: тон завершения запущен */ static uint64_t g_call_id = 0; static int g_gap_samples = 0; /* только аудиопоток; насыщение на пороге */ @@ -47,6 +46,34 @@ static speex_aec_t* g_aec = NULL; static int g_aec_delay_frames = CALL_AEC_DELAY_FRAMES; static int g_aec_enabled = 1; /* по умолчанию: настройка aec_enabled */ static struct audio_compressor* g_compressor = NULL; +static uint64_t g_tx_generation; +static unsigned int g_tx_pending, g_tx_dropped, g_tx_reported; +static unsigned int g_tx_reasons; +static int g_tx_encode_error; +static uint32_t g_aec_reported_over, g_aec_reported_under, g_pcm_events; + +/* Task и данные — одна аллокация; uasync освобождает task после callback и при destroy. */ +struct call_audio_tx { + struct posted_task task; + struct UTUN_INSTANCE* inst; + uint64_t call_id, generation, created_tb; + int len; + uint8_t opus[256]; +}; + +static void call_audio_send(void* arg) { + struct call_audio_tx* tx = arg; + pthread_mutex_lock(&g_mtx); + int current = tx->generation == g_tx_generation; + if (g_tx_pending) --g_tx_pending; + int send = current && g_active && !g_ending && g_inst == tx->inst && g_call_id == tx->call_id; + if (get_time_tb() - tx->created_tb > 2000) { + send = 0; + if (current) { ++g_tx_dropped; g_tx_reasons |= 16; } + } + pthread_mutex_unlock(&g_mtx); + if (send) call_send_media(tx->inst, tx->call_id, tx->opus, tx->len); +} static const struct call_audio_ops g_ops = { .on_media = call_audio_on_media, @@ -64,8 +91,10 @@ static int call_audio_decode_cb(void* arg, const uint8_t* enc, int len, int16_t* int call_audio_init(struct UTUN_INSTANCE* inst) { if (!inst) return -1; - if (g_inst == inst) return 0; - g_inst = inst; + pthread_mutex_lock(&g_mtx); + if (g_inst == inst) { pthread_mutex_unlock(&g_mtx); return 0; } + g_inst = inst; g_tx_pending = 0; + pthread_mutex_unlock(&g_mtx); call_set_audio_ops(inst, &g_ops); DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized", CALL_AUDIO_ID); return 0; @@ -74,7 +103,9 @@ int call_audio_init(struct UTUN_INSTANCE* inst) { void call_audio_destroy(struct UTUN_INSTANCE* inst) { if (!inst || g_inst != inst) return; call_set_audio_ops(inst, NULL); + pthread_mutex_lock(&g_mtx); g_inst = NULL; + pthread_mutex_unlock(&g_mtx); DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: destroyed", CALL_AUDIO_ID); } @@ -183,12 +214,14 @@ int call_audio_start(uint64_t call_id) { g_compressor = ac; g_call_id = call_id; g_active = 1; + ++g_tx_generation; g_tx_dropped = g_tx_reported = 0; + g_tx_reasons = 0; g_tx_encode_error = 0; g_ending = 0; g_ended_started = 0; g_glitch_started = 0; g_playback_started = 0; - g_start_tb = get_time_tb(); g_gap_samples = 0; + g_aec_reported_over = g_aec_reported_under = g_pcm_events = 0; pthread_mutex_unlock(&g_mtx); DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: started call=0x%016llx aec=%d delay=%d compressor=%s gain=%d", CALL_AUDIO_ID, @@ -220,10 +253,10 @@ void call_audio_release(uint64_t call_id) { if (g_active && g_call_id == call_id) { was_active = 1; g_active = 0; + ++g_tx_generation; g_ending = 0; g_ended_started = 0; g_playback_started = 0; - g_start_tb = 0; g_call_id = 0; g_gap_samples = 0; enc = g_encoder; g_encoder = NULL; @@ -255,6 +288,23 @@ void call_audio_stop(void) { if (cid) call_audio_release(cid); } +void call_audio_reset_io(uint64_t call_id, int playback) { + pthread_mutex_lock(&g_mtx); + if (g_active && g_call_id == call_id) { + if (g_aec) speex_aec_reset(g_aec); + if (playback) { + call_jitter_reset(g_vj); + g_gap_samples = 0; + g_glitch_started = 0; + g_playback_started = 0; + call_tones_glitch_stop(g_tones); + } + DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: I/O reset call=%016llx direction=%s", CALL_AUDIO_ID, + (unsigned long long)call_id, playback ? "playback" : "capture"); + } + pthread_mutex_unlock(&g_mtx); +} + /* ── AEC (SpeexDSP): рантайм-управление ── */ /* Создать AEC с текущей задержкой. Вызывается под g_mtx. */ @@ -328,21 +378,30 @@ int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) { } ua = g_inst ? g_inst->ua : NULL; inst = g_inst; - pthread_mutex_unlock(&g_mtx); - - if (len <= 0) return -1; - if (!ua || !inst) { - DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: feed_pcm: instance not set (call_audio_init not called)", CALL_AUDIO_ID); + if (len <= 0 || !ua || !inst || g_ending || g_tx_pending >= 8) { + ++g_tx_dropped; + if (g_tx_pending >= 8) g_tx_reasons |= 1; + if (!ua || !inst) g_tx_reasons |= 4; + if (len <= 0) { g_tx_reasons |= 8; g_tx_encode_error = len; } + if (g_ending) g_tx_reasons |= 32; + pthread_mutex_unlock(&g_mtx); return -1; } - - struct call_media_arg* a = (struct call_media_arg*)u_malloc(sizeof(*a)); - if (!a) return -1; - a->inst = inst; - a->call_id = call_id; - a->len = (uint16_t)len; - memcpy(a->opus, opus, (size_t)len); - uasync_post(ua, call_send_media_trampoline, a); + struct call_audio_tx* task = u_calloc(1, sizeof(*task)); + if (!task) { + ++g_tx_dropped; + g_tx_reasons |= 2; + pthread_mutex_unlock(&g_mtx); + return -1; + } + task->inst = inst; task->call_id = call_id; + task->generation = g_tx_generation; task->created_tb = get_time_tb(); + task->len = len; + memcpy(task->opus, opus, (size_t)len); + task->task.callback = call_audio_send; task->task.arg = task; + ++g_tx_pending; + uasync_post_reserved(ua, &task->task); + pthread_mutex_unlock(&g_mtx); return 0; } @@ -365,6 +424,7 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { if (!active || !j || !tones) return 0; int n = 0; + uint32_t events = 0; /* завершение: доигрываем тон (450мс), затем тишина. Тон стартует лениво * здесь (аудио-поток) — begin_end лишь выставляет флаг, чтобы не гонять @@ -380,9 +440,7 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { n = call_jitter_pull(j, out, max_samples); int previous_gap = g_gap_samples; if (n > 0) { - if (!g_playback_started) - DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: first PCM call=%016llx wait=%llums pcm=%d/%d", CALL_AUDIO_ID, - (unsigned long long)call_id, (unsigned long long)((get_time_tb() - g_start_tb) / 10), n, max_samples); + if (!g_playback_started) events |= 1; g_playback_started = 1; g_gap_samples = 0; } @@ -391,26 +449,20 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { int quiet = g_playback_started ? CALL_STALL_SAMPLES - g_gap_samples : missing; if (quiet > missing) quiet = missing; int tone_samples = missing - quiet; - if (n > 0 && previous_gap > 0) - DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: PCM resumed call=%016llx gap=%dms pcm=%d/%d", CALL_AUDIO_ID, - (unsigned long long)call_id, previous_gap * 1000 / CALL_AUDIO_SAMPLE_RATE, n, max_samples); - if (missing > 0 && g_playback_started && g_gap_samples == 0) - DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: PCM gap started call=%016llx pcm=%d/%d", CALL_AUDIO_ID, - (unsigned long long)call_id, n, max_samples); + if (n > 0 && previous_gap > 0) events |= 2; + if (missing > 0 && g_playback_started && g_gap_samples == 0) events |= 4; if (g_playback_started) g_gap_samples += quiet; /* Новый PCM прерывает предыдущий цикл тона, даже если хвост блока снова пуст. */ if (g_glitch_started && (n > 0 || tone_samples == 0)) { g_glitch_started = 0; call_tones_glitch_stop(tones); - DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: gap tone stopped call=%016llx pcm=%d/%d", CALL_AUDIO_ID, - (unsigned long long)call_id, n, max_samples); + events |= 8; } memset(out + n, 0, (size_t)quiet * sizeof(*out)); if (tone_samples > 0) { if (!g_glitch_started) { g_glitch_started = 1; - DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: gap tone started call=%016llx pcm_gap=500ms pcm=%d/%d", CALL_AUDIO_ID, - (unsigned long long)call_id, n, max_samples); + events |= 16; } call_tones_glitch_start(tones); call_tones_render(tones, out + n + quiet, tone_samples); @@ -422,6 +474,7 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { if (n > 0) { pthread_mutex_lock(&g_mtx); if (g_aec) speex_aec_feed_playback(g_aec, out, n); + g_pcm_events |= events; pthread_mutex_unlock(&g_mtx); } return n; @@ -432,11 +485,38 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100, uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range) { int ok = 0; + struct call_jitter_diagnostics diagnostics = {0}; + uint32_t aec_over = 0, aec_under = 0, events = 0; + unsigned int tx_dropped = 0, tx_pending = 0; + unsigned int reasons = 0; + int encode_error = 0; + int tx_changed = 0, aec_changed = 0; pthread_mutex_lock(&g_mtx); if (g_active && call_id == g_call_id && g_vj) { call_jitter_get_stats(g_vj, buffer_ms, tempo_x100, dropped, underruns, range); + call_jitter_take_diagnostics(g_vj, &diagnostics); + speex_aec_get_stats(g_aec, &aec_over, &aec_under); + aec_changed = aec_over != g_aec_reported_over || aec_under != g_aec_reported_under; + g_aec_reported_over = aec_over; g_aec_reported_under = aec_under; + tx_dropped = g_tx_dropped; tx_pending = g_tx_pending; + tx_changed = tx_dropped != g_tx_reported; g_tx_reported = tx_dropped; + reasons = g_tx_reasons; g_tx_reasons = 0; encode_error = g_tx_encode_error; + events = g_pcm_events; g_pcm_events = 0; ok = 1; } pthread_mutex_unlock(&g_mtx); + if (tx_changed) DEBUG_WARN(DEBUG_CATEGORY_CALL, + "%s: TX dropped=%u pending=%u limit=8 max_age=200ms reasons=queue:%d alloc:%d core:%d encode:%d age:%d ending:%d encode_result=%d", + CALL_AUDIO_ID, tx_dropped, tx_pending, !!(reasons & 1), !!(reasons & 2), !!(reasons & 4), + !!(reasons & 8), !!(reasons & 16), !!(reasons & 32), encode_error); + if (aec_changed) DEBUG_WARN(DEBUG_CATEGORY_AEC, "%s: AEC render overflow=%u capture passthrough=%u", CALL_AUDIO_ID, aec_over, aec_under); + if (diagnostics.decode_errors || diagnostics.budget_hits) + DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: decode_errors=%u last_result=%d len=%d budget_hits=%u depth=%dms", + diagnostics.decode_errors, diagnostics.error_result, diagnostics.error_len, diagnostics.budget_hits, diagnostics.depth_ms); + if (events) DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: PCM first=%d resumed=%d gap=%d tone_started=%d tone_stopped=%d", CALL_AUDIO_ID, + !!(events & 1), !!(events & 2), !!(events & 4), !!(events & 16), !!(events & 8)); + if (ok) DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call_jitter: depth=%dms reserve=%dms tempo=%d waiting=%d catching_up=%d resumed=%d", + diagnostics.depth_ms, diagnostics.reserve_ms, diagnostics.tempo_x100, diagnostics.waiting, + diagnostics.catching_up, diagnostics.resumed); return ok ? 0 : -1; } diff --git a/src/call/call_audio.h b/src/call/call_audio.h index bf80579a..bdc01e05 100644 --- a/src/call/call_audio.h +++ b/src/call/call_audio.h @@ -12,7 +12,7 @@ // // Потоки: // - call_audio_start / begin_end / release — GUI-поток (по ACCEPTED/ENDED); -// - call_audio_feed_pcm / pull_pcm — аудио-поток (single-writer); +// - call_audio_feed_pcm / pull_pcm — по одному аудио-потоку на направление; // - call_audio_on_media / get_stats — uasync-поток (из call.c). // Общие указатели (encoder/decoder/vj/tones) защищены мьютексом; encoder и // decoder — только аудио-поток (decoder дёргается из call_jitter_pull через коллбэк), @@ -63,6 +63,11 @@ void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id, int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100, uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range); +/* После остановки указанного направления: сбросить AEC, для playback также устаревший jitter/PCM. + * Capture и playback могут работать в разных потоках, по одному writer на направление. + * Release допустим только после остановки обоих. */ +void call_audio_reset_io(uint64_t call_id, int playback); + /* Включить/выключить AEC (SpeexDSP) в рантайме. desktop — по настройке GUI, * Android — по спикерфону (route == SPEAKER). Работает и в активном звонке. */ void call_audio_set_aec_enabled(int enabled); diff --git a/src/call/call_jitter.cpp b/src/call/call_jitter.cpp index e33e36ff..2401ec2f 100644 --- a/src/call/call_jitter.cpp +++ b/src/call/call_jitter.cpp @@ -30,6 +30,7 @@ struct call_jitter { int lens[CALL_JITTER_MAX_FRAMES]; int head = 0, count = 0; uint32_t dropped = 0, underruns = 0; + call_jitter_diagnostics diagnostics{}; int processing_ms = 0, tempo_x100 = 100; /* снимок аудиопотока под mtx */ call_jitter_window depth_window, arrival_window; call_jitter_range range = {0, 0, 60}; /* опубликованный снимок под mtx */ @@ -88,16 +89,10 @@ void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len, uint32 (long long)j->arrival_window.last_received_ms, (long long)received_ms); return; } - bool log_arrival = j->arrival_window.observe_arrival(received_ms, sent_ms); + j->arrival_window.observe_arrival(received_ms, sent_ms); j->range.reserve_ms = j->arrival_window.reserve; - if (log_arrival) - DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call_jitter: delivery variation=%lldms reserve=%dms queued=%dms", - (long long)(j->arrival_window.high - j->arrival_window.low), j->arrival_window.reserve, - j->count * CALL_JITTER_FRAME_MS); if (j->count == CALL_JITTER_MAX_FRAMES) { j->head = (j->head + 1) % CALL_JITTER_MAX_FRAMES; j->count--; j->dropped++; - if (j->dropped == 1 || j->dropped % 50 == 0) - DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: overflow capacity=15000ms dropped=%u", j->dropped); } int idx = (j->head + j->count) % CALL_JITTER_MAX_FRAMES; std::memcpy(j->ring[idx], enc, (size_t)len); j->lens[idx] = len; j->count++; @@ -112,18 +107,21 @@ int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) { + (j->st.numUnprocessedSamples() + j->st.numSamples() * j->tempo) * 1000.0 / kSampleRate; int64_t now_ms = std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count(); - bool log_stats = j->depth_window.observe(now_ms, (int)std::lround(depth)); + j->depth_window.observe(now_ms, (int)std::lround(depth)); + int resumed = 0; { std::lock_guard lock(j->mtx); j->range.min_ms = (int)j->depth_window.low; j->range.max_ms = (int)j->depth_window.high; } - if (log_stats) - DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call_jitter: depth=%.0fms min=%lldms max=%lldms reserve=%dms tempo=%.2f", - depth, (long long)j->depth_window.low, (long long)j->depth_window.high, reserve, j->tempo); if (!j->primed) { - if (depth < reserve) return 0; + if (depth < reserve) { + std::lock_guard lock(j->mtx); + j->diagnostics.depth_ms = (int)depth; j->diagnostics.reserve_ms = reserve; + j->diagnostics.tempo_x100 = j->tempo_x100; j->diagnostics.waiting = 1; + return 0; + } j->primed = true; - DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: playback resumed depth=%.0fms reserve=%dms", depth, reserve); + resumed = 1; } j->ema_ms += 0.2 * (depth - j->ema_ms); double target = 1.0 + (kMaxTempo - 1.0) * clamp_value((std::min(depth, j->ema_ms) - reserve - 15.0) / 925.0, 0.0, 1.0); @@ -132,13 +130,14 @@ int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) { j->tempo += clamp_value(target - j->tempo, -step, step); j->st.setTempo(j->tempo); bool catching = j->tempo > 1.05; - if (catching != j->catching_up) { - j->catching_up = catching; - DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: catch-up %s depth=%.0fms tempo=%.2f", - catching ? "started" : "finished", depth, j->tempo); - } + j->catching_up = catching; + uint32_t decode_errors = 0; + int error_result = 0, error_len = 0; uint8_t enc[kMaxEncoded]; - while (j->st.numSamples() < (uint)max_samples) { + // Бюджет учитывает ускорение 1.6 и начальное накопление SoundTouch, включая битые пакеты. + int budget = std::min(CALL_JITTER_MAX_FRAMES, 8 + max_samples / 480); + int attempted = 0; + while (j->st.numSamples() < (uint)max_samples && attempted++ < budget) { int len; { std::lock_guard lock(j->mtx); @@ -148,7 +147,8 @@ int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) { } int n = j->decode(j->arg, enc, len, j->pcm, kFrameSamples); if (n <= 0 || n > kFrameSamples) { - DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: decode failed len=%d result=%d", len, n); + ++decode_errors; + error_result = n; error_len = len; continue; } for (int i = 0; i < n; i++) j->input[i] = j->pcm[i] / 32768.0f; @@ -162,14 +162,48 @@ int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) { std::lock_guard lock(j->mtx); j->processing_ms = processing_ms; j->tempo_x100 = (int)std::lround(j->tempo * 100); if (n < max_samples) j->underruns++; + j->diagnostics.depth_ms = (int)depth; + j->diagnostics.reserve_ms = reserve; + j->diagnostics.tempo_x100 = j->tempo_x100; + j->diagnostics.decode_errors += decode_errors; + if (decode_errors) { j->diagnostics.error_result = error_result; j->diagnostics.error_len = error_len; } + if (attempted > budget) ++j->diagnostics.budget_hits; + j->diagnostics.resumed |= resumed; + j->diagnostics.catching_up = catching; + j->diagnostics.waiting = n < max_samples; } if (n < max_samples) { j->primed = false; - DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call_jitter: waiting for refill pcm=%d/%d processing=%dms", n, max_samples, processing_ms); } return n; } +void call_jitter_take_diagnostics(struct call_jitter* j, struct call_jitter_diagnostics* out) { + std::lock_guard lock(j->mtx); + *out = j->diagnostics; + j->diagnostics.decode_errors = j->diagnostics.budget_hits = 0; + j->diagnostics.resumed = 0; +} + +void call_jitter_reset(struct call_jitter* j) { + if (!j) return; + std::lock_guard lock(j->mtx); + int keep = std::min(j->count, (j->arrival_window.reserve + CALL_JITTER_FRAME_MS - 1) / CALL_JITTER_FRAME_MS); + int discard = j->count - keep; + j->head = (j->head + discard) % CALL_JITTER_MAX_FRAMES; + j->count = keep; + j->dropped += discard; + j->st.clear(); j->st.setTempo(1.0); + j->primed = false; j->catching_up = false; + j->tempo = 1.0; j->ema_ms = j->arrival_window.reserve; + j->processing_ms = 0; j->tempo_x100 = 100; + j->depth_window = {}; + j->diagnostics = {}; + j->range.min_ms = j->range.max_ms = keep * CALL_JITTER_FRAME_MS; + DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: device discontinuity discarded=%dms kept=%dms reserve=%dms", + discard * CALL_JITTER_FRAME_MS, keep * CALL_JITTER_FRAME_MS, j->arrival_window.reserve); +} + void call_jitter_get_stats(struct call_jitter* j, int* depth_ms, int* tempo_x100, uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range) { if (!j) return; diff --git a/src/call/call_jitter.h b/src/call/call_jitter.h index e8e23078..6833f3df 100644 --- a/src/call/call_jitter.h +++ b/src/call/call_jitter.h @@ -15,11 +15,20 @@ extern "C" { typedef int (*call_jitter_decode_fn)(void*, const uint8_t*, int, int16_t*, int); struct call_jitter_range { int min_ms, max_ms, reserve_ms; }; struct call_jitter; +struct call_jitter_diagnostics { + int depth_ms, reserve_ms, tempo_x100, error_result, error_len; + uint32_t decode_errors, budget_hits; + int resumed, catching_up, waiting; +}; +/* Снимок и сброс событий под mutex; логировать после выхода из mutex голосового стека. */ +void call_jitter_take_diagnostics(struct call_jitter*, struct call_jitter_diagnostics*); struct call_jitter* call_jitter_create(call_jitter_decode_fn decode, void* arg, int max_reserve_ms); void call_jitter_destroy(struct call_jitter* j); /* sent_ms — timestamp кадра на проводе (uint32 с переполнением), received_ms — локальные монотонные часы. * Абсолютное смещение часов пиров не влияет на оценку джиттера. */ void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len, uint32_t sent_ms, int64_t received_ms); +/* Только после остановки pull: убрать устаревший PCM, сохранить свежий сетевой запас. */ +void call_jitter_reset(struct call_jitter* j); int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples); void call_jitter_get_stats(struct call_jitter* j, int* depth_ms, int* tempo_x100, uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range); diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h index 3a0bffe2..bb10602a 100644 --- a/src/chat/chat_core.h +++ b/src/chat/chat_core.h @@ -157,7 +157,7 @@ void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_i struct chat_sound_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; int on; }; void chat_core_set_sound_on_message_trampoline(void* arg); -struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[256]; }; +struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[1024]; }; void chat_core_save_ui_state_trampoline(void* arg); /* Установка chat-настройки (name=value) через chat_setting API (GUI → uasync) */ diff --git a/src/radio/radio_audio.c b/src/radio/radio_audio.c index da47fc99..9f9cd292 100644 --- a/src/radio/radio_audio.c +++ b/src/radio/radio_audio.c @@ -96,11 +96,62 @@ static uint64_t g_last_rx_tb = 0; /* последний п /* TX-статистика (кадрирование/кодирование), выводится раз в секунду пока идёт PTT. */ static uint64_t g_tx_stats_tb = 0; +static uint64_t g_vad_stats_tb = 0; static uint32_t g_tx_frames = 0; /* кадров за интервал */ static uint32_t g_tx_bytes = 0; /* суммарный размер opus за интервал */ static uint32_t g_tx_tail = 0; /* дропнутых хвостовых сэмплов за интервал */ -/* rj decode-коллбэк: дёргается только из аудио-потока (rj_pull). */ +static uint64_t g_tx_generation; +static unsigned int g_tx_pending, g_tx_dropped, g_tx_reported; +struct radio_audio_tx { + struct posted_task task; + struct UTUN_INSTANCE* inst; + uint64_t group_id, generation, created_tb; + int len; + uint8_t opus[RADIO_MAX_OPUS]; +}; + +static void radio_audio_send(void* arg) { + struct radio_audio_tx* tx = arg; + pthread_mutex_lock(&g_mtx); + int current = tx->generation == g_tx_generation; + if (g_tx_pending) --g_tx_pending; + int send = current && g_active && g_inst == tx->inst && g_group_id == tx->group_id; + if (get_time_tb() - tx->created_tb > 2000) { send = 0; if (current) ++g_tx_dropped; } + unsigned int dropped = g_tx_dropped, pending = g_tx_pending; + int changed = dropped != g_tx_reported; + g_tx_reported = dropped; + uint32_t frames = 0, bytes = 0, tail = 0; + uint64_t now = get_time_tb(); + if (now - g_tx_stats_tb >= RADIO_AUDIO_STATS_TB) { + frames = g_tx_frames; bytes = g_tx_bytes; tail = g_tx_tail; + g_tx_frames = g_tx_bytes = g_tx_tail = 0; g_tx_stats_tb = now; + } + pthread_mutex_unlock(&g_mtx); + if (changed) { + DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: TX dropped=%u pending=%u limit=8 max_age=200ms", RADIO_AUDIO_ID, + dropped, pending); + } + if (frames) DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: TX frames=%u bytes=%u tail_samples=%u", RADIO_AUDIO_ID, frames, bytes, tail); + if (send) radio_talk_send(tx->inst, tx->group_id, tx->opus, tx->len); +} + +/* Вызывается под g_mtx; post и FIN упорядочены тем же mutex, задачи не накапливаются бесконечно. */ +static int radio_audio_post_frame(struct UTUN_INSTANCE* inst, uint64_t group_id, const uint8_t* opus, int len) { + if (!inst || !inst->ua || g_tx_pending >= 8) { ++g_tx_dropped; return -1; } + struct radio_audio_tx* task = u_calloc(1, sizeof(*task)); + if (!task) { ++g_tx_dropped; return -1; } + task->inst = inst; task->group_id = group_id; + task->generation = g_tx_generation; task->created_tb = get_time_tb(); + task->len = len; + memcpy(task->opus, opus, (size_t)len); + task->task.callback = radio_audio_send; task->task.arg = task; + ++g_tx_pending; + uasync_post_reserved(inst->ua, &task->task); + return 0; +} + +/* rj decode-коллбэк: только единственный playback-поток. */ static int radio_audio_decode_cb(void* arg, const uint8_t* enc, int len, int16_t* pcm, int max_samples) { opus_codec_decoder_t* dec = (opus_codec_decoder_t*)arg; return opus_codec_decode(dec, enc, len, pcm, max_samples); @@ -256,14 +307,16 @@ static struct radio_source* radio_src_acquire(uint64_t src, uint16_t stream) { return NULL; } -static void radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, void (*fn)(void*)); +static void radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, int begin); /* ── lifecycle ── */ int radio_audio_init(struct UTUN_INSTANCE* inst) { if (!inst) return -1; - if (g_inst == inst) return 0; - g_inst = inst; + pthread_mutex_lock(&g_mtx); + if (g_inst == inst) { pthread_mutex_unlock(&g_mtx); return 0; } + g_inst = inst; g_tx_pending = 0; + pthread_mutex_unlock(&g_mtx); radio_set_frame_cb(inst, radio_audio_on_frame, NULL); DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: initialized", RADIO_AUDIO_ID); return 0; @@ -272,7 +325,9 @@ int radio_audio_init(struct UTUN_INSTANCE* inst) { void radio_audio_destroy(struct UTUN_INSTANCE* inst) { if (!inst || g_inst != inst) return; radio_set_frame_cb(inst, NULL, NULL); + pthread_mutex_lock(&g_mtx); g_inst = NULL; + pthread_mutex_unlock(&g_mtx); DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: destroyed", RADIO_AUDIO_ID); } @@ -348,6 +403,7 @@ int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_e } g_vad_manual_ptt = 0; + ++g_tx_generation; g_tx_dropped = g_tx_reported = 0; g_encoder = enc; g_compressor = ac; g_capture_channels = capture_channels; @@ -361,8 +417,9 @@ int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_e void radio_audio_capture_stop(void) { pthread_mutex_lock(&g_mtx); if (g_encoder) { + ++g_tx_generation; if (g_vad_manual_ptt || g_vad_fsm.talking) - radio_audio_post_talk(g_inst ? g_inst->ua : NULL, g_group_id, radio_talk_end_trampoline); + radio_audio_post_talk(g_inst ? g_inst->ua : NULL, g_group_id, 0); opus_codec_encoder_destroy(g_encoder); g_encoder = NULL; if (g_compressor) { audio_compressor_destroy(g_compressor); g_compressor = NULL; } if (g_vad) { silero_vad_destroy(g_vad); g_vad = NULL; } @@ -394,6 +451,15 @@ void radio_audio_stop(void) { } } +void radio_audio_reset_playback(uint64_t group_id) { + pthread_mutex_lock(&g_mtx); + if (g_active && g_group_id == group_id) { + for (int i = 0; i < RADIO_AUDIO_MAX_SOURCES; ++i) radio_src_free(&g_sources[i]); + DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: playback reset group=%016llx", RADIO_AUDIO_ID, (unsigned long long)group_id); + } + pthread_mutex_unlock(&g_mtx); +} + int radio_audio_active(void) { int a = 0; pthread_mutex_lock(&g_mtx); @@ -419,13 +485,26 @@ int radio_audio_transmitting(void) { /* ── PTT (GUI → uasync) ── */ -static void radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, void (*fn)(void*)) { +struct radio_audio_talk { + struct posted_task task; + struct UTUN_INSTANCE* inst; + uint64_t group_id; + int begin; +}; + +static void radio_audio_talk(void* arg) { + struct radio_audio_talk* talk = arg; + if (talk->begin) radio_talk_begin(talk->inst, talk->group_id); + else radio_talk_end(talk->inst, talk->group_id); +} + +static void radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, int begin) { if (!ua) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: talk post without uasync", RADIO_AUDIO_ID); return; } - struct radio_talk_arg* a = (struct radio_talk_arg*)u_malloc(sizeof(*a)); - if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: talk post allocation failed", RADIO_AUDIO_ID); return; } - a->inst = g_inst; - a->group_id = group_id; - uasync_post(ua, fn, a); + struct radio_audio_talk* talk = u_calloc(1, sizeof(*talk)); + if (!talk) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: talk post allocation failed", RADIO_AUDIO_ID); return; } + talk->inst = g_inst; talk->group_id = group_id; talk->begin = begin; + talk->task.callback = radio_audio_talk; talk->task.arg = talk; + uasync_post_reserved(ua, &talk->task); } void radio_audio_talk_begin(uint64_t group_id) { @@ -439,10 +518,10 @@ void radio_audio_talk_begin(uint64_t group_id) { g_vad_manual_ptt = 1; if (g_vad_fsm.talking) { /* VAD вела передачу — закрываем её FIN, затем открываем ручной burst */ - radio_audio_post_talk(g_inst->ua, group_id, radio_talk_end_trampoline); + radio_audio_post_talk(g_inst->ua, group_id, 0); radio_vad_fsm_reset(&g_vad_fsm); } - radio_audio_post_talk(g_inst->ua, group_id, radio_talk_begin_trampoline); + radio_audio_post_talk(g_inst->ua, group_id, 1); pthread_mutex_unlock(&g_mtx); } @@ -456,7 +535,7 @@ void radio_audio_talk_end(uint64_t group_id) { return; } g_vad_manual_ptt = 0; - radio_audio_post_talk(g_inst->ua, group_id, radio_talk_end_trampoline); + radio_audio_post_talk(g_inst->ua, group_id, 0); pthread_mutex_unlock(&g_mtx); } @@ -470,13 +549,7 @@ static void radio_audio_encode_post(uint64_t group_id, const int16_t* frame, uint8_t opus[RADIO_MAX_OPUS]; int l = opus_codec_encode(g_encoder, frame, RADIO_AUDIO_FRAME_SAMPLES, opus, (int)sizeof(opus)); if (l <= 0) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: opus encode failed rc=%d", RADIO_AUDIO_ID, l); return; } - struct radio_media_arg* a = (struct radio_media_arg*)u_malloc(sizeof(*a)); - if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: media allocation failed", RADIO_AUDIO_ID); return; } - a->inst = inst; - a->group_id = group_id; - a->len = (uint16_t)l; - memcpy(a->opus, opus, (size_t)l); - uasync_post(ua, radio_talk_send_trampoline, a); + radio_audio_post_frame(inst, group_id, opus, l); } /* VAD-ветка feed_pcm: ресемпл → Silero → фильтр → старт/стоп/передача (держит g_mtx). */ @@ -517,10 +590,10 @@ static int radio_vad_feed(uint64_t group_id, const int16_t* pcm, int count) { g_vad_win_len = 0; uint64_t now = get_time_tb(); int busy = (g_last_rx_tb != 0 && (now - g_last_rx_tb) < RADIO_VAD_BUSY_TB) ? 1 : 0; - if (!g_tx_stats_tb || now - g_tx_stats_tb >= RADIO_AUDIO_STATS_TB) { + if (!g_vad_stats_tb || now - g_vad_stats_tb >= RADIO_AUDIO_STATS_TB) { DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: vad prob=%.3f threshold=%.2f busy=%d talking=%d confirm=%d", RADIO_AUDIO_ID, prob, g_vad_threshold, busy, g_vad_fsm.talking, g_vad_fsm.confirm_count); - g_tx_stats_tb = now; + g_vad_stats_tb = now; } int action = radio_vad_fsm_update(&g_vad_fsm, prob, g_vad_threshold, RADIO_VAD_CONFIRM_WINDOWS, @@ -528,11 +601,11 @@ static int radio_vad_feed(uint64_t group_id, const int16_t* pcm, int count) { if (action == 1) { DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: vad START prob=%.2f grp=%016llx", RADIO_AUDIO_ID, prob, (unsigned long long)group_id); - radio_audio_post_talk(ua, group_id, radio_talk_begin_trampoline); + radio_audio_post_talk(ua, group_id, 1); } else if (action == -1) { DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: vad STOP grp=%016llx", RADIO_AUDIO_ID, (unsigned long long)group_id); - radio_audio_post_talk(ua, group_id, radio_talk_end_trampoline); + radio_audio_post_talk(ua, group_id, 0); } } } @@ -557,7 +630,6 @@ int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count) { uint8_t opus[MAX_OUT_FRAMES][RADIO_MAX_OPUS]; int opus_len[MAX_OUT_FRAMES]; int n_frames = 0; - struct UASYNC* ua = NULL; struct UTUN_INSTANCE* inst = NULL; if (!g_active || group_id != g_group_id || !g_encoder) { @@ -589,41 +661,12 @@ int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count) { if (g_compressor) audio_compressor_clear_output(g_compressor); g_tx_frames += (uint32_t)n_frames; for (int i = 0; i < n_frames; i++) g_tx_bytes += (uint32_t)opus_len[i]; - uint32_t clip = g_compressor ? audio_compressor_clip_count(g_compressor) : 0; - - ua = g_inst ? g_inst->ua : NULL; inst = g_inst; + int result = n_frames ? 0 : -1; + for (int i = 0; i < n_frames; i++) + if (radio_audio_post_frame(inst, group_id, opus[i], opus_len[i]) != 0) result = -1; pthread_mutex_unlock(&g_mtx); - - if (n_frames == 0) return -1; - if (!ua || !inst) { - DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: feed_pcm: instance not set", RADIO_AUDIO_ID); - return -1; - } - - /* каденс TX: раз в секунду — кадров/с, средний размер opus-кадра, хвост, клиппинг */ - uint64_t now = get_time_tb(); - if (g_tx_stats_tb == 0 || now - g_tx_stats_tb >= RADIO_AUDIO_STATS_TB) { - DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: tx stats frames=%u/s avg=%.0fB/frame tail=%u clip=%u", - RADIO_AUDIO_ID, g_tx_frames, - g_tx_frames ? (double)g_tx_bytes / (double)g_tx_frames : 0.0, - (unsigned)g_tx_tail, (unsigned)clip); - g_tx_stats_tb = now; - g_tx_frames = 0; - g_tx_bytes = 0; - g_tx_tail = 0; - } - - for (int i = 0; i < n_frames; i++) { - struct radio_media_arg* a = (struct radio_media_arg*)u_malloc(sizeof(*a)); - if (!a) return -1; - a->inst = inst; - a->group_id = group_id; - a->len = (uint16_t)opus_len[i]; - memcpy(a->opus, opus[i], (size_t)opus_len[i]); - uasync_post(ua, radio_talk_send_trampoline, a); - } - return 0; + return result; } /* ── RX (uasync-поток): кодированный кадр → rj_push ── */ diff --git a/src/radio/radio_audio.h b/src/radio/radio_audio.h index 91d557eb..12164692 100644 --- a/src/radio/radio_audio.h +++ b/src/radio/radio_audio.h @@ -45,6 +45,8 @@ void radio_audio_capture_stop(void); /* GUI-поток: прекратить прослушивание (освободить всё). */ void radio_audio_stop(void); +/* Playback остановлен: удалить устаревшие источники, сохранив TX и подписку. */ +void radio_audio_reset_playback(uint64_t group_id); /* GUI-поток: PTT зажата/отпущена — постит radio_talk_begin/end в uasync. */ void radio_audio_talk_begin(uint64_t group_id); diff --git a/tests/Makefile.am b/tests/Makefile.am index 56b2646c..a7c6e2a4 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -118,7 +118,8 @@ check_PROGRAMS = \ # Linux-only: test_auto_socket_dynamic требует root + dummy-интерфейсы + iproute2/netlink. if OS_LINUX -check_PROGRAMS += test_auto_socket_dynamic test_standby test_standby_transport +check_PROGRAMS += test_auto_socket_dynamic test_standby test_standby_transport test_android_udp_log test_pool_corruption_log +check_PROGRAMS += test_uasync_regressions test_ll_queue_regressions endif # Silero VAD: только при включённом ONNX Runtime (--with-silero-vad) @@ -161,6 +162,22 @@ test_standby_transport_SOURCES = test_standby_transport.c $(standby_test_sources test_standby_transport_CPPFLAGS = $(standby_test_cppflags) test_standby_transport_LDADD = $(LIBUTUN) $(COMMON_LIBS) test_standby_transport_LDFLAGS = -Wl,--wrap=etcp_encrypt_send -Wl,--wrap=uasync_set_timeout +test_android_udp_log_SOURCES = test_android_udp_log.c ../tools/chatgui-android/jni_bridge/android_udp_log.c +test_android_udp_log_LDADD = $(COMMON_LIBS) +test_android_udp_log_LDFLAGS = -Wl,--wrap=uasync_set_timeout -Wl,--wrap=uasync_cancel_timeout +test_pool_corruption_log_SOURCES = test_pool_corruption_log.c +test_pool_corruption_log_LDADD = $(COMMON_LIBS) + +# Linux/ELF: faults внедряются линкером, тестовых веток в библиотеках нет. +async_regression_sources = async_regression.c async_regression.h +async_regression_ldflags = -Wl,--wrap=u_malloc_impl -Wl,--wrap=u_calloc_impl -Wl,--wrap=u_realloc_impl \ + -Wl,--wrap=gettimeofday -Wl,--wrap=epoll_wait -Wl,--wrap=epoll_ctl -Wl,--wrap=eventfd -Wl,--wrap=epoll_create1 +test_uasync_regressions_SOURCES = test_uasync_regressions.c $(async_regression_sources) +test_uasync_regressions_LDADD = $(COMMON_LIBS) +test_uasync_regressions_LDFLAGS = $(async_regression_ldflags) +test_ll_queue_regressions_SOURCES = test_ll_queue_regressions.c $(async_regression_sources) +test_ll_queue_regressions_LDADD = $(COMMON_LIBS) +test_ll_queue_regressions_LDFLAGS = $(async_regression_ldflags) endif # Test definitions diff --git a/tests/async_regression.c b/tests/async_regression.c new file mode 100644 index 00000000..439a7853 --- /dev/null +++ b/tests/async_regression.c @@ -0,0 +1,169 @@ +#define _GNU_SOURCE +#include "async_regression.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int alloc_mask, alloc_nth, alloc_hit, alloc_persistent; +static const char* alloc_scope; +static int64_t clock_shift_ms; +static struct epoll_event pending_events[64]; +static int pending_count, pending_active, fail_ctl, fail_eventfd, fail_epoll_create; + +void regression_fault_arm(int mask, const char* scope, int nth) { + alloc_mask = mask; alloc_scope = scope; alloc_nth = nth; alloc_hit = 0; alloc_persistent = nth == 0; +} + +int regression_fault_hit(void) { return alloc_hit; } +void regression_fault_reset(void) { alloc_mask = 0; } +void regression_clock_shift(int64_t milliseconds) { clock_shift_ms = milliseconds; } +void regression_epoll_ctl_fail(int operation) { fail_ctl = operation; } +void regression_eventfd_fail(void) { fail_eventfd = 1; } +void regression_epoll_create_fail(void) { fail_epoll_create = 1; } + +static int fail_allocation(int kind, const char* location) { + if (!(alloc_mask & kind) || (alloc_scope && !strstr(location, alloc_scope))) return 0; + // nth=0: устойчивый отказ всех подходящих выделений до reset. + if (!alloc_persistent && --alloc_nth != 0) return 0; + alloc_hit = 1; + fprintf(stderr, "[INJECT] allocation failure: kind=%d at %s\n", kind, location); + return 1; +} + +void* __real_u_malloc_impl(uint32_t size, const char* location); +void* __real_u_calloc_impl(uint32_t count, uint32_t size, const char* location); +void* __real_u_realloc_impl(void* ptr, uint32_t size, const char* location); + +void* __wrap_u_malloc_impl(uint32_t size, const char* location) { + return fail_allocation(FAULT_MALLOC, location) ? NULL : __real_u_malloc_impl(size, location); +} +void* __wrap_u_calloc_impl(uint32_t count, uint32_t size, const char* location) { + return fail_allocation(FAULT_CALLOC, location) ? NULL : __real_u_calloc_impl(count, size, location); +} +void* __wrap_u_realloc_impl(void* ptr, uint32_t size, const char* location) { + return fail_allocation(FAULT_REALLOC, location) ? NULL : __real_u_realloc_impl(ptr, size, location); +} + +int __real_gettimeofday(struct timeval* tv, void* tz); +int __wrap_gettimeofday(struct timeval* tv, void* tz) { + int rc = __real_gettimeofday(tv, tz); + if (rc == 0 && clock_shift_ms) { + int64_t us = (int64_t)tv->tv_sec * 1000000 + tv->tv_usec + clock_shift_ms * 1000; + tv->tv_sec = us / 1000000; tv->tv_usec = us % 1000000; + } + return rc; +} + +void regression_epoll_events(const struct epoll_event* events, int count) { + if (count < 0 || count > 64) abort(); + memcpy(pending_events, events, count * sizeof(*events)); + pending_count = count; pending_active = 1; +} +int __real_epoll_wait(int fd, struct epoll_event* events, int maxevents, int timeout); +int __wrap_epoll_wait(int fd, struct epoll_event* events, int maxevents, int timeout) { + if (!pending_active) return __real_epoll_wait(fd, events, maxevents, timeout); + if (pending_count > maxevents) abort(); + memcpy(events, pending_events, pending_count * sizeof(*events)); + pending_active = 0; + return pending_count; +} +int __real_epoll_ctl(int epfd, int op, int fd, struct epoll_event* event); +int __wrap_epoll_ctl(int epfd, int op, int fd, struct epoll_event* event) { + if (fail_ctl == op) { fail_ctl = 0; errno = ENOMEM; return -1; } + return __real_epoll_ctl(epfd, op, fd, event); +} +int __real_eventfd(unsigned int initval, int flags); +int __wrap_eventfd(unsigned int initval, int flags) { + if (fail_eventfd) { fail_eventfd = 0; errno = EMFILE; return -1; } + return __real_eventfd(initval, flags); +} +int __real_epoll_create1(int flags); +int __wrap_epoll_create1(int flags) { + if (fail_epoll_create) { fail_epoll_create = 0; errno = EMFILE; return -1; } + return __real_epoll_create1(flags); +} + +uint64_t regression_time_ms(void) { + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) abort(); + return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +void regression_log_init(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); + for (int i = 1; i < DEBUG_CATEGORY_COUNT; i++) debug_set_category_level(i, DEBUG_LEVEL_WARN); + if (getenv("UTUN_TEST_DEBUG")) { + debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_DEBUG); + debug_set_category_level(DEBUG_CATEGORY_SOCKET, DEBUG_LEVEL_DEBUG); + debug_set_category_level(DEBUG_CATEGORY_TIMERS, DEBUG_LEVEL_DEBUG); + } +} + +static int open_fds(void) { + DIR* dir = opendir("/proc/self/fd"); + if (!dir) return -1; + int count = 0; + struct dirent* entry; + while ((entry = readdir(dir))) if (entry->d_name[0] != '.') count++; + closedir(dir); + return count; +} + +int regression_run(int argc, char** argv, const struct regression_case* cases, size_t count) { + if (argc > 2) { fprintf(stderr, "Usage: %s [case-name-substring]\n", argv[0]); return 1; } + setvbuf(stdout, NULL, _IONBF, 0); + int passed = 0, failed = 0, skipped = 0; + for (size_t i = 0; i < count; i++) { + if (argc == 2 && !strstr(cases[i].name, argv[1])) continue; + printf("[RUN] %s\n", cases[i].name); + int pipefd[2]; + if (pipe(pipefd) < 0) { perror("regression pipe"); return 1; } + pid_t pid = fork(); + if (pid < 0) { perror("regression fork"); close(pipefd[0]); close(pipefd[1]); return 1; } + if (pid == 0) { + close(pipefd[0]); + struct rlimit core = {0, 0}; setrlimit(RLIMIT_CORE, &core); + regression_log_init(); + size_t allocations = u_get_allocated_count(); + int fds = open_fds(); + int rc = cases[i].run(cases[i].param); + if (rc == 0 && (allocations != u_get_allocated_count() || fds != open_fds())) { + fprintf(stderr, "[FAIL] resource leak: allocations=%zu->%zu fds=%d->%d\n", + allocations, u_get_allocated_count(), fds, open_fds()); + u_report_unfreed_blocks(); rc = 1; + } + close(pipefd[1]); + _exit(rc); + } + close(pipefd[1]); + struct pollfd p = { .fd = pipefd[0], .events = POLLIN }; + uint64_t deadline = regression_time_ms() + 2000; + int ready; + do { + uint64_t now = regression_time_ms(); + int remaining = deadline > now ? (int)(deadline - now) : 0; + ready = poll(&p, 1, remaining); + } while (ready < 0 && errno == EINTR && regression_time_ms() < deadline); + int timed_out = ready <= 0; + if (timed_out && kill(pid, SIGKILL) < 0 && errno != ESRCH) perror("regression kill"); + close(pipefd[0]); + int status; + while (waitpid(pid, &status, 0) < 0) if (errno != EINTR) { perror("regression waitpid"); return 1; } + if (timed_out) { printf("[FAIL] %s: timeout (2000ms)\n", cases[i].name); failed++; } + else if (WIFSIGNALED(status)) { printf("[FAIL] %s: signal %d\n", cases[i].name, WTERMSIG(status)); failed++; } + else if (WEXITSTATUS(status) == 77) { printf("[SKIP] %s\n", cases[i].name); skipped++; } + else if (WEXITSTATUS(status)) { printf("[FAIL] %s: exit %d\n", cases[i].name, WEXITSTATUS(status)); failed++; } + else { printf("[PASS] %s\n", cases[i].name); passed++; } + } + printf("Test Results: %d passed, %d failed, %d skipped\n", passed, failed, skipped); + if (!passed && !failed && !skipped) { fprintf(stderr, "No matching cases\n"); return 1; } + return failed ? 1 : 0; +} diff --git a/tests/async_regression.h b/tests/async_regression.h new file mode 100644 index 00000000..c8ed0d8f --- /dev/null +++ b/tests/async_regression.h @@ -0,0 +1,41 @@ +/* Изоляция регрессий: падение/зависание одного случая не скрывает остальные. */ +#ifndef ASYNC_REGRESSION_H +#define ASYNC_REGRESSION_H + +#include "../lib/u_async.h" +#include "../lib/ll_queue.h" +#include "../lib/mem.h" +#include +#include + +#define CHECK(cond) do { if (!(cond)) { \ + fprintf(stderr, "[FAIL] %s:%d: %s\n", __FILE__, __LINE__, #cond); return 1; \ +} } while (0) +#define CHECK_EQ(actual, expected) do { \ + long long a_ = (long long)(actual), e_ = (long long)(expected); \ + if (a_ != e_) { fprintf(stderr, "[FAIL] %s:%d: %s=%lld expected=%lld\n", \ + __FILE__, __LINE__, #actual, a_, e_); return 1; } \ +} while (0) + +struct regression_case { + const char* name; + int (*run)(int param); + int param; +}; + +int regression_run(int argc, char** argv, const struct regression_case* cases, size_t count); +uint64_t regression_time_ms(void); +void regression_log_init(void); + +enum regression_alloc { FAULT_MALLOC = 1, FAULT_CALLOC = 2, FAULT_REALLOC = 4, FAULT_ALLOC = 7 }; +// nth=0 — устойчивый отказ, nth>0 — отказ только соответствующего выделения. +void regression_fault_arm(int mask, const char* scope, int nth); +int regression_fault_hit(void); +void regression_fault_reset(void); +void regression_clock_shift(int64_t milliseconds); +void regression_epoll_events(const struct epoll_event* events, int count); +void regression_epoll_ctl_fail(int operation); +void regression_eventfd_fail(void); +void regression_epoll_create_fail(void); + +#endif diff --git a/tests/test_android_udp_log.c b/tests/test_android_udp_log.c new file mode 100644 index 00000000..4cd77744 --- /dev/null +++ b/tests/test_android_udp_log.c @@ -0,0 +1,134 @@ +/* Реальный Android UDP-логгер: несколько producers, смена адреса, restart и + * reentrant debug hook. Все операции с таймерами обязаны идти в owner thread. */ +#include "../tools/chatgui-android/jni_bridge/android_udp_log.h" +#include "../lib/u_async.h" +#include "../lib/debug_config.h" +#include "../lib/mem.h" +#include +#include +#include +#include +#include +#include + +static pthread_t owner; +static int fail_timer; +static unsigned timer_sets, timer_cancels; + +void* __real_uasync_set_timeout(struct UASYNC*, int, void*, timeout_callback_t, const char*); +err_t __real_uasync_cancel_timeout(struct UASYNC*, void*); + +void* __wrap_uasync_set_timeout(struct UASYNC* ua, int tb, void* arg, timeout_callback_t cb, const char* name) { + assert(pthread_equal(owner, pthread_self())); + timer_sets++; + if (fail_timer) { + fail_timer = 0; + DEBUG_ERROR(DEBUG_CATEGORY_TIMERS, "injected flush timer allocation failure"); + return NULL; + } + return __real_uasync_set_timeout(ua, tb, arg, cb, name); +} + +err_t __wrap_uasync_cancel_timeout(struct UASYNC* ua, void* timer) { + assert(pthread_equal(owner, pthread_self())); + timer_cancels++; + return __real_uasync_cancel_timeout(ua, timer); +} + +struct producer { unsigned id; int port; }; + +static void* produce(void* arg) { + struct producer* p = arg; + char line[32]; memset(line, 'a' + p->id, sizeof(line)); line[31] = '\n'; + for (unsigned i = 0; i < 1000; i++) { + udp_log_write(line, sizeof(line)); + if (!p->id && i % 250 == 0) udp_log_set_target("127.0.0.1", p->port); + if (i % 32 == 0) usleep(500); + } + return NULL; +} + +static unsigned receive_lines(int fd) { + char buf[65536]; + unsigned count = 0; + ssize_t len; + while ((len = recv(fd, buf, sizeof(buf), MSG_DONTWAIT)) > 0) { + assert(len % 32 == 0); + for (ssize_t off = 0; off < len; off += 32) { + assert(buf[off] >= 'a' && buf[off] <= 'd' && buf[off + 31] == '\n'); + for (int i = 1; i < 31; i++) assert(buf[off + i] == buf[off]); + count++; + } + } + return count; +} + +static void forward_log(int level, const char* category, const char* message) { + (void)category; + udp_log_write(message, (int)strlen(message)); + if (level == DEBUG_LEVEL_ERROR) udp_log_flush(); +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); + owner = pthread_self(); + int fd = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0); assert(fd >= 0); + int buffer_size = 1024 * 1024; + assert(setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &buffer_size, sizeof(buffer_size)) == 0); + struct sockaddr_in addr = {0}; addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + assert(bind(fd, (struct sockaddr*)&addr, sizeof(addr)) == 0); + socklen_t addr_len = sizeof(addr); + assert(getsockname(fd, (struct sockaddr*)&addr, &addr_len) == 0); + int port = ntohs(addr.sin_port); + struct UASYNC* ua = uasync_create(); assert(ua); + udp_log_set_target("127.0.0.1", port); udp_log_attach(ua); + pthread_t threads[4]; struct producer producers[4] = {0}; + for (unsigned i = 0; i < 4; i++) { + producers[i].id = i; producers[i].port = port; + assert(pthread_create(&threads[i], NULL, produce, &producers[i]) == 0); + } + unsigned received = 0; + uint64_t deadline = get_time_tb() + 30000; + while (received < 4000 && get_time_tb() < deadline) { + uasync_poll(ua, 10); + received += receive_lines(fd); + } + for (int i = 0; i < 4; i++) assert(pthread_join(threads[i], NULL) == 0); + assert(received == 4000); + assert(timer_sets && ua->timer_alloc_count - ua->timer_free_count == 1); + puts("PASS: 4000 records from 4 threads, target changes and timed flush without torn records"); + + /* Команда в очереди перед detach не должна обращаться к уничтоженному ua. */ + udp_log_set_target("127.0.0.1", port); + udp_log_detach(ua); uasync_poll(ua, 0); + assert(ua->timer_alloc_count == ua->timer_free_count); + uasync_destroy(ua, 0); + unsigned detached_sets = timer_sets; + udp_log_write("detached\n", 9); + udp_log_set_target("127.0.0.1", port); + assert(timer_sets == detached_sets); + udp_log_stop(); + char error_log[4096]; + while (recv(fd, error_log, sizeof(error_log), MSG_DONTWAIT) > 0) {} + + /* Ошибка установки таймера логируется через тот же hook, без deadlock. */ + ua = uasync_create(); assert(ua); + debug_set_log_hook(forward_log); + udp_log_set_target("127.0.0.1", port); fail_timer = 1; udp_log_attach(ua); + ssize_t error_len = recv(fd, error_log, sizeof(error_log) - 1, MSG_DONTWAIT); + assert(error_len > 0); error_log[error_len] = 0; + assert(strstr(error_log, "injected flush timer allocation failure")); + udp_log_set_target("127.0.0.1", port); uasync_poll(ua, 0); + assert(ua->timer_alloc_count - ua->timer_free_count == 1); + udp_log_write("x", INT_MAX); /* Отказ до чтения данных, без переполнения длины. */ + udp_log_set_target(NULL, 0); uasync_poll(ua, 0); + assert(ua->timer_alloc_count == ua->timer_free_count); + udp_log_set_target("127.0.0.1", port); uasync_poll(ua, 0); + assert(ua->timer_alloc_count - ua->timer_free_count == 1); + udp_log_detach(ua); udp_log_stop(); uasync_poll(ua, 0); + assert(ua->timer_alloc_count == ua->timer_free_count && timer_cancels); + debug_set_log_hook(NULL); + uasync_destroy(ua, 0); close(fd); + puts("PASS: detach/restart, disable/re-enable, oversized log and reentrant error hook"); + return 0; +} diff --git a/tests/test_call_headless.c b/tests/test_call_headless.c index 1ef3eed2..3b42290f 100644 --- a/tests/test_call_headless.c +++ b/tests/test_call_headless.c @@ -502,9 +502,13 @@ int main(int argc, char** argv) { if (t.inst[IDX_A]) utun_instance_destroy(t.inst[IDX_A]); if (t.inst[IDX_B]) utun_instance_destroy(t.inst[IDX_B]); - uasync_poll(ua, 0); // завершить отложенное освобождение CM и роутера + uasync_drain_immediate(ua); // завершить все порции отложенного освобождения CM и роутера + uasync_poll(ua, 0); // удалить отменённые таймеры из heap if (ua->timer_alloc_count != ua->timer_free_count || ua->socket_alloc_count != ua->socket_free_count) { - fprintf(stderr, "headless teardown left live timers or sockets\n"); ok = 0; + fprintf(stderr, "headless teardown left live timers or sockets: timers=%zu/%zu sockets=%zu/%zu\n", + ua->timer_alloc_count, ua->timer_free_count, ua->socket_alloc_count, ua->socket_free_count); + debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_INFO); + uasync_print_resources(ua, "HEADLESS_TEARDOWN_FAILED"); ok = 0; } uasync_destroy(ua, 0); diff --git a/tests/test_ll_queue_regressions.c b/tests/test_ll_queue_regressions.c new file mode 100644 index 00000000..2bf630af --- /dev/null +++ b/tests/test_ll_queue_regressions.c @@ -0,0 +1,402 @@ +/* Проверяем очередь независимой моделью и контрактами callbacks/владения. */ +#include "async_regression.h" +#include +#include + +static void count_cb(struct ll_queue* q, void* arg) { (void)q; ++*(int*)arg; } +static struct ll_entry* new_entry(int value, int len) { + struct ll_entry* e = queue_entry_new(sizeof(value)); + if (e) { memcpy(e->data, &value, sizeof(value)); e->len = len; } + return e; +} +static int entry_value(struct ll_entry* e) { int value; memcpy(&value, e->data, sizeof(value)); return value; } +static void dispose(struct ll_queue* q) { + queue_set_callback(q, NULL, NULL); queue_set_on_get(q, NULL, NULL); queue_set_empty_callback(q, NULL, NULL); + struct ll_entry* e; + while ((e = queue_data_get(q))) { queue_dgram_free(e); queue_entry_free(e); } + queue_free(q); +} + +static int test_empty_notification(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 0, 0, 0, "empty notification"); CHECK(q); + int calls = 0; + if (param == 0 || param == 1) queue_set_empty_callback(q, count_cb, &calls); + struct ll_entry* e = new_entry(1, 12); CHECK(e); CHECK_EQ(queue_data_put(q, e), 0); + if (param == 0) uasync_drain_immediate(ua); /* Очередь успела заполниться до вызова. */ + if (param >= 2) queue_set_empty_callback(q, count_cb, &calls); + if (param == 3) { CHECK_EQ(queue_remove_data(q, e), 0); queue_entry_free(e); } + else queue_entry_free(queue_data_get(q)); + CHECK_EQ(calls, 0); uasync_drain_immediate(ua); CHECK_EQ(calls, 1); + uasync_drain_immediate(ua); CHECK_EQ(calls, 1); + dispose(q); uasync_destroy(ua, 0); return 0; +} +static int test_empty_cancel_replace(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 0, 0, 0, "empty cancellation"); CHECK(q); + int old = 0, current = 0; + queue_set_empty_callback(q, count_cb, &old); + queue_set_empty_callback(q, param ? count_cb : NULL, ¤t); + uasync_drain_immediate(ua); CHECK_EQ(old, 0); CHECK_EQ(current, param ? 1 : 0); + dispose(q); uasync_destroy(ua, 0); return 0; +} +static int test_empty_free(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 0, 0, 0, "empty before free"); CHECK(q); int calls = 0; + if (param) CHECK_EQ(queue_data_put(q, new_entry(1, 0)), 0); + queue_set_empty_callback(q, count_cb, &calls); + if (param) queue_entry_free(queue_data_get(q)); + queue_free(q); uasync_drain_immediate(ua); CHECK_EQ(calls, 0); + uasync_destroy(ua, 0); return 0; +} +static int test_on_get(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 0, 0, 0, "on get"); CHECK(q); int calls = 0; + queue_set_on_get(q, count_cb, &calls); + for (int i = 0; i < 4; i++) CHECK_EQ(queue_data_put(q, new_entry(i, i + 1)), 0); + for (int i = 0; i < 4; i++) queue_entry_free(queue_data_get(q)); + CHECK_EQ(calls, 0); + if (param) queue_set_on_get(q, NULL, NULL); + uasync_drain_immediate(ua); CHECK_EQ(calls, param ? 0 : 4); + dispose(q); uasync_destroy(ua, 0); return 0; +} +static int test_on_get_free(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 0, 0, 0, "on get teardown"); CHECK(q); int calls = 0; + queue_set_on_get(q, count_cb, &calls); + for (int i = 0; i < param + 1; i++) CHECK_EQ(queue_data_put(q, new_entry(i, 1)), 0); + for (int i = 0; i < param + 1; i++) queue_entry_free(queue_data_get(q)); + queue_set_on_get(q, NULL, NULL); queue_free(q); + uasync_drain_immediate(ua); CHECK_EQ(calls, 0); /* ASAN проверяет даже чтение cb=NULL из freed q. */ + uasync_destroy(ua, 0); return 0; +} + +typedef int (*put_fn)(struct ll_queue*, struct ll_entry*); +static put_fn putters[] = {queue_data_put, queue_data_put_first, queue_data_put_with_index, queue_data_put_first_with_index}; +static int test_limits(int param) { + int which = param & 3, limit = param & 4 ? 0 : 1; + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, which >= 2 ? 1 : 0, 0, which >= 2 ? sizeof(int) : 0, "limit"); CHECK(q); + queue_set_size_limit(q, limit); + if (limit) CHECK_EQ(putters[which](q, new_entry(1, 30)), 0); + CHECK_EQ(putters[which](q, new_entry(2, 40)), -1); + CHECK_EQ(q->count, limit); CHECK_EQ(q->total_bytes, limit ? 30 : 0); + dispose(q); uasync_destroy(ua, 0); return 0; +} +static int test_remove_invalid(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 1, 0, sizeof(int), "invalid remove"); CHECK(q); + struct ll_entry* a = new_entry(1, 100), *b = new_entry(2, 200); CHECK(a && b); + CHECK_EQ(queue_data_put_with_index(q, a), 0); + if (param == 1) { + CHECK_EQ(queue_data_put_with_index(q, b), 0); CHECK_EQ(queue_remove_data(q, b), 0); + } + struct ll_queue* other = NULL; + if (param == 2) { + other = queue_new(ua, 1, 0, sizeof(int), "other queue"); CHECK(other); + CHECK_EQ(queue_data_put_with_index(other, b), 0); + } + CHECK_EQ(queue_remove_data(q, b), -1); + CHECK_EQ(q->count, 1); CHECK_EQ(q->total_bytes, 100); CHECK(q->head == a && q->tail == a); + CHECK_EQ(queue_check_consistency(q), 0); + if (other) dispose(other); else queue_entry_free(b); + dispose(q); uasync_destroy(ua, 0); return 0; +} + +struct consume_test { int calls, depth, max_depth, error, action; }; +static void consume_cb(struct ll_queue* q, void* arg) { + struct consume_test* t = arg; + if (++t->depth > t->max_depth) t->max_depth = t->depth; + struct ll_entry* e = queue_data_get(q); + if (!e) t->error = 1; else { t->calls++; queue_entry_free(e); } + if (t->action != 1) queue_resume_callback(q); + t->depth--; +} +static int test_consume(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 0, 0, 0, "consumer"); CHECK(q); + struct consume_test t = {0}; t.action = param & 2 ? 1 : 0; + queue_set_callback(q, consume_cb, &t); queue_set_callback_defer(q, param & 1); + for (int i = 0; i < 8; i++) CHECK_EQ(queue_data_put(q, new_entry(i, 1)), 0); + if (param & 1) CHECK_EQ(t.calls, 0); + uasync_drain_immediate(ua); + if (t.action) { + CHECK_EQ(t.calls, 1); CHECK_EQ(q->count, 7); + t.action = 0; queue_resume_callback(q); uasync_drain_immediate(ua); + } + CHECK_EQ(t.calls, 8); CHECK_EQ(t.error, 0); CHECK_EQ(t.max_depth, 1); CHECK_EQ(q->count, 0); + dispose(q); uasync_destroy(ua, 0); return 0; +} +static int test_consume_free_pending(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 0, 0, 0, "consumer teardown"); CHECK(q); + struct consume_test t = {0}; queue_set_callback(q, consume_cb, &t); queue_set_callback_defer(q, 1); + CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); + if (param) queue_set_callback(q, NULL, NULL); + dispose(q); uasync_drain_immediate(ua); CHECK_EQ(t.calls, 0); + uasync_destroy(ua, 0); return 0; +} +static int test_waiter_cancel(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 0, 0, 0, "waiter cancellation"); CHECK(q); + queue_set_waiter_defer(q, 1); int calls = 0; struct queue_waiter_handle h = {0}; + if (param) CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); + CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &calls), param ? 0 : 1); + if (param == 2) queue_entry_free(queue_data_get(q)); + queue_waiter_cancel(q, &h); CHECK(!h.internal && !h.call_soon_id); + uasync_drain_immediate(ua); CHECK_EQ(calls, 0); + dispose(q); uasync_destroy(ua, 0); return 0; +} +static int test_waiter_bytes(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 0, 0, 0, "waiter bytes"); CHECK(q); + int calls = 0; struct queue_waiter_handle h = {0}; queue_set_waiter_defer(q, param); + queue_set_threshold(q, 10, 20); + CHECK_EQ(queue_data_put(q, new_entry(1, 20)), 0); CHECK_EQ(queue_data_put(q, new_entry(2, 30)), 0); + CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &calls), 0); + queue_entry_free(queue_data_get(q)); uasync_drain_immediate(ua); CHECK_EQ(calls, 0); + queue_entry_free(queue_data_get(q)); uasync_drain_immediate(ua); CHECK_EQ(calls, 1); + CHECK(!h.internal && !h.call_soon_id); + queue_waiter_cancel(q, &h); dispose(q); uasync_destroy(ua, 0); return 0; +} +static int test_waiter_teardown(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 0, 0, 0, "waiter teardown"); CHECK(q); + struct queue_waiter_handle h = {0}; int calls = 0; queue_set_waiter_defer(q, 1); + if (param) CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); + queue_waiter_wait(q, &h, count_cb, &calls); + if (param == 2) queue_entry_free(queue_data_get(q)); + queue_waiter_cancel(q, &h); dispose(q); uasync_drain_immediate(ua); CHECK_EQ(calls, 0); + CHECK(!h.internal && !h.call_soon_id); uasync_destroy(ua, 0); return 0; +} + +/* Teardown без ручной отмены и повторное использование handle до dispatch. */ +static int test_waiter_auto_teardown(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 0, 0, 0, "automatic teardown"); CHECK(q); + queue_set_waiter_defer(q, 1); struct queue_waiter_handle h[8] = {0}; int calls = 0; + if (param) CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); + for (int i = 0; i < 8; i++) CHECK_EQ(queue_waiter_wait(q, &h[i], count_cb, &calls), param ? 0 : 1); + if (param == 2) queue_entry_free(queue_data_get(q)); + dispose(q); + for (int i = 0; i < 8; i++) CHECK(!h[i].internal && !h[i].call_soon_id && !h[i].defer_q); + uasync_drain_immediate(ua); CHECK_EQ(calls, 0); uasync_destroy(ua, 0); return 0; +} +static int test_waiter_rearm(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 0, 0, 0, "rearm"); CHECK(q); + struct queue_waiter_handle h = {0}; int old = 0, current = 0; + queue_set_waiter_defer(q, 1); CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &old), 1); + if (param) CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); + CHECK_EQ(queue_waiter_wait(q, &h, count_cb, ¤t), param ? 0 : 1); + if (param) queue_entry_free(queue_data_get(q)); + uasync_drain_immediate(ua); CHECK_EQ(old, 0); CHECK_EQ(current, 1); + CHECK(!h.internal && !h.call_soon_id && !h.defer_q); + dispose(q); uasync_destroy(ua, 0); return 0; +} +static void free_queue_cb(struct ll_queue* q, void* arg) { ++*(int*)arg; dispose(q); } +static int test_callback_free_queue(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + int which = param < 8 ? param & 3 : 0, deferred = param & 4; + struct ll_queue* q = queue_new(ua, which >= 2 ? 1 : 0, 0, which >= 2 ? sizeof(int) : 0, "callback teardown"); CHECK(q); + int calls = 0; + if (param < 8) { + queue_set_callback(q, free_queue_cb, &calls); queue_set_callback_defer(q, deferred); + CHECK_EQ(putters[which](q, new_entry(1, 10)), 0); + } else if (param == 8) queue_set_empty_callback(q, free_queue_cb, &calls); + else if (param == 9) { + for (int i = 0; i < 4; i++) CHECK_EQ(queue_data_put(q, new_entry(i, 1)), 0); + queue_set_on_get(q, free_queue_cb, &calls); + for (int i = 0; i < 4; i++) queue_entry_free(queue_data_get(q)); + } else { + struct queue_waiter_handle h = {0}; queue_set_waiter_defer(q, param == 11); + CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); + CHECK_EQ(queue_waiter_wait(q, &h, free_queue_cb, &calls), 0); + queue_entry_free(queue_data_get(q)); uasync_drain_immediate(ua); + CHECK(!h.internal && !h.call_soon_id && !h.defer_q); + } + uasync_drain_immediate(ua); CHECK_EQ(calls, 1); uasync_destroy(ua, 0); return 0; +} + +/* Связный список/хеш сравниваются с простым массивом, без использования internals поиска. */ +static int test_model(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, param == 0 ? 0 : param == 1 ? 1 : 32, 0, + param ? sizeof(int) : 0, "model"); CHECK(q); + struct ll_entry* model[64]; int count = 0; uint32_t random = 0x73519321u; + for (int step = 0; step < 10000; step++) { + random = random * 1664525u + 1013904223u; + int op = random >> 28; + if (count == 0 || (count < 64 && op < 8)) { + int value = (random >> 8) % 16, len = random % 1000; + struct ll_entry* e = new_entry(value, len); CHECK(e); + int first = op & 1; + CHECK_EQ(putters[(param ? 2 : 0) + first](q, e), 0); + if (first) memmove(model + 1, model, count * sizeof(*model)); + model[first ? 0 : count] = e; count++; + } else { + int pos = op & 1 ? random % count : 0; + struct ll_entry* e = model[pos]; + if (op & 1) CHECK_EQ(queue_remove_data(q, e), 0); else CHECK(queue_data_get(q) == e); + memmove(model + pos, model + pos + 1, (count - pos - 1) * sizeof(*model)); count--; + CHECK(!e->next && !e->prev && !e->hash_next); queue_entry_free(e); + } + size_t bytes = 0; + struct ll_entry* actual = q->head; + for (int i = 0; i < count; i++) { + CHECK(actual == model[i]); CHECK(actual->prev == (i ? model[i - 1] : NULL)); + bytes += model[i]->len; actual = actual->next; + } + CHECK(!actual); CHECK_EQ(q->count, count); CHECK_EQ(q->total_bytes, bytes); + CHECK(q->tail == (count ? model[count - 1] : NULL)); CHECK_EQ(queue_check_consistency(q), 0); + if (param && step % 31 == 0) for (int key = 0; key < 16; key++) { + int expected = 0, found = 0; + for (int i = 0; i < count; i++) if (entry_value(model[i]) == key) expected++; + struct ll_entry* e = queue_find_data_by_index(q, &key); + while (e) { + CHECK_EQ(entry_value(e), key); int present = 0; + for (int i = 0; i < count; i++) if (model[i] == e) present++; + CHECK_EQ(present, 1); CHECK(++found <= expected); + e = queue_find_next_by_index(q, &key, e); + } + CHECK_EQ(found, expected); + } + } + dispose(q); uasync_destroy(ua, 0); return 0; +} +static int test_snapshot_bytes(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 0, 0, 0, "length snapshot"); CHECK(q); + struct ll_entry* e = new_entry(1, 100); CHECK(e); CHECK_EQ(queue_data_put(q, e), 0); + e->len = 1; CHECK_EQ(q->total_bytes, 100); + if (param) CHECK_EQ(queue_remove_data(q, e), 0); else CHECK(queue_data_get(q) == e); + CHECK_EQ(q->total_bytes, 0); queue_entry_free(e); + dispose(q); uasync_destroy(ua, 0); return 0; +} +static int test_index_bounds(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 1, param & 2 ? 3 : 0, param & 2 ? 4 : 0, "invalid index"); CHECK(q); + CHECK_EQ(putters[2 + (param & 1)](q, new_entry(1, 30)), -1); + CHECK_EQ(q->count, 0); CHECK_EQ(q->total_bytes, 0); + dispose(q); uasync_destroy(ua, 0); return 0; +} +static int test_binary_index(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 1, 3, 7, "binary index"); CHECK(q); + const unsigned char key[] = {0, 1, 0xff, 0x80, 2, 0, 3}; + struct ll_entry* e = queue_entry_new(10); CHECK(e); memcpy(e->data + 3, key, sizeof(key)); + CHECK_EQ(putters[2 + param](q, e), 0); CHECK(queue_find_data_by_index(q, key) == e); + CHECK_EQ(queue_remove_data(q, e), 0); CHECK(!queue_find_data_by_index(q, key)); queue_entry_free(e); + dispose(q); uasync_destroy(ua, 0); return 0; +} +static int custom_frees; +static void custom_free(uint8_t* ptr) { custom_frees++; u_free(ptr); } +static int test_dgram_ownership(int param) { + struct ll_entry* e = queue_entry_new(0); CHECK(e); e->len = 10; + struct memory_pool* pool = NULL; + if (param == 1) { + pool = memory_pool_init(32, "dgram ownership"); CHECK(pool); + e->dgram_pool = pool; e->dgram = memory_pool_alloc(pool); + } else { + e->dgram = u_malloc(32); if (param == 2) e->dgram_free_fn = custom_free; + } + CHECK(e->dgram); queue_dgram_free(e); queue_dgram_free(e); + CHECK(!e->dgram); CHECK_EQ(e->len, 0); CHECK_EQ(custom_frees, param == 2 ? 1 : 0); + queue_entry_free(e); if (pool) memory_pool_destroy(pool); return 0; +} + +static int test_creation_oom(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + regression_fault_arm(FAULT_CALLOC, "ll_queue.c:", param); + CHECK(!queue_new(ua, 8, 0, sizeof(int), "creation oom")); + CHECK(regression_fault_hit()); regression_fault_reset(); uasync_destroy(ua, 0); return 0; +} +static int test_entry_oom(int param) { + regression_fault_arm(FAULT_MALLOC, "ll_queue.c:", param ? 2 : 1); + CHECK(!(param ? ll_alloc_lldgram(32) : queue_entry_new(8))); + CHECK(regression_fault_hit()); regression_fault_reset(); return 0; +} +static int test_waiter_oom(int param) { + (void)param; + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 0, 0, 0, "waiter oom"); CHECK(q); + CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); int calls = 0; struct queue_waiter_handle h = {0}; + regression_fault_arm(FAULT_MALLOC, "ll_queue.c:", 1); + CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &calls), -1); + CHECK(regression_fault_hit()); regression_fault_reset(); CHECK(!h.internal); + CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &calls), 0); + queue_entry_free(queue_data_get(q)); CHECK_EQ(calls, 1); + queue_waiter_cancel(q, &h); dispose(q); uasync_destroy(ua, 0); return 0; +} + +static int test_waiter_schedule_oom(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct ll_queue* q = queue_new(ua, 0, 0, 0, "waiter dispatch oom"); CHECK(q); + queue_set_waiter_defer(q, 1); struct queue_waiter_handle h = {0}; int calls = 0; + if (param) { + queue_set_threshold(q, 1, 0); + CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); CHECK_EQ(queue_data_put(q, new_entry(2, 1)), 0); + CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &calls), 0); + } + regression_fault_arm(FAULT_CALLOC, "u_async.c:", 1); + if (param) queue_entry_free(queue_data_get(q)); + else CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &calls), -1); + CHECK(regression_fault_hit()); regression_fault_reset(); + if (param) { CHECK(h.internal); queue_entry_free(queue_data_get(q)); } + else { CHECK(!h.internal && !h.call_soon_id); CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &calls), 1); } + uasync_drain_immediate(ua); CHECK_EQ(calls, 1); + CHECK(!h.internal && !h.call_soon_id && !h.defer_q); + dispose(q); uasync_destroy(ua, 0); return 0; +} + +#define CASE(name, fn, p) {name, fn, p} +int main(int argc, char** argv) { + const struct regression_case cases[] = { + CASE("empty/refill-before-dispatch", test_empty_notification, 0), + CASE("empty/drain-before-dispatch", test_empty_notification, 1), + CASE("empty/drain-get", test_empty_notification, 2), CASE("empty/drain-remove", test_empty_notification, 3), + CASE("empty/cancel", test_empty_cancel_replace, 0), CASE("empty/replace", test_empty_cancel_replace, 1), + CASE("empty/free-pending-empty", test_empty_free, 0), CASE("empty/free-pending-get", test_empty_free, 1), + CASE("on-get/each-entry", test_on_get, 0), CASE("on-get/cancel", test_on_get, 1), + CASE("on-get/free-one-pending", test_on_get_free, 0), CASE("on-get/free-many-pending", test_on_get_free, 3), + CASE("limit/put", test_limits, 0), CASE("limit/put-first", test_limits, 1), + CASE("limit/put-index", test_limits, 2), CASE("limit/put-first-index", test_limits, 3), + CASE("limit/zero-put", test_limits, 4), CASE("limit/zero-put-first", test_limits, 5), + CASE("limit/zero-put-index", test_limits, 6), CASE("limit/zero-put-first-index", test_limits, 7), + CASE("remove/not-in-any-queue", test_remove_invalid, 0), CASE("remove/already-removed", test_remove_invalid, 1), + CASE("remove/in-another-queue", test_remove_invalid, 2), + CASE("consumer/immediate", test_consume, 0), CASE("consumer/deferred", test_consume, 1), + CASE("consumer/suspend-resume", test_consume, 2), CASE("consumer/deferred-suspend-resume", test_consume, 3), + CASE("consumer/free-pending", test_consume_free_pending, 0), CASE("consumer/cancel-pending", test_consume_free_pending, 1), + CASE("waiter/cancel-free-queue", test_waiter_cancel, 0), CASE("waiter/cancel-busy-queue", test_waiter_cancel, 1), + CASE("waiter/cancel-released-queue", test_waiter_cancel, 2), + CASE("waiter/byte-threshold-immediate", test_waiter_bytes, 0), CASE("waiter/byte-threshold-deferred", test_waiter_bytes, 1), + CASE("waiter/teardown-free", test_waiter_teardown, 0), CASE("waiter/teardown-busy", test_waiter_teardown, 1), + CASE("waiter/teardown-released", test_waiter_teardown, 2), + CASE("waiter/auto-free-pending", test_waiter_auto_teardown, 0), + CASE("waiter/auto-free-busy", test_waiter_auto_teardown, 1), + CASE("waiter/auto-free-released", test_waiter_auto_teardown, 2), + CASE("waiter/rearm-pending", test_waiter_rearm, 0), CASE("waiter/rearm-busy", test_waiter_rearm, 1), + CASE("callback/free-put", test_callback_free_queue, 0), CASE("callback/free-put-first", test_callback_free_queue, 1), + CASE("callback/free-put-index", test_callback_free_queue, 2), CASE("callback/free-put-first-index", test_callback_free_queue, 3), + CASE("callback/free-deferred-put", test_callback_free_queue, 4), + CASE("callback/free-deferred-put-first", test_callback_free_queue, 5), + CASE("callback/free-deferred-put-index", test_callback_free_queue, 6), + CASE("callback/free-deferred-put-first-index", test_callback_free_queue, 7), + CASE("callback/free-empty", test_callback_free_queue, 8), CASE("callback/free-on-get", test_callback_free_queue, 9), + CASE("callback/free-waiter", test_callback_free_queue, 10), CASE("callback/free-deferred-waiter", test_callback_free_queue, 11), + CASE("model/no-index-10000", test_model, 0), CASE("model/collisions-10000", test_model, 1), + CASE("model/hash-10000", test_model, 2), + CASE("bytes/snapshot-get", test_snapshot_bytes, 0), CASE("bytes/snapshot-remove", test_snapshot_bytes, 1), + CASE("index/zero-put", test_index_bounds, 0), CASE("index/zero-put-first", test_index_bounds, 1), + CASE("index/overflow-put", test_index_bounds, 2), CASE("index/overflow-put-first", test_index_bounds, 3), + CASE("index/binary-offset-put", test_binary_index, 0), CASE("index/binary-offset-put-first", test_binary_index, 1), + CASE("ownership/dgram-malloc", test_dgram_ownership, 0), CASE("ownership/dgram-pool", test_dgram_ownership, 1), + CASE("ownership/dgram-custom", test_dgram_ownership, 2), + CASE("oom/waiter-immediate-dispatch-retry", test_waiter_schedule_oom, 0), + CASE("oom/waiter-released-dispatch-retry", test_waiter_schedule_oom, 1), + CASE("oom/queue-object", test_creation_oom, 1), CASE("oom/hash-table", test_creation_oom, 2), + CASE("oom/entry", test_entry_oom, 0), CASE("oom/dgram", test_entry_oom, 1), CASE("oom/waiter-retry", test_waiter_oom, 0), + }; + return regression_run(argc, argv, cases, sizeof(cases) / sizeof(cases[0])); +} diff --git a/tests/test_pool_corruption_log.c b/tests/test_pool_corruption_log.c new file mode 100644 index 00000000..c6ed553a --- /dev/null +++ b/tests/test_pool_corruption_log.c @@ -0,0 +1,95 @@ +/* Проверяем лог до намеренной остановки; зависший child завершает parent. */ +#include "../lib/memory_pool.h" +#include "../lib/u_async.h" +#include "../lib/debug_config.h" +#include +#include +#include +#include +#include +#include +#include + +static int log_fd; +static struct UASYNC* child_ua; +static void* child_timer; + +static void capture_log(int level, const char* category, const char* message) { + (void)level; (void)category; + size_t len = strlen(message); + assert(write(log_fd, message, len) == (ssize_t)len); +} + +static void free_executing_timer(void* arg) { + (void)arg; + memory_pool_free_impl(child_ua->timeout_pool, child_timer, "first free inside callback"); +} + +static void damage(int mode) { + if (mode == 2) { + child_ua = uasync_create(); assert(child_ua); + child_timer = uasync_set_timeout(child_ua, 0, NULL, free_executing_timer, "corrupt_timer_test"); assert(child_timer); + for (int i = 0; i < 10; i++) uasync_poll(child_ua, 10); + } else { + struct memory_pool* pool = memory_pool_init(16, "corrupt_pool_test"); assert(pool); + void* obj = memory_pool_alloc_impl(pool, "allocation test location"); assert(obj); + if (mode == 0) { + memory_pool_free_impl(pool, obj, "first free test location"); + memory_pool_free_impl(pool, obj, "repeated free test location"); + } else { + /* Битые pointers в хвосте нельзя разыменовывать для вывода строки. */ + memset((char*)obj + pool->object_size, 0xa5, 4 + 2 * sizeof(const char*)); + memory_pool_free_impl(pool, obj, "overflow free test location"); + } + } + _exit(1); /* Остановка обязательна. */ +} + +static void check_log_before_halt(int mode) { + int pipe_fd[2]; assert(pipe(pipe_fd) == 0); + pid_t pid = fork(); assert(pid >= 0); + if (!pid) { + close(pipe_fd[0]); log_fd = pipe_fd[1]; + debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_enable_console(0); + debug_set_log_hook(capture_log); + damage(mode); + } + close(pipe_fd[1]); + char output[8192] = {0}; size_t used = 0; + uint64_t deadline = get_time_tb() + 30000; + while (!strstr(output, "stopping thread permanently") && get_time_tb() < deadline) { + struct pollfd p = { .fd = pipe_fd[0], .events = POLLIN }; + if (poll(&p, 1, 100) <= 0) continue; + ssize_t len = read(pipe_fd[0], output + used, sizeof(output) - used - 1); + if (len <= 0) break; + used += len; output[used] = 0; + if (used == sizeof(output) - 1) break; + } + /* Если вместо остановки произошёл abort/exit, pipe получит HUP. */ + struct pollfd stopped = { .fd = pipe_fd[0], .events = POLLIN }; + poll(&stopped, 1, 100); + int status = 0; + int still_running = waitpid(pid, &status, WNOHANG) == 0; + if (still_running) { assert(kill(pid, SIGKILL) == 0); assert(waitpid(pid, &status, 0) == pid); } + close(pipe_fd[0]); + fprintf(stderr, "%s", output); + assert(still_running && strstr(output, "stopping thread permanently")); + if (mode == 1) { + assert(strstr(output, "BUFFER OVERFLOW") && strstr(output, "overflow free test location")); + } else { + assert(strstr(output, "DOUBLE FREE") && strstr(output, "first_free=") && strstr(output, "repeated_free=")); + if (mode == 0) { + assert(strstr(output, "allocation test location") && strstr(output, "first free test location")); + assert(strstr(output, "repeated free test location")); + } else { + assert(strstr(output, "timer freed inside callback") && strstr(output, "corrupt_timer_test")); + assert(strstr(output, "first free inside callback")); + } + } +} + +int main(void) { + for (int i = 0; i < 3; i++) check_log_before_halt(i); + puts("PASS: double free, damaged canary/metadata and timer callback log before permanent halt"); + return 0; +} diff --git a/tests/test_speex_aec.c b/tests/test_speex_aec.c index cb7a6dc6..9a8ca661 100644 --- a/tests/test_speex_aec.c +++ b/tests/test_speex_aec.c @@ -98,11 +98,10 @@ static int test_delay_line(void) { speex_aec_feed_playback(a, pcm + 480, 480); CHECK(speex_aec_delay_fill(a) == 1, "fill=%d after 960 (expect 1)", speex_aec_delay_fill(a)); - /* захват чанками по 480 → 0, потом кадр */ + /* Частичный захват отклоняется без изменения линии задержки. */ n = speex_aec_process_capture(a, in, 480, out); - CHECK(n == 0, "capture 480 should accumulate (returned %d)", n); - n = speex_aec_process_capture(a, in + 480, 480, out); - CHECK(n == FS, "capture 960 should emit frame (returned %d)", n); + CHECK(n == 0, "partial capture must be rejected (returned %d)", n); + CHECK(speex_aec_delay_fill(a) == 1, "invalid capture changed delay line"); /* докормить рендер до глубины 3 */ speex_aec_feed_playback(a, pcm, FS); diff --git a/tests/test_u_async_comprehensive.c b/tests/test_u_async_comprehensive.c index 5fbb0c4e..d34587be 100644 --- a/tests/test_u_async_comprehensive.c +++ b/tests/test_u_async_comprehensive.c @@ -107,6 +107,7 @@ typedef struct { /* Timer callback for testing */ static void test_timer_callback(void* arg) { test_context_t* ctx = (test_context_t*)arg; + ctx->timer_id = NULL; ctx->callback_count++; test_stats.timer_callbacks++; @@ -177,13 +178,12 @@ static void test_timer_cancellation_races(void) { uasync_t* ua = uasync_create(); ASSERT_NOT_NULL(ua, "Failed to create uasync instance"); - test_context_t ctx = {0}; - ctx.expected_count = 2; + test_context_t ctx[3] = {{0}}; /* Create timers that will be cancelled at different stages */ - void* timer1 = uasync_set_timeout(ua, 5, &ctx, test_timer_callback, "test_t1"); /* 0.5ms */ - void* timer2 = uasync_set_timeout(ua, 50, &ctx, test_timer_callback, "test_t2"); /* 5ms */ - void* timer3 = uasync_set_timeout(ua, 100, &ctx, test_timer_callback, "test_t3"); /* 10ms */ + void* timer1 = ctx[0].timer_id = uasync_set_timeout(ua, 5, &ctx[0], test_timer_callback, "test_t1"); + void* timer2 = ctx[1].timer_id = uasync_set_timeout(ua, 5000, &ctx[1], test_timer_callback, "test_t2"); + void* timer3 = ctx[2].timer_id = uasync_set_timeout(ua, 100, &ctx[2], test_timer_callback, "test_t3"); ASSERT_NOT_NULL(timer1, "Failed to set timer 1"); ASSERT_NOT_NULL(timer2, "Failed to set timer 2"); @@ -192,30 +192,34 @@ static void test_timer_cancellation_races(void) { /* Cancel timer1 immediately (before it fires) */ err_t cancel_result = uasync_cancel_timeout(ua, timer1); ASSERT_EQ(cancel_result, ERR_OK, "Failed to cancel timer 1"); + ctx[0].timer_id = NULL; test_stats.timer_cancellations++; /* Poll briefly - timer1 should not fire, others should */ uasync_poll(ua, 10); /* 1ms */ - /* Cancel timer2 while it might be firing */ - cancel_result = uasync_cancel_timeout(ua, timer2); - /* Result could be ERR_OK or ERR_FAIL depending on timing */ + /* После callback handle уже недействителен: отменяем только pending таймер. */ + if (ctx[1].timer_id) { + ASSERT_EQ(uasync_cancel_timeout(ua, ctx[1].timer_id), ERR_OK, "Failed to cancel pending timer 2"); + ctx[1].timer_id = NULL; + } /* Continue polling */ int poll_count = 0; - while (ctx.callback_count < 2 && poll_count < 50) { + while (ctx[2].callback_count < 1 && poll_count < 50) { uasync_poll(ua, 10); poll_count++; } /* Verify we got expected callbacks (timer3 + possibly timer2) */ - ASSERT_TRUE(ctx.callback_count >= 1, "Too few timers fired"); - ASSERT_TRUE(ctx.callback_count <= 2, "Too many timers fired"); + ASSERT_EQ(ctx[0].callback_count, 0, "Cancelled timer fired"); + ASSERT_EQ(ctx[1].callback_count, 0, "Cancelled timer 2 fired"); + ASSERT_EQ(ctx[2].callback_count, 1, "Timer 3 must fire exactly once"); /* Cleanup remaining timer */ - if (timer3) { - uasync_cancel_timeout(ua, timer3); - timer3 = NULL; + if (ctx[2].timer_id) { + uasync_cancel_timeout(ua, ctx[2].timer_id); + ctx[2].timer_id = NULL; } uasync_destroy(ua, 0); @@ -413,9 +417,10 @@ static void test_error_handling(void) { void* error_timer = uasync_set_timeout(ua, 5, &ctx, test_error_callback, "test_err"); ASSERT_NOT_NULL(error_timer, "Failed to set error timer"); - uasync_poll(ua, 10); + for (int i = 0; i < 20 && !ctx.callback_count; i++) uasync_poll(ua, 10); - /* Verify error was recorded */ + /* Проверяем вызов именно этого callback, с учётом округления таймеров вверх. */ + ASSERT_EQ(ctx.callback_count, 1, "Error callback must fire exactly once"); ASSERT_TRUE(test_stats.race_condition_errors > 0, "Error wasn't recorded"); /* Cleanup */ @@ -483,6 +488,7 @@ static void test_concurrent_operations(void) { contexts[i] = individual_ctx; timers[i] = uasync_set_timeout(ua, (i + 1) * 5, individual_ctx, test_timer_callback, "test_stress"); ASSERT_NOT_NULL(timers[i], "Failed to set timer"); + individual_ctx->timer_id = timers[i]; } /* Stress test: poll while operations are happening */ @@ -490,8 +496,9 @@ static void test_concurrent_operations(void) { /* Cancel some timers randomly */ if (cycle % 7 == 0) { int idx = cycle % 10; - if (timers[idx]) { - uasync_cancel_timeout(ua, timers[idx]); + if (contexts[idx]->timer_id) { + ASSERT_EQ(uasync_cancel_timeout(ua, contexts[idx]->timer_id), ERR_OK, "Failed to cancel pending stress timer"); + contexts[idx]->timer_id = NULL; timers[idx] = NULL; } } @@ -520,9 +527,7 @@ static void test_concurrent_operations(void) { } for (int i = 0; i < 10; i++) { - if (contexts[i] && contexts[i]->callback_count == 0 && timers[i]) { - uasync_cancel_timeout(ua, timers[i]); - } + if (contexts[i] && contexts[i]->timer_id) uasync_cancel_timeout(ua, contexts[i]->timer_id); timers[i] = NULL; if (contexts[i]) { u_free(contexts[i]); contexts[i] = NULL; } } @@ -641,6 +646,8 @@ int main(void) { DEBUG_INFO(DEBUG_CATEGORY_SYS, "=== lib Comprehensive Unit Tests ==="); DEBUG_INFO(DEBUG_CATEGORY_SYS, "Testing race conditions, memory management, and error handling"); + size_t allocated_before = u_get_allocated_count(); + /* Run all tests */ test_basic_timers(); test_timer_cancellation_races(); @@ -665,7 +672,10 @@ int main(void) { /* Memory leak detection */ DEBUG_INFO(DEBUG_CATEGORY_SYS, "=== Memory Leak Detection ==="); - DEBUG_INFO(DEBUG_CATEGORY_SYS, "No memory leaks detected during testing"); + if (u_get_allocated_count() != allocated_before) { + TEST_FAIL("Memory allocation count did not return to baseline"); + u_report_unfreed_blocks(); + } else DEBUG_INFO(DEBUG_CATEGORY_SYS, "No memory leaks detected during testing"); return (test_stats.tests_failed > 0) ? 1 : 0; -} \ No newline at end of file +} diff --git a/tests/test_u_async_timeouts.c b/tests/test_u_async_timeouts.c index 8a3fd0ad..1d1544fd 100644 --- a/tests/test_u_async_timeouts.c +++ b/tests/test_u_async_timeouts.c @@ -18,6 +18,7 @@ typedef struct { uint64_t fire_time; /* время срабатывания */ int fired; /* флаг что уже сработал */ void* timer_id; /* ID таймера в uasync */ + void* context; /* Pending контекст принадлежит тесту. */ } timer_data_t; typedef struct { @@ -35,6 +36,8 @@ static int test_failed = 0; /* Callback для таймера */ static void timer_callback(void* arg) { timer_ctx_t* ctx = (timer_ctx_t*)arg; + ctx->timer_data[ctx->timer_number].timer_id = NULL; + ctx->timer_data[ctx->timer_number].context = NULL; /* Записываем время срабатывания */ ctx->timer_data[ctx->timer_number].fire_time = get_time_tb(); @@ -44,6 +47,7 @@ static void timer_callback(void* arg) { /* Если stop_flag установлен - НЕ перезапускаем */ if (*ctx->stop_flag == 1) { (*ctx->active_count)--; + u_free(ctx); return; } @@ -62,6 +66,7 @@ static void timer_callback(void* arg) { void* new_timer = uasync_set_timeout(ctx->ua, random_delay, new_ctx, timer_callback, "test_timer"); if (new_timer) { ctx->timer_data[ctx->timer_number].timer_id = new_timer; + ctx->timer_data[ctx->timer_number].context = new_ctx; } else { /* Не удалось создать - уменьшаем active_count */ (*ctx->active_count)--; @@ -70,6 +75,7 @@ static void timer_callback(void* arg) { } else { (*ctx->active_count)--; } + u_free(ctx); } /* Callback для стоп-таймера */ @@ -84,7 +90,7 @@ int main(void) { debug_set_categories(DEBUG_CATEGORY_ALL); DEBUG_INFO(DEBUG_CATEGORY_TEST, "=== UASYNC Timeout Test ==="); - DEBUG_INFO(DEBUG_CATEGORY_TEST, "Timer count: %d, random delay 0-10ms, stop after 10s", TIMER_COUNT); + DEBUG_INFO(DEBUG_CATEGORY_TEST, "Timer count: %d, random delay 0-10ms, stop after 1s", TIMER_COUNT); DEBUG_INFO(DEBUG_CATEGORY_TEST, "Each timer restarts with new random delay until stop"); srand(42); @@ -132,12 +138,13 @@ int main(void) { } timer_data[i].timer_id = timer_id; + timer_data[i].context = ctx; active_count++; } DEBUG_INFO(DEBUG_CATEGORY_TEST, "Created %d timers, active_count=%d", TIMER_COUNT, active_count); - /* Создаём стоп-таймер на 10 секунд */ + /* Создаём стоп-таймер на 1 секунду */ timer_ctx_t* stop_ctx = u_malloc(sizeof(timer_ctx_t)); if (!stop_ctx) { DEBUG_ERROR(DEBUG_CATEGORY_TEST, "Failed to allocate stop context"); @@ -160,7 +167,7 @@ int main(void) { return 1; } - DEBUG_INFO(DEBUG_CATEGORY_TEST, "Stop timer set for 10 seconds"); + DEBUG_INFO(DEBUG_CATEGORY_TEST, "Stop timer set for 1 second"); DEBUG_INFO(DEBUG_CATEGORY_TEST, "Running event loop..."); /* Ждём пока active_count станет 0 (все таймеры отработают после stop) */ @@ -192,6 +199,10 @@ int main(void) { uasync_cancel_timeout(ua, stop_timer); } u_free(stop_ctx); + for (int i = 0; i < TIMER_COUNT; i++) { + if (timer_data[i].timer_id) uasync_cancel_timeout(ua, timer_data[i].timer_id); + u_free(timer_data[i].context); + } uasync_destroy(ua, 0); diff --git a/tests/test_uasync_regressions.c b/tests/test_uasync_regressions.c new file mode 100644 index 00000000..12e1d744 --- /dev/null +++ b/tests/test_uasync_regressions.c @@ -0,0 +1,551 @@ +/* Контракты event loop: обе ветки опроса, жизненный цикл, fault injection. */ +#define _GNU_SOURCE +#include "async_regression.h" +#include +#include +#include +#include +#include +#include + +struct socket_test { + struct UASYNC* ua; + void* id; + int fd[2]; + int reads, writes, errors, callback_error, action; +}; + +static void count_cb(void* arg) { ++*(int*)arg; } +static void read_cb(int fd, void* arg) { + struct socket_test* t = arg; + char byte; + if (read(fd, &byte, 1) < 0 && errno != EAGAIN) t->callback_error = 1; + t->reads++; + if (t->action == 1) { + if (uasync_remove_socket(t->ua, t->id) != ERR_OK) t->callback_error = 1; + t->id = NULL; + } else if (t->action == 2) { + if (uasync_set_socket_write(t->ua, t->id, 0) != ERR_OK) t->callback_error = 1; + } +} +static void write_cb(int fd, void* arg) { (void)fd; ((struct socket_test*)arg)->writes++; } +static void error_cb(int fd, void* arg) { (void)fd; ((struct socket_test*)arg)->errors++; } + +static int socket_setup(struct socket_test* t, int param, int want_read, int want_write, int want_error) { + memset(t, 0, sizeof(*t)); + if (param & 1) regression_epoll_create_fail(); + t->ua = uasync_create(); CHECK(t->ua); + CHECK_EQ(t->ua->use_epoll, !(param & 1)); + CHECK_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, t->fd), 0); + CHECK_EQ(socket_set_nonblocking(t->fd[0]), 0); + if (param & 2) { + t->id = uasync_add_socket_t(t->ua, t->fd[0], want_read ? read_cb : NULL, + want_write ? write_cb : NULL, want_error ? error_cb : NULL, "regression", t); + } else { + t->id = uasync_add_socket(t->ua, t->fd[0], want_read ? read_cb : NULL, + want_write ? write_cb : NULL, want_error ? error_cb : NULL, "regression", t); + } + CHECK(t->id); + return 0; +} +static void socket_cleanup(struct socket_test* t) { + if (t->id) uasync_remove_socket(t->ua, t->id); + close(t->fd[0]); if (t->fd[1] >= 0) close(t->fd[1]); + uasync_destroy(t->ua, 0); +} + +static int test_socket_read_write(int param) { + struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 1, 1), 0); + CHECK_EQ(write(t.fd[1], "x", 1), 1); + uasync_poll(t.ua, 0); + CHECK_EQ(t.reads, 1); CHECK_EQ(t.writes, 1); CHECK_EQ(t.errors, 0); CHECK_EQ(t.callback_error, 0); + socket_cleanup(&t); return 0; +} +static int test_remove_in_read(int param) { + struct socket_test t; CHECK_EQ(socket_setup(&t, param & 3, 1, !(param & 4), 1), 0); + t.action = 1; + CHECK_EQ(write(t.fd[1], "x", 1), 1); + if (param & 4) { close(t.fd[1]); t.fd[1] = -1; } + uasync_poll(t.ua, 0); + CHECK_EQ(t.reads, 1); CHECK_EQ(t.writes, 0); CHECK_EQ(t.errors, 0); CHECK_EQ(t.callback_error, 0); + socket_cleanup(&t); return 0; +} +static int test_disable_in_read(int param) { + struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 1, 0), 0); + t.action = 2; CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); + CHECK_EQ(t.reads, 1); CHECK_EQ(t.writes, 0); CHECK_EQ(t.callback_error, 0); + socket_cleanup(&t); return 0; +} +static int test_socket_toggle(int param) { + struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 1, 0), 0); + CHECK_EQ(uasync_set_socket_read(t.ua, t.id, 0), ERR_OK); + CHECK_EQ(uasync_set_socket_write(t.ua, t.id, 0), ERR_OK); + CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); + CHECK_EQ(t.reads, 0); CHECK_EQ(t.writes, 0); + CHECK_EQ(uasync_set_socket_read(t.ua, t.id, 1), ERR_OK); uasync_poll(t.ua, 0); + CHECK_EQ(t.reads, 1); CHECK_EQ(t.writes, 0); + CHECK_EQ(uasync_set_socket_write(t.ua, t.id, 1), ERR_OK); uasync_poll(t.ua, 0); + CHECK_EQ(t.writes, 1); + socket_cleanup(&t); return 0; +} +static int test_fd_mapping(int param) { + struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 0, 0), 0); + CHECK_EQ(uasync_remove_socket(t.ua, t.id), ERR_OK); t.id = NULL; + void* other = uasync_add_socket_t(t.ua, t.fd[1], read_cb, NULL, NULL, "other fd", &t); CHECK(other); + if (param & 4) { + CHECK_EQ(uasync_remove_socket_t(t.ua, t.fd[0]), ERR_FAIL); + CHECK_EQ(uasync_set_socket_read(t.ua, other, 0), ERR_OK); + } else { + t.id = uasync_add_socket(t.ua, t.fd[0], read_cb, NULL, NULL, "readded fd", &t); CHECK(t.id); + } + CHECK_EQ(uasync_remove_socket(t.ua, other), ERR_OK); + socket_cleanup(&t); return 0; +} +static int test_lookup(int param) { + struct socket_test t; CHECK_EQ(socket_setup(&t, param & 3, 1, 0, 0), 0); + if (param & 4) { + CHECK_EQ(uasync_remove_socket(t.ua, t.id), ERR_OK); t.id = NULL; + int high = fcntl(t.fd[0], F_DUPFD, FD_SETSIZE + 16); CHECK(high >= FD_SETSIZE); + close(t.fd[0]); t.fd[0] = high; + t.id = uasync_add_socket(t.ua, high, read_cb, NULL, NULL, "high fd", &t); CHECK(t.id); + } + void* found = NULL; CHECK_EQ(uasync_lookup_socket(t.ua, t.fd[0], &found), 0); + CHECK(found == t.id); + CHECK_EQ(uasync_set_socket_read(t.ua, found, 0), ERR_OK); + CHECK_EQ(uasync_set_socket_write(t.ua, found, 0), ERR_OK); + CHECK_EQ(uasync_remove_socket(t.ua, found), ERR_OK); t.id = NULL; + socket_cleanup(&t); return 0; +} +static int test_array_growth(int param) { + struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 0, 0), 0); + int high = fcntl(t.fd[0], F_DUPFD, 128); CHECK(high >= 128); + void* h = uasync_add_socket(t.ua, high, NULL, NULL, NULL, "grow", NULL); CHECK(h); + CHECK_EQ(uasync_remove_socket(t.ua, h), ERR_OK); close(high); + CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); CHECK_EQ(t.reads, 1); + CHECK_EQ(uasync_set_socket_read(t.ua, t.id, 0), ERR_OK); + socket_cleanup(&t); return 0; +} +static int test_duplicate_fd(int param) { + struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 0, 0), 0); + CHECK(!uasync_add_socket_t(t.ua, t.fd[0], read_cb, NULL, NULL, "duplicate", &t)); + CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); CHECK_EQ(t.reads, 1); + socket_cleanup(&t); return 0; +} +static int test_stale_batch(int param) { + (void)param; + struct socket_test t; CHECK_EQ(socket_setup(&t, 0, 0, 1, 0), 0); + struct epoll_event event; + CHECK_EQ(epoll_wait(t.ua->epoll_fd, &event, 1, 0), 1); + CHECK_EQ(uasync_remove_socket(t.ua, t.id), ERR_OK); + t.id = uasync_add_socket(t.ua, t.fd[0], NULL, write_cb, NULL, "new generation", &t); CHECK(t.id); + regression_epoll_events(&event, 1); uasync_poll(t.ua, 0); CHECK_EQ(t.writes, 0); + uasync_poll(t.ua, 0); CHECK_EQ(t.writes, 1); + socket_cleanup(&t); return 0; +} +struct replace_test { struct UASYNC* ua; int fd[4]; void* id[2]; int old_calls, new_calls, error; }; +static void replacement_cb(int fd, void* arg) { (void)fd; ((struct replace_test*)arg)->new_calls++; } +static void replace_other_cb(int fd, void* arg) { + struct replace_test* t = arg; + if (++t->old_calls != 1) return; + int other = fd == t->fd[0] ? 1 : 0; + if (uasync_remove_socket(t->ua, t->id[other]) != ERR_OK) t->error = 1; + t->id[other] = uasync_add_socket(t->ua, t->fd[other * 2], NULL, replacement_cb, NULL, "replacement", t); + if (!t->id[other]) t->error = 1; +} +static int test_replace_other_in_batch(int param) { + struct replace_test t = {0}; + if (param) regression_epoll_create_fail(); + t.ua = uasync_create(); CHECK(t.ua); + for (int i = 0; i < 2; i++) { + CHECK_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, t.fd + i * 2), 0); + t.id[i] = uasync_add_socket(t.ua, t.fd[i * 2], NULL, replace_other_cb, NULL, "original", &t); CHECK(t.id[i]); + } + uasync_poll(t.ua, 0); CHECK_EQ(t.error, 0); CHECK_EQ(t.old_calls, 1); CHECK_EQ(t.new_calls, 0); + uasync_poll(t.ua, 0); CHECK_EQ(t.new_calls, 1); + for (int i = 0; i < 2; i++) CHECK_EQ(uasync_remove_socket(t.ua, t.id[i]), ERR_OK); + for (int i = 0; i < 4; i++) close(t.fd[i]); + uasync_destroy(t.ua, 0); return 0; +} + +static int test_priority_event(int param) { + (void)param; + struct socket_test t; CHECK_EQ(socket_setup(&t, 0, 0, 1, 1), 0); + struct epoll_event event; CHECK_EQ(epoll_wait(t.ua->epoll_fd, &event, 1, 0), 1); + event.events = EPOLLPRI; regression_epoll_events(&event, 1); uasync_poll(t.ua, 0); + CHECK_EQ(t.errors, 1); + socket_cleanup(&t); return 0; +} + +struct free_context { struct UASYNC* ua; void* id; int* writes; }; +static void free_read_cb(int fd, void* arg) { + struct free_context* c = arg; (void)fd; + uasync_remove_socket(c->ua, c->id); u_free(c); +} +static void freed_write_cb(int fd, void* arg) { (void)fd; ++*((struct free_context*)arg)->writes; } +static int test_freed_context(int param) { + (void)param; + struct UASYNC* ua = uasync_create(); CHECK(ua); + int fd[2], writes = 0; CHECK_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, fd), 0); + struct free_context* c = u_calloc(1, sizeof(*c)); CHECK(c); c->ua = ua; c->writes = &writes; + c->id = uasync_add_socket(ua, fd[0], free_read_cb, freed_write_cb, NULL, "free context", c); CHECK(c->id); + CHECK_EQ(write(fd[1], "x", 1), 1); uasync_poll(ua, 0); CHECK_EQ(writes, 0); + close(fd[0]); close(fd[1]); uasync_destroy(ua, 0); return 0; +} + +static int test_due_timer(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); int count = 0; + CHECK(uasync_set_timeout(ua, 0, &count, count_cb, "due")); + if (param) regression_clock_shift(-60000); + uint64_t start = regression_time_ms(); uasync_poll(ua, param ? 0 : 10000); + uint64_t elapsed = regression_time_ms() - start; + regression_clock_shift(0); + fprintf(stderr, "[OBSERVE] due timer: elapsed=%llums callbacks=%d\n", (unsigned long long)elapsed, count); + CHECK_EQ(count, 1); CHECK(elapsed < 500); + uasync_destroy(ua, 0); return 0; +} +static int test_forward_clock(int param) { + (void)param; + struct UASYNC* ua = uasync_create(); CHECK(ua); int count = 0; + void* id = uasync_set_timeout(ua, 100000, &count, count_cb, "ten seconds"); CHECK(id); + regression_clock_shift(60000); uasync_poll(ua, 0); regression_clock_shift(0); + CHECK_EQ(count, 0); CHECK_EQ(uasync_cancel_timeout(ua, id), ERR_OK); + uasync_destroy(ua, 0); return 0; +} +struct timer_item { struct UASYNC* ua; void* id; int* count; int* sequence; int value; }; +static void timer_cb(void* arg) { + struct timer_item* t = arg; t->id = NULL; + if (t->sequence) t->sequence[*t->count] = t->value; + ++*t->count; +} +static int test_timer_cancel(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct timer_item items[96] = {0}; int count = 0; + for (int i = 0; i < 96; i++) { + items[i].ua = ua; items[i].count = &count; + items[i].id = uasync_set_timeout(ua, param ? 100000 : 0, &items[i], timer_cb, "cancel heap growth"); + CHECK(items[i].id); + } + for (int i = 0; i < 96; i += 2) { + CHECK_EQ(uasync_cancel_timeout(ua, items[i].id), ERR_OK); items[i].id = NULL; + } + uasync_poll(ua, 0); CHECK_EQ(count, param ? 0 : 48); + for (int i = 0; i < 96; i++) if (items[i].id) { + CHECK_EQ(uasync_cancel_timeout(ua, items[i].id), ERR_OK); items[i].id = NULL; + } + uasync_poll(ua, 0); + size_t alloc, freed; uasync_get_stats(ua, &alloc, &freed, NULL, NULL); CHECK_EQ(alloc, freed); + uasync_destroy(ua, 0); return 0; +} +static int test_soon_fifo(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); + struct timer_item items[96] = {0}; int sequence[96], count = 0; + for (int i = 0; i < 96; i++) { + items[i].value = i; items[i].count = &count; items[i].sequence = sequence; + items[i].id = uasync_call_soon(ua, &items[i], timer_cb); CHECK(items[i].id); + } + if (param) for (int i = 0; i < 96; i += 2) { + CHECK_EQ(uasync_call_soon_cancel(ua, items[i].id), ERR_OK); items[i].id = NULL; + } + uasync_drain_immediate(ua); CHECK_EQ(count, param ? 48 : 96); + for (int i = 0; i < count; i++) CHECK_EQ(sequence[i], param ? i * 2 + 1 : i); + uasync_destroy(ua, 0); return 0; +} +struct chain_test { struct UASYNC* ua; int runs, timer_runs, observed; }; +static void chain_cb(void* arg) { + struct chain_test* t = arg; + if (++t->runs == 1000) t->observed = t->timer_runs; + if (t->runs < 1000) uasync_call_soon(t->ua, t, chain_cb); +} +static int test_soon_fairness(int param) { + (void)param; + struct chain_test t = {0}; t.ua = uasync_create(); CHECK(t.ua); + CHECK(uasync_set_timeout(t.ua, 0, &t.timer_runs, count_cb, "fairness")); + CHECK(uasync_call_soon(t.ua, &t, chain_cb)); + for (int i = 0; i < 2000 && t.runs < 1000; i++) uasync_poll(t.ua, 0); + CHECK_EQ(t.runs, 1000); CHECK_EQ(t.observed, 1); + uasync_destroy(t.ua, 0); return 0; +} +static void stop_cb(void* arg) { uasync_stop(arg); } +static int test_stop_soon(int param) { + struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 0, 0), 0); + CHECK(uasync_call_soon(t.ua, t.ua, stop_cb)); uasync_mainloop(t.ua); + CHECK_EQ(t.ua->stop, 1); socket_cleanup(&t); return 0; +} +static int test_stopped_poll_cleanup(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); int calls = 0; + void* id = uasync_set_timeout(ua, 100000, &calls, count_cb, "cancel before stopped poll"); CHECK(id); + CHECK_EQ(uasync_cancel_timeout(ua, id), ERR_OK); + if (param) CHECK(uasync_call_soon(ua, ua, stop_cb)); else uasync_stop(ua); + uasync_poll(ua, -1); CHECK_EQ(calls, 0); CHECK_EQ(ua->timer_alloc_count, ua->timer_free_count); + uasync_destroy(ua, 0); return 0; +} +static int test_post_empty(int param) { + if (param) regression_epoll_create_fail(); + struct UASYNC* ua = uasync_create(); CHECK(ua); int count = 0; + uasync_post(ua, count_cb, &count); uasync_poll(ua, -1); CHECK_EQ(count, 1); + uasync_destroy(ua, 0); return 0; +} +static void* worker_post(void* arg) { uasync_post(arg, stop_cb, arg); return NULL; } +static int test_worker_wakeup(int param) { + struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 0, 0), 0); + pthread_t worker; CHECK_EQ(pthread_create(&worker, NULL, worker_post, t.ua), 0); + uasync_mainloop(t.ua); CHECK_EQ(pthread_join(worker, NULL), 0); + CHECK_EQ(t.ua->stop, 1); socket_cleanup(&t); return 0; +} +static void* worker_stop(void* arg) { usleep(20000); uasync_stop(arg); return NULL; } +static int test_worker_stop(int param) { + if (param) regression_epoll_create_fail(); + struct UASYNC* ua = uasync_create(); CHECK(ua); pthread_t worker; + CHECK_EQ(pthread_create(&worker, NULL, worker_stop, ua), 0); + uasync_mainloop(ua); CHECK_EQ(pthread_join(worker, NULL), 0); + CHECK_EQ(ua->stop, 1); uasync_destroy(ua, 0); return 0; +} +static int test_reserved_post_cancel(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); int count = 0; + struct posted_task* tasks[3]; + for (int i = 0; i < 3; i++) { + tasks[i] = u_calloc(1, sizeof(*tasks[i])); CHECK(tasks[i]); + tasks[i]->callback = count_cb; tasks[i]->arg = &count; uasync_post_reserved(ua, tasks[i]); + } + CHECK_EQ(uasync_cancel_post(ua, tasks[param]), ERR_OK); + uasync_poll(ua, 0); CHECK_EQ(count, 2); + uasync_destroy(ua, 0); return 0; +} + +static int test_socket_growth_oom(int param) { + struct socket_test t; CHECK_EQ(socket_setup(&t, 0, 1, 0, 0), 0); + int high = fcntl(t.fd[0], F_DUPFD, 128); CHECK(high >= 128); + regression_fault_arm(FAULT_ALLOC, "u_async.c:", param); + void* id = uasync_add_socket(t.ua, high, read_cb, NULL, NULL, "growth failure", &t); + CHECK(regression_fault_hit()); regression_fault_reset(); CHECK(!id); close(high); + CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); CHECK_EQ(t.reads, 1); + socket_cleanup(&t); return 0; +} +static int test_create_oom(int param) { + regression_fault_arm(FAULT_ALLOC, NULL, param); + struct UASYNC* ua = uasync_create(); + CHECK(regression_fault_hit()); regression_fault_reset(); CHECK(!ua); + return 0; +} +static int test_eventfd_failure(int param) { + (void)param; regression_eventfd_fail(); CHECK(!uasync_create()); return 0; +} +static int test_schedule_oom(int param) { + struct UASYNC* ua = uasync_create(); CHECK(ua); int count = 0; + regression_fault_arm(FAULT_CALLOC, "u_async.c:", 1); + void* id = param ? uasync_call_soon(ua, &count, count_cb) : uasync_set_timeout(ua, 0, &count, count_cb, "oom"); + CHECK(regression_fault_hit()); regression_fault_reset(); CHECK(!id); + uasync_poll(ua, 0); CHECK_EQ(count, 0); + CHECK(uasync_set_timeout(ua, 0, &count, count_cb, "recover")); uasync_poll(ua, 0); CHECK_EQ(count, 1); + uasync_destroy(ua, 0); return 0; +} +static int test_heap_growth_oom(int param) { + (void)param; + struct UASYNC* ua = uasync_create(); CHECK(ua); int count = 0; + void* ids[16]; + for (int i = 0; i < 16; i++) { ids[i] = uasync_set_timeout(ua, 100000, &count, count_cb, "heap"); CHECK(ids[i]); } + regression_fault_arm(FAULT_REALLOC, "timeout_heap.c:", 1); + CHECK(!uasync_set_timeout(ua, 0, &count, count_cb, "heap failure")); + CHECK(regression_fault_hit()); regression_fault_reset(); + for (int i = 0; i < 16; i++) CHECK_EQ(uasync_cancel_timeout(ua, ids[i]), ERR_OK); + uasync_poll(ua, 0); CHECK_EQ(count, 0); + uasync_destroy(ua, 0); return 0; +} +static int test_epoll_control_failure(int param) { + struct socket_test t; CHECK_EQ(socket_setup(&t, 0, 1, 1, 0), 0); + regression_epoll_ctl_fail(EPOLL_CTL_MOD); + int result = param ? uasync_set_socket_write(t.ua, t.id, 0) : uasync_set_socket_read(t.ua, t.id, 0); + CHECK_EQ(result, ERR_FAIL); + CHECK_EQ(param ? uasync_set_socket_write(t.ua, t.id, 0) : uasync_set_socket_read(t.ua, t.id, 0), ERR_OK); + socket_cleanup(&t); return 0; +} +static int test_epoll_add_failure(int param) { + (void)param; + struct UASYNC* ua = uasync_create(); CHECK(ua); + int fd[2]; CHECK_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, fd), 0); + regression_epoll_ctl_fail(EPOLL_CTL_ADD); + CHECK(!uasync_add_socket(ua, fd[0], read_cb, NULL, NULL, "failed add", NULL)); + void* id = uasync_add_socket(ua, fd[0], NULL, NULL, NULL, "retry add", NULL); CHECK(id); + CHECK_EQ(uasync_remove_socket(ua, id), ERR_OK); + close(fd[0]); close(fd[1]); uasync_destroy(ua, 0); return 0; +} + +/* Отказ памяти переносится из poll в create/add; готовые регистрации остаются рабочими. */ +static void* poll_add(struct UASYNC* ua, int fd, socket_callback_t read_fn, socket_callback_t write_fn, void* arg, int sock_api) { + if (sock_api) return uasync_add_socket_t(ua, fd, read_fn, write_fn, NULL, "poll reserve", arg); + return uasync_add_socket(ua, fd, read_fn, write_fn, NULL, "poll reserve", arg); +} +static int test_poll_create_oom(int param) { + regression_epoll_create_fail(); regression_fault_arm(FAULT_REALLOC, "u_async.c:", param); + struct UASYNC* ua = uasync_create(); + CHECK(regression_fault_hit()); regression_fault_reset(); CHECK(!ua); return 0; +} +static int test_poll_growth_oom(int param) { + struct socket_test t; CHECK_EQ(socket_setup(&t, 1, 1, 0, 0), 0); + CHECK(t.ua->poll_fds); CHECK_EQ(t.ua->poll_fds_capacity, 16); + int fds[15]; void* ids[15]; + for (int i = 0; i < 15; i++) { + fds[i] = dup(t.fd[0]); CHECK(fds[i] >= 0); + if (i < 14) { ids[i] = poll_add(t.ua, fds[i], NULL, NULL, NULL, param); CHECK(ids[i]); } + } + uasync_poll(t.ua, 0); CHECK_EQ(t.ua->poll_fds_count, 16); + struct pollfd* previous = t.ua->poll_fds; + size_t socket_alloc = t.ua->socket_alloc_count, socket_free = t.ua->socket_free_count; + regression_fault_arm(FAULT_REALLOC, "u_async.c:", 0); + for (int i = 0; i < 3; i++) CHECK(!poll_add(t.ua, fds[14], NULL, NULL, NULL, param)); + CHECK(regression_fault_hit()); CHECK(t.ua->poll_fds == previous); + CHECK_EQ(t.ua->poll_fds_count, 16); CHECK_EQ(t.ua->poll_fds_capacity, 16); + CHECK_EQ(t.ua->socket_alloc_count, socket_alloc); CHECK_EQ(t.ua->socket_free_count, socket_free); + void* found = NULL; CHECK_EQ(uasync_lookup_socket(t.ua, fds[14], &found), -1); CHECK(!found); + int timers = 0; CHECK(uasync_set_timeout(t.ua, 0, &timers, count_cb, "poll OOM timer")); + CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, -1); + CHECK_EQ(t.reads, 1); CHECK_EQ(timers, 1); + regression_fault_reset(); + ids[14] = poll_add(t.ua, fds[14], NULL, NULL, NULL, param); CHECK(ids[14]); + CHECK_EQ(t.ua->poll_fds_capacity, 32); + CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); CHECK_EQ(t.reads, 2); + for (int i = 0; i < 15; i++) { CHECK_EQ(uasync_remove_socket(t.ua, ids[i]), ERR_OK); close(fds[i]); } + socket_cleanup(&t); return 0; +} +static int test_poll_no_alloc(int param) { + struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 1, 0), 0); + if (!param) CHECK(!t.ua->poll_fds); /* epoll не резервирует ненужный массив. */ + int timers = 0; CHECK(uasync_set_timeout(t.ua, 0, &timers, count_cb, "no allocation in poll")); + regression_fault_arm(FAULT_ALLOC, NULL, 0); + for (int i = 0; i < 8; i++) { + CHECK_EQ(uasync_set_socket_read(t.ua, t.id, i & 1), ERR_OK); + CHECK_EQ(uasync_set_socket_write(t.ua, t.id, i & 1), ERR_OK); + CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); + } + CHECK(!regression_fault_hit()); regression_fault_reset(); CHECK_EQ(timers, 1); + CHECK_EQ(t.reads, 4); CHECK_EQ(t.writes, 4); + socket_cleanup(&t); return 0; +} +struct poll_worker { struct UASYNC* ua; struct posted_task* task; }; +static void* poll_oom_worker(void* arg) { + struct poll_worker* t = arg; usleep(20000); + if (t->task) uasync_post_reserved(t->ua, t->task); else uasync_stop(t->ua); + return NULL; +} +static int test_poll_oom_wakeup(int param) { + regression_epoll_create_fail(); struct UASYNC* ua = uasync_create(); CHECK(ua); + struct poll_worker t = { .ua = ua }; + if (param) { t.task = u_calloc(1, sizeof(*t.task)); CHECK(t.task); t.task->callback = stop_cb; t.task->arg = ua; } + regression_fault_arm(FAULT_ALLOC, NULL, 0); pthread_t worker; + CHECK_EQ(pthread_create(&worker, NULL, poll_oom_worker, &t), 0); + uasync_mainloop(ua); CHECK_EQ(pthread_join(worker, NULL), 0); + CHECK_EQ(ua->stop, 1); CHECK(!regression_fault_hit()); regression_fault_reset(); + uasync_destroy(ua, 0); return 0; +} +struct poll_growth { + struct UASYNC* ua; int fd[2], extra[15]; void* id[16]; + int reads, writes, new_writes, callback_error, fail, sock_api, count_before; +}; +static void poll_old_write_cb(int fd, void* arg) { (void)fd; ((struct poll_growth*)arg)->writes++; } +static void poll_new_write_cb(int fd, void* arg) { (void)fd; ((struct poll_growth*)arg)->new_writes++; } +static void poll_growth_read_cb(int fd, void* arg) { + struct poll_growth* t = arg; char byte; + if (read(fd, &byte, 1) != 1) t->callback_error = 1; + if (t->reads++ != 0) return; + t->id[15] = poll_add(t->ua, t->extra[14], NULL, poll_new_write_cb, t, t->sock_api); + if (!!t->id[15] == t->fail || t->ua->poll_fds_count != t->count_before) t->callback_error = 1; +} +static int test_poll_growth_in_callback(int param) { + regression_epoll_create_fail(); struct poll_growth t = { .fail = param >= 2, .sock_api = param & 1 }; + t.ua = uasync_create(); CHECK(t.ua); CHECK_EQ(t.ua->poll_fds_capacity, 16); + CHECK_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, t.fd), 0); + t.id[0] = poll_add(t.ua, t.fd[0], poll_growth_read_cb, poll_old_write_cb, &t, t.sock_api); CHECK(t.id[0]); + for (int i = 0; i < 15; i++) { + t.extra[i] = i == 14 ? fcntl(t.fd[0], F_DUPFD, 128) : dup(t.fd[0]); CHECK(t.extra[i] >= 0); + if (i < 14) { + t.id[i + 1] = poll_add(t.ua, t.extra[i], NULL, i == 0 ? poll_old_write_cb : NULL, &t, t.sock_api); + CHECK(t.id[i + 1]); + } + } + t.count_before = 16; + if (t.fail) regression_fault_arm(param >= 4 ? FAULT_CALLOC : FAULT_REALLOC, "u_async.c:", param >= 4 ? param - 3 : 0); + CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); + CHECK_EQ(t.callback_error, 0); CHECK_EQ(t.reads, 1); CHECK_EQ(t.writes, 2); CHECK_EQ(t.new_writes, 0); + if (t.fail) { + CHECK(regression_fault_hit()); CHECK_EQ(t.ua->poll_fds_capacity, param >= 4 ? 32 : 16); regression_fault_reset(); + t.id[15] = poll_add(t.ua, t.extra[14], NULL, poll_new_write_cb, &t, t.sock_api); CHECK(t.id[15]); + } + CHECK_EQ(t.ua->poll_fds_capacity, 32); + CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); + CHECK_EQ(t.callback_error, 0); CHECK_EQ(t.reads, 2); CHECK_EQ(t.writes, 4); CHECK_EQ(t.new_writes, 1); + for (int i = 0; i < 16; i++) CHECK_EQ(uasync_remove_socket(t.ua, t.id[i]), ERR_OK); + for (int i = 0; i < 15; i++) close(t.extra[i]); + close(t.fd[0]); close(t.fd[1]); uasync_destroy(t.ua, 0); return 0; +} + +#define CASE(name, fn, p) {name, fn, p} +#define SOCKET_MATRIX(name, fn) \ + CASE(name "/epoll/fd", fn, 0), CASE(name "/poll/fd", fn, 1), \ + CASE(name "/epoll/socket", fn, 2), CASE(name "/poll/socket", fn, 3) +int main(int argc, char** argv) { + const struct regression_case cases[] = { + SOCKET_MATRIX("socket/read-write", test_socket_read_write), + SOCKET_MATRIX("socket/remove-in-read", test_remove_in_read), + CASE("socket/remove-in-read-hup/epoll", test_remove_in_read, 4), + CASE("socket/remove-in-read-hup/poll", test_remove_in_read, 5), + SOCKET_MATRIX("socket/disable-write-in-read", test_disable_in_read), + SOCKET_MATRIX("socket/toggle", test_socket_toggle), + SOCKET_MATRIX("socket/fd-readd-after-slot-reuse", test_fd_mapping), + CASE("socket/remove-absent-fd/epoll", test_fd_mapping, 4), + CASE("socket/remove-absent-fd/poll", test_fd_mapping, 5), + SOCKET_MATRIX("socket/lookup-handle", test_lookup), + CASE("socket/lookup-high-fd/epoll", test_lookup, 4), + CASE("socket/lookup-high-fd/poll", test_lookup, 5), + SOCKET_MATRIX("socket/handle-after-array-growth", test_array_growth), + SOCKET_MATRIX("socket/duplicate-fd", test_duplicate_fd), + CASE("socket/replace-other-in-batch/epoll", test_replace_other_in_batch, 0), + CASE("socket/replace-other-in-batch/poll", test_replace_other_in_batch, 1), + CASE("socket/stale-epoll-generation", test_stale_batch, 0), + CASE("socket/epoll-priority-event", test_priority_event, 0), + CASE("socket/freed-context-in-read", test_freed_context, 0), + CASE("timer/due-before-long-poll", test_due_timer, 0), + CASE("timer/wall-clock-backwards", test_due_timer, 1), + CASE("timer/wall-clock-forwards", test_forward_clock, 0), + CASE("timer/cancel-expired-heap-growth", test_timer_cancel, 0), + CASE("timer/cancel-future-heap-growth", test_timer_cancel, 1), + CASE("soon/fifo", test_soon_fifo, 0), CASE("soon/fifo-with-cancel", test_soon_fifo, 1), + CASE("soon/timer-fairness", test_soon_fairness, 0), + CASE("stop/from-soon/epoll", test_stop_soon, 0), CASE("stop/from-soon/poll", test_stop_soon, 1), + CASE("stop/from-worker/epoll", test_worker_stop, 0), CASE("stop/from-worker/poll", test_worker_stop, 1), + CASE("stop/poll-cleans-cancelled-timers", test_stopped_poll_cleanup, 0), + CASE("stop/from-soon-cleans-cancelled-timers", test_stopped_poll_cleanup, 1), + CASE("post/empty/epoll", test_post_empty, 0), CASE("post/empty/poll", test_post_empty, 1), + CASE("post/worker-wakeup/epoll", test_worker_wakeup, 0), CASE("post/worker-wakeup/poll", test_worker_wakeup, 1), + CASE("post/cancel-head", test_reserved_post_cancel, 0), CASE("post/cancel-middle", test_reserved_post_cancel, 1), + CASE("post/cancel-tail", test_reserved_post_cancel, 2), + CASE("poll-reserve/create-oom-once", test_poll_create_oom, 1), + CASE("poll-reserve/create-oom-persistent", test_poll_create_oom, 0), + CASE("poll-reserve/add-oom-fd-recovery", test_poll_growth_oom, 0), + CASE("poll-reserve/add-oom-socket-recovery", test_poll_growth_oom, 1), + CASE("poll-reserve/no-alloc-in-epoll", test_poll_no_alloc, 0), + CASE("poll-reserve/no-alloc-in-poll", test_poll_no_alloc, 1), + CASE("poll-reserve/stop-during-oom", test_poll_oom_wakeup, 0), + CASE("poll-reserve/post-during-oom", test_poll_oom_wakeup, 1), + CASE("poll-reserve/callback-grow-fd", test_poll_growth_in_callback, 0), + CASE("poll-reserve/callback-grow-socket", test_poll_growth_in_callback, 1), + CASE("poll-reserve/callback-oom-fd", test_poll_growth_in_callback, 2), + CASE("poll-reserve/callback-oom-socket", test_poll_growth_in_callback, 3), + CASE("poll-reserve/callback-socket-array-oom-1", test_poll_growth_in_callback, 4), + CASE("poll-reserve/callback-socket-array-oom-2", test_poll_growth_in_callback, 5), + CASE("poll-reserve/callback-socket-array-oom-3", test_poll_growth_in_callback, 6), + CASE("poll-reserve/callback-socket-array-oom-4", test_poll_growth_in_callback, 7), + CASE("oom/socket-grow-1", test_socket_growth_oom, 1), CASE("oom/socket-grow-2", test_socket_growth_oom, 2), + CASE("oom/socket-grow-3", test_socket_growth_oom, 3), CASE("oom/socket-grow-4", test_socket_growth_oom, 4), + CASE("oom/create-1", test_create_oom, 1), CASE("oom/create-2", test_create_oom, 2), + CASE("oom/create-3", test_create_oom, 3), CASE("oom/create-4", test_create_oom, 4), + CASE("oom/create-5", test_create_oom, 5), CASE("oom/create-6", test_create_oom, 6), + CASE("oom/create-7", test_create_oom, 7), CASE("oom/create-8", test_create_oom, 8), CASE("oom/create-9", test_create_oom, 9), + CASE("oom/eventfd-create", test_eventfd_failure, 0), + CASE("oom/timer-node", test_schedule_oom, 0), CASE("oom/soon-node", test_schedule_oom, 1), + CASE("oom/heap-growth", test_heap_growth_oom, 0), + CASE("epoll/failed-add-retry", test_epoll_add_failure, 0), + CASE("epoll/failed-disable-read", test_epoll_control_failure, 0), + CASE("epoll/failed-disable-write", test_epoll_control_failure, 1), + }; + return regression_run(argc, argv, cases, sizeof(cases) / sizeof(cases[0])); +} diff --git a/tests/test_uasync_socket_race.c b/tests/test_uasync_socket_race.c index 96b98204..0c407726 100644 --- a/tests/test_uasync_socket_race.c +++ b/tests/test_uasync_socket_race.c @@ -32,12 +32,38 @@ static int g_conn_count = 0, g_read_count = 0, g_err_count = 0; static pid_t g_children[CHILDREN]; static int g_port = 0; static void* g_mon_id = NULL; +static void* g_timeout_id = NULL; +static void* g_listener_id = NULL; +static int g_closed_count; +struct race_conn { + struct race_conn* next; + struct tcp_conn* tc; + int received; +}; +static struct race_conn* g_connections; + +static void destroy_connection(struct tcp_conn* tc) { + struct race_conn* c = tc->arg; + struct race_conn** p = &g_connections; + while (*p && *p != c) p = &(*p)->next; + if (*p) *p = c->next; + else { printf("[FAIL] closing untracked connection\n"); g_done = -1; } + g_closed_count++; + tcp_conn_destroy(tc); u_free(c); +} static void on_read_cb(struct ll_queue* q, void* arg) { struct tcp_conn* tc = (struct tcp_conn*)arg; + struct race_conn* c = tc->arg; struct ll_entry* e = queue_data_get(q); if (!e) { queue_resume_callback(q); return; } - if (e->len == 3 && memcmp(e->dgram, "OK\n", 3) == 0) g_read_count++; + /* TCP вправе разделить три байта между несколькими queue entries. */ + if (c->received > 3 || e->len > 3 - c->received || memcmp(e->dgram, "OK\n" + c->received, e->len)) { + printf("[FAIL] unexpected stream data: received=%d len=%u\n", c->received, e->len); g_done = -1; + } else { + c->received += e->len; + if (c->received == 3 && e->len) g_read_count++; + } memory_pool_free(tc->data_pool, e->dgram); queue_entry_free(e); queue_resume_callback(q); } @@ -45,7 +71,8 @@ static void on_read_cb(struct ll_queue* q, void* arg) { static void on_error_cb(struct tcp_conn* tc, int err, void* arg) { (void)err; (void)arg; g_err_count++; - tcp_conn_destroy(tc); + printf("[FAIL] connection error=%d\n", err); + g_done = -1; destroy_connection(tc); } static void on_fin_cb(struct tcp_conn* tc, void* arg) { @@ -55,7 +82,7 @@ static void on_fin_cb(struct tcp_conn* tc, void* arg) { static void on_closed_cb(struct tcp_conn* tc, void* arg) { (void)arg; - tcp_conn_destroy(tc); + destroy_connection(tc); } static void on_accept_cb(int fd, void* arg) { @@ -63,11 +90,18 @@ static void on_accept_cb(int fd, void* arg) { while (1) { struct sockaddr_in addr; socklen_t alen = sizeof(addr); int csock = accept(fd, (struct sockaddr*)&addr, &alen); - if (csock < 0) return; + if (csock < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) return; + if (errno == EINTR) continue; + printf("[FAIL] accept: %s\n", strerror(errno)); g_done = -1; return; + } if (csock == 0) { int r = open("/dev/null", O_RDONLY); if (r > 0 && r != 0) { dup2(r, 0); close(r); } continue; } - socket_set_nonblocking(csock); - struct tcp_conn* tc = tcp_conn_create(g_ua, csock, 512, 512, 4, 0, 0, on_fin_cb, on_error_cb, NULL); - if (!tc) { socket_close_wrapper(csock); continue; } + if (socket_set_nonblocking(csock) < 0) { close(csock); g_done = -1; return; } + struct race_conn* c = u_calloc(1, sizeof(*c)); + if (!c) { printf("[FAIL] allocate connection context\n"); close(csock); g_done = -1; return; } + struct tcp_conn* tc = tcp_conn_create(g_ua, csock, 512, 512, 4, 0, 0, on_fin_cb, on_error_cb, c); + if (!tc) { printf("[FAIL] create connection\n"); u_free(c); close(csock); g_done = -1; return; } + c->tc = tc; c->next = g_connections; g_connections = c; tc->on_closed = on_closed_cb; queue_set_callback(tc->read_queue, on_read_cb, tc); g_conn_count++; @@ -91,20 +125,31 @@ static int child_main(int port, int id, int count) { static void monitor(void* arg) { (void)arg; + g_mon_id = NULL; int alive = 0; for (int i = 0; i < CHILDREN; i++) { if (g_children[i] <= 0) continue; int status; pid_t r = waitpid(g_children[i], &status, WNOHANG); if (r == 0) { alive++; continue; } - if (WIFEXITED(status) && WEXITSTATUS(status) != 0) { g_done = -1; return; } + if (r < 0) { + if (errno == EINTR) { alive++; continue; } + printf("[FAIL] waitpid: %s\n", strerror(errno)); g_done = -1; return; + } g_children[i] = 0; + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + printf("[FAIL] child exit status=%d\n", status); g_done = -1; return; + } + } + if (alive == 0 && g_read_count == CHILDREN * ITER_PER_CHILD && g_closed_count == g_conn_count) { g_ok = 1; g_done = 1; } + if (!g_done) { + g_mon_id = uasync_set_timeout(g_ua, 500, NULL, monitor, "mon"); + if (!g_mon_id) { printf("[FAIL] restart monitor timer\n"); g_done = -1; } } - if (alive == 0 && g_read_count >= CHILDREN * ITER_PER_CHILD) { g_ok = 1; g_done = 1; } - if (!g_done) g_mon_id = uasync_set_timeout(g_ua, 500, NULL, monitor, "mon"); } static void test_timeout(void* arg) { (void)arg; + g_timeout_id = NULL; printf("[FAIL] timeout conn=%d read=%d err=%d\n", g_conn_count, g_read_count, g_err_count); g_done = -1; } @@ -125,20 +170,25 @@ int main(void) { int fd0 = open("/dev/null", O_RDONLY); if (fd0 == 0) { } else if (fd0 > 0) { dup2(fd0, 0); close(fd0); } else { close(0); open("/dev/null", O_RDONLY); } - g_port = 25000 + (rand() % 10000); g_ua = uasync_create(); if (!g_ua) { printf("[FAIL] uasync_create\n"); return 1; } int lsock = socket(AF_INET, SOCK_STREAM, 0); - if (lsock < 0) { printf("[FAIL] socket\n"); goto cleanup; } + if (lsock < 0) { printf("[FAIL] socket: %s\n", strerror(errno)); goto cleanup; } socket_set_reuseaddr(lsock, 1); socket_set_nonblocking(lsock); struct sockaddr_in la; memset(&la, 0, sizeof(la)); - la.sin_family = AF_INET; la.sin_port = htons((uint16_t)g_port); + la.sin_family = AF_INET; la.sin_port = 0; la.sin_addr.s_addr = inet_addr("127.0.0.1"); if (bind(lsock, (struct sockaddr*)&la, sizeof(la)) < 0) { printf("[FAIL] bind: %s\n", strerror(errno)); goto cleanup; } + socklen_t la_len = sizeof(la); + if (getsockname(lsock, (struct sockaddr*)&la, &la_len) < 0) { printf("[FAIL] getsockname: %s\n", strerror(errno)); goto cleanup; } + g_port = ntohs(la.sin_port); if (listen(lsock, 1024) < 0) { printf("[FAIL] listen: %s\n", strerror(errno)); goto cleanup; } - uasync_add_socket(g_ua, lsock, on_accept_cb, NULL, NULL, "test_listen", NULL); + g_listener_id = uasync_add_socket(g_ua, lsock, on_accept_cb, NULL, NULL, "test_listen", NULL); + if (!g_listener_id) { + printf("[FAIL] register listener\n"); goto cleanup; + } for (int i = 0; i < CHILDREN; i++) { pid_t pid = fork(); @@ -149,14 +199,15 @@ int main(void) { printf(" %d children × %d iterations on port %d\n", CHILDREN, ITER_PER_CHILD, g_port); fflush(stdout); g_mon_id = uasync_set_timeout(g_ua, 100, NULL, monitor, "mon"); - void* to_id = uasync_set_timeout(g_ua, TIMEOUT_MS * 10, NULL, test_timeout, "to"); + g_timeout_id = uasync_set_timeout(g_ua, TIMEOUT_MS * 10, NULL, test_timeout, "to"); + if (!g_mon_id || !g_timeout_id) { printf("[FAIL] create timers\n"); goto cleanup; } while (!g_done) uasync_poll(g_ua, 50); - if (to_id) uasync_cancel_timeout(g_ua, to_id); + if (g_timeout_id) { uasync_cancel_timeout(g_ua, g_timeout_id); g_timeout_id = NULL; } int expected = CHILDREN * ITER_PER_CHILD; - if (g_ok && g_conn_count == expected && g_read_count == expected) { + if (g_ok && g_conn_count == expected && g_read_count == expected && g_closed_count == expected && !g_err_count) { printf("[PASS] test_uasync_socket_race — %d/%d/%d conn/read/err\n", g_conn_count, g_read_count, g_err_count); } else { printf("[FAIL] test_uasync_socket_race — expected %d, got %d/%d/%d\n", expected, g_conn_count, g_read_count, g_err_count); @@ -166,6 +217,11 @@ int main(void) { cleanup: for (int i = 0; i < CHILDREN; i++) if (g_children[i] > 0) { kill(g_children[i], SIGKILL); waitpid(g_children[i], NULL, 0); } if (g_mon_id) uasync_cancel_timeout(g_ua, g_mon_id); + if (g_timeout_id) uasync_cancel_timeout(g_ua, g_timeout_id); + if (g_listener_id) uasync_remove_socket(g_ua, g_listener_id); + if (lsock >= 0) close(lsock); + while (g_connections) destroy_connection(g_connections->tc); + uasync_drain_immediate(g_ua); if (g_ua) uasync_destroy(g_ua, 0); return g_ok ? 0 : 1; #endif diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/TelecomCallManager.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/TelecomCallManager.kt index 9ad67b6f..95b68f37 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/TelecomCallManager.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/TelecomCallManager.kt @@ -82,10 +82,13 @@ object TelecomCallManager { val tm = telecomManager() ?: return false val handle = phoneAccountHandle ?: return false val uri = Uri.fromParts("node", "0x%016x".format(peerNodeId), null) - val extras = Bundle().apply { + val callExtras = Bundle().apply { putLong(EXTRA_CALL_ID, callId) putLong(EXTRA_PEER_NODE_ID, peerNodeId) putString(EXTRA_DISPLAY_NAME, displayName) + } + val extras = Bundle().apply { + putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, handle) } return try { diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/UtunConnectionService.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/UtunConnectionService.kt index 12fbcae5..3be3f348 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/UtunConnectionService.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/UtunConnectionService.kt @@ -47,11 +47,17 @@ class UtunConnectionService : ConnectionService() { } private fun parseRequest(request: ConnectionRequest?): Triple? { - val extras = request?.extras ?: return null + val extras = request?.extras ?: run { + LogManager.addLog("WARN", "UtunConnSvc", "connection request has no extras") + return null + } val callId = extras.getLong(TelecomCallManager.EXTRA_CALL_ID, 0L) val peer = extras.getLong(TelecomCallManager.EXTRA_PEER_NODE_ID, 0L) val name = extras.getString(TelecomCallManager.EXTRA_DISPLAY_NAME) ?: "" - if (callId == 0L) return null + if (callId == 0L) { + LogManager.addLog("WARN", "UtunConnSvc", "connection request missing call ID; keys=${extras.keySet()}") + return null + } return Triple(callId, peer, name) } } diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index 468b0cc8..46666669 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -98,14 +98,17 @@ static void bridge_log(int level, const char* fmt, ...) { } static void bridge_debug_hook(int level, const char* category, const char* message) { + /* DEBUG_*: ERROR=1..TRACE=5; bridge/Kotlin: TRACE=0..ERROR=4. */ + int bridge_level = level >= DEBUG_LEVEL_ERROR && level <= DEBUG_LEVEL_TRACE ? DEBUG_LEVEL_TRACE - level : BLEV_DEBUG; #ifdef __ANDROID__ - __android_log_print(blevel_to_android(level), LOG_TAG, "[%s] %s", + __android_log_print(blevel_to_android(bridge_level), LOG_TAG, "[%s] %s", category ? category : "?", message ? message : ""); #else fprintf(stderr, "[%s] %s\n", category ? category : "?", message ? message : ""); #endif udp_log_write(message, (int)strlen(message)); - if (g_on_log) g_on_log(level, category, message); + if (level == DEBUG_LEVEL_ERROR) udp_log_flush(); + if (g_on_log) g_on_log(bridge_level, category, message); } static utun_config_string_fn g_cfg_string = NULL; @@ -1838,7 +1841,6 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetEventCallback( JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDestroy( JNIEnv* env, jobject thiz) { (void)thiz; - udp_log_cancel_timer(); utun_bridge_destroy(); udp_log_stop(); if (g_config_callback) { (*env)->DeleteGlobalRef(env, g_config_callback); g_config_callback = NULL; } diff --git a/tools/chatgui-android/jni_bridge/android_udp_log.c b/tools/chatgui-android/jni_bridge/android_udp_log.c index 21d7f9bf..bf4c1721 100644 --- a/tools/chatgui-android/jni_bridge/android_udp_log.c +++ b/tools/chatgui-android/jni_bridge/android_udp_log.c @@ -1,137 +1,177 @@ -/* - * android_udp_log.c — non-blocking UDP log sender for Android - * - * Called ONLY from uasync worker thread (bridge_debug_hook). - * No mutex needed. Flushes at 16KB threshold or via uasync timer. - */ +/* Android UDP-лог: общий буфер под mutex; таймер принадлежит только uasync-потоку. + * ULOG не проходит через debug hook, поэтому ошибки отправки не вызывают рекурсию. */ #include "android_udp_log.h" -#include "../libutun_lite/instance_lite.h" #include "../../../lib/u_async.h" -#include "../../../lib/platform_compat.h" #include "../../../lib/socket_compat.h" +#include "../../../lib/mem.h" +#include +#include #include -#include #include -#define UDP_BUF_SIZE 65536 +#define UDP_BUF_SIZE 65507 /* Максимальный UDP payload для IPv4. */ #define UDP_FLUSH_BYTES 16384 -#define UDP_FLUSH_TB 2000 /* 200ms in uasync timebase (0.1ms units) */ +#define UDP_FLUSH_TB 2000 #ifdef __ANDROID__ #include -#define ULOG(fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, "utun-udplog", fmt, ##__VA_ARGS__) +#define ULOG(fmt, ...) __android_log_print(ANDROID_LOG_INFO, "utun-udplog", fmt, ##__VA_ARGS__) #else -#define ULOG(fmt, ...) fprintf(stderr, "utun-udplog: " fmt "\n", ##__VA_ARGS__) +#define ULOG(fmt, ...) fprintf(stderr, "utun-udplog: " fmt "\n", ##__VA_ARGS__) #endif -static int g_udp_fd = -1; +static pthread_mutex_t g_udp_mutex = PTHREAD_MUTEX_INITIALIZER; +static struct UASYNC* g_udp_ua; +static int g_udp_fd = -1; static struct sockaddr_in g_udp_addr; -static int g_udp_has_addr = 0; -static int g_udp_stopping = 0; -static char g_udp_buf[UDP_BUF_SIZE]; -static int g_udp_buf_len = 0; -static void* g_udp_timer = NULL; -static int g_udp_sent_first = 0; -static int64_t g_udp_total_sent = 0; -static int g_udp_send_errors = 0; - -static void udp_reschedule_timer(void) { - if (g_udp_stopping) return; - if (g_udp_timer) return; - struct UASYNC* ua = instance_lite_get_uasync(); - if (ua) g_udp_timer = uasync_set_timeout(ua, UDP_FLUSH_TB, NULL, udp_trampoline, "udp_log_flush"); -} - -static void udp_open_socket(void) { - if (g_udp_fd >= 0 || !g_udp_has_addr) return; - g_udp_fd = (int)socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0); - if (g_udp_fd < 0) { ULOG("socket() failed: %s (errno=%d)", strerror(errno), errno); return; } - if (connect(g_udp_fd, (struct sockaddr*)&g_udp_addr, sizeof(g_udp_addr)) < 0) { - ULOG("connect() to %s:%d failed: %s (errno=%d)", - inet_ntoa(g_udp_addr.sin_addr), (int)ntohs(g_udp_addr.sin_port), strerror(errno), errno); - close(g_udp_fd); g_udp_fd = -1; return; - } - ULOG("socket opened fd=%d -> %s:%d", g_udp_fd, - inet_ntoa(g_udp_addr.sin_addr), (int)ntohs(g_udp_addr.sin_port)); -} +static int g_udp_has_addr; +static int g_udp_stopping; +static char g_udp_buf[UDP_BUF_SIZE]; +static int g_udp_buf_len; +static int g_udp_sent_first; +static int64_t g_udp_total_sent; +static unsigned g_udp_send_errors; +/* Только uasync-поток читает и меняет handle. JNI и log hook его не трогают. */ +static void* g_udp_timer; +/* Вызывается под g_udp_mutex; все операции сокета неблокирующие. */ static void udp_do_send(const char* data, int len) { - if (g_udp_fd < 0) { udp_open_socket(); if (g_udp_fd < 0) return; } + if (g_udp_fd < 0) { + g_udp_fd = (int)socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0); + if (g_udp_fd < 0) { ULOG("socket failed: %s (errno=%d)", strerror(errno), errno); return; } + if (connect(g_udp_fd, (struct sockaddr*)&g_udp_addr, sizeof(g_udp_addr)) < 0) { + ULOG("connect failed: %s (errno=%d)", strerror(errno), errno); + close(g_udp_fd); g_udp_fd = -1; return; + } + } ssize_t sent = send(g_udp_fd, data, len, MSG_DONTWAIT); - if (sent >= 0) { + if (sent == len) { g_udp_total_sent += sent; - if (!g_udp_sent_first) { ULOG("first send OK: %zd bytes, total=%lld", sent, (long long)g_udp_total_sent); g_udp_sent_first = 1; } + if (!g_udp_sent_first) { ULOG("first send OK: %zd bytes", sent); g_udp_sent_first = 1; } return; } + int send_errno = sent < 0 ? errno : 0; g_udp_send_errors++; if (g_udp_send_errors <= 3 || g_udp_send_errors % 100 == 0) - ULOG("send() error #%d: %s (errno=%d)", g_udp_send_errors, strerror(errno), errno); - if (errno == ECONNREFUSED || errno == ENETUNREACH || errno == EHOSTUNREACH) { - close(g_udp_fd); - g_udp_fd = -1; + ULOG("send error #%u: sent=%zd expected=%d error=%s (errno=%d)", + g_udp_send_errors, sent, len, send_errno ? strerror(send_errno) : "short datagram", send_errno); + if (send_errno == ECONNREFUSED || send_errno == ENETUNREACH || send_errno == EHOSTUNREACH) { + close(g_udp_fd); g_udp_fd = -1; + } +} + +static void udp_flush_locked(void) { + if (!g_udp_buf_len) return; + udp_do_send(g_udp_buf, g_udp_buf_len); + g_udp_buf_len = 0; +} + +static void udp_update_timer(void* arg); + +static void udp_flush_timer(void* arg) { + g_udp_timer = NULL; + pthread_mutex_lock(&g_udp_mutex); + if (g_udp_ua == arg && g_udp_has_addr && !g_udp_stopping) udp_flush_locked(); + pthread_mutex_unlock(&g_udp_mutex); + udp_update_timer(arg); +} + +/* Только uasync-поток: attach или posted-команда смены адреса. Mutex отпускаем + * до операций с таймером: их DEBUG_ERROR может снова вызвать udp_log_write. */ +static void udp_update_timer(void* arg) { + struct UASYNC* ua = arg; + pthread_mutex_lock(&g_udp_mutex); + int owns = g_udp_ua == ua; + int enabled = g_udp_has_addr && !g_udp_stopping; + pthread_mutex_unlock(&g_udp_mutex); + if (!owns) return; + if (!enabled) { + if (g_udp_timer) uasync_cancel_timeout(ua, g_udp_timer); + g_udp_timer = NULL; + } else if (!g_udp_timer) { + g_udp_timer = uasync_set_timeout(ua, UDP_FLUSH_TB, ua, udp_flush_timer, "udp_log_flush"); + if (!g_udp_timer) ULOG("flush timer allocation failed ua=%p", (void*)ua); } } -void udp_trampoline(void* arg) { - (void)arg; - if (g_udp_buf_len > 0) { - udp_do_send(g_udp_buf, g_udp_buf_len); - g_udp_buf_len = 0; +void udp_log_attach(struct UASYNC* ua) { + if (!ua) { ULOG("attach rejected: ua=NULL"); return; } + pthread_mutex_lock(&g_udp_mutex); + if (g_udp_ua) { + ULOG("attach rejected: owner=%p requested=%p", (void*)g_udp_ua, (void*)ua); + pthread_mutex_unlock(&g_udp_mutex); + return; } + g_udp_ua = ua; + pthread_mutex_unlock(&g_udp_mutex); + udp_update_timer(ua); + ULOG("attached ua=%p: flush timer owned by core thread", (void*)ua); +} + +void udp_log_detach(struct UASYNC* ua) { + if (!ua) return; + pthread_mutex_lock(&g_udp_mutex); + if (g_udp_ua != ua) { pthread_mutex_unlock(&g_udp_mutex); return; } + g_udp_ua = NULL; /* После этого ни один producer не может post в старый ua. */ + pthread_mutex_unlock(&g_udp_mutex); + if (g_udp_timer) uasync_cancel_timeout(ua, g_udp_timer); g_udp_timer = NULL; - /* перевзводим только если остались неотправленные данные */ - if (g_udp_buf_len > 0) udp_reschedule_timer(); + ULOG("detached ua=%p: flush timer cancelled", (void*)ua); } void udp_log_set_target(const char* ip, int port) { - g_udp_buf_len = 0; - g_udp_stopping = 0; + pthread_mutex_lock(&g_udp_mutex); + if (g_udp_has_addr) udp_flush_locked(); + if (g_udp_fd >= 0) { close(g_udp_fd); g_udp_fd = -1; } + memset(&g_udp_addr, 0, sizeof(g_udp_addr)); + g_udp_addr.sin_family = AF_INET; + g_udp_has_addr = ip && port > 0 && port <= 65535 && inet_pton(AF_INET, ip, &g_udp_addr.sin_addr) == 1; + g_udp_addr.sin_port = htons((unsigned short)(g_udp_has_addr ? port : 0)); + g_udp_stopping = !g_udp_has_addr; g_udp_sent_first = 0; g_udp_total_sent = 0; g_udp_send_errors = 0; - if (g_udp_fd >= 0) { close(g_udp_fd); g_udp_fd = -1; } - if (ip && port > 0) { - memset(&g_udp_addr, 0, sizeof(g_udp_addr)); - g_udp_addr.sin_family = AF_INET; - g_udp_addr.sin_port = htons((unsigned short)port); - g_udp_has_addr = (inet_pton(AF_INET, ip, &g_udp_addr.sin_addr) == 1); - ULOG("set target %s:%d -> has_addr=%d", ip, port, g_udp_has_addr); - } else { - g_udp_has_addr = 0; - ULOG("set target disabled (ip=%s port=%d)", ip ? ip : "null", port); + if (g_udp_ua) { + /* Маленький u_malloc не логирует; при отказе используем только ULOG. + * post_reserved тоже не вызывает debug hook. Detach ждёт этот mutex. */ + struct posted_task* task = u_malloc(sizeof(*task)); + if (task) { + task->callback = udp_update_timer; + task->arg = g_udp_ua; + uasync_post_reserved(g_udp_ua, task); + } else ULOG("target changed but timer update allocation failed"); } + ULOG("target %s:%d enabled=%d", ip ? ip : "(null)", port, g_udp_has_addr); + pthread_mutex_unlock(&g_udp_mutex); } void udp_log_write(const char* data, int len) { - if (!data || len <= 0 || !g_udp_has_addr) return; - - if (g_udp_fd < 0) udp_open_socket(); - - if (g_udp_buf_len + len > UDP_BUF_SIZE) { - if (g_udp_buf_len > 0) { udp_do_send(g_udp_buf, g_udp_buf_len); g_udp_buf_len = 0; } - if (len > UDP_BUF_SIZE) return; - } - memcpy(g_udp_buf + g_udp_buf_len, data, len); - g_udp_buf_len += len; - if (g_udp_buf_len >= UDP_FLUSH_BYTES) { - udp_do_send(g_udp_buf, g_udp_buf_len); - g_udp_buf_len = 0; + if (!data || len <= 0) return; + pthread_mutex_lock(&g_udp_mutex); + if (!g_udp_has_addr || g_udp_stopping) { pthread_mutex_unlock(&g_udp_mutex); return; } + if (len > UDP_BUF_SIZE) { + ULOG("log dropped: %d bytes exceeds UDP payload limit %d", len, UDP_BUF_SIZE); } else { - udp_reschedule_timer(); /* взвести flush для частичных данных */ + if (len > UDP_BUF_SIZE - g_udp_buf_len) udp_flush_locked(); + memcpy(g_udp_buf + g_udp_buf_len, data, len); + g_udp_buf_len += len; + if (g_udp_buf_len >= UDP_FLUSH_BYTES) udp_flush_locked(); } + pthread_mutex_unlock(&g_udp_mutex); } -void udp_log_cancel_timer(void) { - g_udp_stopping = 1; - struct UASYNC* ua = instance_lite_get_uasync(); - if (ua && g_udp_timer) uasync_cancel_timeout(ua, g_udp_timer); - g_udp_timer = NULL; +void udp_log_flush(void) { + pthread_mutex_lock(&g_udp_mutex); + if (g_udp_has_addr) udp_flush_locked(); + pthread_mutex_unlock(&g_udp_mutex); } void udp_log_stop(void) { + pthread_mutex_lock(&g_udp_mutex); g_udp_stopping = 1; - if (g_udp_buf_len > 0) { udp_do_send(g_udp_buf, g_udp_buf_len); g_udp_buf_len = 0; } - ULOG("stop: total_sent=%lld bytes, errors=%d", (long long)g_udp_total_sent, g_udp_send_errors); + if (g_udp_has_addr) udp_flush_locked(); + ULOG("stop: total_sent=%lld bytes errors=%u", (long long)g_udp_total_sent, g_udp_send_errors); if (g_udp_fd >= 0) { close(g_udp_fd); g_udp_fd = -1; } g_udp_has_addr = 0; + pthread_mutex_unlock(&g_udp_mutex); } diff --git a/tools/chatgui-android/jni_bridge/android_udp_log.h b/tools/chatgui-android/jni_bridge/android_udp_log.h index 0e7e5951..4f90e5e7 100644 --- a/tools/chatgui-android/jni_bridge/android_udp_log.h +++ b/tools/chatgui-android/jni_bridge/android_udp_log.h @@ -11,22 +11,25 @@ extern "C" { #endif -/* Configure target. ua may be NULL (delayed init, will retry on write). - * Call from ANY thread. Posts timer setup to uasync event loop. */ +struct UASYNC; + +/* Только поток ядра: attach после create, detach перед каждым destroy/restart. */ +void udp_log_attach(struct UASYNC* ua); +void udp_log_detach(struct UASYNC* ua); + +/* Любой поток. Смена адреса передаёт управление таймером в поток ядра. */ void udp_log_set_target(const char* ip, int port); /* Write log line to buffer. Non-blocking send if threshold reached. * Thread-safe — call from any thread (bridge_debug_hook). */ void udp_log_write(const char* data, int len); -/* Cancel flush timer (call before uasync_destroy, while ua is alive). */ -void udp_log_cancel_timer(void); +/* Любой поток: ошибки отправляем немедленно, до возможной остановки ядра. */ +void udp_log_flush(void); /* Flush buffer, close socket. Call after instance shutdown. */ void udp_log_stop(void); -void udp_trampoline(void* arg); - #ifdef __cplusplus } #endif diff --git a/tools/chatgui-android/libutun_lite/instance_lite.c b/tools/chatgui-android/libutun_lite/instance_lite.c index 7cd9848a..d744f0fe 100644 --- a/tools/chatgui-android/libutun_lite/instance_lite.c +++ b/tools/chatgui-android/libutun_lite/instance_lite.c @@ -276,9 +276,8 @@ static void* instance_thread(void* arg) { if (!g_ua) { IL_LOGE("uasync_create failed"); free_config(config); __atomic_store_n(&g_thread_running, 0, __ATOMIC_RELEASE); pthread_detach(pthread_self()); return NULL; } standby_init(g_ua); - if (config->global.log_udp_ip[0] && config->global.log_udp_port > 0) { - udp_log_set_target(config->global.log_udp_ip, config->global.log_udp_port); - } + udp_log_set_target(config->global.log_udp_ip, config->global.log_udp_port); + udp_log_attach(g_ua); IL_LOGI("[EVT_DIAG] thread: about to set chat_event handler, g_event_handler=%p", (void*)g_event_handler); ensure_keys(config); @@ -288,6 +287,7 @@ static void* instance_thread(void* arg) { if (!g_inst) { IL_LOGE("utun_instance_create_from_config failed"); standby_deinit(); + udp_log_detach(g_ua); uasync_destroy(g_ua, 0); g_ua = NULL; u_report_unfreed_blocks(); @@ -305,6 +305,7 @@ static void* instance_thread(void* arg) { utun_instance_destroy(g_inst); g_inst = NULL; standby_deinit(); + udp_log_detach(g_ua); uasync_destroy(g_ua, 0); g_ua = NULL; u_report_unfreed_blocks(); @@ -362,6 +363,7 @@ static void* instance_thread(void* arg) { uasync_print_resources(g_ua, "AFTER_DESTROY"); u_report_unfreed_blocks(); standby_deinit(); + udp_log_detach(g_ua); uasync_destroy(g_ua, 0); my_ua = NULL; g_ua = uasync_create(); @@ -378,6 +380,8 @@ static void* instance_thread(void* arg) { u_free(cfg); if (!config) { IL_LOGE("poll exit: parse_config failed"); break; } config->global.client_type = CLIENT_TYPE_MOBILE; /* Android — всегда мобильный узел */ + udp_log_set_target(config->global.log_udp_ip, config->global.log_udp_port); + udp_log_attach(g_ua); g_inst = utun_instance_create_from_config(g_ua, config); my_inst = g_inst; @@ -414,6 +418,7 @@ static void* instance_thread(void* arg) { } if (my_ua) { standby_deinit(); + udp_log_detach(my_ua); uasync_destroy(my_ua, 0); g_ua = NULL; my_ua = NULL; @@ -428,7 +433,7 @@ static void* instance_thread(void* arg) { IL_LOGI("cleanup (stale thread gen=%d vs %d): destroying own inst=%p ua=%p to avoid fd leak", my_gen, g_generation, (void*)my_inst, (void*)my_ua); if (my_inst) { utun_instance_destroy(my_inst); my_inst = NULL; } - if (my_ua) { uasync_destroy(my_ua, 0); my_ua = NULL; } + if (my_ua) { udp_log_detach(my_ua); uasync_destroy(my_ua, 0); my_ua = NULL; } } __atomic_store_n(&g_thread_running, 0, __ATOMIC_RELEASE); pthread_mutex_lock(&g_stop_mutex); diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index df7b4333..1a9d7ad0 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -130,6 +130,7 @@ add_executable(vibechat src/nodespage.cpp src/soundsettingspage.cpp src/sound_manager.cpp + src/audio_device.cpp src/audiorecorder.cpp src/voicemessageencoder.cpp src/voiceplayback.cpp @@ -150,6 +151,7 @@ add_executable(vibechat transport/config_updater.cpp transport/gui_bridge_impl.cpp transport/miniaudio_impl.c + transport/audio_diagnostics.cpp db/db_manager.cpp ../../lib/sqlite3.c resources/chatgui.qrc @@ -244,7 +246,9 @@ add_executable(test_video_engine tests/test_video_engine.cpp src/videoplayer_engine.cpp src/sound_manager.cpp + src/audio_device.cpp transport/miniaudio_impl.c + transport/audio_diagnostics.cpp ) target_include_directories(test_video_engine PRIVATE ${CMAKE_SOURCE_DIR}/../../lib ${CMAKE_SOURCE_DIR}/../../src ${CMAKE_SOURCE_DIR}/db ${CMAKE_SOURCE_DIR}/../../lib/libopus/include ${FFMPEG_INCLUDE_DIRS}) target_compile_definitions(test_video_engine PRIVATE USE_SQLITE SQLITE_THREADSAFE=1) @@ -262,12 +266,21 @@ add_executable(test_ptt_key_chord tests/test_ptt_key_chord.cpp) target_link_libraries(test_ptt_key_chord PRIVATE ${QT_CORE}) add_test(NAME test_ptt_key_chord COMMAND test_ptt_key_chord) -add_executable(test_voice_file tests/test_voice_file.cpp src/voiceplayback.cpp src/sound_manager.cpp transport/miniaudio_impl.c) +add_executable(test_voice_file tests/test_voice_file.cpp src/voiceplayback.cpp src/sound_manager.cpp src/audio_device.cpp transport/miniaudio_impl.c transport/audio_diagnostics.cpp) target_include_directories(test_voice_file PRIVATE ${UTUN_INCLUDE_DIRS}) target_link_libraries(test_voice_file PRIVATE ${QT_LIBS} utun pthread ${CMAKE_DL_LIBS}) add_test(NAME test_voice_file COMMAND test_voice_file) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_executable(test_audio_recovery tests/test_audio_recovery.cpp src/audio_device.cpp src/sound_manager.cpp + src/audiorecorder.cpp src/call_audio_engine.cpp src/radio_audio_engine.cpp + transport/miniaudio_impl.c transport/audio_diagnostics.cpp) + target_include_directories(test_audio_recovery PRIVATE ${UTUN_INCLUDE_DIRS}) + target_link_libraries(test_audio_recovery PRIVATE ${QT_LIBS} utun_voice utun pthread dl) + target_link_options(test_audio_recovery PRIVATE "-Wl,--wrap=ma_context_init" "-Wl,--wrap=ma_context_uninit" + "-Wl,--wrap=ma_device_init" "-Wl,--wrap=ma_device_start" "-Wl,--wrap=ma_device_uninit" "-Wl,--wrap=audio_context_failed") + add_test(NAME test_audio_recovery COMMAND test_audio_recovery) + set_tests_properties(test_audio_recovery PROPERTIES TIMEOUT 15) add_executable(test_ptt_portal tests/test_ptt_portal.cpp src/ptt_portal.cpp) target_link_libraries(test_ptt_portal PRIVATE ${QT_DBUS} ${QT_TEST_LIB} utun pthread) find_program(DBUS_RUN_SESSION dbus-run-session) @@ -275,4 +288,15 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") message(FATAL_ERROR "dbus-run-session is required for the isolated PTT portal test") endif() add_test(NAME test_ptt_portal COMMAND ${DBUS_RUN_SESSION} -- $) + + add_executable(test_linux_audio tests/test_linux_audio.c transport/audio_diagnostics.cpp) + target_link_libraries(test_linux_audio PRIVATE utun pthread dl m) + # assert выполняет вызовы стенда, поэтому не отключаем его в Release. + target_compile_options(test_linux_audio PRIVATE -UNDEBUG) + add_test(NAME test_linux_audio COMMAND test_linux_audio) + + add_executable(test_audio_diagnostics tests/test_audio_diagnostics.cpp) + target_link_libraries(test_audio_diagnostics PRIVATE utun pthread) + target_compile_options(test_audio_diagnostics PRIVATE -UNDEBUG) + add_test(NAME test_audio_diagnostics COMMAND test_audio_diagnostics) endif() diff --git a/tools/chatgui/src/audio_device.cpp b/tools/chatgui/src/audio_device.cpp new file mode 100644 index 00000000..c76445fc --- /dev/null +++ b/tools/chatgui/src/audio_device.cpp @@ -0,0 +1,96 @@ +#include "audio_device.h" +#include "../transport/audio_diagnostics.h" +#include "debug_config.h" +#include +#include + +AudioDevice::AudioDevice() { m_clock.start(); } +AudioDevice::~AudioDevice() { close(); } + +bool AudioDevice::init(ma_context* context, ma_device_config config, const char* role, uint64_t session, Callback callback) { + close(); + m_role = role; + m_callback = std::move(callback); + if (!context) { retryLater(); return false; } + auto* device = new ma_device{}; + config.dataCallback = data; + config.notificationCallback = notification; + config.pUserData = this; + ma_result result = ma_device_init(context, &config, device); + if (result != MA_SUCCESS) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: %s init failed result=%d", role, result); + delete device; + retryLater(); + return false; + } + m_device = device; + m_frames = 0; + m_seenFrames = 0; + m_suspended = false; + audio_diag_label(device, session, role); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s initialized backend=%s rate=%u capture='%s' playback='%s' period=%u/%u", + role, ma_get_backend_name(context->backend), device->sampleRate, device->capture.name, device->playback.name, + device->capture.internalPeriodSizeInFrames, device->playback.internalPeriodSizeInFrames); + return true; +} + +bool AudioDevice::start() { + if (!m_device) return false; + m_progressAt = m_clock.elapsed(); + ma_result result = ma_device_start(m_device); + if (result != MA_SUCCESS) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: %s start failed result=%d", m_role, result); + close(); + retryLater(); + return false; + } + DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s started", m_role); + return true; +} + +void AudioDevice::close() { + if (m_device) { + ma_device_uninit(m_device); /* uninit включает stop/join, в том числе после неудачного start. */ + delete m_device; + m_device = nullptr; + DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s closed", m_role); + } + m_callback = {}; +} + +void AudioDevice::data(ma_device* device, void* out, const void* in, ma_uint32 frames) { + auto* self = static_cast(device->pUserData); + if (self->m_callback) self->m_callback(in, out, frames); + self->m_frames.fetch_add(frames, std::memory_order_relaxed); +} + +void AudioDevice::notification(const ma_device_notification* event) { + auto* self = static_cast(event->pDevice->pUserData); + if (event->type == ma_device_notification_type_interruption_began) self->m_suspended = true; + if (event->type == ma_device_notification_type_interruption_ended) self->m_suspended = false; +} + +bool AudioDevice::needsRecovery() { + if (!m_device) return false; + uint64_t frames = m_frames.load(std::memory_order_relaxed); + qint64 now = m_clock.elapsed(); + if (frames != m_seenFrames || m_suspended.load()) { + m_seenFrames = frames; + m_progressAt = now; + if (frames) m_retryDelay = 250; + } + bool stopped = ma_device_get_state(m_device) == ma_device_state_stopped; + if (!stopped && now - m_progressAt < 1000) return false; + DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: %s recovery reason=%s idle=%lldms frames=%llu suspended=%d", + m_role, stopped ? "stopped" : "no-progress", (long long)(now - m_progressAt), + (unsigned long long)frames, m_suspended.load()); + return true; +} + +bool AudioDevice::retryReady() const { return !m_device && m_clock.elapsed() >= m_retryAt; } +void AudioDevice::retryLater() { + m_retryAt = m_clock.elapsed() + m_retryDelay; + DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: %s retry in %dms", m_role, m_retryDelay); + m_retryDelay = std::min(m_retryDelay * 2, 2000); +} +void AudioDevice::retryNow() { m_retryAt = 0; m_retryDelay = 250; } diff --git a/tools/chatgui/src/audio_device.h b/tools/chatgui/src/audio_device.h new file mode 100644 index 00000000..f20ebae5 --- /dev/null +++ b/tools/chatgui/src/audio_device.h @@ -0,0 +1,38 @@ +#pragma once + +#include "miniaudio.h" +#include +#include +#include + +/* Один владелец в GUI-потоке. Callback публикует только прогресс и уведомления; + * close() дожидается worker перед освобождением callback и его состояния. */ +class AudioDevice { +public: + using Callback = std::function; + AudioDevice(); + ~AudioDevice(); + AudioDevice(const AudioDevice&) = delete; + AudioDevice& operator=(const AudioDevice&) = delete; + bool init(ma_context* context, ma_device_config config, const char* role, uint64_t session, Callback callback); + bool start(); + void close(); + bool needsRecovery(); + bool retryReady() const; + void retryLater(); + void retryNow(); + bool initialized() const { return m_device != nullptr; } + ma_device* device() const { return m_device; } +private: + static void data(ma_device* device, void* out, const void* in, ma_uint32 frames); + static void notification(const ma_device_notification* event); + ma_device* m_device = nullptr; + Callback m_callback; + const char* m_role = "audio"; + QElapsedTimer m_clock; + std::atomic m_frames{0}; + std::atomic m_suspended{false}; + uint64_t m_seenFrames = 0; + qint64 m_progressAt = 0, m_retryAt = 0; + int m_retryDelay = 250; +}; diff --git a/tools/chatgui/src/audiodevicesettingspage.cpp b/tools/chatgui/src/audiodevicesettingspage.cpp index f63e5f45..92afa60b 100644 --- a/tools/chatgui/src/audiodevicesettingspage.cpp +++ b/tools/chatgui/src/audiodevicesettingspage.cpp @@ -183,25 +183,25 @@ void AudioDeviceSettingsPage::loadFromDb() { /* Output devices */ m_outputCombo->clear(); - m_outputCombo->addItem("Default (auto)", -1); + m_outputCombo->addItem("Default (auto)", QString()); for (int i = 0; i < playbackDevs.size(); i++) { QString label = playbackDevs[i].name; if (playbackDevs[i].isDefault) label += " (default)"; - m_outputCombo->addItem(label, i); + m_outputCombo->addItem(label, playbackDevs[i].id); } /* Input devices */ m_inputCombo->clear(); - m_inputCombo->addItem("Default (auto)", -1); + m_inputCombo->addItem("Default (auto)", QString()); for (int i = 0; i < captureDevs.size(); i++) { QString label = captureDevs[i].name; if (captureDevs[i].isDefault) label += " (default)"; - m_inputCombo->addItem(label, i); + m_inputCombo->addItem(label, captureDevs[i].id); } if (m_db) { - m_savedOutputIdx = m_db->getUiStateInt("audio_output_device", -1); - m_savedInputIdx = m_db->getUiStateInt("audio_input_device", -1); + m_savedOutputId = m_db->getUiState("audio_output_id"); + m_savedInputId = m_db->getUiState("audio_input_id"); m_savedCodecPreset = m_db->getUiStateInt("opus_codec_preset", 1); m_savedRadioOpusMode = m_db->getUiStateInt("radio_opus_mode", 0); m_savedRadioOpusBitrate = m_db->getUiStateInt("radio_opus_bitrate", 32); @@ -215,11 +215,11 @@ void AudioDeviceSettingsPage::loadFromDb() { /* Select saved indices */ for (int i = 0; i < m_outputCombo->count(); i++) { - if (m_outputCombo->itemData(i).toInt() == m_savedOutputIdx) + if (m_outputCombo->itemData(i).toString() == m_savedOutputId) { m_outputCombo->setCurrentIndex(i); break; } } for (int i = 0; i < m_inputCombo->count(); i++) { - if (m_inputCombo->itemData(i).toInt() == m_savedInputIdx) + if (m_inputCombo->itemData(i).toString() == m_savedInputId) { m_inputCombo->setCurrentIndex(i); break; } } for (int i = 0; i < m_codecPresetCombo->count(); i++) { @@ -244,8 +244,8 @@ void AudioDeviceSettingsPage::loadFromDb() { } void AudioDeviceSettingsPage::applyAndSave() { - int outIdx = m_outputCombo->currentData().toInt(); - int inIdx = m_inputCombo->currentData().toInt(); + QString outputId = m_outputCombo->currentData().toString(); + QString inputId = m_inputCombo->currentData().toString(); int codecPreset = m_codecPresetCombo->currentData().toInt(); auto saveKey = [](const char* key, int val) { @@ -260,8 +260,23 @@ void AudioDeviceSettingsPage::applyAndSave() { memcpy(arg->data + klen + 1, v.constData(), vlen + 1); gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg); }; - saveKey("audio_output_device", outIdx); - saveKey("audio_input_device", inIdx); + auto saveDevice = [](const char* key, const QString& id) { + QByteArray value = id.toUtf8(); + size_t keyLen = strlen(key), valueLen = value.size(); + auto* arg = static_cast(u_calloc(1, sizeof(save_ui_state_arg))); + if (!arg) return; + if (keyLen + valueLen + 2 > sizeof(arg->data)) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: device ID too long bytes=%zu", valueLen); + u_free(arg); return; + } + arg->inst = gui_bridge_get_inst(); + memcpy(arg->data, key, keyLen + 1); + memcpy(arg->data + keyLen + 1, value.constData(), valueLen + 1); + gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg); + }; + saveDevice("audio_output_id", outputId); + saveDevice("audio_input_id", inputId); + SoundManager::instance()->setDevices(outputId, inputId); saveKey("opus_codec_preset", codecPreset); VoiceEncoder::setPreset(codecPreset); @@ -312,8 +327,8 @@ void AudioDeviceSettingsPage::applyAndSave() { postChat(m_configPath, "call_jitter_max_reserve_ms", jitterReserveMs); } - m_savedOutputIdx = outIdx; - m_savedInputIdx = inIdx; + m_savedOutputId = outputId; + m_savedInputId = inputId; m_savedCodecPreset = codecPreset; m_savedRadioOpusMode = radioOpusMode; m_savedRadioOpusBitrate = radioOpusBitrate; @@ -322,13 +337,13 @@ void AudioDeviceSettingsPage::applyAndSave() { m_savedCompressorMaxGainDb = compMaxGain; m_savedCompressorRiseRate = compRiseRate; m_savedAecEnabled = aecEnabled; - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "saved output=%d input=%d codec=%d radioOpus=%d/%dkbps stereo=%d compressor=%d gain=%ddB rise=%.1fx aec=%d", - outIdx, inIdx, codecPreset, radioOpusMode, radioOpusBitrate, radioCaptureStereo, + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "saved output_default=%d input_default=%d codec=%d radioOpus=%d/%dkbps stereo=%d compressor=%d gain=%ddB rise=%.1fx aec=%d", + outputId.isEmpty(), inputId.isEmpty(), codecPreset, radioOpusMode, radioOpusBitrate, radioCaptureStereo, compEnabled, compMaxGain, (float)compRiseRate, aecEnabled); if (m_recorder) { m_recorder->setCompressorEnabled(compEnabled != 0); - if (m_recorder->compressor() && compEnabled) { + { audio_compressor_config_t compCfg = {0}; compCfg.sample_rate = 48000; compCfg.channels = 1; @@ -337,7 +352,7 @@ void AudioDeviceSettingsPage::applyAndSave() { compCfg.lookahead_ms = 100; compCfg.rise_rate_per_sec = (float)compRiseRate; compCfg.target_level = 1.0f; - audio_compressor_configure(m_recorder->compressor(), &compCfg); + m_recorder->configureCompressor(compCfg); } } } @@ -346,8 +361,8 @@ void AudioDeviceSettingsPage::onTestMicToggled() { if (m_testMicBtn->isChecked()) { m_testRecorder = new AudioRecorder(this); m_testRecorder->init(); - int inIdx = m_inputCombo->currentData().toInt(); - m_testRecorder->setCaptureDeviceIndex(inIdx); + QString id = m_inputCombo->currentData().toString(); + m_testRecorder->setCaptureDevice(id); m_testRecorder->startRecording(); m_micTestActive = true; m_levelTimer->start(); diff --git a/tools/chatgui/src/audiodevicesettingspage.h b/tools/chatgui/src/audiodevicesettingspage.h index 5b2ddec7..89294751 100644 --- a/tools/chatgui/src/audiodevicesettingspage.h +++ b/tools/chatgui/src/audiodevicesettingspage.h @@ -41,8 +41,8 @@ private: QPushButton* m_testSpeakerBtn; QTimer* m_levelTimer; bool m_micTestActive = false; - int m_savedOutputIdx = -1; - int m_savedInputIdx = -1; + QString m_savedOutputId; + QString m_savedInputId; int m_savedCodecPreset = 1; QCheckBox* m_compressorEnabledCb; diff --git a/tools/chatgui/src/audiorecorder.cpp b/tools/chatgui/src/audiorecorder.cpp index f789febe..b8371de5 100644 --- a/tools/chatgui/src/audiorecorder.cpp +++ b/tools/chatgui/src/audiorecorder.cpp @@ -2,6 +2,7 @@ #include "media_async/attachment_send.h" #include "miniaudio.h" #include "sound_manager.h" +#include "../transport/audio_diagnostics.h" extern "C" { #include "../../lib/audio_compressor.h" @@ -11,57 +12,60 @@ extern "C" { #include #include #include -static AudioRecorder* g_recorder = nullptr; - -void audioCaptureCallback(ma_device* pDevice, void* pOutput, const void* pInput, unsigned int frameCount) { - (void)pOutput; - AudioRecorder* self = g_recorder; - if (!self || !self->m_recording) return; - if (!pInput) return; +void AudioRecorder::captureCallback(const void* pInput, unsigned int frameCount) { + if (!pInput || !frameCount || !m_recording.load()) return; const int16_t* src = (const int16_t*)pInput; - size_t add = (size_t)frameCount * (size_t)self->m_channels; + size_t add = (size_t)frameCount * (size_t)m_channels; - if (self->m_compressor && self->m_compressorEnabled) { - audio_compressor_push(self->m_compressor, src, add); + if (m_compressor && m_captureCompressorEnabled) { + audio_compressor_push(m_compressor, src, add); } else { - size_t cur = self->m_pcmBuffer.size(); - self->m_pcmBuffer.resize(cur + add); - std::memcpy(self->m_pcmBuffer.data() + cur, src, add * sizeof(int16_t)); + size_t cur = m_pcmBuffer.size(); + m_pcmBuffer.resize(cur + add); + std::memcpy(m_pcmBuffer.data() + cur, src, add * sizeof(int16_t)); } float sumSq = 0.0f; - for (unsigned int i = 0; i < frameCount * (unsigned int)self->m_channels; i++) { + for (unsigned int i = 0; i < frameCount * (unsigned int)m_channels; i++) { float v = (float)src[i] / 32768.0f; sumSq += v * v; } - float rms = sqrtf(sumSq / (float)(frameCount * self->m_channels)); - self->m_peakLevel = rms; - - self->m_recentRms[self->m_rmsIdx % 5] = rms; - self->m_rmsIdx++; - if (self->m_rmsIdx >= 5) { - float avg = 0.0f; - for (int i = 0; i < 5; i++) avg += self->m_recentRms[i]; - avg /= 5.0f; - self->m_waveformLevels.push_back(avg); - } + float rms = sqrtf(sumSq / (float)(frameCount * m_channels)); + m_peakLevel = rms; + + m_recordedFrames.fetch_add(frameCount, std::memory_order_relaxed); } AudioRecorder::AudioRecorder(QObject* parent) : QObject(parent) { - g_recorder = this; m_compressor = audio_compressor_create(); if (m_compressor) { audio_compressor_config_t cfg = {0}; cfg.sample_rate = m_sampleRate; cfg.channels = m_channels; + m_compressorConfig = cfg; audio_compressor_configure(m_compressor, &cfg); } + m_captureId = SoundManager::instance()->captureDevice(); + connect(SoundManager::instance(), &SoundManager::devicesChanged, this, [this]() { + QString id = SoundManager::instance()->captureDevice(); + if (!m_customDevice && id != m_captureId) { + m_captureId = id; m_capture.close(); m_capture.retryNow(); recoverCapture(); + } + }); + m_recoveryTimer.setInterval(200); + connect(&m_recoveryTimer, &QTimer::timeout, this, &AudioRecorder::recoverCapture); + connect(SoundManager::instance(), &SoundManager::contextAboutToReset, this, [this]() { + m_capture.close(); m_durationTimer.stop(); + }); + connect(SoundManager::instance(), &SoundManager::contextRestored, this, [this]() { + m_capture.retryNow(); recoverCapture(); + }); m_durationTimer.setInterval(100); connect(&m_durationTimer, &QTimer::timeout, this, [this]() { if (m_recording) { - m_elapsedMs += 100; + m_elapsedMs = (int)(m_recordedFrames.load(std::memory_order_relaxed) * 1000 / m_sampleRate); emit durationChanged(m_elapsedMs); } }); @@ -71,103 +75,71 @@ AudioRecorder::~AudioRecorder() { shutdown(); audio_compressor_destroy(m_compressor); m_compressor = nullptr; - if (g_recorder == this) g_recorder = nullptr; } bool AudioRecorder::init() { - if (m_initialized) return true; - m_device = new ma_device; - std::memset(m_device, 0, sizeof(*m_device)); m_initialized = true; - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "initialized (sampleRate=%d, channels=%d)", m_sampleRate, m_channels); return true; } void AudioRecorder::shutdown() { - if (!m_initialized) return; - if (m_recording) stopRecording(); - if (m_deviceStarted) { - m_deviceStarted = false; - if (m_device) { ma_device_stop(m_device); ma_device_uninit(m_device); } - } - if (m_device) { delete m_device; m_device = nullptr; } + stopRecording(); + m_recoveryTimer.stop(); + m_capture.close(); m_initialized = false; - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "shutdown"); } void AudioRecorder::startRecording() { if (m_recording) return; - if (!m_initialized) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "not initialized"); return; } - + if (!m_initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "recorder: not initialized"); return; } m_pcmBuffer.clear(); - m_waveformLevels.clear(); - m_rmsIdx = 0; - m_elapsedMs = 0; - - if (m_compressor) audio_compressor_reset(m_compressor); - - ma_context* ctx = SoundManager::instance()->context(); - if (!ctx) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "no audio context"); return; } - - ma_device_config config = ma_device_config_init(ma_device_type_capture); - config.capture.format = ma_format_s16; - config.capture.channels = (ma_uint32)m_channels; - config.sampleRate = (ma_uint32)m_sampleRate; - config.periodSizeInFrames = 480; - config.dataCallback = audioCaptureCallback; - config.pUserData = this; - - if (m_captureDeviceIndex >= 0) { - ma_device_info* pCaptureDevices = nullptr; - ma_uint32 captureCount = 0; - ma_result r = ma_context_get_devices(ctx, nullptr, nullptr, &pCaptureDevices, &captureCount); - if (r == MA_SUCCESS && (ma_uint32)m_captureDeviceIndex < captureCount) { - config.capture.pDeviceID = &pCaptureDevices[m_captureDeviceIndex].id; - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "using capture device[%d]: %s", m_captureDeviceIndex, - pCaptureDevices[m_captureDeviceIndex].name); - } - } - - ma_result r = ma_device_init(ctx, &config, m_device); - if (r != MA_SUCCESS) { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "device init failed (%d), trying default", r); - config.capture.pDeviceID = nullptr; - r = ma_device_init(ctx, &config, m_device); - if (r != MA_SUCCESS) { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "default device init also failed (%d)", r); - return; - } - } - - r = ma_device_start(m_device); - if (r != MA_SUCCESS) { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "device start failed (%d)", r); - ma_device_uninit(m_device); - return; + m_recordedFrames = 0; m_elapsedMs = 0; m_peakLevel = 0; + m_captureCompressorEnabled = m_compressorEnabled; + if (m_compressor) { + audio_compressor_configure(m_compressor, &m_compressorConfig); + audio_compressor_set_enabled(m_compressor, m_captureCompressorEnabled); + audio_compressor_reset(m_compressor); } - m_recording = true; - m_deviceStarted = true; - m_durationTimer.start(); - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "recording started"); + m_capture.retryNow(); + recoverCapture(); + m_recoveryTimer.start(); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: recording requested compressor=%d", m_captureCompressorEnabled); emit recordingStarted(); } -void AudioRecorder::stopRecording() { +void AudioRecorder::recoverCapture() { if (!m_recording) return; - - m_durationTimer.stop(); - if (m_deviceStarted) { - ma_device_stop(m_device); - ma_device_uninit(m_device); - m_deviceStarted = false; + if (m_capture.needsRecovery()) { m_capture.close(); m_durationTimer.stop(); m_capture.retryLater(); } + if (!m_capture.retryReady()) return; + ma_context* context = SoundManager::instance()->context(); + if (!context) return; + ma_device_config config = ma_device_config_init(ma_device_type_capture); + config.capture.format = ma_format_s16; + config.capture.channels = m_channels; + config.sampleRate = m_sampleRate; + ma_device_id id; + bool selected = SoundManager::deviceId(m_captureId, &id); + if (selected) config.capture.pDeviceID = &id; + auto callback = [this](const void* input, void*, unsigned int frames) { captureCallback(input, frames); }; + bool ready = m_capture.init(context, config, "voice-recorder", 0, callback) && m_capture.start(); + if (!ready) { + if (!selected) return; + DEBUG_WARN(DEBUG_CATEGORY_CALL, "recorder: selected input unavailable, trying default"); + config.capture.pDeviceID = nullptr; + ready = m_capture.init(context, config, "voice-recorder", 0, callback) && m_capture.start(); } - m_recording = false; + if (ready) m_durationTimer.start(); +} +void AudioRecorder::stopRecording() { + if (!m_recording.exchange(false)) return; + m_recoveryTimer.stop(); m_durationTimer.stop(); + m_capture.close(); + m_elapsedMs = (int)(m_recordedFrames.load() * 1000 / m_sampleRate); int duration = m_elapsedMs; - size_t bufSize = (m_compressor && m_compressorEnabled) ? audio_compressor_output_size(m_compressor) : m_pcmBuffer.size(); - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "recording stopped, duration=%dms, pcmSamples=%zu, compressor=%d", - duration, bufSize, m_compressorEnabled ? 1 : 0); + size_t samples = m_compressor && m_captureCompressorEnabled ? audio_compressor_output_size(m_compressor) : m_pcmBuffer.size(); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: stopped duration=%dms samples=%zu", duration, samples); emit recordingStopped(duration); emit durationChanged(0); } @@ -179,7 +151,7 @@ int AudioRecorder::durationMs() const { /* Worker получает владельца буфера; новая запись имеет отдельное состояние. */ bool AudioRecorder::takeRecording(struct attachment_send_req* request) { if (!request || m_recording) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "recorder: cannot detach active recording"); return false; } - if (m_compressor && m_compressorEnabled) { + if (m_compressor && m_captureCompressorEnabled) { auto* replacement = audio_compressor_clone_config(m_compressor); if (!replacement) return false; request->compressor = m_compressor; @@ -192,24 +164,14 @@ bool AudioRecorder::takeRecording(struct attachment_send_req* request) { return true; } -float AudioRecorder::peakLevel() const { - float v = m_peakLevel; - m_peakLevel = 0.0f; - return v; -} - -const int16_t* AudioRecorder::bufferData() const { - if (m_compressor && m_compressorEnabled) return audio_compressor_output(m_compressor); - return m_pcmBuffer.data(); -} +float AudioRecorder::peakLevel() const { return m_peakLevel.exchange(0.0f); } -size_t AudioRecorder::bufferSize() const { - if (m_compressor && m_compressorEnabled) return audio_compressor_output_size(m_compressor); - return m_pcmBuffer.size(); +void AudioRecorder::configureCompressor(const audio_compressor_config_t& config) { + m_compressorConfig = config; /* Применяется перед следующей записью, когда callback ещё не запущен. */ + DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: compressor configuration saved for next recording"); } void AudioRecorder::setCompressorEnabled(bool enabled) { m_compressorEnabled = enabled; - if (m_compressor) audio_compressor_set_enabled(m_compressor, enabled); - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "compressor %s", enabled ? "enabled" : "disabled"); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: compressor=%d for next recording", enabled); } diff --git a/tools/chatgui/src/audiorecorder.h b/tools/chatgui/src/audiorecorder.h index 6a9242af..e4012672 100644 --- a/tools/chatgui/src/audiorecorder.h +++ b/tools/chatgui/src/audiorecorder.h @@ -5,6 +5,10 @@ #include #include #include +#include "audio_device.h" +extern "C" { +#include "audio_compressor.h" +} struct ma_device; struct audio_compressor; @@ -20,8 +24,7 @@ public: void shutdown(); bool isInitialized() const { return m_initialized; } - void setCaptureDeviceIndex(int index) { m_captureDeviceIndex = index; } - int captureDeviceIndex() const { return m_captureDeviceIndex; } + void setCaptureDevice(const QString& id) { m_captureId = id; m_customDevice = true; } void startRecording(); void stopRecording(); @@ -29,13 +32,10 @@ public: float peakLevel() const; - const int16_t* bufferData() const; - size_t bufferSize() const; - const std::vector& waveformLevels() const { return m_waveformLevels; } int durationMs() const; bool takeRecording(struct attachment_send_req* request); /* Передать остановленную запись без копии PCM. */ - struct audio_compressor* compressor() const { return m_compressor; } + void configureCompressor(const audio_compressor_config_t& config); void setCompressorEnabled(bool enabled); bool isCompressorEnabled() const { return m_compressorEnabled; } @@ -45,24 +45,26 @@ signals: void durationChanged(int ms); private: - ma_device* m_device = nullptr; + AudioDevice m_capture; + QTimer m_recoveryTimer; + void recoverCapture(); + void captureCallback(const void* input, unsigned int frames); bool m_initialized = false; - bool m_recording = false; - bool m_deviceStarted = false; + std::atomic m_recording{false}; int m_sampleRate = 48000; int m_channels = 1; - int m_captureDeviceIndex = -1; + QString m_captureId; + bool m_customDevice = false; std::vector m_pcmBuffer; - mutable float m_peakLevel = 0.0f; - std::vector m_recentRms = std::vector(5, 0.0f); - int m_rmsIdx = 0; - std::vector m_waveformLevels; + mutable std::atomic m_peakLevel{0.0f}; + std::atomic m_recordedFrames{0}; QTimer m_durationTimer; int m_elapsedMs = 0; struct audio_compressor* m_compressor = nullptr; bool m_compressorEnabled = false; + bool m_captureCompressorEnabled = false; + audio_compressor_config_t m_compressorConfig{}; - friend void audioCaptureCallback(ma_device* pDevice, void* pOutput, const void* pInput, unsigned int frameCount); }; diff --git a/tools/chatgui/src/call_audio_engine.cpp b/tools/chatgui/src/call_audio_engine.cpp index 655fa528..6bb3f634 100644 --- a/tools/chatgui/src/call_audio_engine.cpp +++ b/tools/chatgui/src/call_audio_engine.cpp @@ -1,232 +1,164 @@ #include "call_audio_engine.h" #include "sound_manager.h" #include "../transport/gui_bridge.h" - -#include "miniaudio.h" - +#include "../transport/audio_diagnostics.h" extern "C" { #include "call/call_audio.h" -#include "../../lib/debug_config.h" -#include "../../lib/u_async.h" +#include "debug_config.h" } - -#include -#include #include -static CallAudioEngine* g_callAudioEngine = nullptr; - -static void callAudioDataCallback(ma_device* dev, void* out, const void* in, ma_uint32 frames) { - CallAudioEngine* e = (CallAudioEngine*)dev->pUserData; - if (e) e->dataCallback(in, out, frames); +CallAudioEngine* CallAudioEngine::instance() { + static auto* engine = new CallAudioEngine(); + return engine; } -CallAudioEngine* CallAudioEngine::instance() { - if (!g_callAudioEngine) g_callAudioEngine = new CallAudioEngine(); - return g_callAudioEngine; +CallAudioEngine::CallAudioEngine() { + m_playbackId = SoundManager::instance()->playbackDevice(); + m_captureId = SoundManager::instance()->captureDevice(); + connect(SoundManager::instance(), &SoundManager::devicesChanged, this, [this]() { + setPlaybackDevice(SoundManager::instance()->playbackDevice()); + setCaptureDevice(SoundManager::instance()->captureDevice()); + }); + m_recoveryTimer.setInterval(200); + connect(&m_recoveryTimer, &QTimer::timeout, this, &CallAudioEngine::recoverDevices); + connect(SoundManager::instance(), &SoundManager::contextAboutToReset, this, &CallAudioEngine::pauseDevices); + connect(SoundManager::instance(), &SoundManager::contextRestored, this, [this]() { + m_capture.retryNow(); m_playback.retryNow(); + if (m_active) recoverDevices(); + }); } int CallAudioEngine::start(quint64 callId) { - if (m_active) { - if (m_callId == callId) return 0; - stop(); - } - m_callId = callId; - - struct UTUN_INSTANCE* inst = gui_bridge_get_inst(); - if (!inst) { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: no instance, cannot start"); - return -1; - } - if (call_audio_init(inst) != 0) { - DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: call_audio_init failed"); - return -1; - } - if (call_audio_start(callId) != 0) { - DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: call_audio_start failed call=%016llx", (unsigned long long)callId); + if (m_active && m_callId == callId && !m_ending) return 0; + stop(); + if (!gui_bridge_get_inst() || call_audio_start(callId) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "CallAudio: cannot start call=%016llx", (unsigned long long)callId); return -1; } - + ++m_generation; + m_callId = callId; m_captureBuf.assign(CALL_AUDIO_FRAME_SAMPLES, 0); m_capturePos = 0; m_muted = false; - - if (initDevice() != 0) { - call_audio_release(callId); - return -1; - } - + m_ending = false; m_active = true; - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: started call=%016llx", (unsigned long long)callId); + m_capture.retryNow(); m_playback.retryNow(); + recoverDevices(); + m_recoveryTimer.start(); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "CallAudio: session started call=%016llx", (unsigned long long)callId); return 0; } -int CallAudioEngine::initDevice() { - ma_context* ctx = SoundManager::instance()->context(); - if (!ctx) { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: no audio context"); - return -1; +bool CallAudioEngine::openDevice(bool playback) { + AudioDevice& device = playback ? m_playback : m_capture; + ma_context* context = SoundManager::instance()->context(); + if (!context) return false; + ma_device_config config = ma_device_config_init(playback ? ma_device_type_playback : ma_device_type_capture); + config.capture.format = config.playback.format = ma_format_s16; + config.capture.channels = config.playback.channels = 1; + config.sampleRate = CALL_AUDIO_SAMPLE_RATE; + ma_device_id id; + bool selected = SoundManager::deviceId(playback ? m_playbackId : m_captureId, &id); + if (selected) { + if (playback) config.playback.pDeviceID = &id; + else config.capture.pDeviceID = &id; } + auto callback = [this](const void* in, void* out, unsigned int frames) { dataCallback(in, out, frames); }; + const char* role = playback ? "call-playback" : "call-capture"; + auto attempt = [&]() { + if (!playback) m_capturePos = 0; + bool ready = device.init(context, config, role, m_callId.load(), callback) && device.start(); + if (!ready) call_audio_reset_io(m_callId.load(), playback); + return ready; + }; + if (attempt()) return true; + if (!selected) return false; + DEBUG_WARN(DEBUG_CATEGORY_CALL, "CallAudio: %s selected device unavailable, trying default", role); + config.capture.pDeviceID = config.playback.pDeviceID = nullptr; + return attempt(); +} - m_device = new ma_device; - std::memset(m_device, 0, sizeof(*m_device)); - ma_device_config config = ma_device_config_init(ma_device_type_duplex); - config.capture.format = ma_format_s16; - config.capture.channels = 1; - config.playback.format = ma_format_s16; - config.playback.channels = 1; - config.sampleRate = CALL_AUDIO_SAMPLE_RATE; - config.periodSizeInFrames = 480; - config.dataCallback = callAudioDataCallback; - config.pUserData = this; - - if (m_playbackDeviceIndex >= 0) { - ma_device_info* pPlaybackDevices = nullptr; - ma_uint32 playbackCount = 0; - if (ma_context_get_devices(ctx, &pPlaybackDevices, &playbackCount, nullptr, nullptr) == MA_SUCCESS && - (ma_uint32)m_playbackDeviceIndex < playbackCount) { - config.playback.pDeviceID = &pPlaybackDevices[m_playbackDeviceIndex].id; - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "CallAudio: using playback device[%d]: %s", - m_playbackDeviceIndex, pPlaybackDevices[m_playbackDeviceIndex].name); - } else { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: playback device[%d] not found, using default", - m_playbackDeviceIndex); - } +void CallAudioEngine::recoverDevices() { + if (!m_active) return; + if (m_playback.needsRecovery()) { + m_playback.close(); + call_audio_reset_io(m_callId.load(), 1); + m_playback.retryLater(); } - - if (ma_device_init(ctx, &config, m_device) != MA_SUCCESS) { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: device init failed, trying default"); - config.playback.pDeviceID = nullptr; - if (ma_device_init(ctx, &config, m_device) != MA_SUCCESS) { - DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: default device init also failed"); - delete m_device; m_device = nullptr; - return -1; - } + if (!m_ending && m_capture.needsRecovery()) { + m_capture.close(); + m_capturePos = 0; + call_audio_reset_io(m_callId.load(), 0); + m_capture.retryLater(); } - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: device backend=%s rate=%u capture='%s' rate=%u period=%u playback='%s' rate=%u period=%u", - ma_get_backend_name(ctx->backend), m_device->sampleRate, - m_device->capture.name, m_device->capture.internalSampleRate, m_device->capture.internalPeriodSizeInFrames, - m_device->playback.name, m_device->playback.internalSampleRate, m_device->playback.internalPeriodSizeInFrames); + if (m_playback.retryReady()) openDevice(true); + if (!m_ending && m_capture.retryReady()) openDevice(false); +} + +void CallAudioEngine::pauseDevices() { + m_capture.close(); m_playback.close(); m_capturePos = 0; - m_dbg = {}; - if (ma_device_start(m_device) != MA_SUCCESS) { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: device start failed"); - ma_device_uninit(m_device); - delete m_device; m_device = nullptr; - return -1; - } - m_deviceStarted = true; - return 0; + if (m_active) call_audio_reset_io(m_callId.load(), 1); } void CallAudioEngine::setMuted(bool muted) { - if (m_muted.load() == muted) return; - m_muted = muted; - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: mute %s", muted ? "on" : "off"); + if (m_muted.exchange(muted) == muted) return; + DEBUG_INFO(DEBUG_CATEGORY_CALL, "CallAudio: mute=%d", muted); } -void CallAudioEngine::setPlaybackDeviceIndex(int index) { - if (index == m_playbackDeviceIndex) return; - int prev = m_playbackDeviceIndex; - m_playbackDeviceIndex = index; - if (m_active) { - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: switch playback device %d -> %d", prev, index); - teardownDevice(); - if (initDevice() != 0) - DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: switch playback device failed (kept index=%d)", index); - } +void CallAudioEngine::setPlaybackDevice(const QString& id) { + if (id == m_playbackId) return; + m_playbackId = id; + m_playback.close(); m_playback.retryNow(); + if (m_active) { call_audio_reset_io(m_callId.load(), 1); recoverDevices(); } +} + +void CallAudioEngine::setCaptureDevice(const QString& id) { + if (id == m_captureId) return; + m_captureId = id; + m_capture.close(); m_capture.retryNow(); + m_capturePos = 0; + if (m_active) { call_audio_reset_io(m_callId.load(), 0); recoverDevices(); } } void CallAudioEngine::beginEnd(quint64 callId) { - if (!m_active || m_callId != callId) return; - call_audio_begin_end(callId); /* старт тона завершения */ - QTimer::singleShot(500, this, [this, callId]() { - if (m_callId == callId) stop(); - }); + if (!m_active || m_callId != callId || m_ending) return; + m_ending = true; + m_capture.close(); + call_audio_begin_end(callId); + uint64_t generation = m_generation; + QTimer::singleShot(500, this, [this, generation]() { if (m_generation == generation) stop(); }); } void CallAudioEngine::stop() { - if (!m_active && !m_device && !m_deviceStarted) return; + ++m_generation; + m_recoveryTimer.stop(); m_active = false; + pauseDevices(); + uint64_t id = m_callId.exchange(0); + if (id) call_audio_release(id); m_muted = false; - quint64 cid = m_callId.load(); - - teardownDevice(); - call_audio_release(cid); - - m_callId = 0; - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: stopped"); -} - -void CallAudioEngine::teardownDevice() { - if (m_deviceStarted) { - ma_device_stop(m_device); - ma_device_uninit(m_device); - m_deviceStarted = false; - } - if (m_device) { delete m_device; m_device = nullptr; } + m_ending = false; + if (id) DEBUG_INFO(DEBUG_CATEGORY_CALL, "CallAudio: session stopped call=%016llx", (unsigned long long)id); } void CallAudioEngine::dataCallback(const void* in, void* out, unsigned int frames) { - const int16_t* in16 = (const int16_t*)in; - int16_t* out16 = (int16_t*)out; - uint64_t startTb = get_time_tb(); - uint64_t idleTb = m_dbg.lastEndTb ? startTb - m_dbg.lastEndTb : 0; - if (!m_dbg.lastReportTb) m_dbg.lastReportTb = startTb; - - /* диагностика (DEBUG_CATEGORY_DEBUG): фактические параметры miniaudio-коллбэка */ - if (m_dbg.frames != frames) { - m_dbg.frames = frames; - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "CallAudio: callback frames=%u in=%s out=%s", - frames, in16 ? "yes" : "no", out16 ? "yes" : "no"); - } - - /* захват: копим 960 samples → encode+send (сервисный слой). mute → тишина. */ - uint64_t captureStartTb = get_time_tb(); - if (in16) { + if (in) { + const auto* pcm = static_cast(in); bool muted = m_muted.load(); - for (unsigned int i = 0; i < frames; i++) { - m_captureBuf[m_capturePos++] = muted ? 0 : in16[i]; - if (m_capturePos >= (int)m_captureBuf.size()) { - call_audio_feed_pcm(m_callId.load(), m_captureBuf.data(), (int)m_captureBuf.size()); + for (unsigned int i = 0; i < frames; ++i) { + m_captureBuf[m_capturePos++] = muted ? 0 : pcm[i]; + if (m_capturePos == CALL_AUDIO_FRAME_SAMPLES) { + int result = call_audio_feed_pcm(m_callId.load(), m_captureBuf.data(), CALL_AUDIO_FRAME_SAMPLES); + if (result != 0) audio_diag_event(m_capture.device(), AUDIO_DIAG_ERROR, AUDIO_DIAG_FEED, 1, result); m_capturePos = 0; } } } - uint64_t captureEndTb = get_time_tb(); - - /* воспроизведение: финальный PCM из сервисного слоя (jitter+stretch+тоны) */ - int n = call_audio_pull_pcm(m_callId.load(), out16, (int)frames); - if (n < (int)frames) - std::memset(out16 + n, 0, (size_t)(frames - n) * sizeof(int16_t)); - uint64_t endTb = get_time_tb(); - uint64_t captureTb = captureEndTb - captureStartTb; - uint64_t playbackTb = endTb - captureEndTb; - m_dbg.callbacks++; - m_dbg.captureFrames += in16 ? frames : 0; - m_dbg.playbackFrames += out16 ? frames : 0; - m_dbg.maxIdleTb = std::max(m_dbg.maxIdleTb, idleTb); - m_dbg.maxCaptureTb = std::max(m_dbg.maxCaptureTb, captureTb); - m_dbg.maxPlaybackTb = std::max(m_dbg.maxPlaybackTb, playbackTb); - if (idleTb > 500 || endTb - startTb > 200) { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: stall call=%016llx idle=%.1fms work=%.1fms capture=%.1fms playback=%.1fms frames=%u", - (unsigned long long)m_callId.load(), idleTb / 10.0, (endTb - startTb) / 10.0, - captureTb / 10.0, playbackTb / 10.0, frames); - } - uint64_t elapsedTb = endTb - m_dbg.lastReportTb; - if (elapsedTb >= 10000) { - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, - "CallAudio: interval=%.1fms cb=%.1f/s capture=%.0f/s playback=%.0f/s max_idle=%.1fms max_capture=%.1fms max_playback=%.1fms", - elapsedTb / 10.0, m_dbg.callbacks * 10000.0 / elapsedTb, - m_dbg.captureFrames * 10000.0 / elapsedTb, m_dbg.playbackFrames * 10000.0 / elapsedTb, - m_dbg.maxIdleTb / 10.0, m_dbg.maxCaptureTb / 10.0, m_dbg.maxPlaybackTb / 10.0); - m_dbg.callbacks = 0; - m_dbg.captureFrames = 0; - m_dbg.playbackFrames = 0; - m_dbg.maxIdleTb = 0; - m_dbg.maxCaptureTb = 0; - m_dbg.maxPlaybackTb = 0; - m_dbg.lastReportTb = endTb; + if (out) { + auto* pcm = static_cast(out); + int n = call_audio_pull_pcm(m_callId.load(), pcm, (int)frames); + if (n < (int)frames) std::memset(pcm + n, 0, (frames - n) * sizeof(*pcm)); } - m_dbg.lastEndTb = get_time_tb(); } diff --git a/tools/chatgui/src/call_audio_engine.h b/tools/chatgui/src/call_audio_engine.h index 6236b9e5..05dc89c7 100644 --- a/tools/chatgui/src/call_audio_engine.h +++ b/tools/chatgui/src/call_audio_engine.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include "audio_device.h" struct ma_device; @@ -12,8 +14,7 @@ struct ma_device; * слое src/call/call_audio.c; сюда приходит/отсюда уходит финальный PCM. * * Потоки: - * - аудио-поток (miniaudio callback): capture → call_audio_feed_pcm, - * call_audio_pull_pcm → playback; + * - независимые callbacks capture → call_audio_feed_pcm и call_audio_pull_pcm → playback; * - GUI-поток: start()/beginEnd()/stop() по ACCEPTED/ENDED. */ class CallAudioEngine : public QObject { Q_OBJECT @@ -36,40 +37,32 @@ public: void setMuted(bool muted); bool isMuted() const { return m_muted.load(); } - /* Устройство вывода звонка (-1 = default). Смена на лету пересоздаёт ma_device. */ - void setPlaybackDeviceIndex(int index); - int playbackDeviceIndex() const { return m_playbackDeviceIndex; } + /* Backend ID (пустой = default). Смена пересоздаёт только соответствующее устройство. */ + void setPlaybackDevice(const QString& id); + QString playbackDevice() const { return m_playbackId; } + void setCaptureDevice(const QString& id); /* аудио-поток: miniaudio data callback (захват + воспроизведение) */ void dataCallback(const void* in, void* out, unsigned int frames); private: - CallAudioEngine() = default; - void teardownDevice(); - int initDevice(); + CallAudioEngine(); + bool openDevice(bool playback); + void recoverDevices(); + void pauseDevices(); std::atomic m_active{false}; std::atomic m_callId{0}; std::atomic m_muted{false}; - int m_playbackDeviceIndex = -1; /* только GUI-поток */ + QString m_playbackId; /* только GUI-поток */ - ma_device* m_device = nullptr; - bool m_deviceStarted = false; + QString m_captureId; + AudioDevice m_capture, m_playback; + QTimer m_recoveryTimer; + uint64_t m_generation = 0; + bool m_ending = false; std::vector m_captureBuf; /* 960 samples, аудио-поток */ int m_capturePos = 0; - - /* Только аудио-поток; сброс до ma_device_start. Время в 0.1мс. */ - struct { - unsigned int frames = 0; - uint32_t callbacks = 0; - uint64_t captureFrames = 0; - uint64_t playbackFrames = 0; - uint64_t lastReportTb = 0; - uint64_t lastEndTb = 0; - uint64_t maxIdleTb = 0; - uint64_t maxCaptureTb = 0; - uint64_t maxPlaybackTb = 0; - } m_dbg; }; diff --git a/tools/chatgui/src/callwindow.cpp b/tools/chatgui/src/callwindow.cpp index 28e7d2cd..8eaa770a 100644 --- a/tools/chatgui/src/callwindow.cpp +++ b/tools/chatgui/src/callwindow.cpp @@ -279,16 +279,16 @@ void CallWindow::setState(State s) { void CallWindow::populateDeviceCombo() { m_deviceCombo->blockSignals(true); m_deviceCombo->clear(); - m_deviceCombo->addItem(QStringLiteral("Default (auto)"), -1); + m_deviceCombo->addItem(QStringLiteral("Default (auto)"), QString()); const auto devs = SoundManager::enumeratePlaybackDevices(); for (int i = 0; i < devs.size(); i++) { QString label = devs[i].name; if (devs[i].isDefault) label += QStringLiteral(" (default)"); - m_deviceCombo->addItem(label, i); + m_deviceCombo->addItem(label, devs[i].id); } - int cur = CallAudioEngine::instance()->playbackDeviceIndex(); + QString cur = CallAudioEngine::instance()->playbackDevice(); for (int i = 0; i < m_deviceCombo->count(); i++) { - if (m_deviceCombo->itemData(i).toInt() == cur) { m_deviceCombo->setCurrentIndex(i); break; } + if (m_deviceCombo->itemData(i).toString() == cur) { m_deviceCombo->setCurrentIndex(i); break; } } m_deviceCombo->blockSignals(false); } @@ -332,8 +332,8 @@ void CallWindow::onMuteToggled(bool checked) { } void CallWindow::onDeviceChanged(int index) { - int devIdx = m_deviceCombo->itemData(index).toInt(); - CallAudioEngine::instance()->setPlaybackDeviceIndex(devIdx); + QString id = m_deviceCombo->itemData(index).toString(); + CallAudioEngine::instance()->setPlaybackDevice(id); } void CallWindow::closeEvent(QCloseEvent* event) { diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index c3a1c7e0..4cafbf81 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -19,6 +19,7 @@ #include "voicemessageencoder.h" #include "callwindow.h" #include "call_audio_engine.h" +#include "radio_audio_engine.h" #include "ptt_hotkey_manager.h" extern "C" { @@ -177,7 +178,8 @@ MainWindow::~MainWindow() { gui_bridge_set_channel_peers_online_cb(nullptr); gui_bridge_set_db_ready_cb(nullptr); gui_bridge_set_attachment_downloaded_cb(nullptr); - QCoreApplication::processEvents(QEventLoop::AllEvents); + CallAudioEngine::instance()->stop(); + RadioAudioEngine::instance()->stop(); if (m_recorder) { m_recorder->shutdown(); m_recorder = nullptr; } SoundManager::instance()->shutdown(); DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: ~MainWindow — saving channel state"); @@ -335,6 +337,7 @@ void MainWindow::setupSoundFromConfig() { if (!connectFile.isEmpty()) sm->loadSound("connect", resolvePath(connectFile)); if (!disconnectFile.isEmpty()) sm->loadSound("disconnect", resolvePath(disconnectFile)); if (m_db && m_db->isOpen()) { + sm->setDevices(m_db->getUiState("audio_output_id"), m_db->getUiState("audio_input_id")); sm->setEnabled(m_db->getUiStateInt("sound_enabled", 1)); sm->setVolume(m_db->getUiStateInt("sound_volume", 80) / 100.0f); QString se[] = {"msg_incoming", "msg_outgoing", "connect", "disconnect", @@ -349,12 +352,11 @@ void MainWindow::setupSoundFromConfig() { if (m_recorder) { m_recorder->init(); if (m_db && m_db->isOpen()) { - m_recorder->setCaptureDeviceIndex(m_db->getUiStateInt("audio_input_device", -1)); VoiceEncoder::setPreset(m_db->getUiStateInt("opus_codec_preset", 1)); int compEnabled = m_db->getUiStateInt("compressor_enabled", 0); m_recorder->setCompressorEnabled(compEnabled != 0); - if (m_recorder->compressor()) { + { audio_compressor_config_t compCfg = {0}; compCfg.sample_rate = 48000; compCfg.channels = 1; @@ -363,11 +365,11 @@ void MainWindow::setupSoundFromConfig() { compCfg.lookahead_ms = 100; compCfg.rise_rate_per_sec = (float)m_db->getUiStateInt("compressor_rise_rate", 10); compCfg.target_level = 1.0f; - audio_compressor_configure(m_recorder->compressor(), &compCfg); + m_recorder->configureCompressor(compCfg); } } - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "AudioRecorder initialized (captureDevice=%d, opusPreset=%d, compressor=%d)", - m_recorder->captureDeviceIndex(), VoiceEncoder::preset(), + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "AudioRecorder initialized (opusPreset=%d, compressor=%d)", + VoiceEncoder::preset(), m_recorder->isCompressorEnabled() ? 1 : 0); } } @@ -598,8 +600,7 @@ void MainWindow::onCallAccepted(quint64 callId) { SoundManager::instance()->stopLoop(); CallWindow* w = m_activeCalls.value(callId, nullptr); if (w) w->setState(CallWindow::Active); - if (m_db) - CallAudioEngine::instance()->setPlaybackDeviceIndex(m_db->getUiStateInt("audio_output_device", -1)); + if (!s_mainWindow) return; CallAudioEngine::instance()->start(callId); } diff --git a/tools/chatgui/src/radio_audio_engine.cpp b/tools/chatgui/src/radio_audio_engine.cpp index 2bcbe172..3d608bd7 100644 --- a/tools/chatgui/src/radio_audio_engine.cpp +++ b/tools/chatgui/src/radio_audio_engine.cpp @@ -1,5 +1,6 @@ #include "radio_audio_engine.h" #include "sound_manager.h" +#include "../transport/audio_diagnostics.h" #include "../transport/gui_bridge.h" #include "miniaudio.h" @@ -8,162 +9,140 @@ extern "C" { #include "radio/radio_audio.h" #include "chat/chat_setting.h" #include "../../lib/debug_config.h" -#include "../../lib/u_async.h" } #include +#include -static RadioAudioEngine* g_radioAudioEngine = nullptr; - -static void radioAudioCaptureCallback(ma_device* dev, void* out, const void* in, ma_uint32 frames) { - (void)out; - RadioAudioEngine* e = (RadioAudioEngine*)dev->pUserData; - if (e) e->captureCallback(in, frames); -} - -static void radioAudioPlaybackCallback(ma_device* dev, void* out, const void* in, ma_uint32 frames) { - (void)in; - RadioAudioEngine* e = (RadioAudioEngine*)dev->pUserData; - if (e) e->playbackCallback(out, frames); +RadioAudioEngine* RadioAudioEngine::instance() { + static auto* engine = new RadioAudioEngine(); + return engine; } -RadioAudioEngine* RadioAudioEngine::instance() { - if (!g_radioAudioEngine) g_radioAudioEngine = new RadioAudioEngine(); - return g_radioAudioEngine; +RadioAudioEngine::RadioAudioEngine() { + m_captureId = SoundManager::instance()->captureDevice(); + m_playbackId = SoundManager::instance()->playbackDevice(); + connect(SoundManager::instance(), &SoundManager::devicesChanged, this, [this]() { + auto* manager = SoundManager::instance(); + if (m_captureId != manager->captureDevice()) { + m_captureId = manager->captureDevice(); + m_capture.close(); if (m_active) radio_audio_capture_stop(); m_capture.retryNow(); + } + if (m_playbackId != manager->playbackDevice()) { + m_playbackId = manager->playbackDevice(); + m_playback.close(); if (m_active) radio_audio_reset_playback(m_groupId.load()); m_playback.retryNow(); + } + recoverDevices(); + }); + m_recoveryTimer.setInterval(200); + connect(&m_recoveryTimer, &QTimer::timeout, this, &RadioAudioEngine::recoverDevices); + connect(SoundManager::instance(), &SoundManager::contextAboutToReset, this, &RadioAudioEngine::pauseDevices); + connect(SoundManager::instance(), &SoundManager::contextRestored, this, [this]() { + m_capture.retryNow(); m_playback.retryNow(); + recoverDevices(); + }); } int RadioAudioEngine::start(quint64 groupId) { - if (m_active) { - if (m_groupId == groupId) return 0; - stop(); - } - m_groupId = groupId; - - struct UTUN_INSTANCE* inst = gui_bridge_get_inst(); - if (!inst) { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "RadioAudio: no instance, cannot start"); + if (m_active && m_groupId == groupId) return 0; + stop(); + auto* inst = gui_bridge_get_inst(); + if (!inst || radio_audio_start(groupId, 0) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: cannot start group=%016llx", (unsigned long long)groupId); return -1; } - if (radio_audio_init(inst) != 0) { - DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: radio_audio_init failed"); - return -1; - } - - /* желаемые каналы захвата из настройки; initDevice() при недоступности откатывает на моно */ - m_captureChannels = chat_setting_get_int(inst, "radio_capture_stereo", 0) ? 2 : 1; - - if (initDevice() != 0) { - radio_audio_stop(); - return -1; - } - - if (radio_audio_start(groupId, m_captureChannels) != 0) { - DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: radio_audio_start failed grp=%016llx", (unsigned long long)groupId); - teardownDevice(); - radio_audio_stop(); - return -1; - } - - /* VAD авто-PTT: в этом режиме захват фидится постоянно, решение «говорить» — в сервисном слое. */ - m_vadMode = radio_audio_vad_mode() ? true : false; - if (m_vadMode) - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: VAD auto-PTT enabled grp=%016llx", (unsigned long long)groupId); - - m_captureBuf.assign((size_t)RADIO_AUDIO_FRAME_SAMPLES * m_captureChannels, 0); - m_capturePos = 0; - + m_groupId = groupId; + m_wantStereo = chat_setting_get_int(inst, "radio_capture_stereo", 0) != 0; m_active = true; - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: started grp=%016llx captureCh=%d", (unsigned long long)groupId, m_captureChannels); + m_capture.retryNow(); m_playback.retryNow(); + recoverDevices(); + m_recoveryTimer.start(); + DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: listening group=%016llx", (unsigned long long)groupId); return 0; } -int RadioAudioEngine::initDevice() { - ma_context* ctx = SoundManager::instance()->context(); - if (!ctx) { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "RadioAudio: no audio context"); - return -1; - } - - /* ── capture-устройство (стерео → откат на моно → дефолтное устройство) ── */ - const int wantCh = m_captureChannels; - int capCh = 0; - for (int useDefault = 0; useDefault < 2 && capCh == 0; useDefault++) { - for (int ch = wantCh; ch >= 1; ch--) { - ma_device* dev = new ma_device; - std::memset(dev, 0, sizeof(*dev)); - ma_device_config config = ma_device_config_init(ma_device_type_capture); - config.capture.format = ma_format_s16; - config.capture.channels = ch; - config.sampleRate = RADIO_AUDIO_SAMPLE_RATE; - config.periodSizeInFrames = 480; - config.dataCallback = radioAudioCaptureCallback; - config.pUserData = this; - if (useDefault) config.capture.pDeviceID = nullptr; - - if (ma_device_init(ctx, &config, dev) != MA_SUCCESS) { delete dev; continue; } - if (ma_device_start(dev) != MA_SUCCESS) { ma_device_uninit(dev); delete dev; continue; } - m_captureDevice = dev; - m_captureStarted = true; - capCh = ch; - if (ch != wantCh) - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "RadioAudio: stereo capture unavailable — fallback to mono"); - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: capture started ch=%d cap='%s'", - ch, dev->capture.name ? dev->capture.name : "?"); - break; +void RadioAudioEngine::openCapture() { + auto* context = SoundManager::instance()->context(); + if (!context) return; + for (int channels = m_wantStereo ? 2 : 1; channels >= 1; --channels) { + ma_device_config config = ma_device_config_init(ma_device_type_capture); + config.capture.format = ma_format_s16; + config.capture.channels = channels; + config.sampleRate = RADIO_AUDIO_SAMPLE_RATE; + auto callback = [this](const void* in, void*, unsigned int frames) { captureCallback(in, frames); }; + ma_device_id id; + bool selected = SoundManager::deviceId(m_captureId, &id); + for (int attempt = 0; attempt < (selected ? 2 : 1); ++attempt) { + config.capture.pDeviceID = selected && attempt == 0 ? &id : nullptr; + if (attempt) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "RadioAudio: selected input unavailable, trying default ch=%d", channels); + if (!m_capture.init(context, config, "radio-capture", m_groupId.load(), callback)) continue; + m_captureChannels = channels; + m_captureBuf.assign(RADIO_AUDIO_FRAME_SAMPLES * channels, 0); + m_capturePos = 0; + auto* inst = gui_bridge_get_inst(); + int vad = chat_setting_get_int(inst, "radio_vad_enabled", 0); + if (radio_audio_capture_start(m_groupId.load(), channels, vad) != 0) { + m_capture.close(); m_capture.retryLater(); return; + } + m_vadMode = radio_audio_vad_mode() != 0; + if (!m_capture.start()) { radio_audio_capture_stop(); continue; } + if (m_talking) radio_audio_talk_begin(m_groupId.load()); + if (channels == 1 && m_wantStereo) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "RadioAudio: stereo input unavailable, using mono"); + return; } } - if (capCh == 0) { - DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: capture device init failed"); - teardownDevice(); - return -1; - } - m_captureChannels = capCh; +} - /* ── playback-устройство (всегда стерео) ── */ - m_playbackDevice = new ma_device; - std::memset(m_playbackDevice, 0, sizeof(*m_playbackDevice)); - { - ma_device_config config = ma_device_config_init(ma_device_type_playback); - config.playback.format = ma_format_s16; - config.playback.channels = 2; - config.sampleRate = RADIO_AUDIO_SAMPLE_RATE; - config.periodSizeInFrames = 480; - config.dataCallback = radioAudioPlaybackCallback; - config.pUserData = this; +void RadioAudioEngine::openPlayback() { + auto* context = SoundManager::instance()->context(); + if (!context) return; + ma_device_config config = ma_device_config_init(ma_device_type_playback); + config.playback.format = ma_format_s16; + config.playback.channels = 2; + config.sampleRate = RADIO_AUDIO_SAMPLE_RATE; + auto callback = [this](const void*, void* out, unsigned int frames) { playbackCallback(out, frames); }; + ma_device_id id; + bool selected = SoundManager::deviceId(m_playbackId, &id); + if (selected) config.playback.pDeviceID = &id; + if (m_playback.init(context, config, "radio-playback", m_groupId.load(), callback) && m_playback.start()) return; + if (!selected) return; + DEBUG_WARN(DEBUG_CATEGORY_RADIO, "RadioAudio: selected output unavailable, trying default"); + config.playback.pDeviceID = nullptr; + if (m_playback.init(context, config, "radio-playback", m_groupId.load(), callback)) m_playback.start(); +} - if (ma_device_init(ctx, &config, m_playbackDevice) != MA_SUCCESS) { - DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback device init failed"); - teardownDevice(); - return -1; - } - if (ma_device_start(m_playbackDevice) != MA_SUCCESS) { - DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback device start failed"); - teardownDevice(); - return -1; - } - m_playbackStarted = true; +void RadioAudioEngine::recoverDevices() { + if (!m_active) return; + if (m_capture.needsRecovery()) { + m_capture.close(); radio_audio_capture_stop(); + m_capturePos = 0; m_capture.retryLater(); + } + if (m_playback.needsRecovery()) { + m_playback.close(); radio_audio_reset_playback(m_groupId.load()); m_playback.retryLater(); } + if (m_playback.retryReady()) openPlayback(); + if (m_capture.retryReady()) openCapture(); +} +void RadioAudioEngine::pauseDevices() { + m_capture.close(); m_playback.close(); m_capturePos = 0; - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback started ch=2 pb='%s'", - m_playbackDevice->playback.name ? m_playbackDevice->playback.name : "?"); - return 0; + if (m_active) { + radio_audio_capture_stop(); + radio_audio_reset_playback(m_groupId.load()); + } } void RadioAudioEngine::stop() { - if (!m_active && !m_captureDevice && !m_playbackDevice && !m_captureStarted && !m_playbackStarted) return; - quint64 gid = m_groupId.load(); + m_recoveryTimer.stop(); + m_capture.close(); m_playback.close(); + uint64_t group = m_groupId.exchange(0); + bool talking = m_talking.exchange(false); m_buttonTalking = m_hotkeyTalking = false; - bool wasTalking = m_talking.exchange(false); - if (wasTalking) radio_audio_talk_end(gid); /* честный FIN: пиры дренируют звук, а не ждут таймаут */ m_active = false; - - teardownDevice(); - radio_audio_stop(); - - m_groupId = 0; - if (wasTalking) emit talkingChanged(false); - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: stopped"); + if (group) radio_audio_stop(); + if (talking) emit talkingChanged(false); + if (group) DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: stopped group=%016llx", (unsigned long long)group); } void RadioAudioEngine::talkBegin() { @@ -192,21 +171,6 @@ void RadioAudioEngine::updateTalking() { emit talkingChanged(talking); } -void RadioAudioEngine::teardownDevice() { - if (m_captureStarted && m_captureDevice) { - ma_device_stop(m_captureDevice); - ma_device_uninit(m_captureDevice); - m_captureStarted = false; - } - if (m_captureDevice) { delete m_captureDevice; m_captureDevice = nullptr; } - if (m_playbackStarted && m_playbackDevice) { - ma_device_stop(m_playbackDevice); - ma_device_uninit(m_playbackDevice); - m_playbackStarted = false; - } - if (m_playbackDevice) { delete m_playbackDevice; m_playbackDevice = nullptr; } -} - void RadioAudioEngine::captureCallback(const void* in, unsigned int frames) { const int16_t* in16 = (const int16_t*)in; if (!in16) return; @@ -219,7 +183,8 @@ void RadioAudioEngine::captureCallback(const void* in, unsigned int frames) { for (int i = 0; i < totalIn; i++) { m_captureBuf[m_capturePos++] = in16[i]; if (m_capturePos >= (int)m_captureBuf.size()) { - radio_audio_feed_pcm(m_groupId.load(), m_captureBuf.data(), (int)m_captureBuf.size()); + int result = radio_audio_feed_pcm(m_groupId.load(), m_captureBuf.data(), (int)m_captureBuf.size()); + if (result != 0) audio_diag_event(m_capture.device(), AUDIO_DIAG_ERROR, AUDIO_DIAG_FEED, 1, result); m_capturePos = 0; } } @@ -228,36 +193,12 @@ void RadioAudioEngine::captureCallback(const void* in, unsigned int frames) { void RadioAudioEngine::playbackCallback(void* out, unsigned int frames) { int16_t* out16 = (int16_t*)out; - /* диагностика (DEBUG_CATEGORY_DEBUG): cadence playback-коллбэка */ - uint64_t now = get_time_tb(); - if (m_dbgFrames == 0) { - m_dbgFrames = frames; - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback frames=%u", frames); - } - m_dbgCbCount++; - if (m_dbgPrevCbTb != 0) { - uint64_t gap = now - m_dbgPrevCbTb; - if (gap > m_dbgGapMax) m_dbgGapMax = gap; - } - m_dbgPrevCbTb = now; - if (m_dbgLastTb == 0) { - m_dbgLastTb = now; - } else if (now - m_dbgLastTb >= 10000) { - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "RadioAudio: pb cb=%u/s frames=%u gapMax=%.1fms bodyMax=%.1fms", - m_dbgCbCount, frames, (double)m_dbgGapMax / 10.0, (double)m_dbgBodyMax / 10.0); - m_dbgCbCount = 0; - m_dbgLastTb = now; - m_dbgGapMax = 0; - m_dbgBodyMax = 0; - } - /* воспроизведение: микшированный PCM из сервисного слоя (всегда стерео) */ - int total = (int)frames * 2; - int n = radio_audio_pull_pcm(m_groupId.load(), out16, total); - if (n < total) - std::memset(out16 + n, 0, (size_t)(total - n) * sizeof(int16_t)); - - uint64_t end = get_time_tb(); - uint64_t body = end - now; - if (body > m_dbgBodyMax) m_dbgBodyMax = body; + unsigned int offset = 0; + while (offset < frames) { + unsigned int count = std::min(frames - offset, (unsigned int)RADIO_AUDIO_FRAME_SAMPLES); + int n = radio_audio_pull_pcm(m_groupId.load(), out16 + offset * 2, count * 2); + if (n < (int)count * 2) std::memset(out16 + offset * 2 + n, 0, (count * 2 - n) * sizeof(*out16)); + offset += count; + } } diff --git a/tools/chatgui/src/radio_audio_engine.h b/tools/chatgui/src/radio_audio_engine.h index 06309f17..d3ddcb40 100644 --- a/tools/chatgui/src/radio_audio_engine.h +++ b/tools/chatgui/src/radio_audio_engine.h @@ -4,6 +4,8 @@ #include #include #include +#include "audio_device.h" +#include struct ma_device; @@ -43,31 +45,25 @@ signals: void talkingChanged(bool talking); // GUI-поток, суммарное состояние кнопки и хоткея private: - RadioAudioEngine() = default; + RadioAudioEngine(); void updateTalking(); bool m_buttonTalking = false, m_hotkeyTalking = false; // GUI-поток - void teardownDevice(); - int initDevice(); + void pauseDevices(); + void recoverDevices(); + void openCapture(); + void openPlayback(); std::atomic m_active{false}; std::atomic m_talking{false}; std::atomic m_vadMode{false}; /* VAD авто-PTT: фидим PCM постоянно */ std::atomic m_groupId{0}; - ma_device* m_captureDevice = nullptr; - ma_device* m_playbackDevice = nullptr; - bool m_captureStarted = false; - bool m_playbackStarted = false; + AudioDevice m_capture, m_playback; + QTimer m_recoveryTimer; + bool m_wantStereo = false; + QString m_captureId, m_playbackId; std::vector m_captureBuf; /* FRAME_SAMPLES * m_captureChannels, аудио-поток */ int m_capturePos = 0; int m_captureChannels = 1; /* 1=моно, 2=стерео — по настройке/возможности */ - - /* диагностика (DEBUG_CATEGORY_DEBUG): фактический cadence playback-коллбэка */ - unsigned int m_dbgFrames = 0; - uint32_t m_dbgCbCount = 0; - uint64_t m_dbgLastTb = 0; /* момент предыдущего сброса статистики (для cb/s) */ - uint64_t m_dbgPrevCbTb = 0; /* момент входа предыдущего коллбэка */ - uint64_t m_dbgGapMax = 0; /* макс. gap между коллбэками за окно (tb 0.1мс) */ - uint64_t m_dbgBodyMax = 0; /* макс. длительность тела коллбэка за окно (tb 0.1мс) */ }; diff --git a/tools/chatgui/src/sound_manager.cpp b/tools/chatgui/src/sound_manager.cpp index 1c2aca2e..7f495f68 100644 --- a/tools/chatgui/src/sound_manager.cpp +++ b/tools/chatgui/src/sound_manager.cpp @@ -1,8 +1,11 @@ #include "miniaudio.h" #include "sound_manager.h" +#include "../transport/audio_diagnostics.h" #include #include +#include +#include #include "../../lib/debug_config.h" SoundManager* SoundManager::instance() { @@ -15,30 +18,100 @@ SoundManager::~SoundManager() { shutdown(); } +extern "C" int audio_context_failed(ma_context* context); + +SoundManager::SoundManager() { + m_clock.start(); + m_recoveryTimer.setInterval(200); + connect(&m_recoveryTimer, &QTimer::timeout, this, &SoundManager::recoverAudio); +} + bool SoundManager::init() { if (m_initialized) return true; - - ma_engine* engine = new ma_engine; - ma_result result = ma_engine_init(NULL, engine); + audio_diag_start(); + ma_engine_config config = ma_engine_config_init(); + config.noDevice = MA_TRUE; /* Микшер не владеет контекстом/устройством; его PCM и звуки переживают recovery. */ + config.channels = 2; + config.sampleRate = 48000; + auto* engine = new ma_engine{}; + ma_result result = ma_engine_init(&config, engine); if (result != MA_SUCCESS) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "SoundManager: ma_engine_init failed (%d)", result); delete engine; + audio_diag_stop(); return false; } m_engine = engine; - m_context = ma_device_get_context(ma_engine_get_device(engine)); m_initialized = true; + m_contextRetryAt = 0; + recoverAudio(); + m_recoveryTimer.start(); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "SoundManager: mixer initialized independently of output"); + return true; +} - ma_device* dev = ma_engine_get_device(engine); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, - "SoundManager: initialized engine=%p ctx=%p dev='%s' fmt=%d ch=%u sr=%u internalSr=%u", - (void*)engine, (void*)m_context, - dev ? dev->playback.name : "?", dev ? (int)dev->playback.format : -1, - dev ? dev->playback.channels : 0, dev ? dev->sampleRate : 0, - dev ? dev->playback.internalSampleRate : 0); +bool SoundManager::openContext() { + auto* context = new ma_context{}; + ma_result result = ma_context_init(m_backendSelected ? &m_backend : nullptr, m_backendSelected ? 1 : 0, nullptr, context); + if (result != MA_SUCCESS) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "SoundManager: context init failed result=%d retry=%dms", result, m_contextRetryDelay); + delete context; + m_contextRetryAt = m_clock.elapsed() + m_contextRetryDelay; + m_contextRetryDelay = std::min(m_contextRetryDelay * 2, 2000); + return false; + } + m_context = context; + m_backend = context->backend; + m_backendSelected = true; + m_contextRetryDelay = 250; + DEBUG_INFO(DEBUG_CATEGORY_CALL, "SoundManager: context ready backend=%s", ma_get_backend_name(m_backend)); + emit contextRestored(); return true; } +void SoundManager::openOutput() { + ma_device_config config = ma_device_config_init(ma_device_type_playback); + config.playback.format = ma_format_f32; + config.playback.channels = 2; + config.sampleRate = 48000; + ma_device_id id; + bool selected = deviceId(m_playbackId, &id); + if (selected) config.playback.pDeviceID = &id; + auto callback = [this](const void*, void* out, unsigned int frames) { + ma_engine_read_pcm_frames(m_engine, out, frames, nullptr); + }; + if (m_output.init(m_context, config, "sound-engine", 0, callback) && m_output.start()) return; + if (!selected) return; + DEBUG_WARN(DEBUG_CATEGORY_CALL, "SoundManager: selected output unavailable, trying default"); + config.playback.pDeviceID = nullptr; + if (m_output.init(m_context, config, "sound-engine", 0, callback)) m_output.start(); +} + +void SoundManager::recoverAudio() { + if (!m_initialized) return; + if (m_context && audio_context_failed(m_context)) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "SoundManager: context failed, closing dependent devices"); + emit contextAboutToReset(); + m_output.close(); + ma_context_uninit(m_context); delete m_context; m_context = nullptr; + m_contextRetryAt = m_clock.elapsed() + 250; + } + if (!m_context && m_clock.elapsed() >= m_contextRetryAt) openContext(); + if (!m_context) return; + if (m_output.needsRecovery()) { m_output.close(); m_output.retryLater(); } + if (m_output.retryReady()) openOutput(); +} + +void SoundManager::setDevices(const QString& output, const QString& input) { + if (output == m_playbackId && input == m_captureId) return; + bool outputChanged = output != m_playbackId; + m_playbackId = output; m_captureId = input; + DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: device selection changed output_default=%d input_default=%d", output.isEmpty(), input.isEmpty()); + if (outputChanged) { m_output.close(); m_output.retryNow(); } + emit devicesChanged(); + recoverAudio(); +} + void SoundManager::clearCurrentPcm() { if (m_currentPcmSound) { ma_sound_uninit(m_currentPcmSound); delete m_currentPcmSound; m_currentPcmSound = nullptr; } if (m_currentPcmBuffer) { @@ -51,7 +124,10 @@ void SoundManager::clearCurrentPcm() { void SoundManager::shutdown() { if (!m_initialized) return; - + m_recoveryTimer.stop(); + m_initialized = false; + emit contextAboutToReset(); + m_output.close(); clearCurrentPcm(); for (auto it = m_sounds.begin(); it != m_sounds.end(); ++it) { @@ -63,7 +139,8 @@ void SoundManager::shutdown() { DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: before ma_engine_uninit engine=%p", (void*)m_engine); if (m_engine) { ma_engine_uninit(m_engine); delete m_engine; m_engine = nullptr; } DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: after ma_engine_uninit"); - m_initialized = false; + if (m_context) { ma_context_uninit(m_context); delete m_context; m_context = nullptr; } + audio_diag_stop(); m_loopName.clear(); } @@ -279,6 +356,11 @@ static QString decodeDeviceName(const char* name) { return s; } +static QString deviceKey(ma_context* context, const ma_device_id& id) { + QByteArray bytes(reinterpret_cast(&id), sizeof(id)); + return QString::fromLatin1(ma_get_backend_name(context->backend)) + '/' + QString::fromLatin1(bytes.toBase64()); +} + QVector SoundManager::enumeratePlaybackDevices() { QVector result; ma_context* ctx = instance()->m_context; @@ -286,9 +368,10 @@ QVector SoundManager::enumeratePlaybackDevices() { ma_device_info* pDevices = nullptr; ma_uint32 count = 0; - if (ma_context_get_devices(ctx, &pDevices, &count, nullptr, nullptr) != MA_SUCCESS) return result; + ma_result status = ma_context_get_devices(ctx, &pDevices, &count, nullptr, nullptr); + if (status != MA_SUCCESS) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: output enumeration failed result=%d", status); return result; } for (ma_uint32 i = 0; i < count; i++) - result.append({decodeDeviceName(pDevices[i].name), pDevices[i].isDefault != MA_FALSE}); + result.append({decodeDeviceName(pDevices[i].name), pDevices[i].isDefault != MA_FALSE, deviceKey(ctx, pDevices[i].id)}); return result; } @@ -299,8 +382,37 @@ QVector SoundManager::enumerateCaptureDevices() { ma_device_info* pDevices = nullptr; ma_uint32 count = 0; - if (ma_context_get_devices(ctx, nullptr, nullptr, &pDevices, &count) != MA_SUCCESS) return result; + ma_result status = ma_context_get_devices(ctx, nullptr, nullptr, &pDevices, &count); + if (status != MA_SUCCESS) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: input enumeration failed result=%d", status); return result; } for (ma_uint32 i = 0; i < count; i++) - result.append({decodeDeviceName(pDevices[i].name), pDevices[i].isDefault != MA_FALSE}); + result.append({decodeDeviceName(pDevices[i].name), pDevices[i].isDefault != MA_FALSE, deviceKey(ctx, pDevices[i].id)}); return result; } + +bool SoundManager::deviceId(const QString& key, ma_device_id* id) { + if (key.isEmpty()) return false; + ma_context* context = instance()->context(); + if (!context) return false; + QString prefix = QString::fromLatin1(ma_get_backend_name(context->backend)) + '/'; + QByteArray bytes = QByteArray::fromBase64(key.mid(prefix.size()).toLatin1()); + if (!key.startsWith(prefix) || bytes.size() != sizeof(*id) || QString::fromLatin1(bytes.toBase64()) != key.mid(prefix.size())) { + DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: invalid or different-backend device ID, using default backend=%s", + ma_get_backend_name(context->backend)); + return false; + } + std::memcpy(id, bytes.constData(), sizeof(*id)); + /* Сохранённая строка должна иметь terminator внутри соответствующего поля backend. */ + bool valid = true; + switch (context->backend) { + case ma_backend_pulseaudio: valid = std::memchr(id->pulse, 0, sizeof(id->pulse)); break; + case ma_backend_alsa: valid = std::memchr(id->alsa, 0, sizeof(id->alsa)); break; + case ma_backend_coreaudio: valid = std::memchr(id->coreaudio, 0, sizeof(id->coreaudio)); break; + case ma_backend_sndio: valid = std::memchr(id->sndio, 0, sizeof(id->sndio)); break; + case ma_backend_audio4: valid = std::memchr(id->audio4, 0, sizeof(id->audio4)); break; + case ma_backend_oss: valid = std::memchr(id->oss, 0, sizeof(id->oss)); break; + case ma_backend_wasapi: valid = std::find(std::begin(id->wasapi), std::end(id->wasapi), 0) != std::end(id->wasapi); break; + default: break; + } + if (!valid) DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: unterminated device ID, using default"); + return valid; +} diff --git a/tools/chatgui/src/sound_manager.h b/tools/chatgui/src/sound_manager.h index a05db839..56e724bc 100644 --- a/tools/chatgui/src/sound_manager.h +++ b/tools/chatgui/src/sound_manager.h @@ -7,6 +7,8 @@ #include #include #include +#include +#include "audio_device.h" struct ma_engine; struct ma_context; @@ -40,6 +42,10 @@ public: void stopLoop(); ma_context* context(); + static bool deviceId(const QString& key, ma_device_id* id); + void setDevices(const QString& output, const QString& input); + QString playbackDevice() const { return m_playbackId; } + QString captureDevice() const { return m_captureId; } /* owner удерживает готовый PCM без копии в GUI; без owner короткие буферы копируются. */ void playRawPcm(const int16_t* pcm, int sampleCount, int sampleRate, int channels, @@ -55,15 +61,24 @@ public: struct DeviceInfo { QString name; bool isDefault = false; + QString id; }; static QVector enumeratePlaybackDevices(); static QVector enumerateCaptureDevices(); +signals: + void contextAboutToReset(); // Все зависимые устройства закрываются синхронно перед uninit контекста. + void contextRestored(); + void devicesChanged(); + private: - SoundManager() = default; + SoundManager(); ~SoundManager(); void clearCurrentPcm(); + void recoverAudio(); + bool openContext(); + void openOutput(); struct LoadedSound { QByteArray mp3Data; @@ -73,6 +88,14 @@ private: ma_engine* m_engine = nullptr; ma_context* m_context = nullptr; + AudioDevice m_output; + QTimer m_recoveryTimer; + QElapsedTimer m_clock; + qint64 m_contextRetryAt = 0; + int m_contextRetryDelay = 250; + QString m_playbackId, m_captureId; + ma_backend m_backend = ma_backend_null; + bool m_backendSelected = false; ma_sound* m_currentPcmSound = nullptr; void* m_currentPcmBuffer = nullptr; int m_currentPcmSampleRate = 0; diff --git a/tools/chatgui/tests/run_pulse_recovery.py b/tools/chatgui/tests/run_pulse_recovery.py new file mode 100644 index 00000000..7de808ef --- /dev/null +++ b/tools/chatgui/tests/run_pulse_recovery.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Изолированный PipeWire/PulseAudio с виртуальным sink; системный сервер не затрагивается.""" +import os +from pathlib import Path +import shutil +import signal +import subprocess +import sys +import tempfile +import time + + +def wait_for(predicate, seconds=10): + deadline = time.monotonic() + seconds + while time.monotonic() < deadline: + if predicate(): + return + time.sleep(0.02) + raise RuntimeError("private audio server/test did not reach expected state") + + +def main(): + for executable in ("pipewire", "pipewire-pulse", "wireplumber", "dbus-run-session", "pactl"): + if not shutil.which(executable): + raise RuntimeError(f"required executable missing: {executable}") + binary = Path(sys.argv[1] if len(sys.argv) > 1 else "tools/chatgui/build/test_audio_recovery").resolve() + with tempfile.TemporaryDirectory(prefix="utun-pulse-") as root: + directory = Path(root) + config = directory / "config/wireplumber/wireplumber.conf.d" + config.mkdir(parents=True) + (config / "test.conf").write_text('''wireplumber.profiles = { + main = { + monitor.alsa = disabled + monitor.alsa-midi = disabled + monitor.bluez = disabled + monitor.bluez-midi = disabled + monitor.v4l2 = disabled + monitor.libcamera = disabled + } + }''') + env = os.environ.copy() + env.update(XDG_RUNTIME_DIR=root, PIPEWIRE_RUNTIME_DIR=root, XDG_CONFIG_HOME=str(directory / "config"), + XDG_STATE_HOME=str(directory / "state"), PIPEWIRE_REMOTE="pipewire-0", PULSE_SERVER=f"unix:{root}/pulse/native") + processes, logs = [], [] + + def launch(command, label): + log = (directory / f"{label}.log").open("w+") + logs.append(log) + process = subprocess.Popen(command, env=env, stdout=log, stderr=subprocess.STDOUT, start_new_session=True) + processes.append(process) + return process + + def pulse_ready(): + return subprocess.run(["pactl", "info"], env=env, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, timeout=2).returncode == 0 + + try: + launch(["pipewire"], "pipewire") + wait_for(lambda: (directory / "pipewire-0").exists()) + launch(["dbus-run-session", "--", "wireplumber"], "wireplumber") + pulse = launch(["pipewire-pulse"], "pulse-before") + wait_for(pulse_ready) + # module-always-sink создаёт виртуальный dummy sink и его monitor source. + test = launch([str(binary), "--pulse-integration", root], "client") + wait_for(lambda: (directory / "ready").exists() or test.poll() is not None) + if test.poll() is not None: + raise RuntimeError("client failed before server restart") + pulse.terminate(); pulse.wait(timeout=5) + wait_for(lambda: (directory / "closed").exists() or test.poll() is not None) + launch(["pipewire-pulse"], "pulse-after") + wait_for(pulse_ready) + if test.wait(timeout=15) != 0: + raise RuntimeError("client recovery failed") + logs[3].flush(); logs[3].seek(0) + print(logs[3].read()) + print("private PulseAudio server restart: PASS") + except Exception: + for log in logs: + log.flush(); log.seek(0) + print(f"--- {Path(log.name).name} ---\n{log.read()}", file=sys.stderr) + raise + finally: + for process in reversed(processes): + if process.poll() is None: + os.killpg(process.pid, signal.SIGTERM) + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + os.killpg(process.pid, signal.SIGKILL); process.wait() + for log in logs: + log.close() + + +if __name__ == "__main__": + main() diff --git a/tools/chatgui/tests/test_audio_diagnostics.cpp b/tools/chatgui/tests/test_audio_diagnostics.cpp new file mode 100644 index 00000000..753e96fe --- /dev/null +++ b/tools/chatgui/tests/test_audio_diagnostics.cpp @@ -0,0 +1,42 @@ +/* Проверяем настоящую очередь: конкуренция, переполнение, целостность и повторный запуск. */ +#include "../transport/audio_diagnostics.cpp" +#include +#include +#include + +int main() { + debug_config_init(); debug_enable_console(0); + constexpr int producers = 4, perProducer = 20000; + std::vector seen(producers * perProducer); + std::atomic done{0}; + for (uint64_t i = 0; i < kCapacity; ++i) queue[i].sequence.store(i); + running = true; + // Сначала полное кольцо: producer обязан вернуть управление, потеря учтена. + for (uint64_t i = 0; i < kCapacity + 1; ++i) audio_diag_event(nullptr, AUDIO_DIAG_HOLE, AUDIO_DIAG_READ, i, 7); + assert(lost.load() == 1); + Event e; + for (uint64_t i = 0; i < kCapacity; ++i) { + assert(take(e)); assert(e.value == int64_t(i) && e.detail == 7); + } + assert(!take(e)); lost = 0; + std::vector workers; + for (int p = 0; p < producers; ++p) workers.emplace_back([p, &done]() { + for (int i = 0; i < perProducer; ++i) + audio_diag_event(nullptr, AUDIO_DIAG_HOLE, AUDIO_DIAG_READ, p * perProducer + i, p); + ++done; + }); + uint64_t received = 0; + while (done.load() != producers || readPosition != writePosition.load()) { + if (!take(e)) { std::this_thread::yield(); continue; } + assert(e.value >= 0 && e.value < producers * perProducer); + assert(e.kind == AUDIO_DIAG_HOLE && e.stage == AUDIO_DIAG_READ && e.value / perProducer == e.detail); + assert(e.wall && e.cpu && e.tid && !seen[e.value]); + seen[e.value] = true; ++received; + } + for (auto& worker : workers) worker.join(); + assert(received + lost.load() == producers * perProducer); + running = false; + // Lifecycle не оставляет joinable-поток и очищает очередь между сеансами. + for (int i = 0; i < 2; ++i) { audio_diag_start(); audio_diag_event(nullptr, AUDIO_DIAG_OPEN, -1, 1, 48000); audio_diag_stop(); } + puts("test_audio_diagnostics: bounded MPSC, concurrent integrity and restart PASS"); +} diff --git a/tools/chatgui/tests/test_audio_recovery.cpp b/tools/chatgui/tests/test_audio_recovery.cpp new file mode 100644 index 00000000..fe0f6326 --- /dev/null +++ b/tools/chatgui/tests/test_audio_recovery.cpp @@ -0,0 +1,222 @@ +/* Настоящие null devices и Qt timers; linker wrappers инъецируют только сбои lifecycle. */ +#include "../src/audio_device.h" +#include "../src/call_audio_engine.h" +#include "../src/radio_audio_engine.h" +#include "../src/audiorecorder.h" +#include "../src/sound_manager.h" +#include +#include +#include +#include +#include +#include +#include +extern "C" { +#include "utun_instance.h" +#include "call/call_audio.h" +#include "radio/radio_audio.h" +#include "media_async/attachment_send.h" +#include "u_async.h" +#include "debug_config.h" +} + +#define REQUIRE(condition) do { if (!(condition)) qFatal("audio recovery line %d: %s", __LINE__, #condition); } while (0) +static UTUN_INSTANCE instance{}; +static std::map live; +static std::vector devices; +static int starts, captureStarts, playbackStarts, contextStarts, failContexts; +static bool failStart, failContext, failDevice; +static bool pulseIntegration; + +extern "C" UTUN_INSTANCE* gui_bridge_get_inst() { return &instance; } +extern "C" ma_result __real_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context*); +extern "C" void __real_ma_context_uninit(ma_context*); +extern "C" ma_result __real_ma_device_init(ma_context*, const ma_device_config*, ma_device*); +extern "C" ma_result __real_ma_device_start(ma_device*); +extern "C" void __real_ma_device_uninit(ma_device*); +extern "C" int __real_audio_context_failed(ma_context*); + +extern "C" ma_result __wrap_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context* context) { + ++contextStarts; + if (failContexts > 0) { --failContexts; return MA_ERROR; } + ma_backend backend = pulseIntegration ? ma_backend_pulseaudio : ma_backend_null; + return __real_ma_context_init(&backend, 1, nullptr, context); +} +extern "C" void __wrap_ma_context_uninit(ma_context* context) { + REQUIRE(live[context] == 0); + __real_ma_context_uninit(context); +} +extern "C" ma_result __wrap_ma_device_init(ma_context* context, const ma_device_config* config, ma_device* device) { + if (failDevice) { failDevice = false; return MA_ERROR; } + ma_result result = __real_ma_device_init(context, config, device); + if (result == MA_SUCCESS) { ++live[context]; devices.push_back(device); } + return result; +} +extern "C" ma_result __wrap_ma_device_start(ma_device* device) { + ++starts; + if (device->type == ma_device_type_capture) { + ++captureStarts; + // Некоторые backends вызывают data callback ещё до возврата start. + std::vector input(1200 * device->capture.channels, 1000); + if (!pulseIntegration) device->onData(device, nullptr, input.data(), 1200); + } else ++playbackStarts; + if (failStart) { failStart = false; return MA_ERROR; } + return __real_ma_device_start(device); +} +extern "C" void __wrap_ma_device_uninit(ma_device* device) { + auto context = device->pContext; + __real_ma_device_uninit(device); + REQUIRE(live[context] > 0); + --live[context]; + devices.erase(std::find(devices.begin(), devices.end(), device)); +} +extern "C" int __wrap_audio_context_failed(ma_context* context) { + if (failContext) { failContext = false; return 1; } + return __real_audio_context_failed(context); +} + +static void pump(int milliseconds) { + QElapsedTimer timer; timer.start(); + while (timer.elapsed() < milliseconds) { QCoreApplication::processEvents(); QThread::msleep(1); } +} + +static void pulse_recovery(SoundManager* manager, const QString& control) { + REQUIRE(manager->context()->backend == ma_backend_pulseaudio); + AudioRecorder recorder; recorder.init(); recorder.startRecording(); + std::vector pcm(48000 * 20, 1000); + manager->playRawPcm(pcm.data(), (int)pcm.size(), 48000, 1); + for (int i = 0; i < 100 && recorder.durationMs() < 300; ++i) pump(50); + REQUIRE(recorder.durationMs() >= 300 && manager->currentPcmCursor() > 0); + int duration = recorder.durationMs(); + auto cursor = manager->currentPcmCursor(); + QFile ready(control + "/ready"); REQUIRE(ready.open(QIODevice::WriteOnly)); ready.close(); + for (int i = 0; i < 100 && manager->context(); ++i) pump(50); + REQUIRE(!manager->context()); + QFile closed(control + "/closed"); REQUIRE(closed.open(QIODevice::WriteOnly)); closed.close(); + for (int i = 0; i < 150 && (!manager->context() || recorder.durationMs() <= duration + 200); ++i) pump(50); + REQUIRE(manager->context() && recorder.durationMs() > duration + 200 && manager->currentPcmCursor() > cursor); + qInfo("PulseAudio progress: capture=%d -> %dms playback=%llu -> %llu frames", duration, recorder.durationMs(), + (unsigned long long)cursor, manager->currentPcmCursor()); + recorder.stopRecording(); manager->shutdown(); + REQUIRE(devices.empty()); + qInfo("real PulseAudio: capture/playback progress restored after server restart PASS"); +} + +int main(int argc, char** argv) { + QCoreApplication app(argc, argv); + pulseIntegration = argc == 3 && std::strcmp(argv[1], "--pulse-integration") == 0; + debug_config_init(); debug_set_level(pulseIntegration ? DEBUG_LEVEL_INFO : DEBUG_LEVEL_WARN); + auto* manager = SoundManager::instance(); + REQUIRE(manager->init() && manager->context()); + if (pulseIntegration) { pulse_recovery(manager, QString::fromLocal8Bit(argv[2])); return 0; } + ma_device_id id{}; + auto outputs = manager->enumeratePlaybackDevices(); + REQUIRE(!outputs.empty() && SoundManager::deviceId(outputs.front().id, &id)); + REQUIRE(!SoundManager::deviceId(outputs.front().id + '!', &id)); + { + AudioDevice device; + auto config = ma_device_config_init(ma_device_type_capture); + config.capture.format = ma_format_s16; config.capture.channels = 1; config.sampleRate = 48000; + REQUIRE(device.init(manager->context(), config, "test-partial-start", 0, [](const void*, void*, unsigned int) {})); + int count = live[manager->context()]; + failStart = true; + REQUIRE(!device.start() && !device.initialized()); + REQUIRE(live[manager->context()] == count - 1); + failDevice = true; + REQUIRE(!device.init(manager->context(), config, "test-init-failure", 0, [](const void*, void*, unsigned int) {})); + REQUIRE(!device.initialized() && live[manager->context()] == count - 1); + } + { + AudioRecorder first, second; + first.init(); second.init(); + auto inputs = manager->enumerateCaptureDevices(); + REQUIRE(!inputs.empty()); + first.setCaptureDevice(inputs.front().id); + int previous = captureStarts; + failStart = true; + first.startRecording(); second.startRecording(); + REQUIRE(captureStarts == previous + 3); // Выбранный input не стартовал: default запущен после полного uninit. + pump(60); + first.stopRecording(); second.stopRecording(); + attachment_send_req a{}, b{}; + REQUIRE(first.takeRecording(&a) && second.takeRecording(&b)); + REQUIRE(a.pcm_count >= 1200 && b.pcm_count >= 1200); + REQUIRE(a.pcm[0] == 1000 && b.pcm[0] == 1000); // pUserData маршрутизирует каждый callback своему recorder. + a.pcm_release(a.pcm_owner); b.pcm_release(b.pcm_owner); + } + instance.ua = uasync_create(); REQUIRE(instance.ua); + REQUIRE(call_audio_init(&instance) == 0 && radio_audio_init(&instance) == 0); + auto* call = CallAudioEngine::instance(); + REQUIRE(call->start(42) == 0); + pump(60); + ma_device* playback = nullptr; + for (auto* device : devices) + if (device->type == ma_device_type_playback && device->playback.format == ma_format_s16 && device->playback.channels == 1) + playback = device; + REQUIRE(playback); + int previousCapture = captureStarts, previousPlayback = playbackStarts; + REQUIRE(ma_device_stop(playback) == MA_SUCCESS); + pump(800); + REQUIRE(captureStarts == previousCapture && playbackStarts > previousPlayback); + REQUIRE(call->callId() == 42 && call->isActive()); + auto* radio = RadioAudioEngine::instance(); + REQUIRE(radio->start(7) == 0); + AudioRecorder recorder; recorder.init(); recorder.startRecording(); + std::vector notification(48000 * 10, 1000); + manager->playRawPcm(notification.data(), (int)notification.size(), 48000, 1); + pump(60); + auto cursor = manager->currentPcmCursor(); + REQUIRE(cursor > 0); + int previousContext = contextStarts; + failContext = true; failContexts = 2; + pump(1800); + REQUIRE(contextStarts >= previousContext + 3 && manager->context()); + REQUIRE(call->callId() == 42 && radio->groupId() == 7 && recorder.isRecording()); + REQUIRE(manager->currentPcmCursor() > cursor && manager->isCurrentPcmPlaying()); + call->stop(); radio->stop(); recorder.stopRecording(); + int previousStarts = starts; + pump(600); + REQUIRE(starts == previousStarts); // Отложенное recovery не воскрешает остановленную сессию. + REQUIRE(call->start(43) == 0); + call->beginEnd(43); + REQUIRE(call->start(43) == 0); + pump(600); + REQUIRE(call->isActive() && call->callId() == 43); // Старый end timer не закрывает новое поколение того же call ID. + call->stop(); + failContext = true; failContexts = 1; + recorder.startRecording(); + pump(220); + REQUIRE(!manager->context()); + recorder.stopRecording(); + previousCapture = captureStarts; + pump(1000); + REQUIRE(manager->context() && captureStarts == previousCapture); + + // Предел TX действует до помещения задачи в uasync; старое поколение не отправляется после stop/start. + uasync_poll(instance.ua, 0); + REQUIRE(call_audio_start(99) == 0); + int16_t silence[960]{}; + for (int i = 0; i < 100; ++i) call_audio_feed_pcm(99, silence, 960); + int pending = 0; + for (auto* task = instance.ua->posted_tasks_head; task; task = task->next) ++pending; + REQUIRE(pending == 8); + call_audio_stop(); + REQUIRE(call_audio_start(100) == 0); + for (int i = 0; i < 100; ++i) call_audio_feed_pcm(100, silence, 960); + pending = 0; + for (auto* task = instance.ua->posted_tasks_head; task; task = task->next) ++pending; + REQUIRE(pending == 8); // Новый звонок также учитывает ещё не освобождённые задачи старого. + call_audio_stop(); + uasync_poll(instance.ua, 0); + REQUIRE(!instance.ua->posted_tasks_head); + REQUIRE(radio_audio_start(8, 1) == 0); + radio_audio_talk_begin(8); radio_audio_talk_end(8); radio_audio_stop(); + REQUIRE(instance.ua->posted_tasks_head); + // Завершение ядра освобождает ещё не выполненные PTT-задачи вместе с аргументами. + call_audio_destroy(&instance); radio_audio_destroy(&instance); + uasync_destroy(instance.ua, 0); instance.ua = nullptr; + manager->shutdown(); + REQUIRE(devices.empty()); + qInfo("audio recovery: partial start, independent streams, two recorders, context restart, cancellation, bounded TX PASS"); + return 0; +} diff --git a/tools/chatgui/tests/test_call_jitter.cpp b/tools/chatgui/tests/test_call_jitter.cpp index 8469d3fc..27401827 100644 --- a/tools/chatgui/tests/test_call_jitter.cpp +++ b/tools/chatgui/tests/test_call_jitter.cpp @@ -20,6 +20,11 @@ static int decode(void* arg, const uint8_t* enc, int len, int16_t* pcm, int cap) for (int i = 0; i < cap; i++) pcm[i] = (int16_t)(8000 * std::sin(2 * 3.141592653589793 * 440 * i / 48000)); return cap; } +struct broken_decoder { int attempts = 0; }; +static int decode_bad(void* arg, const uint8_t*, int, int16_t*, int) { + ++static_cast(arg)->attempts; + return -1; +} static void push(call_jitter* j, int id, int64_t received_ms = -1) { call_jitter_push(j, (const uint8_t*)&id, sizeof(id), (uint32_t)(id * 20), received_ms < 0 ? id * 20 : received_ms); } @@ -247,6 +252,18 @@ int main() { CHECK(audible); call_audio_release(125); } + { + broken_decoder decoder; + auto* jitter = call_jitter_create(decode_bad, &decoder, 1000); + for (int i = 0; i < CALL_JITTER_MAX_FRAMES; ++i) push(jitter, i); + CHECK(call_jitter_pull(jitter, pcm, 480) == 0); + CHECK(decoder.attempts > 0 && decoder.attempts <= 9); + call_jitter_reset(jitter); + int depth = 0; + call_jitter_get_stats(jitter, &depth, nullptr, nullptr, nullptr, nullptr); + CHECK(depth <= 1000); + call_jitter_destroy(jitter); + } std::printf("call_jitter: %s\n", failures ? "FAILED" : "PASSED"); return failures ? 1 : 0; } diff --git a/tools/chatgui/tests/test_linux_audio.c b/tools/chatgui/tests/test_linux_audio.c new file mode 100644 index 00000000..b81a1e38 --- /dev/null +++ b/tools/chatgui/tests/test_linux_audio.c @@ -0,0 +1,163 @@ +/* Реальные ветки miniaudio на null backend: аппаратный звук и сеть не нужны. */ +#include "../transport/audio_diagnostics.h" +#include "../../../lib/speex_aec.h" +#include +#include +#include +#include + +static uint64_t observed_unfilled, observed_no_progress, observed_holes; +static uint32_t callback_count; +static void test_event(const void* device, int kind, int stage, int64_t value, int64_t detail) { + if (kind == AUDIO_DIAG_UNFILLED) observed_unfilled += (uint64_t)value; + if (kind == AUDIO_DIAG_NO_PROGRESS) observed_no_progress += (uint64_t)value; + if (kind == AUDIO_DIAG_HOLE) observed_holes += (uint64_t)value; + audio_diag_event(device, kind, stage, value, detail); +} +#define MA_DIAGNOSTIC_EVENT(device, kind, stage, value, detail) test_event(device, kind, stage, value, detail) +#define MA_DIAGNOSTIC_LOG(log, level, message) audio_diag_log(log, level, message) +#define MA_PULSE_WAIT_TIMEOUT_MS 20 +#define MINIAUDIO_IMPLEMENTATION +#include "../../../lib/miniaudio.h" + +static void callback(ma_device* device, void* out, const void* in, ma_uint32 count) { + (void)device; (void)in; + callback_count++; + if (out) for (ma_uint32 i = 0; i < count; i++) ((int16_t*)out)[i] = 12000; +} + +static void starvation(int duplex) { + ma_backend backend = ma_backend_null; + ma_context context; + ma_device device; + ma_device_config config = ma_device_config_init(duplex ? ma_device_type_duplex : ma_device_type_playback); + config.capture.format = config.playback.format = ma_format_s16; + config.capture.channels = config.playback.channels = 1; + config.sampleRate = 48000; config.periodSizeInFrames = 480; config.dataCallback = callback; + assert(ma_context_init(&backend, 1, NULL, &context) == MA_SUCCESS); + assert(ma_device_init(&context, &config, &device) == MA_SUCCESS); + if (duplex) assert(ma_duplex_rb_init(ma_format_s16, 1, 48000, 48000, 480, NULL, &device.duplexRB) == MA_SUCCESS); + /* Worker не запускаем: стенд синхронно вызывает backend-обработчик. */ + ma_device__set_state(&device, ma_device_state_started); + audio_diag_label(&device, duplex ? 1 : 2, duplex ? "test-duplex" : "test-playback"); + int16_t input[480] = {0}, output[480]; + observed_unfilled = 0; callback_count = 0; + uint64_t untouched = 0; + for (int tick = 0; tick < 20; tick++) { + if (duplex && tick < 3) assert(ma_device_handle_backend_data_callback(&device, NULL, input, 480) == MA_SUCCESS); + for (int i = 0; i < 480; i++) output[i] = 2345; + assert(ma_device_handle_backend_data_callback(&device, output, NULL, 480) == MA_SUCCESS); + for (int i = 0; i < 480; i++) untouched += output[i] == 2345; + } + assert(untouched == observed_unfilled); + assert(untouched == 0 && callback_count >= 20); + printf("starvation duplex=%d unfilled=%llu callback_count=%u PASS\n", duplex, + (unsigned long long)untouched, callback_count); + ma_device__set_state(&device, ma_device_state_stopped); + if (duplex) ma_duplex_rb_uninit(&device.duplexRB); + ma_device_uninit(&device); ma_context_uninit(&context); +} + +#ifdef MA_SUPPORT_PULSEAUDIO +static size_t zero_writable(const ma_pa_stream* stream) { + (void)stream; + /* Без внешнего stop callback обязан вернуть управление сам. */ + return 0; +} +static void no_progress(void) { + ma_context context; + ma_device device; + memset(&context, 0, sizeof(context)); memset(&device, 0, sizeof(device)); + device.pContext = &context; + device.playback.internalFormat = ma_format_s16; device.playback.internalChannels = 1; + context.pulse.pa_stream_writable_size = (ma_proc)zero_writable; + ma_device__set_state(&device, ma_device_state_started); + observed_no_progress = 0; + ma_device_on_write__pulse((ma_pa_stream*)&device, 960, &device); + assert(observed_no_progress == 1); + audio_diag_event(&device, AUDIO_DIAG_CLOSE, -1, 0, 0); + puts("PulseAudio zero-progress detection PASS"); +} +static unsigned int wait_cancelled, wait_unrefed; +static ma_pa_operation_state_t wait_state(const ma_pa_operation* operation) { (void)operation; return MA_PA_OPERATION_RUNNING; } +static void wait_cancel(ma_pa_operation* operation) { (void)operation; ++wait_cancelled; } +static void wait_unref(ma_pa_operation* operation) { (void)operation; ++wait_unrefed; } +static int wait_iterate(ma_pa_mainloop* loop, int block, int* result) { + (void)loop; (void)result; + assert(block == 0); + return 0; +} +static void bounded_wait(void) { + ma_context context; + ma_timer timer; + memset(&context, 0, sizeof(context)); + context.pulse.pa_operation_get_state = (ma_proc)wait_state; + context.pulse.pa_operation_cancel = (ma_proc)wait_cancel; + context.pulse.pa_operation_unref = (ma_proc)wait_unref; + context.pulse.pa_mainloop_iterate = (ma_proc)wait_iterate; + wait_cancelled = wait_unrefed = 0; + ma_timer_init(&timer); + assert(ma_wait_for_operation_and_unref__pulse(&context, NULL, (ma_pa_operation*)&context) == MA_TIMEOUT); + assert(wait_cancelled == 1 && wait_unrefed == 1); + assert(ma_timer_get_time_in_seconds(&timer) < 0.5); + puts("PulseAudio deadline and callback cancellation PASS"); +} + +static unsigned int hole_frames, hole_drops; +static int hole_peek(ma_pa_stream* stream, const void** data, size_t* bytes) { + (void)stream; *data = NULL; *bytes = 9600; return 0; +} +static int hole_drop(ma_pa_stream* stream) { (void)stream; ++hole_drops; return 0; } +static void hole_capture(ma_device* device, void* out, const void* in, ma_uint32 frames) { + (void)device; (void)out; + assert(in); + for (ma_uint32 i = 0; i < frames; ++i) assert(((const int16_t*)in)[i] == 0); + hole_frames += frames; +} +static void capture_hole(void) { + ma_backend backend = ma_backend_null; + ma_context context; + ma_device device; + ma_device_config config = ma_device_config_init(ma_device_type_capture); + config.capture.format = ma_format_s16; config.capture.channels = 1; config.sampleRate = 48000; + config.noFixedSizedCallback = MA_TRUE; config.dataCallback = hole_capture; + assert(ma_context_init(&backend, 1, NULL, &context) == MA_SUCCESS); + assert(ma_device_init(&context, &config, &device) == MA_SUCCESS); + context.pulse.pa_stream_peek = (ma_proc)hole_peek; + context.pulse.pa_stream_drop = (ma_proc)hole_drop; + observed_holes = hole_frames = hole_drops = 0; + ma_device__set_state(&device, ma_device_state_started); + ma_device_on_read__pulse((ma_pa_stream*)&device, 9600, &device); + assert(observed_holes == 4800 && hole_frames == 4800 && hole_drops == 1); + ma_device__set_state(&device, ma_device_state_stopped); + ma_device_uninit(&device); ma_context_uninit(&context); + puts("PulseAudio capture hole preserves duration and silence PASS"); +} + +#endif + +/* Характеризация найденной потери хвоста; после исправления ожидаем два кадра. */ +static void aec_variable_chunks(void) { + speex_aec_t* aec = speex_aec_create(48000, 960, 14400, 8); + int16_t pcm[1920] = {0}; + assert(aec != NULL); + speex_aec_feed_playback(aec, pcm, 1200); + speex_aec_feed_playback(aec, pcm + 1200, 720); + const int frames = speex_aec_delay_fill(aec); + assert(frames == 2); + printf("AEC characterization: input=1920 queued=%d*960; all samples preserved\n", frames); + speex_aec_destroy(aec); +} + +int main(void) { + audio_diag_start(); + starvation(1); starvation(0); +#ifdef MA_SUPPORT_PULSEAUDIO + no_progress(); + bounded_wait(); + capture_hole(); +#endif + aec_variable_chunks(); + audio_diag_stop(); + return 0; +} diff --git a/tools/chatgui/transport/audio_diagnostics.cpp b/tools/chatgui/transport/audio_diagnostics.cpp new file mode 100644 index 00000000..5ff64257 --- /dev/null +++ b/tools/chatgui/transport/audio_diagnostics.cpp @@ -0,0 +1,253 @@ +#include "audio_diagnostics.h" + +#ifdef __linux__ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include "../../../lib/debug_config.h" +} + +namespace { +constexpr uint64_t kCapacity = 8192; +constexpr uint64_t kSecond = 1000000000; +const char* const kFaults[] = {"hole", "unfilled", "capture-drop", "no-progress", "backend-underrun", "backend-overflow", "error"}; +const char* const kStages[] = {"loop", "read", "write", "map", "submit", "duplex", "callback", "feed", "pull"}; +static_assert(std::atomic::is_always_lock_free); + +struct Event { + uintptr_t device; + uint64_t wall, cpu; + int64_t value, detail; + int kind, stage; + long tid; + char text[192]; +}; +struct Slot { std::atomic sequence{0}; Event event{}; }; +std::array queue; +std::atomic writePosition{0}, lost{0}; +std::atomic running{false}; +uint64_t readPosition = 0; +std::thread reporter; + +uint64_t now(clockid_t clock) { + timespec ts{}; + clock_gettime(clock, &ts); + return uint64_t(ts.tv_sec) * kSecond + uint64_t(ts.tv_nsec); +} + +/* Ограниченная MPSC-очередь: producer не ждёт, не выделяет память и не пишет лог. */ +void publish(const void* device, int kind, int stage, int64_t value, int64_t detail, const char* text) { + if (!running.load(std::memory_order_relaxed)) return; + const auto wall = now(CLOCK_MONOTONIC), cpu = now(CLOCK_THREAD_CPUTIME_ID); + uint64_t pos = writePosition.load(std::memory_order_relaxed); + for (int attempt = 0; attempt < 4; ++attempt) { + auto& slot = queue[pos % kCapacity]; + const auto seq = slot.sequence.load(std::memory_order_acquire); + if (seq == pos && writePosition.compare_exchange_weak(pos, pos + 1, std::memory_order_relaxed)) { + auto& e = slot.event; + e.device = reinterpret_cast(device); e.wall = wall; e.cpu = cpu; + e.kind = kind; e.stage = stage; e.value = value; e.detail = detail; + e.tid = syscall(SYS_gettid); + e.text[0] = 0; + if (text) { + std::strncpy(e.text, text, sizeof(e.text) - 1); + e.text[sizeof(e.text) - 1] = 0; + } + slot.sequence.store(pos + 1, std::memory_order_release); + return; + } + if (seq < pos) break; // Полное кольцо. + pos = writePosition.load(std::memory_order_relaxed); + } + lost.fetch_add(1, std::memory_order_relaxed); +} + +bool take(Event& event) { + auto& slot = queue[readPosition % kCapacity]; + if (slot.sequence.load(std::memory_order_acquire) != readPosition + 1) return false; + event = slot.event; + slot.sequence.store(readPosition + kCapacity, std::memory_order_release); + ++readPosition; + return true; +} + +struct Stage { + uint64_t entered = 0, cpu = 0, last = 0, calls = 0, frames = 0, maxGap = 0, maxWork = 0, maxCpu = 0; + int64_t input = 0, output = 0; + long tid = 0; +}; +struct Device { + std::string label = "unlabelled"; + uint64_t id = 0; + std::array stages{}; + std::array faults{}; +}; + +void consume(const Event& e, std::map& devices) { + if (e.kind == AUDIO_DIAG_LOG) { + // Hole учитывается кадрами в read-ветке; строка на каждый hole засоряет лог. + if (std::strstr(e.text, "Hole.")) return; + std::string message(e.text); + while (!message.empty() && (message.back() == '\n' || message.back() == '\r')) message.pop_back(); + if (e.value == 1) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "AudioDiag: miniaudio log=%p %s", (void*)e.device, message.c_str()); + else if (e.value == 2) DEBUG_WARN(DEBUG_CATEGORY_CALL, "AudioDiag: miniaudio log=%p level=%lld %s", + (void*)e.device, (long long)e.value, message.c_str()); + else if (e.value == 3) DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "AudioDiag: miniaudio log=%p %s", (void*)e.device, message.c_str()); + else DEBUG_TRACE(DEBUG_CATEGORY_CALL, "AudioDiag: miniaudio log=%p %s", (void*)e.device, message.c_str()); + return; + } + if (e.kind == AUDIO_DIAG_CLOSE) { + DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: close dev=%p mono_ms=%llu", (void*)e.device, (unsigned long long)(e.wall / 1000000)); + devices.erase(e.device); + return; + } + auto& d = devices[e.device]; + if (e.kind == AUDIO_DIAG_LABEL) { + d.label = e.text; d.id = uint64_t(e.value); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: label dev=%p role=%s id=%016llx", (void*)e.device, e.text, (unsigned long long)d.id); + } else if (e.kind == AUDIO_DIAG_OPEN) { + DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: open dev=%p type=%lld rate=%lld mono_ms=%llu", (void*)e.device, + (long long)e.value, (long long)e.detail, (unsigned long long)(e.wall / 1000000)); + } else if (e.kind == AUDIO_DIAG_BACKEND_STARTED) { + DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: backend playback started/resumed dev=%p mono_ms=%llu", + (void*)e.device, (unsigned long long)(e.wall / 1000000)); + } else if (e.kind == AUDIO_DIAG_NOTIFY) { + DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: notification dev=%p type=%lld state=%lld mono_ms=%llu", (void*)e.device, + (long long)e.value, (long long)e.detail, (unsigned long long)(e.wall / 1000000)); + } else if (e.kind == AUDIO_DIAG_ENTER && e.stage >= 0 && e.stage < AUDIO_DIAG_STAGE_COUNT) { + auto& s = d.stages[e.stage]; + s.input = e.detail; + if (s.last && e.wall >= s.last) s.maxGap = std::max(s.maxGap, e.wall - s.last); + s.last = s.entered = e.wall; s.cpu = e.cpu; s.tid = e.tid; + ++s.calls; + if (e.value > 0) s.frames += uint64_t(e.value); + } else if (e.kind == AUDIO_DIAG_LEAVE && e.stage >= 0 && e.stage < AUDIO_DIAG_STAGE_COUNT) { + auto& s = d.stages[e.stage]; + if (s.entered && s.tid == e.tid && e.wall >= s.entered) { + s.maxWork = std::max(s.maxWork, e.wall - s.entered); + s.maxCpu = std::max(s.maxCpu, e.cpu - s.cpu); + } + s.output = e.detail; + s.entered = 0; + } else if (e.kind >= AUDIO_DIAG_HOLE && e.kind <= AUDIO_DIAG_ERROR) { + d.faults[e.kind] += uint64_t(std::max(e.value, 1)); + // Первая аномалия за окно, остальные попадут в сводку. + if (d.faults[e.kind] == uint64_t(std::max(e.value, 1))) + DEBUG_WARN(DEBUG_CATEGORY_CALL, + "AudioDiag: fault dev=%p role=%s id=%016llx kind=%s stage=%s value=%lld detail=%lld tid=%ld mono_ms=%llu", + (void*)e.device, d.label.c_str(), (unsigned long long)d.id, kFaults[e.kind - AUDIO_DIAG_HOLE], + e.stage >= 0 && e.stage < AUDIO_DIAG_STAGE_COUNT ? kStages[e.stage] : "none", + (long long)e.value, (long long)e.detail, e.tid, (unsigned long long)(e.wall / 1000000)); + } +} + +void report(std::map& devices, uint64_t wall, double elapsed) { + const auto dropped = lost.exchange(0, std::memory_order_relaxed); + if (dropped) DEBUG_WARN(DEBUG_CATEGORY_CALL, "AudioDiag: diagnostic events lost=%llu; interval is incomplete", + (unsigned long long)dropped); + std::set stalledThreads; + for (auto& [key, d] : devices) { + for (int i = 0; i < AUDIO_DIAG_STAGE_COUNT; ++i) { + auto& s = d.stages[i]; + if (!s.last) continue; + const double pending = s.entered && wall >= s.entered ? (wall - s.entered) / 1e6 : 0; + const double idle = wall >= s.last ? (wall - s.last) / 1e6 : 0; + const auto level = s.maxWork > 50000000 || s.maxGap > 50000000 || pending > 50 || idle > 100 + ? DEBUG_LEVEL_WARN : DEBUG_LEVEL_DEBUG; + if (level == DEBUG_LEVEL_WARN) stalledThreads.insert(s.tid); + // Без вызова backend-API: reporter никогда не трогает живые ma_device/pa_stream. + if (debug_should_output(level, DEBUG_CATEGORY_CALL)) debug_output(level, DEBUG_CATEGORY_CALL, __func__, __FILE__, __LINE__, + "AudioDiag: dev=%p role=%s id=%016llx stage=%s tid=%ld interval=%.3fs calls=%.1f/s frames=%.0f/s " + "gap_max=%.2fms work_max=%.2fms cpu_max=%.2fms pending=%.2fms since_enter=%.2fms in_detail=%lld out_detail=%lld mono_ms=%llu", + (void*)key, d.label.c_str(), (unsigned long long)d.id, kStages[i], s.tid, elapsed, + s.calls / elapsed, s.frames / elapsed, s.maxGap / 1e6, s.maxWork / 1e6, s.maxCpu / 1e6, + pending, idle, (long long)s.input, (long long)s.output, (unsigned long long)(wall / 1000000)); + s.calls = s.frames = s.maxGap = s.maxWork = s.maxCpu = 0; + } + if (d.faults[AUDIO_DIAG_HOLE] || d.faults[AUDIO_DIAG_UNFILLED] || d.faults[AUDIO_DIAG_CAPTURE_DROP] || + d.faults[AUDIO_DIAG_NO_PROGRESS] || d.faults[AUDIO_DIAG_BACKEND_UNDERRUN] || + d.faults[AUDIO_DIAG_BACKEND_OVERFLOW] || d.faults[AUDIO_DIAG_ERROR]) + DEBUG_WARN(DEBUG_CATEGORY_CALL, "AudioDiag: dev=%p hole_frames=%llu unfilled_frames=%llu capture_drop_frames=%llu " + "no_progress=%llu backend_underruns=%llu backend_overflows=%llu errors=%llu", (void*)key, + (unsigned long long)d.faults[AUDIO_DIAG_HOLE], (unsigned long long)d.faults[AUDIO_DIAG_UNFILLED], + (unsigned long long)d.faults[AUDIO_DIAG_CAPTURE_DROP], + (unsigned long long)d.faults[AUDIO_DIAG_NO_PROGRESS], + (unsigned long long)d.faults[AUDIO_DIAG_BACKEND_UNDERRUN], + (unsigned long long)d.faults[AUDIO_DIAG_BACKEND_OVERFLOW], (unsigned long long)d.faults[AUDIO_DIAG_ERROR]); + d.faults.fill(0); + } + for (auto tid : stalledThreads) { + const auto path = std::string("/proc/self/task/") + std::to_string(tid) + "/"; + std::ifstream waitFile(path + "wchan"), schedFile(path + "schedstat"); + std::string wait; + uint64_t cpu = 0, ready = 0, slices = 0; + if (waitFile >> wait && schedFile >> cpu >> ready >> slices) + DEBUG_WARN(DEBUG_CATEGORY_CALL, "AudioDiag: thread tid=%ld wchan=%s cpu_total=%.2fms ready_wait_total=%.2fms slices=%llu", + tid, wait.c_str(), cpu / 1e6, ready / 1e6, (unsigned long long)slices); + else DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "AudioDiag: thread tid=%ld proc snapshot unavailable (thread may have exited)", tid); + } +} + +void run() { + std::map devices; + auto lastReport = now(CLOCK_MONOTONIC); + for (;;) { + Event e; + for (uint64_t n = 0; n < kCapacity && take(e); ++n) consume(e, devices); + const auto wall = now(CLOCK_MONOTONIC); + if (wall - lastReport >= kSecond) { report(devices, wall, (wall - lastReport) / double(kSecond)); lastReport = wall; } + if (!running.load(std::memory_order_acquire)) break; + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + report(devices, now(CLOCK_MONOTONIC), 1.0); +} +} // namespace + +void audio_diag_start(void) { + if (running.load()) return; + writePosition = 0; readPosition = 0; lost = 0; + for (uint64_t i = 0; i < kCapacity; ++i) queue[i].sequence.store(i, std::memory_order_relaxed); + running.store(true, std::memory_order_release); + try { reporter = std::thread(run); } + catch (const std::system_error& e) { + running = false; + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "AudioDiag: reporter start failed: %s", e.what()); + return; + } + DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: started capacity=%llu reporting=1s category=call; timestamps=CLOCK_MONOTONIC", + (unsigned long long)kCapacity); +} +void audio_diag_stop(void) { + if (!running.exchange(false, std::memory_order_acq_rel)) return; + reporter.join(); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: stopped"); +} +void audio_diag_event(const void* device, int kind, int stage, int64_t value, int64_t detail) { + publish(device, kind, stage, value, detail, nullptr); +} +void audio_diag_label(const void* device, uint64_t id, const char* name) { + publish(device, AUDIO_DIAG_LABEL, -1, int64_t(id), 0, name); +} +void audio_diag_log(const void* log, unsigned int level, const char* message) { + publish(log, AUDIO_DIAG_LOG, -1, level, 0, message); +} +#else +void audio_diag_start(void) {} +void audio_diag_stop(void) {} +void audio_diag_event(const void*, int, int, int64_t, int64_t) {} +void audio_diag_label(const void*, uint64_t, const char*) {} +void audio_diag_log(const void*, unsigned int, const char*) {} +#endif diff --git a/tools/chatgui/transport/audio_diagnostics.h b/tools/chatgui/transport/audio_diagnostics.h new file mode 100644 index 00000000..8bc3ddb8 --- /dev/null +++ b/tools/chatgui/transport/audio_diagnostics.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Только метаданные: PCM и содержимое сообщений не записываются. */ +enum audio_diag_stage { + AUDIO_DIAG_LOOP, AUDIO_DIAG_READ, AUDIO_DIAG_WRITE, AUDIO_DIAG_MAP, + AUDIO_DIAG_SUBMIT, AUDIO_DIAG_DUPLEX, AUDIO_DIAG_CALLBACK, + AUDIO_DIAG_FEED, AUDIO_DIAG_PULL, AUDIO_DIAG_STAGE_COUNT +}; +enum audio_diag_kind { + AUDIO_DIAG_ENTER, AUDIO_DIAG_LEAVE, AUDIO_DIAG_OPEN, AUDIO_DIAG_CLOSE, + AUDIO_DIAG_NOTIFY, AUDIO_DIAG_BACKEND_STARTED, AUDIO_DIAG_HOLE, AUDIO_DIAG_UNFILLED, AUDIO_DIAG_CAPTURE_DROP, + AUDIO_DIAG_NO_PROGRESS, AUDIO_DIAG_BACKEND_UNDERRUN, AUDIO_DIAG_BACKEND_OVERFLOW, AUDIO_DIAG_ERROR, AUDIO_DIAG_LABEL, AUDIO_DIAG_LOG +}; + +/* start/stop — управляющий поток; перед stop все producers должны быть остановлены. */ +void audio_diag_start(void); +void audio_diag_stop(void); +void audio_diag_event(const void* device, int kind, int stage, int64_t value, int64_t detail); +void audio_diag_label(const void* device, uint64_t id, const char* name); +void audio_diag_log(const void* log, unsigned int level, const char* message); + +#ifdef __cplusplus +} +#endif diff --git a/tools/chatgui/transport/miniaudio_impl.c b/tools/chatgui/transport/miniaudio_impl.c index 5f5f3551..3829a742 100644 --- a/tools/chatgui/transport/miniaudio_impl.c +++ b/tools/chatgui/transport/miniaudio_impl.c @@ -1,2 +1,71 @@ +#include "audio_diagnostics.h" +#ifdef __linux__ +#include +static void audio_diag_pulse_init(void* device); +#define MA_DIAGNOSTIC_EVENT(device, kind, stage, value, detail) audio_diag_event(device, kind, stage, value, detail) +#define MA_DIAGNOSTIC_LOG(log, level, message) audio_diag_log(log, level, message) +#define MA_DIAGNOSTIC_DEVICE_INIT(device) audio_diag_pulse_init(device) +#endif #define MINIAUDIO_IMPLEMENTATION #include "../../../lib/miniaudio.h" + +#ifdef __linux__ +#ifdef MA_SUPPORT_PULSEAUDIO +static void audio_diag_pulse_underflow(ma_pa_stream* stream, void* userdata) { + (void)stream; + audio_diag_event(userdata, AUDIO_DIAG_BACKEND_UNDERRUN, AUDIO_DIAG_WRITE, 1, 0); +} +static void audio_diag_pulse_overflow(ma_pa_stream* stream, void* userdata) { + (void)stream; + audio_diag_event(userdata, AUDIO_DIAG_BACKEND_OVERFLOW, AUDIO_DIAG_WRITE, 1, 0); +} +static void audio_diag_pulse_started(ma_pa_stream* stream, void* userdata) { + (void)stream; + audio_diag_event(userdata, AUDIO_DIAG_BACKEND_STARTED, AUDIO_DIAG_WRITE, 1, 0); +} +#endif + +/* Только init-поток: backend-callbacks остаются короткими наблюдателями. */ +static void audio_diag_pulse_init(void* device) { +#ifdef MA_SUPPORT_PULSEAUDIO + ma_device* dev = (ma_device*)device; + ma_pa_stream_set_suspended_callback_proc underflow, overflow, started; + if (dev->pContext->backend != ma_backend_pulseaudio || !dev->pulse.pStreamPlayback) return; +#ifndef MA_NO_RUNTIME_LINKING + underflow = (ma_pa_stream_set_suspended_callback_proc)dlsym(dev->pContext->pulse.pulseSO, "pa_stream_set_underflow_callback"); + overflow = (ma_pa_stream_set_suspended_callback_proc)dlsym(dev->pContext->pulse.pulseSO, "pa_stream_set_overflow_callback"); + started = (ma_pa_stream_set_suspended_callback_proc)dlsym(dev->pContext->pulse.pulseSO, "pa_stream_set_started_callback"); +#else + underflow = pa_stream_set_underflow_callback; + overflow = pa_stream_set_overflow_callback; + started = pa_stream_set_started_callback; +#endif + if (!underflow || !overflow || !started) { + audio_diag_log(ma_device_get_log(dev), MA_LOG_LEVEL_WARNING, "PulseAudio xrun/started callbacks unavailable"); + return; + } + underflow((ma_pa_stream*)dev->pulse.pStreamPlayback, audio_diag_pulse_underflow, dev); + overflow((ma_pa_stream*)dev->pulse.pStreamPlayback, audio_diag_pulse_overflow, dev); + started((ma_pa_stream*)dev->pulse.pStreamPlayback, audio_diag_pulse_started, dev); + audio_diag_log(ma_device_get_log(dev), MA_LOG_LEVEL_INFO, "PulseAudio xrun/started observers registered"); +#else + (void)device; +#endif +} +#endif + +/* Вызывается только владельцем общего контекста (GUI), вне device callbacks. */ +int audio_context_failed(ma_context* context) { +#ifdef MA_SUPPORT_PULSEAUDIO + if (context && context->backend == ma_backend_pulseaudio) { + int result = ((ma_pa_mainloop_iterate_proc)context->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)context->pulse.pMainLoop, 0, NULL); + ma_pa_context_state_t state = ((ma_pa_context_get_state_proc)context->pulse.pa_context_get_state)((ma_pa_context*)context->pulse.pPulseContext); + if (result < 0 || state == MA_PA_CONTEXT_FAILED || state == MA_PA_CONTEXT_TERMINATED) { + ma_log_postf(ma_context_get_log(context), MA_LOG_LEVEL_ERROR, "[PulseAudio] Shared context failed iterate=%d state=%d errno=%d.\n", + result, state, ((ma_pa_context_errno_proc)context->pulse.pa_context_errno)((ma_pa_context*)context->pulse.pPulseContext)); + return 1; + } + } +#endif + return 0; +} diff --git a/tools/chatgui/transport/utun_node.cpp b/tools/chatgui/transport/utun_node.cpp index 0203a84f..1944570a 100644 --- a/tools/chatgui/transport/utun_node.cpp +++ b/tools/chatgui/transport/utun_node.cpp @@ -24,6 +24,8 @@ extern "C" { #include "chat/chat_event.h" #include "chat/db_sync.h" #include "call/call.h" +#include "call/call_audio.h" +#include "radio/radio_audio.h" #include "dm/dm_core.h" #include "dm/dm_mailbox.h" #include "gui_bridge.h" @@ -239,6 +241,8 @@ void UtunNode::runLoop() { utun_instance_destroy(m_instance); m_instance = nullptr; uasync_destroy(ua, 0); m_ua = nullptr; return; } + call_audio_init(m_instance); + radio_audio_init(m_instance); chat_core_sync_my_addresses(m_instance); DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "chat_core + chat_sync initialized"); @@ -252,6 +256,8 @@ void UtunNode::runLoop() { if (m_instance) { DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: destroying instance in worker thread"); + call_audio_destroy(m_instance); + radio_audio_destroy(m_instance); utun_instance_destroy(m_instance); m_instance = nullptr; }