#pragma once #include "config/config_manager.h" #include #include #include #include #include namespace radiantwave::db { class DatabaseManager; } namespace radiantwave::audio { struct AudioDeviceItem { std::string raw_name; // e.g. alsa_output.pci-0000_00_1f.3.analog-stereo std::string friendly_name; // e.g. "Built-In Speakers" SDL_AudioDeviceID id = 0; // SDL playback device id (0 = synthetic fallback) }; // Maps raw technical audio device strings to user-friendly labels std::string mapToFriendlyDeviceName(const std::string& raw_name); class AudioManager { public: AudioManager(); ~AudioManager(); // Non-copyable AudioManager(const AudioManager&) = delete; AudioManager& operator=(const AudioManager&) = delete; // Initialize SDL3_mixer audio system bool initialize(config::ConfigManager& config); // Provide a database handle for user-facing display-name lookups. void attachDatabase(db::DatabaseManager& db); // Shutdown audio subsystem void shutdown(); // Enumerate playback devices with friendly names std::vector getPlaybackDevices() const; // Master volume control (0 - 100) void setVolume(int volume_0_to_100); int getVolume() const; // Master audio enable / mute toggle void setAudioEnabled(bool enabled); bool isAudioEnabled() const; // Audio device selection bool switchDevice(const std::string& raw_device_name); bool switchDeviceById(SDL_AudioDeviceID id); std::string getCurrentDevice() const; SDL_AudioDeviceID getCurrentDeviceId() const; // Handle an SDL audio device event (SDL_EVENT_AUDIO_DEVICE_ADDED / REMOVED). // Returns true if the active device was affected and the mixer was recreated. bool handleDeviceEvent(const SDL_Event& event); // Pure helper (no SDL audio open): find the playback device with the given // SDL id, returning its raw name ("" when not found / id invalid). Exposed // for tests; the real lookup is through getPlaybackDevices(). static std::string resolvePlaybackDeviceName(SDL_AudioDeviceID id); // Builds the SDL_PropertiesID passed to MIX_PlayTrack() for tracks that // loop forever (MIX_PROP_PLAY_LOOPS_NUMBER = -1). Exposed for tests; no // mixer, track, or audio device is required. The caller owns the property // set and must release it with SDL_DestroyProperties(). static SDL_PropertiesID createInfiniteLoopOptions(); // Playback control bool playTrack(const std::string& wav_filename); void stopTrack(); // Filename of the track currently loaded in the active slot ("" if none). std::string currentTrack() const; // Fade playback control (used by the scroller page) bool playTrackFadeIn(const std::string& wav_filename, float seconds); bool playTrackCrossfade(const std::string& wav_filename, float seconds); // Fades any playing track out over `seconds`. Returns false when nothing // was playing, so callers can skip teardown delays tied to the fade. bool stopTrackFadeOut(float seconds); // Drive in-progress fade ramps; call once per frame. void update(float dt); private: struct TrackSlot { MIX_Track* track = nullptr; MIX_Audio* audio = nullptr; float fadeInDuration = 0.0f; // >0 while ramping in (seconds) float fadeInElapsed = 0.0f; float fadeOutRemaining = 0.0f; // >0 while fading out (seconds) }; float effectiveGain() const; bool loadTrackInto(int slotIndex, const std::string& wav_filename); void startFadeIn(int slotIndex, float seconds); std::string displayNameFor(const std::string& wav_filename) const; // Starts the track on the given slot with infinite looping. The track must // already be loaded on the slot. Calling this on a playing/stopped/fading // track restarts it (cancelling any in-flight stop-fade). bool playTrackLooping(int slotIndex); // Recreates the mixer on the given playback device id (0 = system default). // Called with mutex_ held. Preserves playback state: whichever track was // playing on the active slot is reloaded and resumed at full gain, an // in-progress fade is restarted. The replacement mixer is opened before the // current one is destroyed so SDL's device ids stay valid (destroy-first // quits the whole audio subsystem and orphans the id being opened). // Returns false on failure without touching the current mixer, so the // existing output keeps playing. bool recreateMixerOnDevice(SDL_AudioDeviceID targetId); config::ConfigManager* configManager_ = nullptr; db::DatabaseManager* db_ = nullptr; bool initialized_ = false; bool enabled_ = true; int volume_ = 80; // 0 - 100 std::string currentDevice_ = ""; SDL_AudioDeviceID currentDeviceId_ = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; std::string currentTrack_ = ""; MIX_Mixer* mixer_ = nullptr; TrackSlot slots_[2]; int activeSlot_ = 0; mutable std::mutex mutex_; }; } // namespace radiantwave::audio