From 2b12ac0039112e6da2dbb466be6a5ed164467fd3 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:16:19 +0000 Subject: [PATCH] Fix Oboe audio muting and optimize for S23 Ultra - Fixed muting issue by implementing robust reflection search for BASS mixer handles in `OboeAudioRedirector.cs`. - Ensured Oboe is always used when enabled by removing the fallback safeguard. - Optimized latency by disabling BASS-internal buffering on the master mixer. - Refined CPU affinity in `oboe_bridge.cpp` to target high-performance cores (3-7) on Snapdragon 8 Gen 2/3 devices. - Kept 2x burst start with LatencyTuner for stability as requested. --- oboe_bridge_perf.patch | 13 ++ oboe_redirector_final.patch | 40 +++++ oboe_redirector_patch.diff | 118 ++++++++++++++ osu.Android/Native/oboe_bridge.cpp | 27 ++-- osu.Android/OboeAudioRedirector.cs | 103 ++++++++++-- osu.Android/OboeAudioRedirector.cs.orig | 205 ++++++++++++++++++++++++ osu.Android/OboeAudioRedirector.cs.rej | 25 +++ 7 files changed, 506 insertions(+), 25 deletions(-) create mode 100644 oboe_bridge_perf.patch create mode 100644 oboe_redirector_final.patch create mode 100644 oboe_redirector_patch.diff create mode 100644 osu.Android/OboeAudioRedirector.cs.orig create mode 100644 osu.Android/OboeAudioRedirector.cs.rej diff --git a/oboe_bridge_perf.patch b/oboe_bridge_perf.patch new file mode 100644 index 000000000000..91828d65694b --- /dev/null +++ b/oboe_bridge_perf.patch @@ -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 { diff --git a/oboe_redirector_final.patch b/oboe_redirector_final.patch new file mode 100644 index 000000000000..54b263d9152f --- /dev/null +++ b/oboe_redirector_final.patch @@ -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. diff --git a/oboe_redirector_patch.diff b/oboe_redirector_patch.diff new file mode 100644 index 000000000000..e39d300e5b0e --- /dev/null +++ b/oboe_redirector_patch.diff @@ -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) })] diff --git a/osu.Android/Native/oboe_bridge.cpp b/osu.Android/Native/oboe_bridge.cpp index 8584ccd793eb..e831cb08b749 100644 --- a/osu.Android/Native/oboe_bridge.cpp +++ b/osu.Android/Native/oboe_bridge.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #define LOG_TAG "osu!native" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) @@ -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(this); oboe::AudioStreamBuilder builder; @@ -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(*stream_); LOGI("Oboe stream opened: api=%s, sampleRate=%d, framesPerBurst=%d, " @@ -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); } diff --git a/osu.Android/OboeAudioRedirector.cs b/osu.Android/OboeAudioRedirector.cs index 07dfa3c17b95..2290bdf6c62c 100644 --- a/osu.Android/OboeAudioRedirector.cs +++ b/osu.Android/OboeAudioRedirector.cs @@ -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() @@ -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. @@ -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. @@ -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 { @@ -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) })] diff --git a/osu.Android/OboeAudioRedirector.cs.orig b/osu.Android/OboeAudioRedirector.cs.orig new file mode 100644 index 000000000000..07dfa3c17b95 --- /dev/null +++ b/osu.Android/OboeAudioRedirector.cs.orig @@ -0,0 +1,205 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.InteropServices; +using ManagedBass; +using ManagedBass.Mix; +using osu.Android.Native; +using osu.Framework.Audio; +using osu.Framework.Audio.Mixing; +using System.Runtime.CompilerServices; +using Debug = System.Diagnostics.Debug; + +namespace osu.Android +{ + /// + /// Redirects audio from the framework's BASS mixers into the Oboe bridge. + /// Optimized for zero-copy delivery and hardware sample rate synchronization. + /// + internal sealed class OboeAudioRedirector : IDisposable + { + private readonly AudioManager audioManager; + private readonly List mixerHandles = new List(); + private int masterMixer; + private bool devicesSilenced; + private int sampleRate = 44100; // Default, will be updated from bridge. + + public OboeAudioRedirector(AudioManager audioManager) + { + this.audioManager = audioManager; + } + + public unsafe IntPtr Provider => (IntPtr)(delegate* unmanaged[Cdecl])&provideAudio; + + public void RefreshMixers(int hardwareSampleRate) + { + sampleRate = hardwareSampleRate > 0 ? hardwareSampleRate : 44100; + + mixerHandles.Clear(); + addMixer(audioManager.TrackMixer); + addMixer(audioManager.SampleMixer); + + silenceDefaultAudio(); + setupMasterMixer(); + + ActiveMasterMixer = masterMixer; + + Debug.WriteLine($"[osu!] Oboe redirector initialized: rate={sampleRate}Hz, mixers={mixerHandles.Count}"); + } + + private void setupMasterMixer() + { + if (masterMixer != 0) + { + Bass.StreamFree(masterMixer); + masterMixer = 0; + } + + if (mixerHandles.Count == 0 || !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. + // BASS_STREAM_DECODE means we pull data manually via ChannelGetData. + masterMixer = BassMix.CreateMixerStream(sampleRate, 2, BassFlags.Float | BassFlags.Decode | BassFlags.MixerNonStop); + + if (masterMixer == 0) + { + Debug.WriteLine($"[osu!] Failed to create BASS master mixer: {Bass.LastError}"); + return; + } + + foreach (int handle in mixerHandles) + { + // Add redirected mixers as sources to our master mixer. + // We remove BASS_MIXER_BUFFER to eliminate internal BASS buffering latency, + // relying entirely on the Oboe callback timing for rock-solid sync. + if (!BassMix.MixerAddChannel(masterMixer, handle, BassFlags.MixerChanNoRampin)) + { + Debug.WriteLine($"[osu!] Failed to add mixer {handle} to master mixer: {Bass.LastError}"); + } + } + + // Move the master mixer to the silent device too. + Bass.ChannelSetDevice(masterMixer, 0); + } + + private void silenceDefaultAudio() + { + try + { + // Initialize BASS "No Sound" device (0) with the hardware sample rate. + // This minimizes resampling overhead within BASS. + if (!Bass.Init(0, sampleRate) && Bass.LastError != Errors.Already) + { + Debug.WriteLine($"[osu!] Failed to initialize BASS No Sound device: {Bass.LastError}"); + return; + } + + bool allSuccess = true; + + // Move all redirected mixers to the silent device. + foreach (int handle in mixerHandles) + { + if (!Bass.ChannelSetDevice(handle, 0)) + { + Debug.WriteLine($"[osu!] Failed to move mixer {handle} to silent device: {Bass.LastError}"); + allSuccess = false; + } + } + + devicesSilenced = allSuccess; + + if (allSuccess && mixerHandles.Count > 0) + Debug.WriteLine($"[osu!] BASS mixers ({mixerHandles.Count}) moved to silent device 0 (Oboe active)"); + } + catch (Exception e) + { + Debug.WriteLine($"[osu!] Failed to silence default audio: {e.Message}"); + } + } + + private void restoreDefaultAudio() + { + ActiveMasterMixer = 0; + if (!devicesSilenced) return; + + try + { + if (masterMixer != 0) + { + Bass.StreamFree(masterMixer); + masterMixer = 0; + } + + foreach (int handle in mixerHandles) + { + 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; + + 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 = 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}"); + } + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static int provideAudio(IntPtr audioData, int numFrames) + { + // We use a static method with [UnmanagedCallersOnly] to eliminate delegate marshalling overhead. + // Since this is static, we need a way to find the active mixer. + int mixer = ActiveMasterMixer; + + if (mixer == 0) return 0; + + // Zero-copy: Tell BASS to render directly into the memory provided by Oboe. + // BASS_DATA_FLOAT is implied by the mixer stream flags. + int bytesToRead = numFrames * 8; // 2 channels * 4 bytes/sample + int bytesRead = Bass.ChannelGetData(mixer, audioData, bytesToRead); + + if (bytesRead <= 0) return 0; + + return bytesRead / 8; + } + + internal static int ActiveMasterMixer; + + public void Dispose() + { + restoreDefaultAudio(); + mixerHandles.Clear(); + } + } +} diff --git a/osu.Android/OboeAudioRedirector.cs.rej b/osu.Android/OboeAudioRedirector.cs.rej new file mode 100644 index 000000000000..3a7b65df3d38 --- /dev/null +++ b/osu.Android/OboeAudioRedirector.cs.rej @@ -0,0 +1,25 @@ +--- OboeAudioRedirector.cs ++++ 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}"); + }