diff --git a/osu.Android/Native/oboe_bridge.cpp b/osu.Android/Native/oboe_bridge.cpp index 3c49bc205004..6e1c4f23dbee 100644 --- a/osu.Android/Native/oboe_bridge.cpp +++ b/osu.Android/Native/oboe_bridge.cpp @@ -134,6 +134,7 @@ void OboeBridge::stop() { stream_.reset(); } + affinitySet_.store(false); latencyMs_.store(-1.0); callbackCount_.store(0); LOGI("Oboe stream stopped"); @@ -202,8 +203,30 @@ oboe::DataCallbackResult OboeBridge::onAudioReady( // instead of every single callback. calculateLatencyMillis() issues a // system call; keeping it out of the majority of callbacks reduces jitter // in this real-time audio thread. - if ((callbackCount_.fetch_add(1, std::memory_order_relaxed) & 127) == 0) { + uint32_t count = callbackCount_.fetch_add(1, std::memory_order_relaxed); + + if ((count & 127) == 0) { updateLatency(); + + // Attempt to set CPU affinity to high-performance cores on the first few callbacks. + // Doing this inside the callback ensures we are targeting the actual audio thread + // created by Oboe/AAudio. + if (!affinitySet_.load(std::memory_order_relaxed)) { + std::vector exclusiveCores = oboe::Process::getExclusiveCores(); + + if (!exclusiveCores.empty()) { + oboe::Result result = oboe::Process::setThreadAffinity( + oboe::Process::getThreadId(), + exclusiveCores); + + if (result == oboe::Result::OK) { + LOGI("Oboe audio thread pinned to exclusive cores"); + } else { + LOGI("Failed to pin Oboe audio thread: %s", oboe::convertToText(result)); + } + } + affinitySet_.store(true); + } } return oboe::DataCallbackResult::Continue; diff --git a/osu.Android/Native/oboe_bridge.h b/osu.Android/Native/oboe_bridge.h index d7ccbb0efc69..a1f42da4c68f 100644 --- a/osu.Android/Native/oboe_bridge.h +++ b/osu.Android/Native/oboe_bridge.h @@ -67,6 +67,7 @@ class OboeBridge : public oboe::AudioStreamCallback { std::shared_ptr stream_; std::mutex streamLock_; std::atomic active_{false}; + std::atomic affinitySet_{false}; std::atomic latencyMs_{-1.0}; std::atomic callbackCount_{0}; std::atomic provider_{nullptr}; diff --git a/osu.Android/OboeAudioRedirector.cs b/osu.Android/OboeAudioRedirector.cs index 4fe9a5b777bc..4a39f335a79e 100644 --- a/osu.Android/OboeAudioRedirector.cs +++ b/osu.Android/OboeAudioRedirector.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Numerics; using System.Reflection; using System.Runtime.InteropServices; using ManagedBass; @@ -20,6 +21,7 @@ internal sealed class OboeAudioRedirector : IDisposable { private readonly AudioManager audioManager; private readonly List mixerHandles = new List(); + private bool devicesSilenced; private readonly OboeAudioBridge.OboeAudioProvider providerDelegate; private float[]? mixBuffer; @@ -40,9 +42,65 @@ public void RefreshMixers() addMixer(audioManager.TrackMixer); addMixer(audioManager.SampleMixer); + silenceDefaultAudio(); + Debug.WriteLine($"[osu!] Oboe redirector initialized with {mixerHandles.Count} BASS mixers"); } + private void silenceDefaultAudio() + { + if (devicesSilenced) return; + + try + { + // Initialize BASS "No Sound" device (0) if not already. + // This device allows BASS to process audio streams without outputting to hardware. + if (!Bass.Init(0) && Bass.LastError != Errors.Already) + { + Debug.WriteLine($"[osu!] Failed to initialize BASS No Sound device: {Bass.LastError}"); + return; + } + + // Move all redirected mixers to the silent device. + // This "unplugs" them from the system hardware while keeping them active so we can pull data. + foreach (int handle in mixerHandles) + { + if (!Bass.ChannelSetDevice(handle, 0)) + Debug.WriteLine($"[osu!] Failed to move mixer {handle} to silent device: {Bass.LastError}"); + } + + devicesSilenced = true; + Debug.WriteLine($"[osu!] BASS mixers moved to silent device 0 (Oboe active)"); + } + catch (Exception e) + { + Debug.WriteLine($"[osu!] Failed to silence default audio: {e.Message}"); + } + } + + private void restoreDefaultAudio() + { + if (!devicesSilenced) return; + + try + { + // Move mixers back to the default device (usually 1 on Android). + foreach (int handle in mixerHandles) + { + // On Android, Device 1 is typically the default output. + if (!Bass.ChannelSetDevice(handle, 1)) + Debug.WriteLine($"[osu!] Failed to restore mixer {handle} to default device: {Bass.LastError}"); + } + + devicesSilenced = false; + Debug.WriteLine($"[osu!] BASS mixers restored to default device 1"); + } + catch (Exception e) + { + Debug.WriteLine($"[osu!] Failed to restore default audio: {e.Message}"); + } + } + private void addMixer(AudioMixer mixer) { if (mixer == null) return; @@ -55,7 +113,7 @@ private void addMixer(AudioMixer mixer) if (field != null) { - int handle = (int)field.GetValue(mixer); + int handle = field.GetValue(mixer) is int h ? h : 0; if (handle != 0) mixerHandles.Add(handle); } else @@ -64,7 +122,7 @@ private void addMixer(AudioMixer mixer) var prop = mixer.GetType().GetProperty("Handle", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (prop != null) { - int handle = (int)prop.GetValue(mixer); + int handle = prop.GetValue(mixer) is int h ? h : 0; if (handle != 0) mixerHandles.Add(handle); } } @@ -100,7 +158,21 @@ private int provideAudio(IntPtr audioData, int numFrames) anyRead = true; int samplesRead = bytesRead / 4; - for (int i = 0; i < samplesRead; i++) + int i = 0; + + if (Vector.IsHardwareAccelerated) + { + int vectorSize = Vector.Count; + + for (; i <= samplesRead - vectorSize; i += vectorSize) + { + var vMix = new Vector(mixBuffer, i); + var vChan = new Vector(channelBuffer, i); + (vMix + vChan).CopyTo(mixBuffer, i); + } + } + + for (; i < samplesRead; i++) { mixBuffer[i] += channelBuffer[i]; } @@ -114,6 +186,7 @@ private int provideAudio(IntPtr audioData, int numFrames) public void Dispose() { + restoreDefaultAudio(); mixerHandles.Clear(); } } diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 101ad8451a19..29eb74090018 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -163,7 +163,11 @@ protected override void LoadComplete() }, audioRedirector?.Provider); } else if (nativeBridges != null) + { stopOboeBridge(); + audioRedirector?.Dispose(); + audioRedirector = new OboeAudioRedirector(Audio); + } } catch (Exception ex) {