diff --git a/tools/chatgui/src/sound_manager.h b/tools/chatgui/src/sound_manager.h index 810343b3..10bbf5ed 100644 --- a/tools/chatgui/src/sound_manager.h +++ b/tools/chatgui/src/sound_manager.h @@ -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 @@ -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; - 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> 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 enumeratePlaybackDevices(); static QVector 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> 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> 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 voices{}; + AudioDevice device; // Собственный отдельный playback, закрывается перед освобождением voices. + QString wanted, actual; // Разрешённый ID маршрута и последний успешный ID с учётом default fallback. + std::array 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& voices, float* out, unsigned frames, int64_t firstTimeUs, bool measured, SoundManager* streams = nullptr); - std::mutex m_streamLock; - std::map m_streams; - std::atomic m_outputGeneration{0}; + std::mutex m_streamLock; // Защищает список и всё выполнение stream callbacks. + std::map m_streams; // Callbacks живут до явного detachStream, в том числе через recovery/shutdown. + std::atomic m_outputGeneration{0}; // Метка переустройства output для stream callback. float m_streamLimiter = 1.0f; // Только output callback; быстрый attack, release 100ms. - std::atomic m_renderErrors{0}; - std::array m_voices{}; - std::map> m_voiceRoutes; - std::map m_voiceSelections; // Desired выбор сохраняется при объединении physical outputs. - QString m_actualOutput; - bool m_resetting = false; - bool m_restoreSelections = false; + std::atomic m_renderErrors{0}; // Ошибки общего output, потребляются/логируются GUI-таймером. + std::array m_voices{}; // Заимствованные voice общего выхода; изменение при остановленном output. + std::map> m_voiceRoutes; // Собственные отдельные выходы по разрешённому ID. + std::map 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 pcm; + std::shared_ptr 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> m_pcmSessions; - PlaybackId m_nextPlaybackId = 0; - bool m_initialized = false; - bool m_enabled = true; - float m_volume = 0.8f; - QHash m_eventSettings; - QHash 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> 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 m_eventSettings; // Настройки событий в памяти, сохраняются через shutdown. + QHash m_sounds; // Собственные загруженные сигналы, очищаются shutdown. + QString m_loopName; // Имя единственного отслеживаемого циклического сигнала либо пусто. };