Fix switching of audio devices on Windows.

Fixes #21337
This commit is contained in:
Henrik Rydgård
2026-03-04 14:40:30 +01:00
parent 892ef99daa
commit 2052ebc60c
8 changed files with 93 additions and 26 deletions
+1
View File
@@ -32,5 +32,6 @@ public:
virtual int BufferSize() const = 0;
virtual int PeriodFrames() const = 0;
virtual void DescribeOutputFormat(char *buffer, size_t bufferSize) const { buffer[0] = '-'; buffer[1] = '\0'; }
virtual std::string GetCurrentDeviceName() const { return ""; }
virtual void FrameUpdate(bool allowAutoChange) {}
};
+1 -1
View File
@@ -886,7 +886,7 @@ static const ConfigSetting soundSettings[] = {
ConfigSetting("GamePreviewVolume", SETTING(g_Config, iGamePreviewVolume), &DefaultGamePreviewVolume, CfgFlag::DEFAULT),
ConfigSetting("AudioDevice", SETTING(g_Config, sAudioDevice), "", CfgFlag::DEFAULT),
ConfigSetting("AutoAudioDevice", SETTING(g_Config, bAutoAudioDevice), true, CfgFlag::DEFAULT),
ConfigSetting("AutoAudioDevice", SETTING(g_Config, bAutoSwitchAudioDevice), true, CfgFlag::DEFAULT),
ConfigSetting("AudioMixWithOthers", SETTING(g_Config, bAudioMixWithOthers), true, CfgFlag::DEFAULT),
ConfigSetting("AudioRespectSilentMode", SETTING(g_Config, bAudioRespectSilentMode), false, CfgFlag::DEFAULT),
ConfigSetting("UseOldAtrac", SETTING(g_Config, bUseOldAtrac), false, CfgFlag::DEFAULT),
+1 -1
View File
@@ -410,7 +410,7 @@ public:
bool bExtraAudioBuffering; // For bluetooth
std::string sAudioDevice;
bool bAutoAudioDevice;
bool bAutoSwitchAudioDevice;
bool bUseOldAtrac;
// iOS only for now
+1 -1
View File
@@ -1290,7 +1290,7 @@ static void ProcessSDLEvent(SDL_Window *window, const SDL_Event &event, InputSta
break;
}
// Don't start auto switching for a couple of seconds, because some devices init on start.
bool doAutoSwitch = g_Config.bAutoAudioDevice;
bool doAutoSwitch = g_Config.bAutoSwitchAudioDevice;
if ((time_now_d() - g_audioStartTime) < 3.0) {
INFO_LOG(Log::Audio, "Ignoring new audio device: %s (current: %s)", name, g_Config.sAudioDevice.c_str());
doAutoSwitch = false;
+2 -2
View File
@@ -796,7 +796,7 @@ void GameSettingsScreen::CreateAudioSettings(UI::ViewGroup *audioSettings) {
WARN_LOG(Log::Audio, "InitOutputDevice failed");
}
});
CheckBox *autoAudio = audioSettings->Add(new CheckBox(&g_Config.bAutoAudioDevice, a->T("Use new audio devices automatically")));
CheckBox *autoAudio = audioSettings->Add(new CheckBox(&g_Config.bAutoSwitchAudioDevice, a->T("Use new audio devices automatically")));
autoAudio->SetEnabledFunc([]()->bool {
return g_Config.sAudioDevice.empty();
});
@@ -808,7 +808,7 @@ void GameSettingsScreen::CreateAudioSettings(UI::ViewGroup *audioSettings) {
audioSettings->Add(new ItemHeader(a->T("Audio backend")));
if (sdlAudio) {
audioSettings->Add(new CheckBox(&g_Config.bAutoAudioDevice, a->T("Use new audio devices automatically")));
audioSettings->Add(new CheckBox(&g_Config.bAutoSwitchAudioDevice, a->T("Use new audio devices automatically")));
}
#if PPSSPP_PLATFORM(ANDROID)
+1 -1
View File
@@ -1017,7 +1017,7 @@ void NativeFrame(GraphicsContext *graphicsContext) {
g_iconCache.FrameUpdate();
if (g_audioBackend) {
g_audioBackend->FrameUpdate(g_Config.bAutoAudioDevice);
g_audioBackend->FrameUpdate(g_Config.bAutoSwitchAudioDevice);
}
// NOTE: We must begin the frame before update, so we can do texture size queries and stuff in Measure etc.
+70 -6
View File
@@ -152,7 +152,7 @@ bool WASAPIContext::InitOutputDevice(std::string_view uniqueId, LatencyMode late
GetDeviceDesc(device.Get(), &desc);
INFO_LOG(Log::Audio, "Activating audio device: %s", desc.name.c_str());
deviceId_ = uniqueId;
curDeviceId_ = uniqueId;
HRESULT hr = E_FAIL;
// Try IAudioClient3 first if not in "safe" mode. It's probably safe anyway, but still, let's use the legacy client as a safe fallback option.
@@ -242,6 +242,7 @@ bool WASAPIContext::InitOutputDevice(std::string_view uniqueId, LatencyMode late
}
} else {
// All good, nothing to convert.
_dbg_assert_(format_);
}
} else {
// Some other format.
@@ -291,13 +292,13 @@ bool WASAPIContext::InitOutputDevice(std::string_view uniqueId, LatencyMode late
}
void WASAPIContext::Start() {
_dbg_assert_(!audioThread_.joinable());
running_ = true;
audioThread_ = std::thread([this]() { AudioLoop(); });
}
void WASAPIContext::Stop() {
running_ = false;
if (audioClient_) audioClient_->Stop();
if (audioEvent_) SetEvent(audioEvent_);
if (audioThread_.joinable()) audioThread_.join();
@@ -311,14 +312,37 @@ void WASAPIContext::Stop() {
CoTaskMemFree(format_);
format_ = nullptr;
}
{
std::lock_guard<std::mutex> guard(deviceLock_);
curDeviceId_.clear();
}
}
void WASAPIContext::FrameUpdate(bool allowAutoChange) {
if (deviceId_.empty() && defaultDeviceChanged_ && allowAutoChange) {
defaultDeviceChanged_ = false;
Stop();
Start();
std::string deviceIdToInit;
{
std::lock_guard<std::mutex> guard(deviceLock_);
if (!defaultDeviceChanged_) {
return;
}
if (allowAutoChange) {
// Check if there actually was a change, we ignore false positives.
{
if (newDeviceId_ == curDeviceId_) {
// False positive, ignore.
defaultDeviceChanged_ = false;
return;
}
deviceIdToInit = newDeviceId_;
newDeviceId_.clear();
}
defaultDeviceChanged_ = false;
}
}
bool reverted;
InitOutputDevice(deviceIdToInit, latencyMode_, &reverted);
}
void WASAPIContext::AudioLoop() {
@@ -343,6 +367,11 @@ void WASAPIContext::AudioLoop() {
return;
}
if (!format_) {
ERROR_LOG(Log::Audio, "Can't start audio - no format");
return;
}
const AudioFormat format = Classify(format_);
const int nChannels = format_->nChannels;
@@ -445,3 +474,38 @@ void WASAPIContext::DescribeOutputFormat(char *buffer, size_t bufferSize) const
}
snprintf(buffer, bufferSize, "%d Hz %s %d-bit, %d ch%s", sampleRateHz, fmt, sampleBits, numChannels, audioClient3_ ? " (ac3)" : " (ac)");
}
HRESULT STDMETHODCALLTYPE WASAPIContext::DeviceNotificationClient::OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR device) {
if (flow != eRender) {
INFO_LOG(Log::Audio, "Default WASAPI audio recording device changed! Currently ignoring.");
return S_OK;
}
INFO_LOG(Log::Audio, "Default device changed to %s! role=%d", ConvertWStringToUTF8(device).c_str(), role);
if (role == eConsole) {
// PostMessage(hwnd, WM_APP + 1, 0, 0);
std::lock_guard<std::mutex> guard(engine_->deviceLock_);
engine_->defaultDeviceChanged_ = true;
engine_->newDeviceId_ = ConvertWStringToUTF8(device);
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE WASAPIContext::DeviceNotificationClient::OnDeviceAdded(LPCWSTR device) {
INFO_LOG(Log::Audio, "Audio device added! device=%s", ConvertWStringToUTF8(device).c_str());
return S_OK;
}
HRESULT STDMETHODCALLTYPE WASAPIContext::DeviceNotificationClient::OnDeviceRemoved(LPCWSTR device) {
INFO_LOG(Log::Audio, "Audio device removed! device=%s", ConvertWStringToUTF8(device).c_str());
return S_OK;
}
HRESULT STDMETHODCALLTYPE WASAPIContext::DeviceNotificationClient::OnDeviceStateChanged(LPCWSTR device, DWORD state) {
INFO_LOG(Log::Audio, "Audio device state changed! device=%s state=%08x", ConvertWStringToUTF8(device).c_str(), state);
return S_OK;
}
HRESULT STDMETHODCALLTYPE WASAPIContext::DeviceNotificationClient::OnPropertyValueChanged(LPCWSTR device, const PROPERTYKEY key) {
return S_OK;
}
+16 -14
View File
@@ -2,6 +2,7 @@
#include "Audio/AudioBackend.h"
#include <atomic>
#include <mutex>
#include <windows.h>
#include <mmdeviceapi.h>
@@ -47,24 +48,22 @@ public:
return E_NOINTERFACE;
}
HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR) override {
if (flow == eRender && role == eConsole) {
// PostMessage(hwnd, WM_APP + 1, 0, 0);
engine_->defaultDeviceChanged_ = true;
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR) override { return S_OK; }
HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR) override { return S_OK; }
HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR, DWORD) override { return S_OK; }
HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR, const PROPERTYKEY) override { return S_OK; }
HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR device) override;
HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR device) override;
HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR device) override;
HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR device, DWORD state) override;
HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR device, const PROPERTYKEY key) override;
private:
WASAPIContext *engine_;
};
void DescribeOutputFormat(char *buffer, size_t bufferSize) const override;
std::string GetCurrentDeviceName() const override {
std::lock_guard<std::mutex> guard(deviceLock_);
return curDeviceId_;
}
private:
void Start();
void Stop();
@@ -95,8 +94,11 @@ private:
RenderCallback callback_{};
void *userdata_ = nullptr;
LatencyMode latencyMode_ = LatencyMode::Aggressive;
std::string deviceId_;
std::atomic<bool> defaultDeviceChanged_{};
mutable std::mutex deviceLock_;
std::string curDeviceId_;
std::string newDeviceId_;
bool defaultDeviceChanged_ = false;
std::unique_ptr<float[]> tempBuf_;
};