Document sound manager recovery, session ownership and callback rules
This commit is contained in:
1 parent
027a4b4d9e
commit
4dc02d74b8
1 file changed
+100
-89
@@ -1,6 +1,7 @@
|
||||
/* Общий miniaudio-контекст и выход desktop-приложения: звуки событий, PCM и голосовые потоки.
|
||||
* GUI управляет устройствами и playback-сессиями; output смешивает звук и отдаёт reference для AEC.
|
||||
* init/shutdown ограничивают время жизни контекста; зависимые capture devices закрываются перед его сбросом. */
|
||||
/* Общий miniaudio-контекст и микшер desktop: звуки событий, PCM, stream и до двух VoiceAudioIo (call/radio).
|
||||
* API и singleton принадлежат GUI; аппаратный output смешивает 48k stereo и отдаёт итоговый PCM как reference для AEC.
|
||||
* init подготавливает микшер независимо от устройства; GUI-таймер восстанавливает context/output после сбоев.
|
||||
* Владельцы закрывают зависимые capture devices в прямом contextAboutToReset и отсоединяют stream callbacks до shutdown. */
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
@@ -26,132 +27,142 @@ class SoundManager : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
struct EventSettings {
|
||||
bool enabled = true;
|
||||
float volume = 0.8f;
|
||||
bool enabled = true; // Допуск нового play/playLoop этого события.
|
||||
float volume = 0.8f; // Множитель общей громкости сигнала, 0..1.
|
||||
};
|
||||
|
||||
static SoundManager* instance();
|
||||
static SoundManager* instance(); // Процессный singleton; первый вызов в GUI, destructor выполняет shutdown.
|
||||
|
||||
bool init();
|
||||
void shutdown();
|
||||
bool init(); // Идемпотентно создать микшер и запустить recovery; true не гарантирует context/работающий output.
|
||||
void shutdown(); // Закрыть устройства, PCM и сигналы; contextAboutToReset предшествует uninit. Повтор безопасен.
|
||||
|
||||
void setEnabled(bool enabled);
|
||||
void setEnabled(bool enabled); // Разрешение последующих play/playLoop; текущие звуки, raw PCM и voice не останавливает.
|
||||
bool isEnabled() const;
|
||||
void setVolume(float volume);
|
||||
void setVolume(float volume); // Ограничить 0..1 для нового сигнала/PCM-узла; уже созданные узлы и voice не меняет.
|
||||
float volume() const;
|
||||
|
||||
/* Сохранить enable/volume для следующих play/playLoop; volume ограничивается 0..1. Настройки не пишутся в БД. */
|
||||
void setEvent(const QString& name, bool enabled, float volume);
|
||||
EventSettings eventSettings(const QString& name) const;
|
||||
EventSettings eventSettings(const QString& name) const; // Неизвестное имя возвращает defaults EventSettings.
|
||||
|
||||
bool loadSound(const QString& name, const QString& filePath);
|
||||
void play(const QString& name);
|
||||
void playLoop(const QString& name);
|
||||
void stopLoop();
|
||||
bool loadSound(const QString& name, const QString& filePath); // После init: декодировать короткий файл в 48k stereo PCM; false при дубле/сбое.
|
||||
void play(const QString& name); // С начала перезапустить загруженный сигнал, если разрешён; громкость master * event.
|
||||
void playLoop(const QString& name); // Один отслеживаемый цикл: при общем enable сначала останавливает предыдущий, затем проверяет событие.
|
||||
void stopLoop(); // Остановить/перемотать отслеживаемый цикл независимо от enable; остальные PCM не затрагивает.
|
||||
|
||||
ma_context* context();
|
||||
ma_context* context(); // Заимствован до contextAboutToReset; nullptr до готовности/во время recovery/после shutdown.
|
||||
/* Разобрать key текущего backend в обязательный id. false для default/нет context/битого ID; id тогда не использовать. */
|
||||
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; }
|
||||
void setDevices(const QString& output, const QString& input); // Сохранить желаемые ID (пусто=default), смена output переоткрывает его.
|
||||
QString playbackDevice() const { return m_playbackId; } // Желаемый ID, может отличаться от fallback/физического выхода.
|
||||
QString captureDevice() const { return m_captureId; } // Желаемый вход; зависимые владельцы применяют его по devicesChanged.
|
||||
|
||||
/* GUI: voice живёт до detachVoice; изменение списка выполняется при остановленном output. */
|
||||
/* GUI: voice заимствован до detachVoice/shutdown; список меняется при остановленном output.
|
||||
* Сначала снимает прежнее подключение voice; false при неготовом context/превышении общего лимита двух источников.
|
||||
* true означает регистрацию для выбранного выхода с default fallback/recovery, не аппаратную готовность. */
|
||||
bool attachVoice(VoiceAudioIo* voice, const QString& output);
|
||||
void detachVoice(VoiceAudioIo* voice);
|
||||
/* GUI: callback добавляет PCM 48k stereo в общий float output; firstTimeUs — время первого sample.
|
||||
* generation меняется после restart/смены устройства. detach ждёт текущий callback, не останавливая других.
|
||||
* ID/владельцы отдельны от голосовых звонков; у каждого источника собственные media-часы. */
|
||||
void detachVoice(VoiceAudioIo* voice); // Завершает доступ output callback к voice; nullptr/отсутствующий voice допустимы.
|
||||
/* mix в output-потоке добавляет PCM 48k stereo в общий float-буфер: frames на канал, firstTimeUs — steady clock первого фрейма.
|
||||
* generation — метка переустройства общего output, сравнивать на неравенство. Аргументы заимствованы до возврата.
|
||||
* Callback быстрый, не вызывает API менеджера: выполняется под m_streamLock. GUI detach ждёт текущий callback.
|
||||
* ID/владельцы отдельны от VoiceAudioIo; каждому источнику нужны собственные media-часы. */
|
||||
using StreamMix = std::function<void(float*, unsigned, int64_t, uint64_t)>;
|
||||
bool attachStream(uint64_t id, StreamMix mix);
|
||||
void detachStream(uint64_t id);
|
||||
bool outputAvailable() const { return m_output.initialized() && !m_resetting; }
|
||||
bool attachStream(uint64_t id, StreamMix mix); // После init: принять callback с ненулевым уникальным id; false при неверном аргументе/дубле.
|
||||
void detachStream(uint64_t id); // Удаляет callback синхронно; освобождать его внешние ресурсы можно после возврата.
|
||||
bool outputAvailable() const { return m_output.initialized() && !m_resetting; } // Наличие device вне reset, не проверка прогресса callback.
|
||||
|
||||
using PlaybackId = uint64_t;
|
||||
/* GUI: независимая сессия. owner удерживает PCM до detach; без owner данные копируются. */
|
||||
/* GUI: отдельная сессия interleaved s16, frameCount>0 на канал, sampleRate>0, channels=1/2.
|
||||
* Нужен микшер после init; устройство может восстанавливаться. Возврат: ненулевой handle либо 0 при ошибке.
|
||||
* owner должен содержать PCM начиная с data() и удерживает неизменяемые данные; без owner они копируются.
|
||||
* Обычная сессия живёт до stopPcm/shutdown, включая конец/паузу. transient удаляется recovery-таймером, когда перестаёт играть. */
|
||||
PlaybackId playRawPcm(const int16_t* pcm, int frameCount, int sampleRate, int channels,
|
||||
std::shared_ptr<std::vector<int16_t>> owner = {}, bool transient = false);
|
||||
void pausePcm(PlaybackId id);
|
||||
void resumePcm(PlaybackId id);
|
||||
void stopPcm(PlaybackId id);
|
||||
bool seekPcm(PlaybackId id, uint64_t frame);
|
||||
uint64_t pcmCursor(PlaybackId id) const;
|
||||
bool isPcmPlaying(PlaybackId id) const;
|
||||
int pcmSampleRate(PlaybackId id) const;
|
||||
void pausePcm(PlaybackId id); // Остановить и отсоединить узел от графа, дождавшись текущего чтения; неизвестный id — no-op.
|
||||
void resumePcm(PlaybackId id); // Подключить/запустить существующий узел; неизвестный id — no-op.
|
||||
void stopPcm(PlaybackId id); // Удалить сессию и освободить её owner после завершения чтения графа.
|
||||
bool seekPcm(PlaybackId id, uint64_t frame); // Позиция в исходных PCM-фреймах, ограничивается длиной; сохраняет playing/paused, false при сбое.
|
||||
uint64_t pcmCursor(PlaybackId id) const; // Исходные PCM-фреймы с учётом seek; для отсутствующего узла 0, максимум длина сессии.
|
||||
bool isPcmPlaying(PlaybackId id) const; // Флаг playing узла; не гарантия работающего устройства, неизвестный id — false.
|
||||
int pcmSampleRate(PlaybackId id) const; // Исходная частота либо 0 для неизвестного id.
|
||||
|
||||
struct DeviceInfo {
|
||||
QString name;
|
||||
bool isDefault = false;
|
||||
QString id;
|
||||
QString name; // Читаемое имя устройства.
|
||||
bool isDefault = false; // Default, сообщённый backend при перечислении.
|
||||
QString id; // Непрозрачный backend/base64 ID для настроек/deviceId.
|
||||
};
|
||||
/* GUI: снимок устройств текущего context; пустой список при отсутствии context/ошибке перечисления. */
|
||||
static QVector<DeviceInfo> enumeratePlaybackDevices();
|
||||
static QVector<DeviceInfo> enumerateCaptureDevices();
|
||||
|
||||
signals:
|
||||
void contextAboutToReset(); // Все зависимые устройства закрываются синхронно перед uninit контекста.
|
||||
void contextRestored();
|
||||
void devicesChanged();
|
||||
void contextAboutToReset(); // Прямые GUI-обработчики синхронно закрывают зависимые устройства перед uninit, также при shutdown.
|
||||
void contextRestored(); // Новый context готов; output в этот момент ещё может быть не открыт.
|
||||
void devicesChanged(); // Изменились желаемые ID setDevices, не событие обнаружения hotplug.
|
||||
|
||||
private:
|
||||
SoundManager();
|
||||
~SoundManager();
|
||||
|
||||
struct PcmSession {
|
||||
ma_sound* sound = nullptr;
|
||||
ma_audio_buffer* buffer = nullptr;
|
||||
std::shared_ptr<std::vector<int16_t>> owner;
|
||||
uint64_t frames = 0, origin = 0;
|
||||
int rate = 0;
|
||||
bool transient = false;
|
||||
bool detached = false;
|
||||
~PcmSession();
|
||||
ma_sound* sound = nullptr; // Собственный узел графа; пересоздаётся при seek/restart сигнала.
|
||||
ma_audio_buffer* buffer = nullptr; // Собственный источник: копия PCM либо заимствование из owner.
|
||||
std::shared_ptr<std::vector<int16_t>> owner; // Удерживает исходный PCM до удаления узла/буфера.
|
||||
uint64_t frames = 0, origin = 0; // Полная длина и начало текущего узла, исходные фреймы.
|
||||
int rate = 0; // Частота исходного PCM, Гц.
|
||||
bool transient = false; // Удалить из m_pcmSessions, когда узел больше не playing (включая pause).
|
||||
bool detached = false; // Узел отсоединён от engine endpoint при pause.
|
||||
~PcmSession(); // Завершает чтение графа и уничтожает sound/buffer до освобождения owner.
|
||||
};
|
||||
PcmSession* pcmSession(PlaybackId id) const;
|
||||
bool createPcmNode(PcmSession& session, bool playing);
|
||||
void recoverAudio();
|
||||
bool openContext();
|
||||
void openOutput();
|
||||
PcmSession* pcmSession(PlaybackId id) const; // GUI: заимствованная сессия либо nullptr.
|
||||
bool createPcmNode(PcmSession& session, bool playing); // Новый sound поверх готового buffer; false при init/start ошибке.
|
||||
void recoverAudio(); // GUI-таймер: очистка transient, диагностика, восстановление context/outputs и объединение маршрутов.
|
||||
bool openContext(); // Создать context с backoff 250..2000мс при ошибке; true публикует contextRestored.
|
||||
void openOutput(); // Открыть/запустить общий output, сбросить playback voice; явный ID имеет fallback на default.
|
||||
|
||||
struct VoiceRoute {
|
||||
AudioDevice device;
|
||||
QString wanted, actual;
|
||||
std::array<VoiceAudioIo*, 2> voices{};
|
||||
AudioDevice device; // Собственный отдельный playback, закрывается перед освобождением voices.
|
||||
QString wanted, actual; // Разрешённый ID маршрута и последний успешный ID с учётом default fallback.
|
||||
std::array<VoiceAudioIo*, 2> voices{}; // Заимствованные voice этого выхода, меняются только при остановленном device.
|
||||
};
|
||||
QString resolveOutput(const QString& requested);
|
||||
bool openVoiceRoute(VoiceRoute& route);
|
||||
void mergeVoiceRoutes();
|
||||
QString resolveOutput(const QString& requested); // Найденный ID либо ID default; пусто при отсутствии подходящего устройства.
|
||||
bool openVoiceRoute(VoiceRoute& route); // Открыть отдельный output с fallback; false при неготовности/отказе backend.
|
||||
void mergeVoiceRoutes(); // GUI: объединить совпавшие физические выходы, исключая двойное проигрывание/AEC reference.
|
||||
/* Output: смешать engine/voices/streams, ограничить PCM и передать AEC reference; вернуть число ошибок чтения engine. */
|
||||
static unsigned renderOutput(ma_engine* engine, const std::array<VoiceAudioIo*, 2>& voices,
|
||||
float* out, unsigned frames, int64_t firstTimeUs, bool measured, SoundManager* streams = nullptr);
|
||||
std::mutex m_streamLock;
|
||||
std::map<uint64_t, StreamMix> m_streams;
|
||||
std::atomic<uint64_t> m_outputGeneration{0};
|
||||
std::mutex m_streamLock; // Защищает список и всё выполнение stream callbacks.
|
||||
std::map<uint64_t, StreamMix> m_streams; // Callbacks живут до явного detachStream, в том числе через recovery/shutdown.
|
||||
std::atomic<uint64_t> m_outputGeneration{0}; // Метка переустройства output для stream callback.
|
||||
float m_streamLimiter = 1.0f; // Только output callback; быстрый attack, release 100ms.
|
||||
std::atomic<unsigned> m_renderErrors{0};
|
||||
std::array<VoiceAudioIo*, 2> m_voices{};
|
||||
std::map<QString, std::unique_ptr<VoiceRoute>> m_voiceRoutes;
|
||||
std::map<VoiceAudioIo*, QString> m_voiceSelections; // Desired выбор сохраняется при объединении physical outputs.
|
||||
QString m_actualOutput;
|
||||
bool m_resetting = false;
|
||||
bool m_restoreSelections = false;
|
||||
std::atomic<unsigned> m_renderErrors{0}; // Ошибки общего output, потребляются/логируются GUI-таймером.
|
||||
std::array<VoiceAudioIo*, 2> m_voices{}; // Заимствованные voice общего выхода; изменение при остановленном output.
|
||||
std::map<QString, std::unique_ptr<VoiceRoute>> m_voiceRoutes; // Собственные отдельные выходы по разрешённому ID.
|
||||
std::map<VoiceAudioIo*, QString> m_voiceSelections; // Исходный выбор voice сохраняется при объединении physical outputs.
|
||||
QString m_actualOutput; // Последний успешный ID общего выхода с учётом default fallback.
|
||||
bool m_resetting = false; // Context закрывается/ожидает восстановления; запрещает подключение voice и открытие их выходов.
|
||||
bool m_restoreSelections = false; // После запуска общего output нужно восстановить отдельные желаемые выходы voice.
|
||||
|
||||
struct LoadedSound {
|
||||
std::shared_ptr<PcmSession> pcm;
|
||||
std::shared_ptr<PcmSession> pcm; // Предварительно декодированный сигнал и собственный узел графа.
|
||||
};
|
||||
|
||||
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;
|
||||
std::map<PlaybackId, std::unique_ptr<PcmSession>> m_pcmSessions;
|
||||
PlaybackId m_nextPlaybackId = 0;
|
||||
bool m_initialized = false;
|
||||
bool m_enabled = true;
|
||||
float m_volume = 0.8f;
|
||||
QHash<QString, EventSettings> m_eventSettings;
|
||||
QHash<QString, LoadedSound> m_sounds;
|
||||
QString m_loopName;
|
||||
ma_engine* m_engine = nullptr; // Собственный микшер 48k stereo, не владеет аппаратным устройством/context.
|
||||
ma_context* m_context = nullptr; // Общий собственный context, заменяется при recovery.
|
||||
AudioDevice m_output; // Общий выход для engine, streams и присоединённых voice.
|
||||
QTimer m_recoveryTimer; // GUI: проверка восстановления/очистки каждые 200мс.
|
||||
QElapsedTimer m_clock; // Монотонные мс для backoff context.
|
||||
qint64 m_contextRetryAt = 0; // Следующая допустимая попытка создать context, мс m_clock.
|
||||
int m_contextRetryDelay = 250; // Следующий backoff context, 250..2000мс.
|
||||
QString m_playbackId, m_captureId; // Сохранённые желаемые output/input, пусто=default.
|
||||
ma_backend m_backend = ma_backend_null; // Последний успешно открытый backend, сохраняется при recovery.
|
||||
bool m_backendSelected = false; // После первого открытия восстанавливается тот же backend.
|
||||
std::map<PlaybackId, std::unique_ptr<PcmSession>> m_pcmSessions; // Независимые raw PCM, owner освобождается с сессией.
|
||||
PlaybackId m_nextPlaybackId = 0; // Последний выданный handle; не сбрасывается shutdown.
|
||||
bool m_initialized = false; // Микшер создан, recovery разрешён; context/output могут отсутствовать.
|
||||
bool m_enabled = true; // Допуск новых сигналов play/playLoop.
|
||||
float m_volume = 0.8f; // Громкость последующих сигналов/создаваемых PCM-узлов.
|
||||
QHash<QString, EventSettings> m_eventSettings; // Настройки событий в памяти, сохраняются через shutdown.
|
||||
QHash<QString, LoadedSound> m_sounds; // Собственные загруженные сигналы, очищаются shutdown.
|
||||
QString m_loopName; // Имя единственного отслеживаемого циклического сигнала либо пусто.
|
||||
};
|
||||
Reference in new issue
Block a user