Describe shared audio capture and buffer tuning contracts
This commit is contained in:
1 parent
cb49b9d4a4
commit
29e2afc3ce
3 files changed
+65
-40
No files matched your search
@@ -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; }
|
||||
|
||||
@@ -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.
|
||||
};
|
||||
@@ -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.
|
||||
};
|
||||
Reference in new issue
Block a user