Skip to content

Commit 103b925

Browse files
Fix Oboe audio redirection and Android build warnings
This commit addresses the issue where enabling Oboe audio redirection was not correctly rerouting audio from BASS mixers to the Oboe stream. Additionally, it fixes build warnings related to unused fields in `AndroidNativeBridgeManager.cs`. Key changes: - **Build Warning Fix:** Implemented caching for `cachedVulkanStatus` and `cachedOboeStatus` in `AndroidNativeBridgeManager.cs`, ensuring these fields are used and reducing per-frame allocations in the HUD. - **Improved Oboe Redirection:** Enhanced the reflection-based BASS handle discovery in `OboeAudioRedirector.cs`. It now searches for a wider range of field/property names (`handle`, `mixer`, `id`, `stream`, `channel`) and supports multiple underlying types (`int`, `long`, `IntPtr`) to maintain compatibility with recent framework updates. - **Robust Redirection Trigger:** Updated `OsuGameAndroid.cs` to ensure `RefreshMixers` is called with a valid hardware sample rate even if the initial Oboe stream state reports zero. - **Clean State Transitions:** Improved the 'hijack and restore' pattern in `OboeAudioRedirector.cs` to ensure mixers are correctly returned to their original parents when Oboe is disabled or refreshed.
1 parent 3e8b3f1 commit 103b925

3 files changed

Lines changed: 62 additions & 42 deletions

File tree

osu.Android/AndroidNativeBridgeManager.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public void StartOboeBridge(Scheduler scheduler, Action<double> onLatencyMeasure
3030
}
3131

3232
Debug.WriteLine($"[osu!] Starting Oboe bridge (sampleRate={sampleRate}, hasProvider={provider != IntPtr.Zero})");
33+
cachedOboeStatus = null;
3334

3435
try
3536
{
@@ -83,6 +84,7 @@ public void StopOboeBridge()
8384
Debug.WriteLine("[osu!] Stopping Oboe bridge...");
8485
(oboeBridge as OboeAudioBridge)?.Dispose();
8586
oboeBridge = null;
87+
cachedOboeStatus = null;
8688
Debug.WriteLine("[osu!] Oboe bridge stopped");
8789
}
8890

@@ -96,7 +98,7 @@ public void StopOboeBridge()
9698
public string GetOboeStatus()
9799
{
98100
if (oboeBridge is not OboeAudioBridge bridge) return string.Empty;
99-
return $"{(bridge.IsAAudio ? "AAudio" : "OpenSLES")} [{(bridge.IsMMap ? "MMAP" : "Legacy")}]";
101+
return cachedOboeStatus ??= $"{(bridge.IsAAudio ? "AAudio" : "OpenSLES")} [{(bridge.IsMMap ? "MMAP" : "Legacy")}]";
100102
}
101103

102104
public double GetMeasuredAudioLatencyMs()
@@ -110,6 +112,7 @@ public void StartVulkanProbe()
110112
if (vulkanProbe != null) return;
111113

112114
Debug.WriteLine("[osu!] Starting Vulkan probe...");
115+
cachedVulkanStatus = null;
113116

114117
try
115118
{
@@ -140,13 +143,14 @@ public void StartVulkanProbe()
140143
public string GetVulkanStatus()
141144
{
142145
if (vulkanProbe is not VulkanProbe probe) return string.Empty;
143-
return $"{(probe.SupportsMailboxPresentMode ? "MAILBOX" : "FIFO")}{(probe.DisablePresentId ? " [NoID]" : "")}{(probe.DisablePresentWait ? " [NoWait]" : "")}{(probe.DisableGraphicsPipelineLibrary ? " [NoGPL]" : "")}";
146+
return cachedVulkanStatus ??= $"{(probe.SupportsMailboxPresentMode ? "MAILBOX" : "FIFO")}{(probe.DisablePresentId ? " [NoID]" : "")}{(probe.DisablePresentWait ? " [NoWait]" : "")}{(probe.DisableGraphicsPipelineLibrary ? " [NoGPL]" : "")}";
144147
}
145148

146149
public void StopVulkanProbe()
147150
{
148151
(vulkanProbe as VulkanProbe)?.Dispose();
149152
vulkanProbe = null;
153+
cachedVulkanStatus = null;
150154
Debug.WriteLine("[osu!] Vulkan probe stopped");
151155
}
152156

osu.Android/OboeAudioRedirector.cs

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,20 @@ public void RefreshMixers(int hardwareSampleRate)
4343
lastHardwareSampleRate = hardwareSampleRate;
4444

4545
Console.WriteLine($"[osu!] Oboe redirector: Refreshing mixers with rate {lastHardwareSampleRate}Hz");
46-
restoreDefaultAudio();
46+
47+
// Clean up previous state but keep the silencing if we are already silenced.
48+
ActiveMasterMixer = 0;
49+
if (masterMixer != 0)
50+
{
51+
Bass.StreamFree(masterMixer);
52+
masterMixer = 0;
53+
}
54+
55+
restoreToParents();
4756
mixerHandles.Clear();
4857
originalParents.Clear();
4958

5059
sampleRate = lastHardwareSampleRate;
51-
mixerHandles.Clear();
5260

5361
addRootMixer(audioManager.TrackMixer);
5462
addRootMixer(audioManager.SampleMixer);
@@ -81,8 +89,6 @@ public void RefreshMixers(int hardwareSampleRate)
8189
{
8290
Console.WriteLine("[osu!] Oboe redirector: Failed to setup master mixer, restoring default audio.");
8391
restoreDefaultAudio();
84-
mixerHandles.Clear();
85-
originalParents.Clear();
8692
return;
8793
}
8894

@@ -92,19 +98,28 @@ public void RefreshMixers(int hardwareSampleRate)
9298

9399
private IEnumerable<AudioMixer> getActiveMixers()
94100
{
95-
// Use reflection to access the internal activeMixers list in AudioManager.
96-
// In the official framework, it is an internal BindableList<AudioMixer> activeMixers.
97-
FieldInfo? field = typeof(AudioManager).GetField("activeMixers", BindingFlags.Instance | BindingFlags.NonPublic);
98-
if (field == null) yield break;
101+
// Exhaustive search for the activeMixers list in AudioManager.
102+
Type type = typeof(AudioManager);
99103

100-
object? val = field.GetValue(audioManager);
101-
if (val is IEnumerable enumerable)
104+
while (type != null && type != typeof(object))
102105
{
103-
foreach (var item in enumerable)
106+
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
104107
{
105-
if (item is AudioMixer mixer)
106-
yield return mixer;
108+
if (field.FieldType.IsGenericType && field.FieldType.GetGenericArguments().Contains(typeof(AudioMixer)))
109+
{
110+
object? val = field.GetValue(audioManager);
111+
if (val is IEnumerable enumerable)
112+
{
113+
foreach (var item in enumerable)
114+
{
115+
if (item is AudioMixer mixer)
116+
yield return mixer;
117+
}
118+
yield break;
119+
}
120+
}
107121
}
122+
type = type.BaseType!;
108123
}
109124
}
110125

@@ -129,6 +144,7 @@ private bool setupMasterMixer()
129144
return false;
130145
}
131146

147+
// Disable BASS-internal buffering for the lowest possible latency.
132148
Bass.ChannelSetAttribute(masterMixer, ChannelAttribute.Buffer, 0);
133149

134150
int successfullyAdded = 0;
@@ -137,7 +153,7 @@ private bool setupMasterMixer()
137153
{
138154
int parent = BassMix.ChannelGetMixer(handle);
139155

140-
if (parent != 0)
156+
if (parent != 0 && parent != masterMixer)
141157
{
142158
originalParents[handle] = parent;
143159
BassMix.MixerRemoveChannel(handle);
@@ -149,7 +165,7 @@ private bool setupMasterMixer()
149165
if (!Bass.ChannelSetDevice(handle, 0))
150166
{
151167
Console.WriteLine($"[osu!] Failed to move source mixer {handle} to silent device: {Bass.LastError}");
152-
continue;
168+
// Try to proceed anyway, as some devices might behave strangely with device 0.
153169
}
154170
}
155171

@@ -186,6 +202,20 @@ private bool silenceDefaultAudio()
186202
}
187203
}
188204

205+
private void restoreToParents()
206+
{
207+
foreach (var kvp in originalParents)
208+
{
209+
int handle = kvp.Key;
210+
int parent = kvp.Value;
211+
212+
BassMix.MixerRemoveChannel(handle);
213+
Bass.ChannelSetDevice(handle, 1);
214+
BassMix.MixerAddChannel(parent, handle, BassFlags.MixerChanNoRampin);
215+
}
216+
originalParents.Clear();
217+
}
218+
189219
private void restoreDefaultAudio()
190220
{
191221
ActiveMasterMixer = 0;
@@ -198,16 +228,7 @@ private void restoreDefaultAudio()
198228
masterMixer = 0;
199229
}
200230

201-
// Restore hijacked mixers to their original parents.
202-
foreach (var kvp in originalParents)
203-
{
204-
int handle = kvp.Key;
205-
int parent = kvp.Value;
206-
207-
BassMix.MixerRemoveChannel(handle);
208-
Bass.ChannelSetDevice(handle, 1);
209-
BassMix.MixerAddChannel(parent, handle, BassFlags.MixerChanNoRampin);
210-
}
231+
restoreToParents();
211232

212233
// Restore any other discovered mixers to the default device.
213234
foreach (int handle in mixerHandles)
@@ -218,7 +239,6 @@ private void restoreDefaultAudio()
218239
Bass.ChannelSetDevice(handle, 1);
219240
}
220241

221-
originalParents.Clear();
222242
devicesSilenced = false;
223243
}
224244
catch (Exception e)
@@ -261,31 +281,29 @@ private int findHandle(object obj)
261281

262282
while (type != null && type != typeof(object))
263283
{
284+
// Broad search for anything that looks like a BASS handle.
285+
// We check int, long, and IntPtr as different wrappers use different types.
264286
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
265287
{
266-
if (field.FieldType == typeof(int) || field.FieldType == typeof(IntPtr))
288+
if (isHandleType(field.FieldType))
267289
{
268290
string name = field.Name.ToLowerInvariant();
269-
270-
if (name.Contains("handle") || name.Contains("mixer"))
291+
if (name.Contains("handle") || name.Contains("mixer") || name.Contains("id") || name.Contains("stream") || name.Contains("channel"))
271292
{
272293
int h = convertToHandle(field.GetValue(obj));
273-
274294
if (h != 0) return h;
275295
}
276296
}
277297
}
278298

279299
foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
280300
{
281-
if (prop.PropertyType == typeof(int) || prop.PropertyType == typeof(IntPtr))
301+
if (isHandleType(prop.PropertyType))
282302
{
283303
string name = prop.Name.ToLowerInvariant();
284-
285-
if (name.Contains("handle") || name.Contains("mixer"))
304+
if (name.Contains("handle") || name.Contains("mixer") || name.Contains("id") || name.Contains("stream") || name.Contains("channel"))
286305
{
287306
int h = convertToHandle(prop.GetValue(obj));
288-
289307
if (h != 0) return h;
290308
}
291309
}
@@ -297,16 +315,14 @@ private int findHandle(object obj)
297315
return 0;
298316
}
299317

318+
private bool isHandleType(Type type) => type == typeof(int) || type == typeof(IntPtr) || type == typeof(long);
319+
300320
private int convertToHandle(object? val)
301321
{
302322
if (val == null) return 0;
303-
304323
if (val is int ih) return ih;
305-
306324
if (val is long lh) return (int)lh;
307-
308325
if (val is IntPtr ph) return (int)ph.ToInt64();
309-
310326
return 0;
311327
}
312328

@@ -317,7 +333,7 @@ private static int provideAudio(IntPtr audioData, int numFrames)
317333

318334
if (mixer == 0) return 0;
319335

320-
int bytesToRead = numFrames * 8;
336+
int bytesToRead = numFrames * 8; // 2 channels * 4 bytes (float)
321337
int bytesRead = Bass.ChannelGetData(mixer, audioData, bytesToRead);
322338

323339
if (bytesRead <= 0) return 0;

osu.Android/OsuGameAndroid.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ protected override void LoadComplete()
256256
{
257257
// Only redirect audio once the Oboe stream has successfully started.
258258
// This prevents silence if the bridge fails to initialize.
259-
audioRedirector?.RefreshMixers(sampleRate);
259+
audioRedirector?.RefreshMixers(sampleRate > 0 ? sampleRate : hardwareSampleRate);
260260
Debug.WriteLine("[osu!] Audio redirector refreshed with hardware sample rate: " + sampleRate);
261261
});
262262
}

0 commit comments

Comments
 (0)