Skip to content

Commit bbca166

Browse files
authored
Merge pull request #146 from winnerspiros/fix-oboe-audio-final-9634658476078144659
android: Resolve Oboe audio implementation and improve diagnostics
2 parents fbb0cf6 + 5d69255 commit bbca166

4 files changed

Lines changed: 79 additions & 77 deletions

File tree

osu.Android/Native/OboeAudioBridge.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,18 @@ public double GetOutputLatencyMs()
8080
catch { return -1; }
8181
}
8282

83-
public bool IsActive
83+
public string GetLastErrorMessage()
84+
{
85+
if (disposed || nativePtr == IntPtr.Zero) return "Not initialized";
86+
try
87+
{
88+
IntPtr ptr = nOboeGetLastErrorMessage(nativePtr);
89+
return ptr == IntPtr.Zero ? "Unknown" : Marshal.PtrToStringAnsi(ptr) ?? "Unknown";
90+
}
91+
catch { return "P/Invoke error"; }
92+
}
93+
94+
public bool IsActive
8495
{
8596
[MethodImpl(MethodImplOptions.AggressiveInlining)]
8697
get
@@ -121,7 +132,7 @@ public int BufferSizeInFrames
121132
}
122133
}
123134

124-
public bool IsAAudio
135+
public bool IsAAudio
125136
{
126137
[MethodImpl(MethodImplOptions.AggressiveInlining)]
127138
get
@@ -132,7 +143,7 @@ public bool IsAAudio
132143
}
133144
}
134145

135-
public bool IsMMap
146+
public bool IsMMap
136147
{
137148
[MethodImpl(MethodImplOptions.AggressiveInlining)]
138149
get
@@ -177,6 +188,7 @@ public void Dispose()
177188
[DllImport(lib_name)] private static extern byte nOboeIsAAudio(IntPtr ptr);
178189
[DllImport(lib_name)] private static extern byte nOboeIsMMap(IntPtr ptr);
179190
[DllImport(lib_name)] private static extern void nOboeSetProvider(IntPtr ptr, IntPtr provider);
191+
[DllImport(lib_name)] private static extern IntPtr nOboeGetLastErrorMessage(IntPtr ptr);
180192
[DllImport(lib_name)] internal static extern byte nSetThreadAffinity(int coreMask);
181193
[DllImport(lib_name)] internal static extern IntPtr nADPFCreateSession(long targetDurationNanos);
182194
[DllImport(lib_name)] internal static extern void nADPFReportActualDuration(IntPtr sessionPtr, long actualDurationNanos);

osu.Android/OboeAudioRedirector.cs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,20 @@
44
using System;
55
using System.Collections;
66
using System.Collections.Generic;
7-
using System.Diagnostics;
87
using System.Linq;
98
using System.Reflection;
10-
using System.Runtime.CompilerServices;
119
using System.Runtime.InteropServices;
10+
using System.Runtime.CompilerServices;
1211
using ManagedBass;
1312
using ManagedBass.Mix;
1413
using osu.Framework.Audio;
1514
using osu.Framework.Audio.Mixing;
16-
using osu.Framework.Bindables;
1715

1816
namespace osu.Android
1917
{
18+
/// <summary>
19+
/// Redirects audio from BASS mixers into an unmanaged callback (Oboe).
20+
/// </summary>
2021
public class OboeAudioRedirector : IDisposable
2122
{
2223
public bool IsRedirecting => ActiveMasterMixer != 0;
@@ -44,7 +45,6 @@ public void RefreshMixers(int hardwareSampleRate)
4445

4546
Console.WriteLine($"[osu!] Oboe redirector: Refreshing mixers with rate {lastHardwareSampleRate}Hz");
4647

47-
// Clean up previous state but keep the silencing if we are already silenced.
4848
ActiveMasterMixer = 0;
4949
if (masterMixer != 0)
5050
{
@@ -98,7 +98,6 @@ public void RefreshMixers(int hardwareSampleRate)
9898

9999
private IEnumerable<AudioMixer> getActiveMixers()
100100
{
101-
// Exhaustive search for the activeMixers list in AudioManager.
102101
Type type = typeof(AudioManager);
103102

104103
while (type != null && type != typeof(object))
@@ -133,7 +132,6 @@ private bool setupMasterMixer()
133132

134133
if (!devicesSilenced) return false;
135134

136-
// Ensure we are working with the correct device context.
137135
Bass.CurrentDevice = 0;
138136

139137
masterMixer = BassMix.CreateMixerStream(sampleRate, 2, BassFlags.Float | BassFlags.Decode | BassFlags.MixerNonStop);
@@ -144,7 +142,6 @@ private bool setupMasterMixer()
144142
return false;
145143
}
146144

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

150147
int successfullyAdded = 0;
@@ -159,13 +156,11 @@ private bool setupMasterMixer()
159156
BassMix.MixerRemoveChannel(handle);
160157
}
161158

162-
// If the channel was on another device, move it to the silent device (0).
163159
if (Bass.ChannelGetDevice(handle) != 0)
164160
{
165161
if (!Bass.ChannelSetDevice(handle, 0))
166162
{
167163
Console.WriteLine($"[osu!] Failed to move source mixer {handle} to silent device: {Bass.LastError}");
168-
// Try to proceed anyway, as some devices might behave strangely with device 0.
169164
}
170165
}
171166

@@ -179,6 +174,9 @@ private bool setupMasterMixer()
179174
}
180175
}
181176

177+
// Restore current device to 1 after setup to avoid affecting other audio operations
178+
Bass.CurrentDevice = 1;
179+
182180
return successfullyAdded > 0;
183181
}
184182

@@ -230,7 +228,6 @@ private void restoreDefaultAudio()
230228

231229
restoreToParents();
232230

233-
// Restore any other discovered mixers to the default device.
234231
foreach (int handle in mixerHandles)
235232
{
236233
if (originalParents.ContainsKey(handle)) continue;
@@ -281,14 +278,12 @@ private int findHandle(object obj)
281278

282279
while (type != null && type != typeof(object))
283280
{
284-
// Broad search for anything that looks like a BASS handle.
285-
// We check int, long, and IntPtr as different wrappers use different types.
286281
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
287282
{
288283
if (isHandleType(field.FieldType))
289284
{
290285
string name = field.Name.ToLowerInvariant();
291-
if (name.Contains("handle") || name.Contains("mixer") || name.Contains("id") || name.Contains("stream") || name.Contains("channel"))
286+
if (name.Contains("handle") || name.Contains("mixer") || name.Contains("id") || name.Contains("stream") || name.Contains("channel") || name.Contains("source"))
292287
{
293288
int h = convertToHandle(field.GetValue(obj));
294289
if (h != 0) return h;
@@ -301,14 +296,26 @@ private int findHandle(object obj)
301296
if (isHandleType(prop.PropertyType))
302297
{
303298
string name = prop.Name.ToLowerInvariant();
304-
if (name.Contains("handle") || name.Contains("mixer") || name.Contains("id") || name.Contains("stream") || name.Contains("channel"))
299+
if (name.Contains("handle") || name.Contains("mixer") || name.Contains("id") || name.Contains("stream") || name.Contains("channel") || name.Contains("source"))
305300
{
306301
int h = convertToHandle(prop.GetValue(obj));
307302
if (h != 0) return h;
308303
}
309304
}
310305
}
311306

307+
if (type.Name.Contains("Mixer") || type.Name.Contains("Channel") || type.Name.Contains("Stream"))
308+
{
309+
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic))
310+
{
311+
if (field.FieldType == typeof(int))
312+
{
313+
int h = (int)field.GetValue(obj)!;
314+
if (h > 0 && h < 1000000) return h;
315+
}
316+
}
317+
}
318+
312319
type = type.BaseType;
313320
}
314321

@@ -326,7 +333,7 @@ private int convertToHandle(object? val)
326333
return 0;
327334
}
328335

329-
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
336+
[UnmanagedCallersOnly(EntryPoint = "provideAudio", CallConvs = new[] { typeof(CallConvCdecl) })]
330337
private static int provideAudio(IntPtr audioData, int numFrames)
331338
{
332339
int mixer = ActiveMasterMixer;

osu.Android/OsuGameAndroid.cs

Lines changed: 41 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,39 @@
11
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22
// See the LICENCE file in the repository root for full licence text.
3-
using System.Diagnostics;
4-
using Microsoft.Maui.Devices;
5-
using osu.Android.Performance;
6-
using osu.Framework.Development;
73

8-
using Android.Content.PM;
9-
using osu.Game.Performance;
10-
using osu.Game.Updater;
11-
using System.Collections.Specialized;
124
using System;
135
using System.Collections;
146
using System.Collections.Generic;
7+
using System.Collections.Specialized;
158
using Debug = System.Diagnostics.Debug;
169
using System.Linq;
1710
using System.Reflection;
1811
using System.Runtime.CompilerServices;
19-
using Context = global::Android.Content.Context;
20-
using Android.Media;
12+
using Android.App;
13+
using Android.Content.PM;
2114
using Android.OS;
2215
using Android.Views;
2316
using osu.Android.Native;
17+
using osu.Framework;
2418
using osu.Android.Input;
2519
using osu.Framework.Allocation;
26-
using AudioManager = osu.Framework.Audio.AudioManager;
2720
using osu.Framework.Bindables;
28-
using osu.Framework.Configuration;
29-
using osu.Framework.Extensions.IEnumerableExtensions;
3021
using osu.Framework.Graphics;
31-
using osu.Framework.Input;
3222
using osu.Framework.Platform;
33-
using osu.Framework.Threading;
3423
using osu.Game;
3524
using osu.Game.Configuration;
3625
using osu.Game.Overlays;
37-
using osu.Game.Overlays.Notifications;
26+
using osu.Game.Overlays.Settings;
3827
using osu.Game.Screens;
28+
using osu.Game.Screens.Play;
29+
using osuTK;
30+
using osu.Framework.Audio;
31+
using osu.Framework.Audio.Mixing;
32+
using osu.Framework.Threading;
33+
using osu.Android.Performance;
3934
using osu.Game.Utils;
40-
using Vector2 = osuTK.Vector2;
35+
using osu.Game.Updater;
36+
using osu.Game.Performance;
4137

4238
namespace osu.Android
4339
{
@@ -89,15 +85,6 @@ public partial class OsuGameAndroid : OsuGame
8985
private Delegate? activeMixersHandler;
9086
private object? activeMixersList;
9187

92-
/// <summary>
93-
/// Boxed reference to the native bridge manager.
94-
/// Declared as <c>object?</c> so that the runtime never resolves the concrete
95-
/// AndroidNativeBridgeManager type (and its P/Invoke field types) during
96-
/// OsuGameAndroid class initialisation — which would trigger
97-
/// NativeLibrary.TryLoad before the framework is ready and crash on some
98-
/// Samsung devices.
99-
/// All access goes through [NoInlining] helpers below.
100-
/// </summary>
10188
private object? nativeBridges;
10289

10390
public OsuGameAndroid(OsuGameActivity activity)
@@ -158,42 +145,36 @@ private void load()
158145

159146
try
160147
{
161-
// Use reflection to bind to collection changes of the internal activeMixers list in AudioManager.
162-
FieldInfo? field = typeof(AudioManager).GetField("activeMixers", BindingFlags.Instance | BindingFlags.NonPublic);
163-
if (field != null)
148+
Type? audioType = typeof(AudioManager);
149+
activeMixersList = audioType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
150+
.FirstOrDefault(f => f.FieldType.IsGenericType && f.FieldType.GetGenericArguments().Contains(typeof(AudioMixer)))
151+
?.GetValue(Audio);
152+
153+
if (activeMixersList != null)
164154
{
165-
activeMixersList = field.GetValue(Audio);
166-
object? val = field.GetValue(Audio);
167-
if (val != null)
155+
MethodInfo? bindMethod = activeMixersList.GetType().GetMethod("BindCollectionChanged", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
156+
if (bindMethod != null)
168157
{
169-
MethodInfo? bindMethod = val.GetType().GetMethod("BindCollectionChanged", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
170-
if (bindMethod != null)
171-
{
172-
var del = Delegate.CreateDelegate(bindMethod.GetParameters()[0].ParameterType, this, typeof(OsuGameAndroid).GetMethod(nameof(onActiveMixersChanged), BindingFlags.Instance | BindingFlags.NonPublic)!);
173-
bindMethod.Invoke(val, new object[] { del, true });
174-
}
158+
activeMixersHandler = new NotifyCollectionChangedEventHandler(onActiveMixersChanged);
159+
bindMethod.Invoke(activeMixersList, new object[] { activeMixersHandler });
160+
Debug.WriteLine("[osu!] Oboe redirector: Successfully bound to ActiveMixers collection");
175161
}
176162
}
177163
}
178-
catch (Exception ex)
179-
{
180-
Debug.WriteLine($"[osu!] Failed to bind to activeMixers via reflection: {ex.Message}");
181-
}
164+
catch (Exception e) { Debug.WriteLine($"[osu!] Oboe redirector: Failed to bind to ActiveMixers: {e.Message}"); }
182165
}
183166

184-
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
167+
private void onActiveMixersChanged(object? sender, NotifyCollectionChangedEventArgs args) => Schedule(() => { if (lowLatencyAudio.Value) audioRedirector?.RefreshMixers(0); });
168+
185169
protected override void LoadComplete()
186170
{
187-
// Pin the current thread (Update thread) to high-performance cores.
188-
// On S23 Ultra, cores 3-7 are high-performance. Mask = 0xF8 (11111000 in binary)
189171
try
190172
{
191173
if (OboeAudioBridge.nSetThreadAffinity(0xF8) != 0)
192174
Debug.WriteLine("[osu!] Update thread pinned to big cores");
193175

194176
Scheduler.Add(() =>
195177
{
196-
// Dispatch to the draw thread to pin it.
197178
Host.DrawThread.Scheduler.Add(() =>
198179
{
199180
try { if (OboeAudioBridge.nSetThreadAffinity(0xF8) != 0) Debug.WriteLine("[osu!] Render thread pinned to big cores"); } catch { }
@@ -261,8 +242,6 @@ protected override void LoadComplete()
261242
Debug.WriteLine($"[osu!] Audio offset auto-suggested: {suggested:F1}ms (hardware latency={latency:F1}ms)");
262243
}, audioRedirector != null ? audioRedirector.Provider : IntPtr.Zero, sampleRate =>
263244
{
264-
// Only redirect audio once the Oboe stream has successfully started.
265-
// This prevents silence if the bridge fails to initialize.
266245
audioRedirector?.RefreshMixers(sampleRate > 0 ? sampleRate : hardwareSampleRate);
267246
Debug.WriteLine("[osu!] Audio redirector refreshed with hardware sample rate: " + sampleRate);
268247
});
@@ -277,7 +256,6 @@ protected override void LoadComplete()
277256
{
278257
stopOboeBridge();
279258
audioRedirector?.Dispose();
280-
// Re-create the redirector instance so it's fresh if re-enabled.
281259
audioRedirector = new OboeAudioRedirector(Audio);
282260
}
283261
}, true);
@@ -295,9 +273,8 @@ protected override void LoadComplete()
295273
{
296274
Debug.WriteLine($"[osu!] Failed to toggle Vulkan probe: {ex.Message}");
297275
}
298-
}, false); // Already started in load() if true.
276+
}, false);
299277

300-
// Apply unbuffered touch dispatch.
301278
try
302279
{
303280
if (OperatingSystem.IsAndroidVersionAtLeast(31))
@@ -332,8 +309,7 @@ private void applyPerformanceOptimizations(bool enabled)
332309

333310
if (enabled)
334311
{
335-
highPerformanceSession?.Dispose();
336-
highPerformanceSession = highPerformanceSessionManager.BeginSession();
312+
highPerformanceSession ??= highPerformanceSessionManager.BeginSession();
337313
}
338314
else
339315
{
@@ -405,15 +381,22 @@ private void selectHighestRefreshRate()
405381

406382
public override string VulkanStatus => (nativeBridges as AndroidNativeBridgeManager)?.GetVulkanStatus() ?? string.Empty;
407383

408-
409-
410384
public override bool IsOboeActive => (nativeBridges as AndroidNativeBridgeManager)?.IsOboeActive() ?? false;
411385

412-
public override string OboeStatus => (nativeBridges as AndroidNativeBridgeManager)?.GetOboeStatus() ?? string.Empty;
386+
public override bool IsOboeEnabled => lowLatencyAudio.Value;
413387

414-
public override double OboeLatency => (nativeBridges as AndroidNativeBridgeManager)?.GetMeasuredAudioLatencyMs() ?? -1;
388+
public override string OboeStatus
389+
{
390+
get
391+
{
392+
string status = (nativeBridges as AndroidNativeBridgeManager)?.GetOboeStatus() ?? (IsOboeEnabled ? "Initializing..." : string.Empty);
393+
if (IsOboeEnabled && audioRedirector != null && !audioRedirector.IsRedirecting && IsOboeActive)
394+
status += " [No Redirect]";
395+
return status;
396+
}
397+
}
415398

416-
private void onActiveMixersChanged(object? sender, NotifyCollectionChangedEventArgs args) => Schedule(() => { if (lowLatencyAudio.Value) audioRedirector?.RefreshMixers(0); });
399+
public override double OboeLatency => (nativeBridges as AndroidNativeBridgeManager)?.GetMeasuredAudioLatencyMs() ?? -1;
417400

418401
public double GetMeasuredAudioLatencyMs() => getMeasuredAudioLatencyFromBridge();
419402

osu.Game/OsuGameBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public virtual EndpointConfiguration CreateEndpoints() =>
123123
public virtual bool IsVulkanSupported => false;
124124

125125
public virtual bool IsOboeActive => false;
126-
126+
public virtual bool IsOboeEnabled => false;
127127
public virtual string OboeStatus => string.Empty;
128128

129129
public virtual double OboeLatency => -1;

0 commit comments

Comments
 (0)