From d97d05191d1f592ac2dc92b78a6133b57fa1c741 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 25 Oct 2025 13:10:38 +0900 Subject: [PATCH] Expose experimental WASAPI support as a user config toggle --- osu.Framework/Audio/AudioManager.cs | 20 ++++++++++++++++--- .../Configuration/FrameworkConfigManager.cs | 2 ++ osu.Framework/FrameworkEnvironment.cs | 3 --- osu.Framework/Game.cs | 1 + osu.Framework/Threading/AudioThread.cs | 17 ++++++++-------- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/osu.Framework/Audio/AudioManager.cs b/osu.Framework/Audio/AudioManager.cs index 356a31074..4736a4e4c 100644 --- a/osu.Framework/Audio/AudioManager.cs +++ b/osu.Framework/Audio/AudioManager.cs @@ -96,6 +96,13 @@ namespace osu.Framework.Audio /// public readonly Bindable AudioDevice = new Bindable(); + /// + /// Whether to use experimental WASAPI initialisation on windows. + /// This generally results in lower audio latency, but also changes the audio synchronisation from + /// historical expectations, meaning users / application will have to account for different offsets. + /// + public readonly BindableBool UseExperimentalWasapi = new BindableBool(); + /// /// Volume of all samples played game-wide. /// @@ -176,6 +183,7 @@ namespace osu.Framework.Audio thread.RegisterManager(this); AudioDevice.ValueChanged += _ => onDeviceChanged(); + UseExperimentalWasapi.ValueChanged += _ => onDeviceChanged(); GlobalMixerHandle.ValueChanged += handle => { onDeviceChanged(); @@ -430,10 +438,16 @@ namespace osu.Framework.Audio // See https://www.un4seen.com/forum/?topic=19601 for more information. Bass.Configure((ManagedBass.Configuration)70, false); - if (!thread.InitDevice(device)) - return false; + if (UseExperimentalWasapi.Value) + { + if (thread.InitDevice(device, true)) + return true; - return true; + Logger.Log($"BASS device {device} failed to initialise with experimental WASAPI, disabling", level: LogLevel.Error); + UseExperimentalWasapi.Value = false; + } + + return thread.InitDevice(device, false); } private void syncAudioDevices() diff --git a/osu.Framework/Configuration/FrameworkConfigManager.cs b/osu.Framework/Configuration/FrameworkConfigManager.cs index c38bd77d6..8a1f261ab 100644 --- a/osu.Framework/Configuration/FrameworkConfigManager.cs +++ b/osu.Framework/Configuration/FrameworkConfigManager.cs @@ -32,6 +32,7 @@ namespace osu.Framework.Configuration SetDefault(FrameworkSetting.WindowedPositionY, 0.5, -0.5, 1.5); SetDefault(FrameworkSetting.LastDisplayDevice, DisplayIndex.Default); SetDefault(FrameworkSetting.AudioDevice, string.Empty); + SetDefault(FrameworkSetting.AudioUseExperimentalWasapi, false); SetDefault(FrameworkSetting.VolumeUniversal, 1.0, 0.0, 1.0, 0.01); SetDefault(FrameworkSetting.VolumeMusic, 1.0, 0.0, 1.0, 0.01); SetDefault(FrameworkSetting.VolumeEffect, 1.0, 0.0, 1.0, 0.01); @@ -79,6 +80,7 @@ namespace osu.Framework.Configuration ShowLogOverlay, AudioDevice, + AudioUseExperimentalWasapi, VolumeUniversal, VolumeEffect, VolumeMusic, diff --git a/osu.Framework/FrameworkEnvironment.cs b/osu.Framework/FrameworkEnvironment.cs index b6cb95646..7d2d96e08 100644 --- a/osu.Framework/FrameworkEnvironment.cs +++ b/osu.Framework/FrameworkEnvironment.cs @@ -21,7 +21,6 @@ namespace osu.Framework public static bool NoStructuredBuffers { get; } public static string? DeferredRendererEventsOutputPath { get; } public static bool UseSDL3 { get; } - public static bool UseWasapi { get; } /// /// Whether non-SSL requests should be allowed. Debug only. Defaults to disabled. @@ -56,8 +55,6 @@ namespace osu.Framework // Desktop has many issues, see https://github.com/ppy/osu-framework/issues/6540. UseSDL3 = RuntimeInfo.IsMobile || (parseBool(Environment.GetEnvironmentVariable("OSU_SDL3")) ?? false); - - UseWasapi = parseBool(Environment.GetEnvironmentVariable("OSU_AUDIO_WASAPI_EXPERIMENTAL")) ?? false; } private static bool? parseBool(string? value) diff --git a/osu.Framework/Game.cs b/osu.Framework/Game.cs index bc4ff1809..3ca7ab425 100644 --- a/osu.Framework/Game.cs +++ b/osu.Framework/Game.cs @@ -196,6 +196,7 @@ namespace osu.Framework // attach our bindables to the audio subsystem. config.BindWith(FrameworkSetting.AudioDevice, Audio.AudioDevice); + config.BindWith(FrameworkSetting.AudioUseExperimentalWasapi, Audio.UseExperimentalWasapi); config.BindWith(FrameworkSetting.VolumeUniversal, Audio.Volume); config.BindWith(FrameworkSetting.VolumeEffect, Audio.VolumeSample); config.BindWith(FrameworkSetting.VolumeMusic, Audio.VolumeTrack); diff --git a/osu.Framework/Threading/AudioThread.cs b/osu.Framework/Threading/AudioThread.cs index 742c5e141..2c4383d30 100644 --- a/osu.Framework/Threading/AudioThread.cs +++ b/osu.Framework/Threading/AudioThread.cs @@ -130,7 +130,7 @@ namespace osu.Framework.Threading /// private readonly Bindable globalMixerHandle = new Bindable(); - internal bool InitDevice(int deviceId) + internal bool InitDevice(int deviceId, bool useExperimentalWasapi) { Debug.Assert(ThreadSafety.IsAudioThread); Trace.Assert(deviceId != -1); // The real device ID should always be used, as the -1 device has special cases which are hard to work with. @@ -139,9 +139,7 @@ namespace osu.Framework.Threading if (!Bass.Init(deviceId, Flags: (DeviceInitFlags)128)) // 128 == BASS_DEVICE_REINIT return false; - // That this has not been mass-tested since https://github.com/ppy/osu-framework/pull/6651 and probably needs to be. - // Currently envvar gated for users to test at their own discretion. - if (FrameworkEnvironment.UseWasapi) + if (useExperimentalWasapi) attemptWasapiInitialisation(); initialised_devices.Add(deviceId); @@ -182,10 +180,10 @@ namespace osu.Framework.Threading } } - private void attemptWasapiInitialisation() + private bool attemptWasapiInitialisation() { if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) - return; + return false; Logger.Log("Attempting local BassWasapi initialisation"); @@ -219,10 +217,10 @@ namespace osu.Framework.Threading // To keep things in a sane state let's only keep one device initialised via wasapi. freeWasapi(); - initWasapi(wasapiDevice); + return initWasapi(wasapiDevice); } - private void initWasapi(int wasapiDevice) + private bool initWasapi(int wasapiDevice) { // This is intentionally initialised inline and stored to a field. // If we don't do this, it gets GC'd away. @@ -246,13 +244,14 @@ namespace osu.Framework.Threading Logger.Log($"Initialising BassWasapi for device {wasapiDevice}...{(initialised ? "success!" : "FAILED")}"); if (!initialised) - return; + return false; BassWasapi.GetInfo(out var wasapiInfo); globalMixerHandle.Value = BassMix.CreateMixerStream(wasapiInfo.Frequency, wasapiInfo.Channels, BassFlags.MixerNonStop | BassFlags.Decode | BassFlags.Float); BassWasapi.Start(); BassWasapi.SetNotify(wasapiNotifyProcedure); + return true; } private void freeWasapi()