diff --git a/osu.Android/AndroidNativeBridgeManager.cs b/osu.Android/AndroidNativeBridgeManager.cs index 8825851b4f7c..1e11b28600cb 100644 --- a/osu.Android/AndroidNativeBridgeManager.cs +++ b/osu.Android/AndroidNativeBridgeManager.cs @@ -118,7 +118,7 @@ public void StartVulkanProbe() [MethodImpl(MethodImplOptions.NoInlining)] public bool IsVulkanRecommended() => (vulkanProbe as VulkanProbe)?.IsRecommended ?? false; - public bool IsVulkanAvailable() => (vulkanProbe as VulkanProbe)?.IsAvailable ?? false; + public bool IsVulkanAvailable() => (vulkanProbe as VulkanProbe)?.IsAvailable ?? (vulkanProbe != null); public void StopVulkanProbe() { (vulkanProbe as VulkanProbe)?.Dispose(); diff --git a/osu.Android/Native/OboeAudioBridge.cs b/osu.Android/Native/OboeAudioBridge.cs index ce58acf106d8..9eaa2f05d5c4 100644 --- a/osu.Android/Native/OboeAudioBridge.cs +++ b/osu.Android/Native/OboeAudioBridge.cs @@ -26,12 +26,12 @@ static OboeAudioBridge() { try { - // Use DllImportSearchPath.ApplicationDirectory to avoid searching system paths + // Use null to avoid searching system paths // which can crash on some Samsung devices with aggressive security policies. native_loaded = NativeLibrary.TryLoad( lib_name, typeof(OboeAudioBridge).Assembly, - DllImportSearchPath.ApplicationDirectory, + null, out _); } catch (Exception e) diff --git a/osu.Android/Native/VulkanProbe.cs b/osu.Android/Native/VulkanProbe.cs index 4bb8d9f94883..e0c08239d565 100644 --- a/osu.Android/Native/VulkanProbe.cs +++ b/osu.Android/Native/VulkanProbe.cs @@ -16,7 +16,7 @@ public sealed class VulkanProbe : IDisposable static VulkanProbe() { - try { native_loaded = NativeLibrary.TryLoad(lib_name, typeof(VulkanProbe).Assembly, DllImportSearchPath.ApplicationDirectory, out _); } + try { native_loaded = NativeLibrary.TryLoad(lib_name, typeof(VulkanProbe).Assembly, null, out _); } catch { native_loaded = false; } } diff --git a/osu.Android/OboeAudioRedirector.cs b/osu.Android/OboeAudioRedirector.cs index e75bd76344b4..fdd604da1f4b 100644 --- a/osu.Android/OboeAudioRedirector.cs +++ b/osu.Android/OboeAudioRedirector.cs @@ -19,6 +19,8 @@ namespace osu.Android { public class OboeAudioRedirector : IDisposable { + public bool IsRedirecting => ActiveMasterMixer != 0; + private readonly AudioManager audioManager; private readonly List mixerHandles = new List(); private readonly Dictionary originalParents = new Dictionary(); @@ -26,6 +28,7 @@ public class OboeAudioRedirector : IDisposable private int masterMixer; private bool devicesSilenced; private int sampleRate = 44100; + private int lastHardwareSampleRate = 44100; public OboeAudioRedirector(AudioManager audioManager) { @@ -36,10 +39,13 @@ public OboeAudioRedirector(AudioManager audioManager) public void RefreshMixers(int hardwareSampleRate) { - Console.WriteLine($"[osu!] Oboe redirector: Refreshing mixers with rate {hardwareSampleRate}Hz"); + if (hardwareSampleRate > 0) + lastHardwareSampleRate = hardwareSampleRate; + + Console.WriteLine($"[osu!] Oboe redirector: Refreshing mixers with rate {lastHardwareSampleRate}Hz"); restoreDefaultAudio(); - sampleRate = hardwareSampleRate > 0 ? hardwareSampleRate : 44100; + sampleRate = lastHardwareSampleRate; mixerHandles.Clear(); addRootMixer(audioManager.TrackMixer); @@ -59,15 +65,25 @@ public void RefreshMixers(int hardwareSampleRate) if (mixerHandles.Count == 0) { - Console.WriteLine("[osu!] Oboe redirector: CRITICAL - No BASS mixers discovered."); + Console.WriteLine("[osu!] Oboe redirector: No BASS mixers discovered yet, deferring redirection."); return; } - silenceDefaultAudio(); - setupMasterMixer(); + if (!silenceDefaultAudio()) + { + Console.WriteLine("[osu!] Oboe redirector: Failed to silence default audio, aborting redirection."); + return; + } + + if (!setupMasterMixer()) + { + Console.WriteLine("[osu!] Oboe redirector: Failed to setup master mixer, restoring default audio."); + restoreDefaultAudio(); + return; + } ActiveMasterMixer = masterMixer; - Console.WriteLine($"[osu!] Oboe redirector initialized: master={masterMixer}, sources={string.Join(',', mixerHandles)}"); + Console.WriteLine($"[osu!] Oboe redirector initialized successfully: master={masterMixer}, sources={string.Join(',', mixerHandles)}"); } private IEnumerable getActiveMixers() @@ -88,7 +104,7 @@ private IEnumerable getActiveMixers() } } - private void setupMasterMixer() + private bool setupMasterMixer() { if (masterMixer != 0) { @@ -96,7 +112,7 @@ private void setupMasterMixer() masterMixer = 0; } - if (!devicesSilenced) return; + if (!devicesSilenced) return false; // Ensure we are working with the correct device context. Bass.CurrentDevice = 0; @@ -106,11 +122,13 @@ private void setupMasterMixer() if (masterMixer == 0) { Console.WriteLine($"[osu!] Failed to create BASS master mixer: {Bass.LastError}"); - return; + return false; } Bass.ChannelSetAttribute(masterMixer, ChannelAttribute.Buffer, 0); + int successfullyAdded = 0; + foreach (int handle in mixerHandles) { int parent = BassMix.ChannelGetMixer(handle); @@ -121,31 +139,46 @@ private void setupMasterMixer() BassMix.MixerRemoveChannel(handle); } - if (!Bass.ChannelSetDevice(handle, 0)) - Console.WriteLine($"[osu!] Failed to move source mixer {handle} to silent device: {Bass.LastError}"); + // If the channel was on another device, move it to the silent device (0). + if (Bass.ChannelGetDevice(handle) != 0) + { + if (!Bass.ChannelSetDevice(handle, 0)) + { + Console.WriteLine($"[osu!] Failed to move source mixer {handle} to silent device: {Bass.LastError}"); + continue; + } + } - if (!BassMix.MixerAddChannel(masterMixer, handle, BassFlags.MixerChanNoRampin)) + if (BassMix.MixerAddChannel(masterMixer, handle, BassFlags.MixerChanNoRampin)) + { + successfullyAdded++; + } + else { Console.WriteLine($"[osu!] Failed to add mixer {handle} to master mixer: {Bass.LastError}"); } } + + return successfullyAdded > 0; } - private void silenceDefaultAudio() + private bool silenceDefaultAudio() { try { if (!Bass.Init(0, sampleRate) && Bass.LastError != Errors.Already) { Console.WriteLine($"[osu!] Failed to initialize BASS No Sound device: {Bass.LastError}"); - return; + return false; } devicesSilenced = true; + return true; } catch (Exception e) { Console.WriteLine($"[osu!] Failed to silence default audio: {e.Message}"); + return false; } } @@ -161,19 +194,24 @@ private void restoreDefaultAudio() masterMixer = 0; } - foreach (int handle in mixerHandles) + // Restore hijacked mixers to their original parents. + foreach (var kvp in originalParents) { + int handle = kvp.Key; + int parent = kvp.Value; + BassMix.MixerRemoveChannel(handle); + Bass.ChannelSetDevice(handle, 1); + BassMix.MixerAddChannel(parent, handle, BassFlags.MixerChanNoRampin); + } - if (originalParents.TryGetValue(handle, out int parent)) - { - Bass.ChannelSetDevice(handle, 1); - BassMix.MixerAddChannel(parent, handle, BassFlags.MixerChanNoRampin); - } - else - { - Bass.ChannelSetDevice(handle, 1); - } + // Restore any other discovered mixers to the default device. + foreach (int handle in mixerHandles) + { + if (originalParents.ContainsKey(handle)) continue; + + BassMix.MixerRemoveChannel(handle); + Bass.ChannelSetDevice(handle, 1); } originalParents.Clear(); diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 149f8250329f..691d00b768e0 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -145,6 +145,10 @@ private void load() LocalConfig.BindWith(OsuSetting.AndroidVulkanProbe, vulkanProbeEnabled); LocalConfig.BindWith(OsuSetting.AudioOffset, audioOffset); + // Start Vulkan probe as early as possible so it's ready for RendererSettings. + if (vulkanProbeEnabled.Value) + startVulkanProbe(); + audioRedirector = new OboeAudioRedirector(Audio); try @@ -245,28 +249,24 @@ protected override void LoadComplete() lowLatencyAudio.BindValueChanged(e => { - int hardwareSampleRate = 0; - try + if (e.NewValue) { - if (gameActivity.GetSystemService(global::Android.Content.Context.AudioService) is global::Android.Media.AudioManager audioManager) + int hardwareSampleRate = 0; + try { - string? rateStr = audioManager.GetProperty(global::Android.Media.AudioManager.PropertyOutputSampleRate); - - if (!string.IsNullOrEmpty(rateStr)) - hardwareSampleRate = int.Parse(rateStr); + if (gameActivity.GetSystemService(global::Android.Content.Context.AudioService) is global::Android.Media.AudioManager audioManager) + { + string? rateStr = audioManager.GetProperty(global::Android.Media.AudioManager.PropertyOutputSampleRate); + if (!string.IsNullOrEmpty(rateStr)) + hardwareSampleRate = int.Parse(rateStr); + } } - } - catch { } + catch { } - try - { - if (e.NewValue) + try { - audioRedirector?.RefreshMixers(hardwareSampleRate); - startOboeBridge(latency => { - // Only auto-suggest when the user hasn't already configured a manual offset. if (Math.Abs(audioOffset.Value) >= 0.01) return; @@ -275,16 +275,23 @@ protected override void LoadComplete() Debug.WriteLine($"[osu!] Audio offset auto-suggested: {suggested:F1}ms (hardware latency={latency:F1}ms)"); }, audioRedirector != null ? audioRedirector.Provider : IntPtr.Zero, sampleRate => { - // Initialise BASS mixers at the hardware sample rate to eliminate resampling latency. + // Only redirect audio once the Oboe stream has successfully started. + // This prevents silence if the bridge fails to initialize. audioRedirector?.RefreshMixers(sampleRate); }); } - else if (nativeBridges != null) - stopOboeBridge(); + catch (Exception ex) + { + Debug.WriteLine($"[osu!] Failed to start Oboe bridge: {ex.Message}"); + lowLatencyAudio.Value = false; + } } - catch (Exception ex) + else { - Debug.WriteLine($"[osu!] Failed to toggle Oboe bridge: {ex.Message}"); + stopOboeBridge(); + audioRedirector?.Dispose(); + // Re-create the redirector instance so it's fresh if re-enabled. + audioRedirector = new OboeAudioRedirector(Audio); } }, true); @@ -294,14 +301,14 @@ protected override void LoadComplete() { if (e.NewValue) startVulkanProbe(); - else if (nativeBridges != null) + else stopVulkanProbe(); } catch (Exception ex) { Debug.WriteLine($"[osu!] Failed to toggle Vulkan probe: {ex.Message}"); } - }, true); + }, false); // Already started in load() if true. // Apply unbuffered touch dispatch. try diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs index d34b768b0ef3..78ebc718c131 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs @@ -32,11 +32,14 @@ private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, IDi var rendererItems = host.GetPreferredRenderersForCurrentPlatform().ToList(); - // Surgically inject Vulkan on Android if recommended, even if the host doesn't report it. + // Surgically inject Vulkan on Android if supported, even if the host doesn't report it. // This allows us to use official framework NuGet while still supporting Vulkan in the game. - if (RuntimeInfo.OS == RuntimeInfo.Platform.Android && (game?.IsVulkanSupported ?? false)) + if (RuntimeInfo.OS == RuntimeInfo.Platform.Android) { - if (!rendererItems.Contains(RendererType.Vulkan)) + bool isSupported = game?.IsVulkanSupported ?? false; + bool isCurrentlySelected = renderer.Value == RendererType.Vulkan; + + if ((isSupported || isCurrentlySelected) && !rendererItems.Contains(RendererType.Vulkan)) rendererItems.Add(RendererType.Vulkan); }