Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add low latency initialisation support on windows by directly interacting with WASAPI #6088

Merged
merged 19 commits into from
Dec 26, 2023
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
2 changes: 1 addition & 1 deletion osu.Framework.Tests/Audio/BassTestComponents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public void Add(params AudioComponent[] component)

internal BassAudioMixer CreateMixer()
{
var mixer = new BassAudioMixer(Mixer, "Test mixer");
var mixer = new BassAudioMixer(null, Mixer, "Test mixer");
mixerComponents.AddItem(mixer);
return mixer;
}
Expand Down
46 changes: 39 additions & 7 deletions osu.Framework/Audio/AudioManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,30 @@ public class AudioManager : AudioCollectionManager<AudioComponent>
MaxValue = 1
};

/// <summary>
/// Whether a global mixer is being used for audio routing.
/// For now, this is only the case on Windows when using shared mode WASAPI initialisation.
/// </summary>
public IBindable<bool> UsingGlobalMixer => usingGlobalMixer;

private readonly Bindable<bool> usingGlobalMixer = new BindableBool();

/// <summary>
/// If a global mixer is being used, this will be the BASS handle for it.
/// If non-null, all game mixers should be added to this mixer.
/// </summary>
/// <remarks>
/// When this is non-null, all mixers created via <see cref="CreateAudioMixer"/>
/// will themselves be added to the global mixer, which will handle playback itself.
///
/// In this mode of operation, nested mixers will be created with the <see cref="BassFlags.Decode"/>
/// flag, meaning they no longer handle playback directly.
///
/// An eventual goal would be to use a global mixer across all platforms as it can result
/// in more control and better playback performance.
/// </remarks>
internal readonly IBindable<int?> GlobalMixerHandle = new Bindable<int?>();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get this may be a bit messy, but I don't want to over-complicate things. In the future we probably still want to revisit having a global mixer in all cases (#4915) at which point this will become the norm, not the "exception" for windows-only.

I could have added the global mixer in this PR but I felt that's probably a bad move to limit potential breakage.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a global parent mixer is probably fine by itself, but what I worry about more is the changes in flags that the presence of this global mixer forces (BassFlags.Decode, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin).

I'd probably expect more commentary in xmldoc here, such that if the global mixer is present, then it basically takes over the duties of playback and all child mixers do not play on their own but rather expose audio data which the global pulls from as required. It seems like a major paradigm shift that isn't really documented anywhere (in fact I'm attempting to back-reason this currently using bass docs, but I'm not really completely sure if I read the intent of setting the aforementioned flags correctly).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but I'm not really completely sure if I read the intent of setting the aforementioned flags correctly

the flags are required and it's all just bass nuances. i can document it but at the end of the day all i'll be doing is saying something like "this is done this way because it works". ie. without the decode flag it just breaks everything.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought there was some more nuance to it other than "make bass work in this configuration". If that is the case then I guess I can just not care.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of this stuff was trial and error in an isolated project until shit just worked 😅 💦


public override bool IsLoaded => base.IsLoaded &&
// bass default device is a null device (-1), not the actual system default.
Bass.CurrentDevice != Bass.DefaultDevice;
Expand Down Expand Up @@ -146,7 +170,12 @@ public AudioManager(AudioThread audioThread, ResourceStore<byte[]> trackStore, R

thread.RegisterManager(this);

AudioDevice.ValueChanged += onDeviceChanged;
AudioDevice.ValueChanged += _ => onDeviceChanged();
GlobalMixerHandle.ValueChanged += handle =>
{
onDeviceChanged();
usingGlobalMixer.Value = handle.NewValue.HasValue;
};

AddItem(TrackMixer = createAudioMixer(null, nameof(TrackMixer)));
AddItem(SampleMixer = createAudioMixer(null, nameof(SampleMixer)));
Expand Down Expand Up @@ -205,9 +234,9 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
}

private void onDeviceChanged(ValueChangedEvent<string> args)
private void onDeviceChanged()
{
scheduler.Add(() => setAudioDevice(args.NewValue));
scheduler.Add(() => setAudioDevice(AudioDevice.Value));
}

private void onDevicesChanged()
Expand Down Expand Up @@ -236,7 +265,7 @@ public AudioMixer CreateAudioMixer(string identifier = default) =>

private AudioMixer createAudioMixer(AudioMixer fallbackMixer, string identifier)
{
var mixer = new BassAudioMixer(fallbackMixer, identifier);
var mixer = new BassAudioMixer(this, fallbackMixer, identifier);
AddItem(mixer);
return mixer;
}
Expand Down Expand Up @@ -312,7 +341,7 @@ private bool setAudioDevice(string deviceName = null)
if (setAudioDevice(Bass.NoSoundDevice))
return true;

//we're fucked. even "No sound" device won't initialise.
// we're boned. even "No sound" device won't initialise.
return false;
}

Expand Down Expand Up @@ -365,7 +394,7 @@ protected virtual bool InitBass(int device)
return true;

// this likely doesn't help us but also doesn't seem to cause any issues or any cpu increase.
Bass.UpdatePeriod = 5;
Bass.UpdatePeriod = 1;

// reduce latency to a known sane minimum.
Bass.DeviceBufferLength = 10;
Expand All @@ -390,7 +419,10 @@ protected virtual bool InitBass(int device)
// See https://www.un4seen.com/forum/?topic=19601 for more information.
Bass.Configure((ManagedBass.Configuration)70, false);

return AudioThread.InitDevice(device);
if (!thread.InitDevice(device))
return false;

return true;
}

private void syncAudioDevices()
Expand Down
18 changes: 16 additions & 2 deletions osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ namespace osu.Framework.Audio.Mixing.Bass
/// </summary>
internal class BassAudioMixer : AudioMixer, IBassAudio
{
private readonly AudioManager? manager;

/// <summary>
/// The handle for this mixer.
/// </summary>
Expand All @@ -42,11 +44,13 @@ internal class BassAudioMixer : AudioMixer, IBassAudio
/// <summary>
/// Creates a new <see cref="BassAudioMixer"/>.
/// </summary>
/// <param name="manager">The game's audio manager.</param>
/// <param name="fallbackMixer"><inheritdoc /></param>
/// <param name="identifier">An identifier displayed on the audio mixer visualiser.</param>
public BassAudioMixer(AudioMixer? fallbackMixer, string identifier)
public BassAudioMixer(AudioManager? manager, AudioMixer? fallbackMixer, string identifier)
: base(fallbackMixer, identifier)
{
this.manager = manager;
EnqueueAction(createMixer);
}

Expand Down Expand Up @@ -248,7 +252,12 @@ public void UpdateDevice(int deviceIndex)
if (Handle == 0)
createMixer();
else
{
ManagedBass.Bass.ChannelSetDevice(Handle, deviceIndex);

if (manager?.GlobalMixerHandle.Value != null)
BassMix.MixerAddChannel(manager.GlobalMixerHandle.Value.Value, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin);
}
}

protected override void UpdateState()
Expand Down Expand Up @@ -277,7 +286,9 @@ private void createMixer()
if (!ManagedBass.Bass.GetDeviceInfo(ManagedBass.Bass.CurrentDevice, out var deviceInfo) || !deviceInfo.IsInitialized)
return;

Handle = BassMix.CreateMixerStream(frequency, 2, BassFlags.MixerNonStop);
Handle = manager?.GlobalMixerHandle.Value != null
? BassMix.CreateMixerStream(frequency, 2, BassFlags.MixerNonStop | BassFlags.Decode)
: BassMix.CreateMixerStream(frequency, 2, BassFlags.MixerNonStop);

if (Handle == 0)
return;
Expand All @@ -293,6 +304,9 @@ private void createMixer()

Effects.BindCollectionChanged(onEffectsChanged, true);

if (manager?.GlobalMixerHandle.Value != null)
BassMix.MixerAddChannel(manager.GlobalMixerHandle.Value.Value, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin);

ManagedBass.Bass.ChannelPlay(Handle);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using System;
using ManagedBass;
using ManagedBass.Mix;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
Expand All @@ -28,6 +30,7 @@ public partial class AudioChannelDisplay : CompositeDrawable
private float maxPeak = float.MinValue;
private double lastMaxPeakTime;
private readonly bool isOutputChannel;
private IBindable<bool> usingGlobalMixer = null!;

public AudioChannelDisplay(int channelHandle, bool isOutputChannel = false)
{
Expand Down Expand Up @@ -100,13 +103,19 @@ public AudioChannelDisplay(int channelHandle, bool isOutputChannel = false)
};
}

[BackgroundDependencyLoader]
private void load(AudioManager audioManager)
{
usingGlobalMixer = audioManager.UsingGlobalMixer.GetBoundCopy();
}

protected override void Update()
{
base.Update();

float[] levels = new float[2];

if (isOutputChannel)
if (isOutputChannel && !usingGlobalMixer.Value)
Bass.ChannelGetLevel(ChannelHandle, levels, 1 / 1000f * sample_window, LevelRetrievalFlags.Stereo);
else
BassMix.ChannelGetLevel(ChannelHandle, levels, 1 / 1000f * sample_window, LevelRetrievalFlags.Stereo);
Expand Down
121 changes: 113 additions & 8 deletions osu.Framework/Threading/AudioThread.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
using System.Diagnostics;
using System.Linq;
using ManagedBass;
using ManagedBass.Mix;
using ManagedBass.Wasapi;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Development;
using osu.Framework.Platform.Linux.Native;

Expand Down Expand Up @@ -73,12 +76,16 @@ internal void RegisterManager(AudioManager manager)

managers.Add(manager);
}

manager.GlobalMixerHandle.BindTo(globalMixerHandle);
}

internal void UnregisterManager(AudioManager manager)
{
lock (managers)
managers.Remove(manager);

manager.GlobalMixerHandle.UnbindFrom(globalMixerHandle);
}

protected override void OnExit()
Expand Down Expand Up @@ -109,22 +116,35 @@ protected override void OnExit()
FreeDevice(d);
}

internal static bool InitDevice(int deviceId)
#region BASS Initialisation

// TODO: All this bass init stuff should probably not be in this class.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether this is something to be actioned at this time or left for later but I most definitely agree. Should be in its own class, preferably behind a [SupportedOSPlatform("windows")] attribute.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could move it now, but was planning to leave for a follow-up effort, because I guarantee that will increase review overhead ten-fold.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll look away for now then.


private WasapiProcedure? wasapiProcedure;
private WasapiNotifyProcedure? wasapiNotifyProcedure;

/// <summary>
/// If a global mixer is being used, this will be the BASS handle for it.
/// If non-null, all game mixers should be added to this mixer.
/// </summary>
private readonly Bindable<int?> globalMixerHandle = new Bindable<int?>();

internal bool InitDevice(int deviceId)
{
Debug.Assert(ThreadSafety.IsAudioThread);
Trace.Assert(deviceId != -1); // The real device ID should always be used, as the -1 device has special cases which are hard to work with.

// Try to initialise the device, or request a re-initialise.
if (Bass.Init(deviceId, Flags: (DeviceInitFlags)128)) // 128 == BASS_DEVICE_REINIT
{
initialised_devices.Add(deviceId);
return true;
}
if (!Bass.Init(deviceId, Flags: (DeviceInitFlags)128)) // 128 == BASS_DEVICE_REINIT
return false;

attemptWasapiInitialisation();

return false;
initialised_devices.Add(deviceId);
return true;
}

internal static void FreeDevice(int deviceId)
internal void FreeDevice(int deviceId)
{
Debug.Assert(ThreadSafety.IsAudioThread);

Expand All @@ -136,6 +156,8 @@ internal static void FreeDevice(int deviceId)
Bass.Free();
}

freeWasapi();

if (selectedDevice != deviceId && canSelectDevice(selectedDevice))
Bass.CurrentDevice = selectedDevice;

Expand All @@ -155,5 +177,88 @@ internal static void PreloadBass()
Library.Load("libbass.so", Library.LoadFlags.RTLD_LAZY | Library.LoadFlags.RTLD_GLOBAL);
}
}

private void attemptWasapiInitialisation()
{
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
return;

int wasapiDevice = -1;

// WASAPI device indices don't match normal BASS devices.
// Each device is listed multiple times with each supported channel/frequency pair.
//
// Working backwards to find the correct device is how bass does things internally (see BassWasapi.GetBassDevice).
if (Bass.CurrentDevice > 0)
{
string driver = Bass.GetDeviceInfo(Bass.CurrentDevice).Driver;

if (!string.IsNullOrEmpty(driver))
{
// In the normal execution case, BassWasapi.GetDeviceInfo will return false as soon as we reach the end of devices.
// This while condition is just a safety to avoid looping forever.
// It's intentionally quite high because if a user has many audio devices, this list can get long.
//
// Retrieving device info here isn't free. In the future we may want to investigate a better method.
while (wasapiDevice < 16384)
{
if (!BassWasapi.GetDeviceInfo(++wasapiDevice, out WasapiDeviceInfo info))
break;

if (info.ID == driver)
break;
}
}
}

// To keep things in a sane state let's only keep one device initialised via wasapi.
freeWasapi();
initWasapi(wasapiDevice);
}

private void initWasapi(int wasapiDevice)
{
// This is intentionally initialised inline and stored to a field.
// If we don't do this, it gets GC'd away.
wasapiProcedure = (buffer, length, _) =>
{
if (globalMixerHandle.Value == null)
return 0;

return Bass.ChannelGetData(globalMixerHandle.Value!.Value, buffer, length);
};
wasapiNotifyProcedure = (notify, device, _) => Scheduler.Add(() =>
{
if (notify == WasapiNotificationType.DefaultOutput)
{
freeWasapi();
initWasapi(device);
}
});

bool initialised = BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Buffer: 0.001f, Period: 0.001f);

if (!initialised)
return;

BassWasapi.GetInfo(out var wasapiInfo);
globalMixerHandle.Value = BassMix.CreateMixerStream(wasapiInfo.Frequency, wasapiInfo.Channels, BassFlags.MixerNonStop | BassFlags.Decode | BassFlags.Float);
BassWasapi.Start();

BassWasapi.SetNotify(wasapiNotifyProcedure);
}

private void freeWasapi()
{
if (globalMixerHandle.Value == null) return;

// The mixer probably doesn't need to be recycled. Just keeping things sane for now.
Bass.StreamFree(globalMixerHandle.Value.Value);
BassWasapi.Stop();
BassWasapi.Free();
globalMixerHandle.Value = null;
}

#endregion
}
}
3 changes: 2 additions & 1 deletion osu.Framework/osu.Framework.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<PackageReference Include="ppy.ManagedBass" Version="2022.1216.0" />
<PackageReference Include="ppy.ManagedBass.Fx" Version="2022.1216.0" />
<PackageReference Include="ppy.ManagedBass.Mix" Version="2022.1216.0" />
<PackageReference Include="ppy.ManagedBass.Wasapi" Version="2022.1216.0" />
<PackageReference Include="ppy.Veldrid" Version="4.9.3-g91ce5a6cda" />
<PackageReference Include="ppy.Veldrid.SPIRV" Version="1.0.15-gca6cec7843" />
<PackageReference Include="SharpFNT" Version="2.0.0" />
Expand All @@ -43,7 +44,7 @@

<!-- DO NOT use ProjectReference for native packaging project.
See https://github.com/NuGet/Home/issues/4514 and https://github.com/dotnet/sdk/issues/765 . -->
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2023.1205.0-nativelibs" />
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2023.1225.0-nativelibs" />

<!-- Any version ahead of this will cause AOT issues with iOS
See https://github.com/mono/mono/issues/21188 -->
Expand Down
Loading