From 42b915bd25750f69184aeae7df3aa1665f17944d Mon Sep 17 00:00:00 2001 From: NeilClack Date: Fri, 28 Aug 2026 16:18:36 -0500 Subject: [PATCH] fixed (bug) Audio device switch killing playback via stale SDL device id 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. --- src/audio/audio_manager.cpp | 67 ++++++++++++++++++++++++++----------- src/audio/audio_manager.h | 7 ++-- src/audio_settings_page.cpp | 2 +- 3 files changed, 53 insertions(+), 23 deletions(-) diff --git a/src/audio/audio_manager.cpp b/src/audio/audio_manager.cpp index d7cc723..a62c14c 100644 --- a/src/audio/audio_manager.cpp +++ b/src/audio/audio_manager.cpp @@ -512,9 +512,19 @@ namespace radiantwave::audio 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. + // Must be called with mutex_ held. Creates a fresh mixer on the given + // device, recreating the two track slots and resuming the active track if + // one was playing. + // + // The new mixer is created BEFORE the current one is destroyed. Reversing + // the order (destroy-then-reopen) quits the whole SDL audio subsystem the + // moment the last device closes: MIX_DestroyMixer() follows up with + // SDL_QuitSubSystem(SDL_INIT_AUDIO), which tears down SDL's device registry + // and renumbers every enumerated device id on the way back up. The id this + // switch was asked to open is then stale and the reopen fails with + // "Invalid audio device instance ID". Keeping the current device open while + // the replacement opens also means a failed switch leaves the current + // output playing instead of going permanently mute. bool AudioManager::recreateMixerOnDevice(SDL_AudioDeviceID targetId) { // Capture the playback state we want to preserve across the switch. @@ -523,7 +533,37 @@ namespace radiantwave::audio ? slots_[activeSlot_].fadeInDuration : 0.0f; - // Tear down the existing mixer (its tracks die with it). + // Open the replacement mixer on the target device first, while the + // current device (and SDL's device registry) is still alive. + MIX_Mixer *newMixer = MIX_CreateMixerDevice(targetId, nullptr); + if (!newMixer) + { + std::string err = SDL_GetError(); + logger::Logger::instance().error( + "Audio", + "Could not open audio output device " + + mapToFriendlyDeviceName(resolvePlaybackDeviceName(targetId)) + + "; keeping the current device (MIX_CreateMixerDevice error: " + + err + ")"); + return false; + } + + MIX_Track *newTracks[2]; + for (int i = 0; i < 2; ++i) + { + newTracks[i] = MIX_CreateTrack(newMixer); + } + if (!newTracks[0] || !newTracks[1]) + { + MIX_DestroyMixer(newMixer); + logger::Logger::instance().error( + "Audio", "Could not create mixer tracks; keeping the current device (SDL error: " + + std::string(SDL_GetError()) + ")"); + return false; + } + + // Tear down the existing mixer (its tracks die with it) and adopt the + // new one. for (int i = 0; i < 2; ++i) { slots_[i].track = nullptr; @@ -539,22 +579,9 @@ namespace radiantwave::audio 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; - } + mixer_ = newMixer; + slots_[0].track = newTracks[0]; + slots_[1].track = newTracks[1]; float gain = enabled_ ? (volume_ / 100.0f) : 0.0f; MIX_SetMixerGain(mixer_, gain); diff --git a/src/audio/audio_manager.h b/src/audio/audio_manager.h index 082a693..33ee175 100644 --- a/src/audio/audio_manager.h +++ b/src/audio/audio_manager.h @@ -111,8 +111,11 @@ private: // 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. Returns false on failure (previous mixer is - // left destroyed but the app remains safe to keep running muted). + // 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; diff --git a/src/audio_settings_page.cpp b/src/audio_settings_page.cpp index f28aa3a..90f9738 100644 --- a/src/audio_settings_page.cpp +++ b/src/audio_settings_page.cpp @@ -288,7 +288,7 @@ void AudioSettingsPage::applyDeviceSelection() { if (ok) { statusMessage_ = "Switched to " + devices_[currentDeviceIndex_].friendly_name; } else { - statusMessage_ = "Device unavailable - using system default"; + statusMessage_ = "Device unavailable - kept the current output"; } refreshDeviceAndTrackLists(); }