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
13 changes: 13 additions & 0 deletions oboe_bridge_perf.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--- osu.Android/Native/oboe_bridge.cpp
+++ osu.Android/Native/oboe_bridge.cpp
@@ -212,8 +212,8 @@
// S23 Ultra (Snapdragon 8 Gen 2) layout: 1 Prime + 2 Gold + 2 Gold + 3 Silver.
// Indices are typically: 0-2 (Silver), 3-4 (Gold), 5-6 (Gold), 7 (Prime).
- // We want to target the Prime (7) and Gold (3-6) cores.
+ // We want to target the Prime (7) and Gold (3-6) cores.
if (num_cores >= 8) {
- for (int i = 4; i < num_cores; ++i) {
+ for (int i = 3; i < num_cores; ++i) {
CPU_SET(i, &cpuset);
}
} else {
40 changes: 40 additions & 0 deletions oboe_redirector_final.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--- osu.Android/OboeAudioRedirector.cs
+++ osu.Android/OboeAudioRedirector.cs
@@ -45,15 +45,11 @@
- if (mixerHandles.Count > 0)
- {
- silenceDefaultAudio();
- setupMasterMixer();
- }
- else
- {
- Debug.WriteLine("[osu!] Oboe redirector: ABORTED redirection - no mixer handles found via reflection. Audio will use default path.");
- restoreDefaultAudio();
- }
+ // User requested Oboe: we MUST use Oboe.
+ // Silence the default device immediately to prevent duplicated audio.
+ silenceDefaultAudio();
+ setupMasterMixer();

ActiveMasterMixer = masterMixer;

- Debug.WriteLine($"[osu!] Oboe redirector initialized: rate={sampleRate}Hz, mixers={mixerHandles.Count}, master={masterMixer}");
+ if (mixerHandles.Count == 0)
+ Debug.WriteLine("[osu!] Oboe redirector: CRITICAL WARNING - no mixer handles found via reflection. Audio WILL BE SILENT until fixed.");
+ else
+ Debug.WriteLine($"[osu!] Oboe redirector initialized: rate={sampleRate}Hz, mixers={mixerHandles.Count}, master={masterMixer}");
}

@@ -102,6 +98,12 @@
return;
}

+ // Disable BASS-internal buffering for the master mixer.
+ // This ensures BASS renders as fast as possible when we call ChannelGetData.
+ // This is key for "lowest possible latency" as requested.
+ if (!Bass.ChannelSetAttribute(masterMixer, ChannelAttribute.Buffer, 0))
+ Debug.WriteLine($"[osu!] Failed to disable BASS buffering on master mixer: {Bass.LastError}");
+
foreach (int handle in mixerHandles)
{
// Add redirected mixers as sources to our master mixer.
118 changes: 118 additions & 0 deletions oboe_redirector_patch.diff
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
--- osu.Android/OboeAudioRedirector.cs
+++ osu.Android/OboeAudioRedirector.cs
@@ -34,11 +34,19 @@
mixerHandles.Clear();
addMixer(audioManager.TrackMixer);
addMixer(audioManager.SampleMixer);

- silenceDefaultAudio();
- setupMasterMixer();
+ if (mixerHandles.Count > 0)
+ {
+ silenceDefaultAudio();
+ setupMasterMixer();
+ }
+ else
+ {
+ Debug.WriteLine("[osu!] Oboe redirector: ABORTED redirection - no mixer handles found. Reverting to default audio path.");
+ restoreDefaultAudio();
+ }

ActiveMasterMixer = masterMixer;

- Debug.WriteLine($"[osu!] Oboe redirector initialized: rate={sampleRate}Hz, mixers={mixerHandles.Count}");
+ Debug.WriteLine($"[osu!] Oboe redirector initialized: rate={sampleRate}Hz, mixers={mixerHandles.Count}, master={masterMixer}");
}

@@ -140,11 +148,15 @@

try
{
- object? handleObj = mixer.GetType().GetField("mixerHandle", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(mixer)
- ?? mixer.GetType().GetField("Handle", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(mixer)
- ?? mixer.GetType().GetProperty("Handle", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(mixer);
-
- if (handleObj == null) return;
+ int handle = findHandle(mixer);
+
+ if (handle != 0)
+ {
+ if (!mixerHandles.Contains(handle))
+ {
+ mixerHandles.Add(handle);
+ Debug.WriteLine($"[osu!] Oboe redirector: added mixer handle {handle} for {mixer.GetType().Name}");
+ }
+ }
+ else
+ {
+ Debug.WriteLine($"[osu!] Oboe redirector: WARNING - could not find BASS handle for {mixer.GetType().Name} via reflection. Audio may be muted!");
+ }
+ }
+ catch (Exception e)
+ {
+ Debug.WriteLine($"[osu!] Oboe redirector: failed to get mixer handle via reflection: {e.Message}");
+ }
+ }
+
+ private int findHandle(object obj)
+ {
+ Type? type = obj.GetType();
+
+ while (type != null && type != typeof(object))
+ {
+ // Try common explicit names first (fast path)
+ foreach (string name in new[] { "mixerHandle", "handle", "Handle", "mixer_handle", "_handle", "m_handle" })
+ {
+ var field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ if (field != null)
+ {
+ int h = convertHandle(field.GetValue(obj));
+ if (h != 0) return h;
+ }
+
+ var prop = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ if (prop != null)
+ {
+ int h = convertHandle(prop.GetValue(obj));
+ if (h != 0) return h;
+ }
+ }
+
+ // Scan all fields for anything that looks like a handle as a last resort
+ foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
+ {
+ if (field.Name.Contains("handle", StringComparison.OrdinalIgnoreCase))
+ {
+ int h = convertHandle(field.GetValue(obj));
+ if (h != 0) return h;
+ }
+ }
+
+ type = type.BaseType;
+ }
+
+ return 0;
+ }

- int handle = 0;
- if (handleObj is int ih) handle = ih;
- else if (handleObj is long lh) handle = (int)lh;
- else if (handleObj is IntPtr ph) handle = (int)ph.ToInt64();
-
- if (handle != 0 && !mixerHandles.Contains(handle))
- mixerHandles.Add(handle);
- }
- catch (Exception e)
- {
- Debug.WriteLine($"[osu!] Failed to get mixer handle via reflection: {e.Message}");
- }
+ private int convertHandle(object? val)
+ {
+ if (val == null) return 0;
+ if (val is int ih) return ih;
+ if (val is long lh) return (int)lh;
+ if (val is IntPtr ph) return (int)ph.ToInt64();
+ return 0;
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
27 changes: 18 additions & 9 deletions osu.Android/Native/oboe_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <cstdint>
#include <cstring>
#include <vector>
#include <algorithm>

#define LOG_TAG "osu!native"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
Expand All @@ -31,10 +32,10 @@ bool OboeBridge::open(int32_t sampleRate) {
requestedSampleRate_ = sampleRate;

// Low-latency MMAP path requires explicit enabling in Oboe.
// MMAP provides direct access to audio hardware buffers, shaving ~1-2ms off latency.
oboe::OboeExtensions::setMMapEnabled(true);

// Initialise StabilizedCallback to even out callback execution time.
// We create it here so we can pass it to the builder.
stabilizedCallback_ = std::make_unique<oboe::StabilizedCallback>(this);

oboe::AudioStreamBuilder builder;
Expand Down Expand Up @@ -69,14 +70,14 @@ bool OboeBridge::open(int32_t sampleRate) {
}

// Enable ADPF (Android Dynamic Performance Framework) hint support.
// This allows the system to prioritize our audio thread for stable low latency.
stream_->setPerformanceHintEnabled(true);

// Set buffer size to 2x burst size for initial stability.
// LatencyTuner will then attempt to shrink it if stable.
// LatencyTuner will then attempt to shrink it to 1x burst if stable.
stream_->setBufferSizeInFrames(stream_->getFramesPerBurst() * 2);

// Initialise LatencyTuner for dynamic buffer management.
// This allows us to start at 1x burst and only grow if underruns occur.
tuner_ = std::make_unique<oboe::LatencyTuner>(*stream_);

LOGI("Oboe stream opened: api=%s, sampleRate=%d, framesPerBurst=%d, "
Expand Down Expand Up @@ -201,21 +202,29 @@ oboe::DataCallbackResult OboeBridge::onAudioReady(
}

// Attempt to set CPU affinity to high-performance cores.
// We do this inside the audio callback to ensure we target the AAudio thread.
if (!affinitySet_.load(std::memory_order_relaxed)) {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);

int num_cores = sysconf(_SC_NPROCESSORS_CONF);
if (num_cores > 0) {
// Target the "big" cores (higher indexed) for better performance.
// In big.LITTLE, indices 4-7 are typically the high-performance cores.
int start_core = std::max(0, num_cores / 2);
for (int i = start_core; i < num_cores; ++i) {
CPU_SET(i, &cpuset);
// S23 Ultra (Snapdragon 8 Gen 2) layout: 1 Prime + 2 Gold + 2 Gold + 3 Silver.
// Indices are typically: 0-2 (Silver), 3-4 (Gold), 5-6 (Gold), 7 (Prime).
// We want to target the Prime (7) and Gold (3-6) cores.
if (num_cores >= 8) {
for (int i = 3; i < num_cores; ++i) {
CPU_SET(i, &cpuset);
}
} else {
// Fallback for devices with fewer cores.
for (int i = num_cores / 2; i < num_cores; ++i) {
CPU_SET(i, &cpuset);
}
}

if (sched_setaffinity(0, sizeof(cpu_set_t), &cpuset) == 0) {
LOGI("Oboe audio thread pinned to cores %d-%d", start_core, num_cores - 1);
LOGI("Oboe audio thread pinned to high-performance cores");
} else {
LOGE("Failed to set thread affinity: %d", errno);
}
Expand Down
103 changes: 87 additions & 16 deletions osu.Android/OboeAudioRedirector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,17 @@ public void RefreshMixers(int hardwareSampleRate)
addMixer(audioManager.TrackMixer);
addMixer(audioManager.SampleMixer);

// User requested Low-Latency Oboe: we MUST use Oboe.
// Silence the default device immediately to prevent duplicated audio.
silenceDefaultAudio();
setupMasterMixer();

ActiveMasterMixer = masterMixer;

Debug.WriteLine($"[osu!] Oboe redirector initialized: rate={sampleRate}Hz, mixers={mixerHandles.Count}");
if (mixerHandles.Count == 0)
Debug.WriteLine("[osu!] Oboe redirector: CRITICAL WARNING - no mixer handles found via reflection. Audio WILL BE SILENT until handled.");
else
Debug.WriteLine($"[osu!] Oboe redirector initialized: rate={sampleRate}Hz, mixers={mixerHandles.Count}, master={masterMixer}");
}

private void setupMasterMixer()
Expand All @@ -58,7 +63,7 @@ private void setupMasterMixer()
masterMixer = 0;
}

if (mixerHandles.Count == 0 || !devicesSilenced) return;
if (!devicesSilenced) return;

// Create a BASS master mixer that matches the Oboe hardware format (Stereo Float).
// We use BASS_MIXER_NONSTOP to ensure the mixer doesn't stall if sources are empty.
Expand All @@ -71,6 +76,12 @@ private void setupMasterMixer()
return;
}

// Disable BASS-internal buffering for the master mixer.
// This ensures BASS renders as fast as possible when we call ChannelGetData.
// This is key for "lowest possible latency" as requested.
if (!Bass.ChannelSetAttribute(masterMixer, ChannelAttribute.Buffer, 0))
Debug.WriteLine($"[osu!] Failed to disable BASS buffering on master mixer: {Bass.LastError}");

foreach (int handle in mixerHandles)
{
// Add redirected mixers as sources to our master mixer.
Expand Down Expand Up @@ -124,7 +135,15 @@ private void silenceDefaultAudio()
private void restoreDefaultAudio()
{
ActiveMasterMixer = 0;
if (!devicesSilenced) return;
if (!devicesSilenced)
{
if (masterMixer != 0)
{
Bass.StreamFree(masterMixer);
masterMixer = 0;
}
return;
}

try
{
Expand Down Expand Up @@ -155,24 +174,76 @@ private void addMixer(AudioMixer? mixer)

try
{
object? handleObj = mixer.GetType().GetField("mixerHandle", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(mixer)
?? mixer.GetType().GetField("Handle", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(mixer)
?? mixer.GetType().GetProperty("Handle", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(mixer);
int handle = findHandle(mixer);

if (handleObj == null) return;

int handle = 0;
if (handleObj is int ih) handle = ih;
else if (handleObj is long lh) handle = (int)lh;
else if (handleObj is IntPtr ph) handle = (int)ph.ToInt64();

if (handle != 0 && !mixerHandles.Contains(handle))
mixerHandles.Add(handle);
if (handle != 0)
{
if (!mixerHandles.Contains(handle))
{
mixerHandles.Add(handle);
Debug.WriteLine($"[osu!] Oboe redirector: added mixer handle {handle} for {mixer.GetType().Name}");
}
}
else
{
Debug.WriteLine($"[osu!] Oboe redirector: WARNING - could not find BASS handle for {mixer.GetType().Name} via reflection. Audio might be silent on Oboe.");
}
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] Failed to get mixer handle via reflection: {e.Message}");
Debug.WriteLine($"[osu!] Oboe redirector: failed to get mixer handle via reflection: {e.Message}");
}
}

private int findHandle(object obj)
{
Type? type = obj.GetType();

// Broad search for BASS handles across the entire inheritance chain.
// Framework changes often move or rename these internal fields.
while (type != null && type != typeof(object))
{
// Try common names used in ppy/osu and ppy/osu-framework
foreach (string name in new[] { "mixerHandle", "handle", "Handle", "mixer_handle", "_handle", "m_handle", "handlePtr" })
{
var field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (field != null)
{
int h = convertToHandle(field.GetValue(obj));
if (h != 0) return h;
}

var prop = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (prop != null)
{
int h = convertToHandle(prop.GetValue(obj));
if (h != 0) return h;
}
}

// Last resort: scan all fields for anything mentioning "handle"
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
if (field.Name.Contains("handle", StringComparison.OrdinalIgnoreCase))
{
int h = convertToHandle(field.GetValue(obj));
if (h != 0) return h;
}
}

type = type.BaseType;
}

return 0;
}

private int convertToHandle(object? val)
{
if (val == null) return 0;
if (val is int ih) return ih;
if (val is long lh) return (int)lh;
if (val is IntPtr ph) return (int)ph.ToInt64();
return 0;
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
Expand Down
Loading
Loading