build-dev / nix-build (push) Successful in 45s
recreateMixerOnDevice destroyed the active mixer before opening the
replacement. MIX_DestroyMixer quits SDL audio at refcount 0, tearing down
the device registry and renumbering device ids, so the cached id being
reopened went stale ('Invalid audio device instance ID') and the app went
permanently mute. Open the new mixer first while the old device is alive,
then tear the old one down; a failed switch now keeps the current output
playing instead of killing audio. Also log SDL_GetError() on open failure.
138 lines
5.0 KiB
C++
138 lines
5.0 KiB
C++
#pragma once
|
|
|
|
#include "config/config_manager.h"
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3_mixer/SDL_mixer.h>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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<AudioDeviceItem> 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
|