Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion osu.Android/AndroidNativeBridgeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions osu.Android/Native/OboeAudioBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion osu.Android/Native/VulkanProbe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}

Expand Down
86 changes: 62 additions & 24 deletions osu.Android/OboeAudioRedirector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ namespace osu.Android
{
public class OboeAudioRedirector : IDisposable
{
public bool IsRedirecting => ActiveMasterMixer != 0;

private readonly AudioManager audioManager;
private readonly List<int> mixerHandles = new List<int>();
private readonly Dictionary<int, int> originalParents = new Dictionary<int, int>();

private int masterMixer;
private bool devicesSilenced;
private int sampleRate = 44100;
private int lastHardwareSampleRate = 44100;

public OboeAudioRedirector(AudioManager audioManager)
{
Expand All @@ -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);
Expand All @@ -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<AudioMixer> getActiveMixers()
Expand All @@ -88,15 +104,15 @@ private IEnumerable<AudioMixer> getActiveMixers()
}
}

private void setupMasterMixer()
private bool setupMasterMixer()
{
if (masterMixer != 0)
{
Bass.StreamFree(masterMixer);
masterMixer = 0;
}

if (!devicesSilenced) return;
if (!devicesSilenced) return false;

// Ensure we are working with the correct device context.
Bass.CurrentDevice = 0;
Expand All @@ -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);
Expand All @@ -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;
}
}

Expand All @@ -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();
Expand Down
51 changes: 29 additions & 22 deletions osu.Android/OsuGameAndroid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand All @@ -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);

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading