diff --git a/tools/chatgui/src/audio_buffer_tuning.h b/tools/chatgui/src/audio_buffer_tuning.h index 2a3ce5f2..57f782b0 100644 --- a/tools/chatgui/src/audio_buffer_tuning.h +++ b/tools/chatgui/src/audio_buffer_tuning.h @@ -1,23 +1,29 @@ +/* Политика роста периода desktop-аудио при повторных backend XRUN; только управляющий поток. + * Перед открытием задать baseMs, после получения фактического периода вызвать begin; observe получает ошибки за один опрос. + * Increase означает запрос нового периода для внешнего reopen, сам объект backend не меняет. + * Размер только растёт; для нового устройства/сессии сбросить объект, при reopen сохранить его. */ #pragma once #include #include -/* Только управляющий поток. Исходный запрос не меняется без повторных backend XRUN. - * Рост периода на 5мс, запрос полной ёмкости до 100мс; прогрев 1с, между изменениями 5с. - * Нужны два опроса с ошибками за 5с: пачка уведомлений одного сбоя не увеличивает буфер. - * Состояние живёт до смены устройства/сессии; begin() сохраняет подобранный размер при reopen. */ struct AudioBufferTuning { - enum Result { Unchanged, Increase, Limit }; - unsigned requestedMs = 0, baseMs = 0, actualMs = 0, periods = 1; - int64_t settleUntil = 0, changeAfter = 0, firstFaultAt = -1; - unsigned faultPolls = 0; + enum Result { Unchanged, Increase, Limit }; // Нет действия, обновлён requestedMs, дальнейший рост невозможен. + unsigned requestedMs = 0, baseMs = 0, actualMs = 0, periods = 1; // Подобранный/исходный/фактический период в мс и число периодов. + int64_t settleUntil = 0, changeAfter = 0, firstFaultAt = -1; // Конец прогрева/cooldown и первый сбой, монотонные мс; -1=нет сбоя. + unsigned faultPolls = 0; // Число опросов с ошибками в текущем окне, ограничено двумя. + /* now — монотонные мс; запоминает actualPeriodMs и max(1,periodCount), прогрев 1с, сброс серии сбоев. + * Сохраняет requestedMs/baseMs и срок changeAfter от прежнего reopen. */ void begin(int64_t now, unsigned actualPeriodMs, unsigned periodCount) { actualMs = actualPeriodMs; periods = std::max(1u, periodCount); settleUntil = now + 1000; firstFaultAt = -1; faultPolls = 0; } + /* Нужны два опроса с ненулевым glitches за <=5с после прогрева; число ошибок внутри одного опроса неважно. + * При достижении changeAfter повышает max(actual/requested/base) на 5мс до floor(100/periods), обновляя requestedMs. + * Нулевой исходный период/достигнутый предел возвращают Limit; после Increase/Limit cooldown 5с. + * Без нового сбоя один таймер/cooldown не вызывает действия; 100мс ограничивают запрос, не фактическую ёмкость backend. */ Result observe(int64_t now, unsigned glitches) { if (!glitches || now < settleUntil) return Unchanged; if (firstFaultAt < 0 || now - firstFaultAt > 5000) { firstFaultAt = now; faultPolls = 0; } diff --git a/tools/chatgui/src/audio_capture.h b/tools/chatgui/src/audio_capture.h index 52c23dbc..bb030cab 100644 --- a/tools/chatgui/src/audio_capture.h +++ b/tools/chatgui/src/audio_capture.h @@ -1,3 +1,9 @@ +/* Общий desktop-захват PCM s16/48кГц для трансляции и рации. Объект — GUI-подписка, Session владеет AudioDevice; + * один выбранный вход открывается один раз, в сессии не более одного потребителя каждой Role. + * Stream переоткрывает прежний radio-профиль под свои параметры; они сохраняются, пока живёт сессия, даже после ухода Stream. + * Управление и ready/stopped — GUI-поток, Callback — аппаратный поток, где допустимо только быстро скопировать PCM. + * ready/stopped подготавливают/освобождают потребителя и не должны реентерабельно менять подписки. + * close/closeAsync отсоединяют свой callback с ожиданием его окончания; оставшийся потребитель сохраняет устройство. */ #pragma once #include #include @@ -6,45 +12,52 @@ #include #include -/* GUI владеет подпиской, сессия — устройством. Одинаковый выбранный вход открывается один раз. - * Профиль трансляции имеет приоритет и сохраняется для рации после её остановки. - * Callback только копирует PCM; ready/stopped вызываются в GUI вне аппаратного callback. - * close() ждёт завершения своего callback, не останавливая устройство другого потребителя. */ class AudioCapture { public: - enum class Role { Stream, Radio }; + enum class Role { Stream, Radio }; // Два отдельных слота одной сессии; приоритет параметров у Stream. struct Frame { - const int16_t* pcm; - unsigned frames; - int channels; - int64_t timeUs; - uint64_t route; - bool measured, missing; + const int16_t* pcm; // Заимствованный interleaved PCM на время callback; возможен nullptr при отсутствии данных. + unsigned frames; // Число PCM-фреймов на канал, не число отдельных int16_t. + int channels; // Фактические каналы подписки после fallback, частота всегда 48000Гц. + int64_t timeUs; // Монотонное время первого физического PCM-фрейма в микросекундах, измеренное либо оценённое. + uint64_t route; // Идентификатор физического маршрута из диагностики backend. + bool measured, missing; // Достоверность аппаратного времени и признак отсутствующих данных (не цифровой тишины). }; + /* Фактические выходные/native каналы; true разрешает Callback, false оставляет подписку для повтора prepare через recover. + * Может вызываться до запуска backend; успешная подготовка сама по себе не гарантирует поступление PCM. */ using Ready = std::function; + /* Заимствованный Frame нужно обработать синхронно; PCM/ссылку на аргумент после callback не хранить. */ using Callback = std::function; AudioCapture() = default; - ~AudioCapture(); + ~AudioCapture(); // close с ожиданием собственного close worker; SoundManager context ещё должен быть жив. AudioCapture(const AudioCapture&) = delete; AudioCapture& operator=(const AudioCapture&) = delete; + /* Заменяет прежнюю подписку: input — ID SoundManager, пустой при open выбирает известный default текущего списка. + * channels — запрос 1/2 с fallback до mono, tag — диагностический ID сессии. Все три callbacks обязательны. + * ready/stopped запускают/останавливают ресурсы потребителя, stopped следует только после успешного ready. + * Возвращает running() после первой попытки: false может оставить subscribed() для периодического recover. + * Дублирующая Role/закрывающийся общий backend не получают подписку; closing() блокирует замену старой. */ bool open(const QString& input, Role role, int channels, uint64_t tag, Ready ready, std::function stopped, Callback callback); - void close(); - void closeAsync(); // GUI отсоединяет подписчика; последний backend закрывается в worker. - bool closing() const; // Включает переоткрытие общего backend для подстройки буфера. - void recover(); + void close(); // Join собственного close worker, detach и синхронное закрытие последнего backend; повтор безопасен. + /* Detach и stopped выполняются сразу в GUI; освобождение последней Session передаётся собственному worker. + * Пока worker занят, повтор ничего не делает; объект и SoundManager context должны пережить его завершение. */ + void closeAsync(); + bool closing() const; // Собственный close worker либо переоткрытие общего backend для подстройки буфера. + void recover(); // GUI периодически повторяет открытие/подготовку с учётом backoff и восстановления устройства. + /* Подписка может существовать без работающей аппаратуры; running требует также ready и запущенное устройство. */ bool subscribed() const { return bool(m_session); } bool running() const; - bool captureTimedOut() const; + bool captureTimedOut() const; // Подготовленная подписка не получила настоящий PCM >=1с; сама её не закрывает. private: struct Session; - std::shared_ptr detach(); - std::shared_ptr m_session; - std::thread m_closeThread; - std::atomic m_closeDone{true}; - Ready m_ready; - std::function m_stopped; - Callback m_callback; - Role m_role = Role::Radio; - bool m_prepared = false; + std::shared_ptr detach(); // Отключает свой слот/колбэк, вызывает stopped, отдаёт владение Session без её закрытия. + std::shared_ptr m_session; // Общая Session выбранного входа; статический реестр хранит только weak_ptr. + std::thread m_closeThread; // Временно владеет отсоединённой Session при closeAsync. + std::atomic m_closeDone{true}; // Worker отпустил Session; thread может ещё требовать join. + Ready m_ready; // GUI-подготовка потребителя при открытии/восстановлении. + std::function m_stopped; // GUI-остановка подготовленного потребителя после завершения callback. + Callback m_callback; // Аппаратная доставка PCM после successful ready. + Role m_role = Role::Radio; // Выбранный слот текущей подписки. + bool m_prepared = false; // ready успешно выполнен и должен быть уравновешен stopped. }; diff --git a/tools/chatgui/src/audio_capture_lease.h b/tools/chatgui/src/audio_capture_lease.h index 047fb21e..ea31c718 100644 --- a/tools/chatgui/src/audio_capture_lease.h +++ b/tools/chatgui/src/audio_capture_lease.h @@ -1,16 +1,21 @@ -/* Publisher исключает звонок и запись. Рация согласует устройство через AudioCapture, - * поэтому может работать с publisher и не участвует в этом запрете. +/* Межмодульный допуск к capture: Shared (звонок/запись) исключает Exclusive (publisher) и наоборот. + * Несколько Shared допускаются одновременно; этот класс сам не открывает/не разделяет аппаратное устройство. + * Radio не участвует в общем счётчике и не запрещает другие режимы; общий вход с publisher согласует AudioCapture. * Lease берётся до открытия backend, сохраняется при recovery и освобождается после close/join. * Каждый lease использует управляющий поток, общее atomic состояние доступно из разных потоков. */ #pragma once #include class AudioCaptureLease { public: + /* Shared — общий счётчик >=0, Exclusive — единственный владелец при счётчике 0, Radio — локальная отметка без счётчика. */ enum class Mode { Shared, Exclusive, Radio }; + /* Задаёт режим без захвата; acquire нужно вызвать отдельно. */ explicit AudioCaptureLease(Mode mode = Mode::Shared) : m_mode(mode) {} ~AudioCaptureLease() { release(); } AudioCaptureLease(const AudioCaptureLease&) = delete; AudioCaptureLease& operator=(const AudioCaptureLease&) = delete; + /* Неблокирующая попытка допуска: true при успехе/повторном вызове уже взятого lease, false при несовместимом режиме. + * Успех не означает аппаратную готовность/эксклюзивность ОС; Radio всегда получает локальный допуск. */ bool acquire() { if (m_held) return true; if (m_mode == Mode::Radio) return m_held = true; @@ -22,15 +27,16 @@ public: } return m_held; } + /* Снимает допуск своего режима, повтор безопасен. Не закрывает backend/worker — это обязанность владельца до release. */ void release() { if (!m_held) return; if (m_mode == Mode::Exclusive) owners.store(0); else if (m_mode == Mode::Shared) owners.fetch_sub(1); m_held = false; } - bool held() const { return m_held; } + bool held() const { return m_held; } // Локальное состояние объекта, читать в его управляющем потоке. private: - Mode m_mode; - bool m_held = false; - static inline std::atomic owners{0}; + Mode m_mode; // Неизменяемый режим допуска этого объекта. + bool m_held = false; // Только управляющий поток объекта, не atomic. + static inline std::atomic owners{0}; // Общий процессный счётчик: -1=Exclusive, 0=свободно, >0=число Shared. };