804 lines
26 KiB
C++
804 lines
26 KiB
C++
#include "audio_manager.h"
|
|
#include "db/database_manager.h"
|
|
#include "logger/logger.h"
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <filesystem>
|
|
#include <iostream>
|
|
|
|
namespace radiantwave::audio
|
|
{
|
|
|
|
std::string mapToFriendlyDeviceName(const std::string &raw_name)
|
|
{
|
|
if (raw_name.empty())
|
|
return "Default Audio Output";
|
|
|
|
std::string lower = raw_name;
|
|
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c)
|
|
{ return std::tolower(c); });
|
|
|
|
if (lower.find("hdmi") != std::string::npos ||
|
|
lower.find("displayport") != std::string::npos ||
|
|
lower.find("radeon") != std::string::npos ||
|
|
lower.find("nvidia") != std::string::npos ||
|
|
lower.find("tv") != std::string::npos)
|
|
{
|
|
return "TV / HDMI Monitor";
|
|
}
|
|
|
|
if (lower.find("analog") != std::string::npos ||
|
|
lower.find("built-in") != std::string::npos ||
|
|
lower.find("builtin") != std::string::npos ||
|
|
lower.find("realtek") != std::string::npos ||
|
|
lower.find("hda") != std::string::npos ||
|
|
lower.find("speaker") != std::string::npos ||
|
|
lower.find("pci") != std::string::npos)
|
|
{
|
|
return "Built-In Speakers";
|
|
}
|
|
|
|
if (lower.find("usb") != std::string::npos ||
|
|
lower.find("headset") != std::string::npos ||
|
|
lower.find("dongle") != std::string::npos)
|
|
{
|
|
return "USB Audio Device";
|
|
}
|
|
|
|
if (lower.find("bluez") != std::string::npos ||
|
|
lower.find("bluetooth") != std::string::npos)
|
|
{
|
|
return "Bluetooth Audio";
|
|
}
|
|
|
|
// Fallback cleanly
|
|
return "Audio Device (" + raw_name.substr(0, 20) + ")";
|
|
}
|
|
|
|
// ─── AudioManager Implementation ───────────────────────────────────────
|
|
|
|
AudioManager::AudioManager() = default;
|
|
|
|
AudioManager::~AudioManager()
|
|
{
|
|
shutdown();
|
|
}
|
|
|
|
bool AudioManager::initialize(config::ConfigManager &config)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
configManager_ = &config;
|
|
|
|
auto cfg = config.get();
|
|
volume_ = cfg.audio.volume;
|
|
enabled_ = cfg.audio.enabled;
|
|
currentDevice_ = cfg.audio.output_device;
|
|
currentTrack_ = cfg.audio.current_track;
|
|
if (!MIX_Init())
|
|
{
|
|
std::string err = SDL_GetError();
|
|
logger::Logger::instance().warning("Audio",
|
|
"Audio library initialization failed. Sound will be muted until hardware is available (MIX_Init error: " + err + ")");
|
|
initialized_ = false;
|
|
return false;
|
|
}
|
|
|
|
// Try to open the mixer on the device previously persisted in config;
|
|
// fall back to the system default when the configured device is gone.
|
|
SDL_AudioDeviceID targetId = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
|
|
if (!currentDevice_.empty())
|
|
{
|
|
int count = 0;
|
|
SDL_AudioDeviceID *devices = SDL_GetAudioPlaybackDevices(&count);
|
|
if (devices)
|
|
{
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
const char *name = SDL_GetAudioDeviceName(devices[i]);
|
|
if (name && currentDevice_ == name)
|
|
{
|
|
targetId = devices[i];
|
|
break;
|
|
}
|
|
}
|
|
SDL_free(devices);
|
|
}
|
|
}
|
|
|
|
mixer_ = MIX_CreateMixerDevice(targetId, nullptr);
|
|
if (!mixer_)
|
|
{
|
|
std::string err = SDL_GetError();
|
|
logger::Logger::instance().warning("Audio",
|
|
"Could not open audio playback device. Check speaker connections (MIX_CreateMixerDevice error: " + err + ")");
|
|
initialized_ = false;
|
|
return false;
|
|
}
|
|
|
|
currentDeviceId_ = targetId;
|
|
|
|
slots_[0].track = MIX_CreateTrack(mixer_);
|
|
slots_[1].track = MIX_CreateTrack(mixer_);
|
|
initialized_ = true;
|
|
|
|
// Apply initial volume
|
|
float gain = enabled_ ? (volume_ / 100.0f) : 0.0f;
|
|
MIX_SetMixerGain(mixer_, gain);
|
|
|
|
logger::Logger::instance().debug("Audio", "SDL3_mixer audio system initialized successfully");
|
|
return true;
|
|
}
|
|
|
|
void AudioManager::attachDatabase(db::DatabaseManager& db)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
db_ = &db;
|
|
}
|
|
|
|
std::string AudioManager::displayNameFor(const std::string& wav_filename) const
|
|
{
|
|
if (db_)
|
|
{
|
|
for (const auto& record : db_->getAudioFiles())
|
|
{
|
|
if (record.filename == wav_filename)
|
|
{
|
|
return record.display_name;
|
|
}
|
|
}
|
|
}
|
|
return wav_filename;
|
|
}
|
|
|
|
float AudioManager::effectiveGain() const
|
|
{
|
|
return enabled_ ? (volume_ / 100.0f) : 0.0f;
|
|
}
|
|
|
|
SDL_PropertiesID AudioManager::createInfiniteLoopOptions()
|
|
{
|
|
// MIX_PlayTrack()'s options default to a single pass; requesting
|
|
// infinite loops is what keeps a track going until it is explicitly
|
|
// faded out or the app shuts down.
|
|
SDL_PropertiesID props = SDL_CreateProperties();
|
|
if (props)
|
|
{
|
|
SDL_SetNumberProperty(props, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
|
|
}
|
|
return props;
|
|
}
|
|
|
|
bool AudioManager::playTrackLooping(int slotIndex)
|
|
{
|
|
if (!initialized_ || !mixer_ || slotIndex < 0 || slotIndex >= 2 ||
|
|
!slots_[slotIndex].track)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
SDL_PropertiesID props = createInfiniteLoopOptions();
|
|
if (!props)
|
|
{
|
|
logger::Logger::instance().error(
|
|
"Audio", "Failed to create playback options for " +
|
|
displayNameFor(currentTrack_));
|
|
return false;
|
|
}
|
|
bool ok = MIX_PlayTrack(slots_[slotIndex].track, props);
|
|
SDL_DestroyProperties(props);
|
|
return ok;
|
|
}
|
|
|
|
void AudioManager::shutdown()
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
// Tracks are owned by the mixer: MIX_DestroyMixer() stops and destroys
|
|
// every track created for it, so MIX_DestroyTrack must not be called
|
|
// here (it can race the mixer thread's deferred-destroy path). Only the
|
|
// MIX_Audio objects are ours to release; each track holds its own
|
|
// reference to the audio attached to it, and any audio left behind is
|
|
// reclaimed by MIX_Quit().
|
|
for (int i = 0; i < 2; ++i)
|
|
{
|
|
if (slots_[i].audio)
|
|
{
|
|
MIX_DestroyAudio(slots_[i].audio);
|
|
slots_[i].audio = nullptr;
|
|
}
|
|
slots_[i].track = nullptr;
|
|
}
|
|
|
|
if (mixer_)
|
|
{
|
|
MIX_DestroyMixer(mixer_);
|
|
mixer_ = nullptr;
|
|
}
|
|
|
|
if (initialized_)
|
|
{
|
|
MIX_Quit();
|
|
initialized_ = false;
|
|
}
|
|
}
|
|
|
|
std::vector<AudioDeviceItem> AudioManager::getPlaybackDevices() const
|
|
{
|
|
std::vector<AudioDeviceItem> list;
|
|
|
|
int count = 0;
|
|
SDL_AudioDeviceID *devices = SDL_GetAudioPlaybackDevices(&count);
|
|
|
|
if (devices && count > 0)
|
|
{
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
const char *name = SDL_GetAudioDeviceName(devices[i]);
|
|
std::string raw = name ? name : "";
|
|
AudioDeviceItem item;
|
|
item.raw_name = raw;
|
|
item.friendly_name = mapToFriendlyDeviceName(raw);
|
|
item.id = devices[i];
|
|
list.push_back(item);
|
|
}
|
|
SDL_free(devices);
|
|
}
|
|
else
|
|
{
|
|
list.push_back({"", "Built-In Speakers", 0});
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
std::string AudioManager::resolvePlaybackDeviceName(SDL_AudioDeviceID id)
|
|
{
|
|
if (id == 0 || id == SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
int count = 0;
|
|
SDL_AudioDeviceID *devices = SDL_GetAudioPlaybackDevices(&count);
|
|
std::string found;
|
|
if (devices)
|
|
{
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
if (devices[i] == id)
|
|
{
|
|
const char *name = SDL_GetAudioDeviceName(devices[i]);
|
|
found = name ? name : "";
|
|
break;
|
|
}
|
|
}
|
|
SDL_free(devices);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
void AudioManager::setVolume(int volume_0_to_100)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
volume_ = std::clamp(volume_0_to_100, 0, 100);
|
|
|
|
if (mixer_)
|
|
{
|
|
float gain = enabled_ ? (volume_ / 100.0f) : 0.0f;
|
|
MIX_SetMixerGain(mixer_, gain);
|
|
}
|
|
|
|
if (configManager_)
|
|
{
|
|
configManager_->update([this](config::AppConfig &cfg)
|
|
{ cfg.audio.volume = volume_; });
|
|
}
|
|
}
|
|
|
|
int AudioManager::getVolume() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
return volume_;
|
|
}
|
|
|
|
void AudioManager::setAudioEnabled(bool enabled)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
enabled_ = enabled;
|
|
|
|
if (mixer_)
|
|
{
|
|
float gain = enabled_ ? (volume_ / 100.0f) : 0.0f;
|
|
MIX_SetMixerGain(mixer_, gain);
|
|
}
|
|
|
|
if (enabled_)
|
|
{
|
|
logger::Logger::instance().info("Audio", "Audio unmuted");
|
|
}
|
|
else
|
|
{
|
|
logger::Logger::instance().info("Audio", "Audio muted");
|
|
}
|
|
|
|
if (configManager_)
|
|
{
|
|
configManager_->update([enabled](config::AppConfig &cfg)
|
|
{ cfg.audio.enabled = enabled; });
|
|
}
|
|
}
|
|
|
|
bool AudioManager::isAudioEnabled() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
return enabled_;
|
|
}
|
|
|
|
bool AudioManager::switchDevice(const std::string &raw_device_name)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
// Resolve the config's raw name to an SDL device id so the mixer is
|
|
// actually reopened on the requested device (""/unresolvable = default).
|
|
SDL_AudioDeviceID targetId = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
|
|
if (!raw_device_name.empty())
|
|
{
|
|
int count = 0;
|
|
SDL_AudioDeviceID *devices = SDL_GetAudioPlaybackDevices(&count);
|
|
if (devices)
|
|
{
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
const char *name = SDL_GetAudioDeviceName(devices[i]);
|
|
if (name && raw_device_name == name)
|
|
{
|
|
targetId = devices[i];
|
|
break;
|
|
}
|
|
}
|
|
SDL_free(devices);
|
|
}
|
|
}
|
|
|
|
return switchDeviceById(targetId);
|
|
}
|
|
|
|
bool AudioManager::switchDeviceById(SDL_AudioDeviceID id)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
// 0 is the synthetic "default / built-in" entry from getPlaybackDevices().
|
|
if (id == 0)
|
|
{
|
|
id = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
|
|
}
|
|
|
|
// No mixer yet: just record the choice for initialize().
|
|
if (!mixer_ || !initialized_)
|
|
{
|
|
currentDeviceId_ = id;
|
|
currentDevice_ = resolvePlaybackDeviceName(id);
|
|
if (configManager_)
|
|
{
|
|
configManager_->update([name = currentDevice_](config::AppConfig &cfg)
|
|
{ cfg.audio.output_device = name; });
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (!recreateMixerOnDevice(id))
|
|
{
|
|
logger::Logger::instance().error("Audio",
|
|
"Failed to switch audio output device to: " + mapToFriendlyDeviceName(resolvePlaybackDeviceName(id)));
|
|
return false;
|
|
}
|
|
|
|
currentDeviceId_ = id;
|
|
// After recreation SDL may have normalized the id (e.g. default).
|
|
if (SDL_GetAudioDeviceName(id) == nullptr && id != SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK)
|
|
{
|
|
currentDeviceId_ = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
|
|
}
|
|
currentDevice_ = resolvePlaybackDeviceName(currentDeviceId_);
|
|
|
|
logger::Logger::instance().info("Audio", "Switched audio output device to: " + mapToFriendlyDeviceName(currentDevice_));
|
|
|
|
if (configManager_)
|
|
{
|
|
configManager_->update([name = currentDevice_](config::AppConfig &cfg)
|
|
{ cfg.audio.output_device = name; });
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::string AudioManager::getCurrentDevice() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
return currentDevice_;
|
|
}
|
|
|
|
SDL_AudioDeviceID AudioManager::getCurrentDeviceId() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
return currentDeviceId_;
|
|
}
|
|
|
|
bool AudioManager::handleDeviceEvent(const SDL_Event &event)
|
|
{
|
|
if (event.type != SDL_EVENT_AUDIO_DEVICE_ADDED &&
|
|
event.type != SDL_EVENT_AUDIO_DEVICE_REMOVED)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (!mixer_ || !initialized_)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Only playback device events are relevant to the active output.
|
|
if (event.adevice.recording)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const SDL_AudioDeviceID changedId = event.adevice.which;
|
|
|
|
// If a specific device (not the default) was removed, fail over to the
|
|
// system default so playback keeps going on whatever remains.
|
|
if (event.type == SDL_EVENT_AUDIO_DEVICE_REMOVED &&
|
|
currentDeviceId_ != SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK &&
|
|
changedId == currentDeviceId_)
|
|
{
|
|
logger::Logger::instance().warning("Audio",
|
|
"Active audio output device removed; failing over to the system default");
|
|
if (recreateMixerOnDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK))
|
|
{
|
|
currentDeviceId_ = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
|
|
currentDevice_ = "";
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// A device arrived or a non-active device left: nothing to remap, but
|
|
// the audio settings page refreshes its list on its own timer.
|
|
return false;
|
|
}
|
|
|
|
// Must be called with mutex_ held. Destroys the current mixer and creates a
|
|
// fresh one on the given device, recreating the two track slots and
|
|
// resuming the active track if one was playing.
|
|
bool AudioManager::recreateMixerOnDevice(SDL_AudioDeviceID targetId)
|
|
{
|
|
// Capture the playback state we want to preserve across the switch.
|
|
std::string resumeTrack = currentTrack_;
|
|
float fadeSeconds = slots_[activeSlot_].fadeInDuration > 0.0f
|
|
? slots_[activeSlot_].fadeInDuration
|
|
: 0.0f;
|
|
|
|
// Tear down the existing mixer (its tracks die with it).
|
|
for (int i = 0; i < 2; ++i)
|
|
{
|
|
slots_[i].track = nullptr;
|
|
if (slots_[i].audio)
|
|
{
|
|
MIX_DestroyAudio(slots_[i].audio);
|
|
slots_[i].audio = nullptr;
|
|
}
|
|
}
|
|
if (mixer_)
|
|
{
|
|
MIX_DestroyMixer(mixer_);
|
|
mixer_ = nullptr;
|
|
}
|
|
|
|
mixer_ = MIX_CreateMixerDevice(targetId, nullptr);
|
|
if (!mixer_)
|
|
{
|
|
initialized_ = false;
|
|
return false;
|
|
}
|
|
|
|
slots_[0].track = MIX_CreateTrack(mixer_);
|
|
slots_[1].track = MIX_CreateTrack(mixer_);
|
|
if (!slots_[0].track || !slots_[1].track)
|
|
{
|
|
MIX_DestroyMixer(mixer_);
|
|
mixer_ = nullptr;
|
|
initialized_ = false;
|
|
return false;
|
|
}
|
|
|
|
float gain = enabled_ ? (volume_ / 100.0f) : 0.0f;
|
|
MIX_SetMixerGain(mixer_, gain);
|
|
|
|
// Resume the previously active track at full gain (or restart its
|
|
// fade-in if it was still ramping).
|
|
if (!resumeTrack.empty())
|
|
{
|
|
if (loadTrackInto(activeSlot_, resumeTrack))
|
|
{
|
|
if (fadeSeconds > 0.0f)
|
|
{
|
|
startFadeIn(activeSlot_, fadeSeconds);
|
|
}
|
|
else
|
|
{
|
|
MIX_SetTrackGain(slots_[activeSlot_].track, 1.0f);
|
|
playTrackLooping(activeSlot_); // Loop playback
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool AudioManager::loadTrackInto(int slotIndex, const std::string &wav_filename)
|
|
{
|
|
if (!initialized_ || !mixer_ || !slots_[slotIndex].track)
|
|
return false;
|
|
|
|
// Look for audio file in ~/.local/radiantwave/audio/ or assets/audio/
|
|
std::string path1 = std::string(std::getenv("HOME") ? std::getenv("HOME") : "/tmp") + "/.local/radiantwave/audio/" + wav_filename;
|
|
std::string path2 = "assets/audio/" + wav_filename;
|
|
|
|
std::string chosen_path = std::filesystem::exists(path1) ? path1 : path2;
|
|
|
|
if (!std::filesystem::exists(chosen_path))
|
|
{
|
|
logger::Logger::instance().warning("Audio",
|
|
"Audio track file is missing. Please ensure audio assets are downloaded (File not found: " + wav_filename + ")");
|
|
return false;
|
|
}
|
|
|
|
MIX_Audio *audio = MIX_LoadAudio(mixer_, chosen_path.c_str(), true);
|
|
if (!audio)
|
|
{
|
|
logger::Logger::instance().error("Audio",
|
|
"Failed to load audio track (MIX_LoadAudio error: " + std::string(SDL_GetError()) + ")");
|
|
return false;
|
|
}
|
|
|
|
// Stop and free whatever was on this slot before.
|
|
MIX_StopTrack(slots_[slotIndex].track, 0);
|
|
if (slots_[slotIndex].audio)
|
|
{
|
|
MIX_DestroyAudio(slots_[slotIndex].audio);
|
|
}
|
|
slots_[slotIndex].audio = audio;
|
|
slots_[slotIndex].fadeInDuration = 0.0f;
|
|
slots_[slotIndex].fadeInElapsed = 0.0f;
|
|
slots_[slotIndex].fadeOutRemaining = 0.0f;
|
|
|
|
MIX_SetTrackAudio(slots_[slotIndex].track, audio);
|
|
|
|
currentTrack_ = wav_filename;
|
|
|
|
return true;
|
|
}
|
|
|
|
void AudioManager::startFadeIn(int slotIndex, float seconds)
|
|
{
|
|
TrackSlot &slot = slots_[slotIndex];
|
|
slot.fadeInDuration = seconds;
|
|
slot.fadeInElapsed = 0.0f;
|
|
MIX_SetTrackGain(slot.track, 0.0f);
|
|
playTrackLooping(slotIndex); // Loop playback
|
|
logger::Logger::instance().info("Audio", "Started playback of " + displayNameFor(currentTrack_) + " (fade-in)");
|
|
}
|
|
|
|
bool AudioManager::playTrack(const std::string &wav_filename)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (!initialized_ || !mixer_ || !slots_[activeSlot_].track)
|
|
return false;
|
|
|
|
if (!loadTrackInto(activeSlot_, wav_filename))
|
|
return false;
|
|
|
|
MIX_SetTrackGain(slots_[activeSlot_].track, 1.0f);
|
|
playTrackLooping(activeSlot_); // Loop playback
|
|
|
|
// Explicit user selection: persist the manual track so it survives a
|
|
// restart and is used whenever dynamic audio is turned off. Dynamic
|
|
// switches never write here, so they cannot clobber this choice.
|
|
if (configManager_)
|
|
{
|
|
configManager_->update([wav_filename](config::AppConfig &cfg)
|
|
{ cfg.audio.current_track = wav_filename; });
|
|
}
|
|
|
|
logger::Logger::instance().info("Audio", "Started playback of " + displayNameFor(wav_filename));
|
|
return true;
|
|
}
|
|
|
|
std::string AudioManager::currentTrack() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
return currentTrack_;
|
|
}
|
|
|
|
bool AudioManager::playTrackFadeIn(const std::string &wav_filename, float seconds)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (!initialized_ || !mixer_ || !slots_[activeSlot_].track)
|
|
return false;
|
|
|
|
// Same track already loaded in the active slot: bring it straight back
|
|
// to full volume instead of reloading. This avoids an audible glitch
|
|
// when re-entering the scroller over a track that is still loaded.
|
|
if (!wav_filename.empty() && wav_filename == currentTrack_ && slots_[activeSlot_].audio)
|
|
{
|
|
TrackSlot &slot = slots_[activeSlot_];
|
|
slot.fadeInDuration = 0.0f;
|
|
slot.fadeInElapsed = 0.0f;
|
|
slot.fadeOutRemaining = 0.0f;
|
|
MIX_SetTrackGain(slot.track, 1.0f);
|
|
// Always restart: MIX_PlayTrack() re-starts a playing track too, so
|
|
// this cancels any in-flight stop-fade inherited from a quick
|
|
// pop->push within the fade window. Gating on MIX_TrackPlaying()
|
|
// left a fading track to finish its fade and go silently stopped.
|
|
playTrackLooping(activeSlot_); // Loop playback
|
|
return true;
|
|
}
|
|
|
|
if (!loadTrackInto(activeSlot_, wav_filename))
|
|
return false;
|
|
|
|
startFadeIn(activeSlot_, seconds > 0.0f ? seconds : 1.0f);
|
|
return true;
|
|
}
|
|
|
|
bool AudioManager::playTrackCrossfade(const std::string &wav_filename, float seconds)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (!initialized_ || !mixer_)
|
|
return false;
|
|
|
|
// Not if the track is the same: the current track keeps playing
|
|
// untouched instead of being crossfaded back into itself.
|
|
if (!wav_filename.empty() && wav_filename == currentTrack_)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
float fade = seconds > 0.0f ? seconds : 1.0f;
|
|
int otherSlot = 1 - activeSlot_;
|
|
TrackSlot &other = slots_[otherSlot];
|
|
if (!other.track)
|
|
return false;
|
|
|
|
// Load the new track into the inactive slot first; on failure keep the
|
|
// current track playing untouched.
|
|
if (!loadTrackInto(otherSlot, wav_filename))
|
|
return false;
|
|
|
|
// Fade out whatever is currently playing on the active slot. Clear any
|
|
// in-flight fade-in bookkeeping first so update() stops ramping this
|
|
// slot's gain up while the mixer fades it down.
|
|
TrackSlot &active = slots_[activeSlot_];
|
|
if (active.audio)
|
|
{
|
|
active.fadeInDuration = 0.0f;
|
|
active.fadeInElapsed = 0.0f;
|
|
MIX_StopTrack(active.track, MIX_TrackMSToFrames(active.track, static_cast<Sint64>(fade * 1000.0)));
|
|
active.fadeOutRemaining = fade;
|
|
}
|
|
|
|
activeSlot_ = otherSlot;
|
|
startFadeIn(activeSlot_, fade);
|
|
return true;
|
|
}
|
|
|
|
bool AudioManager::stopTrackFadeOut(float seconds)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (!initialized_)
|
|
return false;
|
|
|
|
// Fade both slots so an in-flight crossfade cannot leave the other
|
|
// track audible after the fade. Returns false when nothing is playing,
|
|
// letting callers skip any waiting tied to the fade.
|
|
float fade = seconds > 0.0f ? seconds : 1.0f;
|
|
bool faded = false;
|
|
for (int i = 0; i < 2; ++i)
|
|
{
|
|
TrackSlot &slot = slots_[i];
|
|
if (!slot.track || !slot.audio)
|
|
{
|
|
continue;
|
|
}
|
|
// The mixer owns the ramp now; drop any pending fade-in so the
|
|
// per-frame update() cannot fight the fade-out.
|
|
slot.fadeInDuration = 0.0f;
|
|
slot.fadeInElapsed = 0.0f;
|
|
MIX_StopTrack(slot.track, MIX_TrackMSToFrames(slot.track, static_cast<Sint64>(fade * 1000.0)));
|
|
slot.fadeOutRemaining = fade;
|
|
faded = true;
|
|
}
|
|
return faded;
|
|
}
|
|
|
|
void AudioManager::stopTrack()
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (slots_[activeSlot_].track)
|
|
{
|
|
MIX_StopTrack(slots_[activeSlot_].track, 0);
|
|
}
|
|
if (slots_[activeSlot_].audio)
|
|
{
|
|
MIX_DestroyAudio(slots_[activeSlot_].audio);
|
|
slots_[activeSlot_].audio = nullptr;
|
|
}
|
|
slots_[activeSlot_].fadeInDuration = 0.0f;
|
|
slots_[activeSlot_].fadeInElapsed = 0.0f;
|
|
slots_[activeSlot_].fadeOutRemaining = 0.0f;
|
|
}
|
|
|
|
void AudioManager::update(float dt)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (!initialized_)
|
|
return;
|
|
|
|
for (int i = 0; i < 2; ++i)
|
|
{
|
|
TrackSlot &slot = slots_[i];
|
|
if (!slot.track)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// A slot mid stop-fade belongs to the mixer: skip both the fade-in
|
|
// ramp and the active-gain pin so nothing drives its gain up while
|
|
// the mixer ramps it down (this also keeps a quit/pop fade clean).
|
|
if (slot.fadeOutRemaining > 0.0f)
|
|
{
|
|
slot.fadeOutRemaining -= dt;
|
|
if (slot.fadeOutRemaining <= 0.0f)
|
|
{
|
|
slot.fadeOutRemaining = 0.0f;
|
|
// Free the audio once the fade-out has finished; the track
|
|
// object itself is reused.
|
|
if (slot.audio)
|
|
{
|
|
MIX_DestroyAudio(slot.audio);
|
|
slot.audio = nullptr;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (slot.fadeInDuration > 0.0f)
|
|
{
|
|
slot.fadeInElapsed += dt;
|
|
float t = std::min(1.0f, slot.fadeInElapsed / slot.fadeInDuration);
|
|
MIX_SetTrackGain(slot.track, t);
|
|
if (t >= 1.0f)
|
|
{
|
|
slot.fadeInDuration = 0.0f;
|
|
}
|
|
}
|
|
else if (i == activeSlot_)
|
|
{
|
|
// Keep the active track at full per-track gain; the mixer gain
|
|
// carries the user's volume / mute state.
|
|
MIX_SetTrackGain(slot.track, 1.0f);
|
|
// Self-heal: playback must continue indefinitely, so restart a
|
|
// track that stopped unexpectedly (there is no pause state and
|
|
// nothing else should legitimately stop the active track).
|
|
if (slot.audio && enabled_ && !MIX_TrackPlaying(slot.track))
|
|
{
|
|
playTrackLooping(i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace radiantwave::audio
|