/* Общий miniaudio-контекст и выход desktop-приложения: звуки событий, PCM и голосовые потоки. * GUI управляет устройствами и playback-сессиями; output смешивает звук и отдаёт reference для AEC. * init/shutdown ограничивают время жизни контекста; зависимые capture devices закрываются перед его сбросом. */ #pragma once #include #include #include #include #include #include #include #include #include #include #include "audio_device.h" struct ma_engine; struct ma_context; struct ma_decoder; struct ma_sound; class VoiceAudioIo; class SoundManager : public QObject { Q_OBJECT public: struct EventSettings { bool enabled = true; float volume = 0.8f; }; static SoundManager* instance(); bool init(); void shutdown(); void setEnabled(bool enabled); bool isEnabled() const; void setVolume(float volume); float volume() const; void setEvent(const QString& name, bool enabled, float volume); EventSettings eventSettings(const QString& name) const; bool loadSound(const QString& name, const QString& filePath); void play(const QString& name); void playLoop(const QString& name); void stopLoop(); ma_context* context(); 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; } /* GUI: voice живёт до detachVoice; изменение списка выполняется при остановленном output. */ bool attachVoice(VoiceAudioIo* voice, const QString& output); void detachVoice(VoiceAudioIo* voice); using PlaybackId = uint64_t; /* GUI: независимая сессия. owner удерживает PCM до detach; без owner данные копируются. */ 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; struct DeviceInfo { QString name; bool isDefault = false; QString id; }; static QVector enumeratePlaybackDevices(); static QVector enumerateCaptureDevices(); signals: void contextAboutToReset(); // Все зависимые устройства закрываются синхронно перед uninit контекста. void contextRestored(); void devicesChanged(); 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(); }; PcmSession* pcmSession(PlaybackId id) const; bool createPcmNode(PcmSession& session, bool playing); void recoverAudio(); bool openContext(); void openOutput(); struct VoiceRoute { AudioDevice device; QString wanted, actual; std::array voices{}; }; QString resolveOutput(const QString& requested); bool openVoiceRoute(VoiceRoute& route); void mergeVoiceRoutes(); static unsigned renderOutput(ma_engine* engine, const std::array& voices, float* out, unsigned frames, int64_t firstTimeUs); 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; struct LoadedSound { 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; };