Describe shared audio capture and buffer tuning contracts

This commit is contained in:
evgeny committed 2026-10-27 14:27:20 +01:00
1 parent cb49b9d4a4
commit 29e2afc3ce
3 files changed
+65 -40

No files matched your search

+14 -8
View File
@@ -1,23 +1,29 @@
/* Политика роста периода desktop-аудио при повторных backend XRUN; только управляющий поток.
* Перед открытием задать baseMs, после получения фактического периода вызвать begin; observe получает ошибки за один опрос.
* Increase означает запрос нового периода для внешнего reopen, сам объект backend не меняет.
* Размер только растёт; для нового устройства/сессии сбросить объект, при reopen сохранить его. */
#pragma once
#include <algorithm>
#include <cstdint>
/* Только управляющий поток. Исходный запрос не меняется без повторных 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; }
+39 -26
View File
@@ -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 <QString>
#include <cstdint>
@@ -6,45 +12,52 @@
#include <atomic>
#include <thread>
/* 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<bool(int channels, int nativeChannels)>;
/* Заимствованный Frame нужно обработать синхронно; PCM/ссылку на аргумент после callback не хранить. */
using Callback = std::function<void(const Frame&)>;
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<void()> 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<Session> detach();
std::shared_ptr<Session> m_session;
std::thread m_closeThread;
std::atomic<bool> m_closeDone{true};
Ready m_ready;
std::function<void()> m_stopped;
Callback m_callback;
Role m_role = Role::Radio;
bool m_prepared = false;
std::shared_ptr<Session> detach(); // Отключает свой слот/колбэк, вызывает stopped, отдаёт владение Session без её закрытия.
std::shared_ptr<Session> m_session; // Общая Session выбранного входа; статический реестр хранит только weak_ptr.
std::thread m_closeThread; // Временно владеет отсоединённой Session при closeAsync.
std::atomic<bool> m_closeDone{true}; // Worker отпустил Session; thread может ещё требовать join.
Ready m_ready; // GUI-подготовка потребителя при открытии/восстановлении.
std::function<void()> m_stopped; // GUI-остановка подготовленного потребителя после завершения callback.
Callback m_callback; // Аппаратная доставка PCM после successful ready.
Role m_role = Role::Radio; // Выбранный слот текущей подписки.
bool m_prepared = false; // ready успешно выполнен и должен быть уравновешен stopped.
};
+12 -6
View File
@@ -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 <atomic>
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<int> owners{0};
Mode m_mode; // Неизменяемый режим допуска этого объекта.
bool m_held = false; // Только управляющий поток объекта, не atomic.
static inline std::atomic<int> owners{0}; // Общий процессный счётчик: -1=Exclusive, 0=свободно, >0=число Shared.
};