From 23fe527dcdd5371e4089e2edd2f7fab1aa85e59a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 14:32:34 +0900 Subject: [PATCH 01/18] Update nativelibs to include wasapi dll --- osu.Framework/osu.Framework.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index 6cc9517b08..dd9e1985dc 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -43,7 +43,7 @@ - + From fb86f55c162d55a8bb8f80f0587d9f75f9a75477 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 00:44:43 +0900 Subject: [PATCH 02/18] Transfer initial implementation of `BassWasapi` initialisation from test project --- osu.Framework/Audio/AudioManager.cs | 2 +- .../Audio/Mixing/Bass/BassAudioMixer.cs | 6 +++- osu.Framework/Threading/AudioThread.cs | 28 +++++++++++++++++++ osu.Framework/osu.Framework.csproj | 1 + 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/osu.Framework/Audio/AudioManager.cs b/osu.Framework/Audio/AudioManager.cs index 2e4cfe5e21..e1ae5d4a8e 100644 --- a/osu.Framework/Audio/AudioManager.cs +++ b/osu.Framework/Audio/AudioManager.cs @@ -312,7 +312,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; } diff --git a/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs b/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs index c0d2443f6b..cebfc5fcb6 100644 --- a/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs +++ b/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs @@ -14,6 +14,7 @@ using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Statistics; +using osu.Framework.Threading; namespace osu.Framework.Audio.Mixing.Bass { @@ -277,7 +278,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 = AudioThread.WasapiMixer != 0 + ? BassMix.CreateMixerStream(frequency, 2, BassFlags.MixerNonStop | BassFlags.Decode) + : BassMix.CreateMixerStream(frequency, 2, BassFlags.MixerNonStop); if (Handle == 0) return; @@ -293,6 +296,7 @@ private void createMixer() Effects.BindCollectionChanged(onEffectsChanged, true); + BassMix.MixerAddChannel(AudioThread.WasapiMixer, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin); ManagedBass.Bass.ChannelPlay(Handle); } diff --git a/osu.Framework/Threading/AudioThread.cs b/osu.Framework/Threading/AudioThread.cs index d454c02811..b714aa90c5 100644 --- a/osu.Framework/Threading/AudioThread.cs +++ b/osu.Framework/Threading/AudioThread.cs @@ -7,6 +7,8 @@ using System.Diagnostics; using System.Linq; using ManagedBass; +using ManagedBass.Mix; +using ManagedBass.Wasapi; using osu.Framework.Audio; using osu.Framework.Development; using osu.Framework.Platform.Linux.Native; @@ -49,6 +51,11 @@ internal sealed override void MakeCurrent() private long frameCount; + private static bool usingWasapi; + private static WasapiProcedure? wasapiProcedure; + + public static int WasapiMixer { get; private set; } + private void onNewFrame() { if (frameCount++ % 1000 == 0) @@ -117,6 +124,22 @@ internal static bool InitDevice(int deviceId) // Try to initialise the device, or request a re-initialise. if (Bass.Init(deviceId, Flags: (DeviceInitFlags)128)) // 128 == BASS_DEVICE_REINIT { + if (WasapiMixer == 0) + { + wasapiProcedure = (buffer, length, user) => Bass.ChannelGetData(WasapiMixer, buffer, length); + + usingWasapi = BassWasapi.Init(-1, Procedure: wasapiProcedure, Buffer: 0.02f, Period: 0.005f); + + if (usingWasapi) + { + BassWasapi.GetInfo(out var wasapiInfo); + WasapiMixer = BassMix.CreateMixerStream(wasapiInfo.Frequency, wasapiInfo.Channels, BassFlags.MixerNonStop | BassFlags.Decode | BassFlags.Float); + BassWasapi.Start(); + + Bass.ChannelSetAttribute(WasapiMixer, ChannelAttribute.Buffer, 0); + } + } + initialised_devices.Add(deviceId); return true; } @@ -124,6 +147,9 @@ internal static bool InitDevice(int deviceId) return false; } + private static int wasapiProc(IntPtr buffer, int length, IntPtr user) => + Bass.ChannelGetData(WasapiMixer, buffer, length); + internal static void FreeDevice(int deviceId) { Debug.Assert(ThreadSafety.IsAudioThread); @@ -136,6 +162,8 @@ internal static void FreeDevice(int deviceId) Bass.Free(); } + // TODO: wasapi free? + if (selectedDevice != deviceId && canSelectDevice(selectedDevice)) Bass.CurrentDevice = selectedDevice; diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index dd9e1985dc..2deb0eb967 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -27,6 +27,7 @@ + From 5807165384cf2b3bd7385308a8fd1844be02eaba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 02:18:25 +0900 Subject: [PATCH 03/18] Fix mixers not being added back to wasapi --- osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs b/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs index cebfc5fcb6..d72aafc7a1 100644 --- a/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs +++ b/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs @@ -249,7 +249,12 @@ public void UpdateDevice(int deviceIndex) if (Handle == 0) createMixer(); else + { ManagedBass.Bass.ChannelSetDevice(Handle, deviceIndex); + + if (AudioThread.WasapiMixer != 0) + BassMix.MixerAddChannel(AudioThread.WasapiMixer, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin); + } } protected override void UpdateState() @@ -296,7 +301,9 @@ private void createMixer() Effects.BindCollectionChanged(onEffectsChanged, true); - BassMix.MixerAddChannel(AudioThread.WasapiMixer, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin); + if (AudioThread.WasapiMixer != 0) + BassMix.MixerAddChannel(AudioThread.WasapiMixer, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin); + ManagedBass.Bass.ChannelPlay(Handle); } From 82663ab1f6c859b0f36e6976e84c8c0b1ba42e44 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 02:18:48 +0900 Subject: [PATCH 04/18] Fix wasapi initialisation device target --- osu.Framework/Threading/AudioThread.cs | 43 ++++++++++++++++++-------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/osu.Framework/Threading/AudioThread.cs b/osu.Framework/Threading/AudioThread.cs index b714aa90c5..491ee118c2 100644 --- a/osu.Framework/Threading/AudioThread.cs +++ b/osu.Framework/Threading/AudioThread.cs @@ -124,22 +124,42 @@ internal static bool InitDevice(int deviceId) // Try to initialise the device, or request a re-initialise. if (Bass.Init(deviceId, Flags: (DeviceInitFlags)128)) // 128 == BASS_DEVICE_REINIT { - if (WasapiMixer == 0) - { - wasapiProcedure = (buffer, length, user) => Bass.ChannelGetData(WasapiMixer, buffer, length); + int wasapiDevice = -1; - usingWasapi = BassWasapi.Init(-1, Procedure: wasapiProcedure, Buffer: 0.02f, Period: 0.005f); + if (Bass.CurrentDevice > 0) + { + string driver = Bass.GetDeviceInfo(Bass.CurrentDevice).Driver; - if (usingWasapi) + if (!string.IsNullOrEmpty(driver)) { - BassWasapi.GetInfo(out var wasapiInfo); - WasapiMixer = BassMix.CreateMixerStream(wasapiInfo.Frequency, wasapiInfo.Channels, BassFlags.MixerNonStop | BassFlags.Decode | BassFlags.Float); - BassWasapi.Start(); - - Bass.ChannelSetAttribute(WasapiMixer, ChannelAttribute.Buffer, 0); + while (true) + { + if (!BassWasapi.GetDeviceInfo(++wasapiDevice, out WasapiDeviceInfo info)) + break; + + if (info.ID == driver) + break; + } } } + if (WasapiMixer != 0) + { + Bass.StreamFree(WasapiMixer); + BassWasapi.Free(); + WasapiMixer = 0; + } + + wasapiProcedure = (buffer, length, _) => Bass.ChannelGetData(WasapiMixer, buffer, length); + usingWasapi = BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Buffer: 0.02f, Period: 0.005f); + + if (usingWasapi) + { + BassWasapi.GetInfo(out var wasapiInfo); + WasapiMixer = BassMix.CreateMixerStream(wasapiInfo.Frequency, wasapiInfo.Channels, BassFlags.MixerNonStop | BassFlags.Decode | BassFlags.Float); + BassWasapi.Start(); + } + initialised_devices.Add(deviceId); return true; } @@ -147,9 +167,6 @@ internal static bool InitDevice(int deviceId) return false; } - private static int wasapiProc(IntPtr buffer, int length, IntPtr user) => - Bass.ChannelGetData(WasapiMixer, buffer, length); - internal static void FreeDevice(int deviceId) { Debug.Assert(ThreadSafety.IsAudioThread); From f03169d721aae8eaffc7ca7dfdc3ed393850b20f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 14:54:50 +0900 Subject: [PATCH 05/18] Isolate wasapi initialisation to windows platform --- osu.Framework/Threading/AudioThread.cs | 72 ++++++++++++++------------ 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/osu.Framework/Threading/AudioThread.cs b/osu.Framework/Threading/AudioThread.cs index 491ee118c2..5086818f67 100644 --- a/osu.Framework/Threading/AudioThread.cs +++ b/osu.Framework/Threading/AudioThread.cs @@ -122,49 +122,55 @@ internal static bool InitDevice(int deviceId) 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 - { - int wasapiDevice = -1; + if (!Bass.Init(deviceId, Flags: (DeviceInitFlags)128)) // 128 == BASS_DEVICE_REINIT + return false; - if (Bass.CurrentDevice > 0) - { - string driver = Bass.GetDeviceInfo(Bass.CurrentDevice).Driver; + attemptWasapiInitialisation(); - if (!string.IsNullOrEmpty(driver)) - { - while (true) - { - if (!BassWasapi.GetDeviceInfo(++wasapiDevice, out WasapiDeviceInfo info)) - break; - - if (info.ID == driver) - break; - } - } - } + initialised_devices.Add(deviceId); + return true; + } - if (WasapiMixer != 0) - { - Bass.StreamFree(WasapiMixer); - BassWasapi.Free(); - WasapiMixer = 0; - } + private static void attemptWasapiInitialisation() + { + if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) + return; - wasapiProcedure = (buffer, length, _) => Bass.ChannelGetData(WasapiMixer, buffer, length); - usingWasapi = BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Buffer: 0.02f, Period: 0.005f); + int wasapiDevice = -1; - if (usingWasapi) + if (Bass.CurrentDevice > 0) + { + string driver = Bass.GetDeviceInfo(Bass.CurrentDevice).Driver; + + if (!string.IsNullOrEmpty(driver)) { - BassWasapi.GetInfo(out var wasapiInfo); - WasapiMixer = BassMix.CreateMixerStream(wasapiInfo.Frequency, wasapiInfo.Channels, BassFlags.MixerNonStop | BassFlags.Decode | BassFlags.Float); - BassWasapi.Start(); + while (true) + { + if (!BassWasapi.GetDeviceInfo(++wasapiDevice, out WasapiDeviceInfo info)) + break; + + if (info.ID == driver) + break; + } } + } - initialised_devices.Add(deviceId); - return true; + if (WasapiMixer != 0) + { + Bass.StreamFree(WasapiMixer); + BassWasapi.Free(); + WasapiMixer = 0; } - return false; + wasapiProcedure = (buffer, length, _) => Bass.ChannelGetData(WasapiMixer, buffer, length); + usingWasapi = BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Buffer: 0.02f, Period: 0.005f); + + if (usingWasapi) + { + BassWasapi.GetInfo(out var wasapiInfo); + WasapiMixer = BassMix.CreateMixerStream(wasapiInfo.Frequency, wasapiInfo.Channels, BassFlags.MixerNonStop | BassFlags.Decode | BassFlags.Float); + BassWasapi.Start(); + } } internal static void FreeDevice(int deviceId) From c5db3e7bd0081cc91fc8b04d5658391706390ee8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 15:13:47 +0900 Subject: [PATCH 06/18] Add comments and tidy up wasapi initialisation code --- osu.Framework/Threading/AudioThread.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Framework/Threading/AudioThread.cs b/osu.Framework/Threading/AudioThread.cs index 5086818f67..6e9b3aeb22 100644 --- a/osu.Framework/Threading/AudioThread.cs +++ b/osu.Framework/Threading/AudioThread.cs @@ -51,7 +51,6 @@ internal sealed override void MakeCurrent() private long frameCount; - private static bool usingWasapi; private static WasapiProcedure? wasapiProcedure; public static int WasapiMixer { get; private set; } @@ -138,13 +137,22 @@ private static void attemptWasapiInitialisation() 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)) { - while (true) + // 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; @@ -155,6 +163,8 @@ private static void attemptWasapiInitialisation() } } + // To keep things in a sane state let's only keep one device initialised via wasapi. + // TODO: The mixer probably doesn't need to be recycled. Just keeping things sane for now. if (WasapiMixer != 0) { Bass.StreamFree(WasapiMixer); @@ -162,10 +172,11 @@ private static void attemptWasapiInitialisation() WasapiMixer = 0; } + // This is intentionally initialised inline and stored to a field. + // If we don't do this, it gets GC'd away. wasapiProcedure = (buffer, length, _) => Bass.ChannelGetData(WasapiMixer, buffer, length); - usingWasapi = BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Buffer: 0.02f, Period: 0.005f); - if (usingWasapi) + if (BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Buffer: 0.02f, Period: 0.005f)) { BassWasapi.GetInfo(out var wasapiInfo); WasapiMixer = BassMix.CreateMixerStream(wasapiInfo.Frequency, wasapiInfo.Channels, BassFlags.MixerNonStop | BassFlags.Decode | BassFlags.Float); From 37e5f8b2631d72ff3d6b5f191a7b757051d54cd5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 15:25:14 +0900 Subject: [PATCH 07/18] Add note about bass init things being in the wrong place --- osu.Framework/Threading/AudioThread.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Framework/Threading/AudioThread.cs b/osu.Framework/Threading/AudioThread.cs index 6e9b3aeb22..807e490242 100644 --- a/osu.Framework/Threading/AudioThread.cs +++ b/osu.Framework/Threading/AudioThread.cs @@ -115,6 +115,8 @@ protected override void OnExit() FreeDevice(d); } + // TODO: All this bass init stuff should proably not be in this class. + internal static bool InitDevice(int deviceId) { Debug.Assert(ThreadSafety.IsAudioThread); From a7a254aac992e3562741a20238aa6c100e7f8532 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 15:27:41 +0900 Subject: [PATCH 08/18] Rename concept of "global" mixer with "fallback" --- osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs b/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs index d72aafc7a1..21074509c1 100644 --- a/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs +++ b/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs @@ -252,8 +252,8 @@ public void UpdateDevice(int deviceIndex) { ManagedBass.Bass.ChannelSetDevice(Handle, deviceIndex); - if (AudioThread.WasapiMixer != 0) - BassMix.MixerAddChannel(AudioThread.WasapiMixer, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin); + if (AudioThread.GlobalMixer != 0) + BassMix.MixerAddChannel(AudioThread.GlobalMixer, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin); } } @@ -283,7 +283,7 @@ private void createMixer() if (!ManagedBass.Bass.GetDeviceInfo(ManagedBass.Bass.CurrentDevice, out var deviceInfo) || !deviceInfo.IsInitialized) return; - Handle = AudioThread.WasapiMixer != 0 + Handle = AudioThread.GlobalMixer != 0 ? BassMix.CreateMixerStream(frequency, 2, BassFlags.MixerNonStop | BassFlags.Decode) : BassMix.CreateMixerStream(frequency, 2, BassFlags.MixerNonStop); @@ -301,8 +301,8 @@ private void createMixer() Effects.BindCollectionChanged(onEffectsChanged, true); - if (AudioThread.WasapiMixer != 0) - BassMix.MixerAddChannel(AudioThread.WasapiMixer, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin); + if (AudioThread.GlobalMixer != 0) + BassMix.MixerAddChannel(AudioThread.GlobalMixer, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin); ManagedBass.Bass.ChannelPlay(Handle); } From f3c39e8c3907aa44991583c7a98511485e511fa4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 15:27:49 +0900 Subject: [PATCH 09/18] Remove `static` usage --- .../Audio/BassTestComponents.cs | 2 +- osu.Framework/Audio/AudioManager.cs | 13 +++++-- .../Audio/Mixing/Bass/BassAudioMixer.cs | 17 +++++---- osu.Framework/Threading/AudioThread.cs | 35 +++++++++++++------ 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/osu.Framework.Tests/Audio/BassTestComponents.cs b/osu.Framework.Tests/Audio/BassTestComponents.cs index 0d476420bf..416b0ccbf9 100644 --- a/osu.Framework.Tests/Audio/BassTestComponents.cs +++ b/osu.Framework.Tests/Audio/BassTestComponents.cs @@ -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; } diff --git a/osu.Framework/Audio/AudioManager.cs b/osu.Framework/Audio/AudioManager.cs index e1ae5d4a8e..b5395868a5 100644 --- a/osu.Framework/Audio/AudioManager.cs +++ b/osu.Framework/Audio/AudioManager.cs @@ -108,6 +108,12 @@ public class AudioManager : AudioCollectionManager MaxValue = 1 }; + /// + /// 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. + /// + internal readonly Bindable GlobalMixerHandle = new Bindable(); + public override bool IsLoaded => base.IsLoaded && // bass default device is a null device (-1), not the actual system default. Bass.CurrentDevice != Bass.DefaultDevice; @@ -236,7 +242,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; } @@ -390,7 +396,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() diff --git a/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs b/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs index 21074509c1..2605a84008 100644 --- a/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs +++ b/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs @@ -14,7 +14,6 @@ using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Statistics; -using osu.Framework.Threading; namespace osu.Framework.Audio.Mixing.Bass { @@ -23,6 +22,8 @@ namespace osu.Framework.Audio.Mixing.Bass /// internal class BassAudioMixer : AudioMixer, IBassAudio { + private readonly AudioManager? manager; + /// /// The handle for this mixer. /// @@ -43,11 +44,13 @@ internal class BassAudioMixer : AudioMixer, IBassAudio /// /// Creates a new . /// + /// The game's audio manager. /// /// An identifier displayed on the audio mixer visualiser. - public BassAudioMixer(AudioMixer? fallbackMixer, string identifier) + public BassAudioMixer(AudioManager? manager, AudioMixer? fallbackMixer, string identifier) : base(fallbackMixer, identifier) { + this.manager = manager; EnqueueAction(createMixer); } @@ -252,8 +255,8 @@ public void UpdateDevice(int deviceIndex) { ManagedBass.Bass.ChannelSetDevice(Handle, deviceIndex); - if (AudioThread.GlobalMixer != 0) - BassMix.MixerAddChannel(AudioThread.GlobalMixer, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin); + if (manager?.GlobalMixerHandle.Value != null) + BassMix.MixerAddChannel(manager.GlobalMixerHandle.Value.Value, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin); } } @@ -283,7 +286,7 @@ private void createMixer() if (!ManagedBass.Bass.GetDeviceInfo(ManagedBass.Bass.CurrentDevice, out var deviceInfo) || !deviceInfo.IsInitialized) return; - Handle = AudioThread.GlobalMixer != 0 + Handle = manager?.GlobalMixerHandle.Value != null ? BassMix.CreateMixerStream(frequency, 2, BassFlags.MixerNonStop | BassFlags.Decode) : BassMix.CreateMixerStream(frequency, 2, BassFlags.MixerNonStop); @@ -301,8 +304,8 @@ private void createMixer() Effects.BindCollectionChanged(onEffectsChanged, true); - if (AudioThread.GlobalMixer != 0) - BassMix.MixerAddChannel(AudioThread.GlobalMixer, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin); + if (manager?.GlobalMixerHandle.Value != null) + BassMix.MixerAddChannel(manager.GlobalMixerHandle.Value.Value, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin); ManagedBass.Bass.ChannelPlay(Handle); } diff --git a/osu.Framework/Threading/AudioThread.cs b/osu.Framework/Threading/AudioThread.cs index 807e490242..8c14293fa8 100644 --- a/osu.Framework/Threading/AudioThread.cs +++ b/osu.Framework/Threading/AudioThread.cs @@ -10,6 +10,7 @@ using ManagedBass.Mix; using ManagedBass.Wasapi; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Development; using osu.Framework.Platform.Linux.Native; @@ -51,10 +52,6 @@ internal sealed override void MakeCurrent() private long frameCount; - private static WasapiProcedure? wasapiProcedure; - - public static int WasapiMixer { get; private set; } - private void onNewFrame() { if (frameCount++ % 1000 == 0) @@ -79,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() @@ -115,9 +116,19 @@ protected override void OnExit() FreeDevice(d); } + #region BASS Initialisation + // TODO: All this bass init stuff should proably not be in this class. - internal static bool InitDevice(int deviceId) + private WasapiProcedure? wasapiProcedure; + + /// + /// 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. + /// + private readonly Bindable globalMixerHandle = new Bindable(); + + 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. @@ -132,7 +143,7 @@ internal static bool InitDevice(int deviceId) return true; } - private static void attemptWasapiInitialisation() + private void attemptWasapiInitialisation() { if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) return; @@ -167,21 +178,21 @@ private static void attemptWasapiInitialisation() // To keep things in a sane state let's only keep one device initialised via wasapi. // TODO: The mixer probably doesn't need to be recycled. Just keeping things sane for now. - if (WasapiMixer != 0) + if (globalMixerHandle.Value != null) { - Bass.StreamFree(WasapiMixer); + Bass.StreamFree(globalMixerHandle.Value.Value); BassWasapi.Free(); - WasapiMixer = 0; + globalMixerHandle.Value = null; } // This is intentionally initialised inline and stored to a field. // If we don't do this, it gets GC'd away. - wasapiProcedure = (buffer, length, _) => Bass.ChannelGetData(WasapiMixer, buffer, length); + wasapiProcedure = (buffer, length, _) => Bass.ChannelGetData(globalMixerHandle.Value!.Value, buffer, length); if (BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Buffer: 0.02f, Period: 0.005f)) { BassWasapi.GetInfo(out var wasapiInfo); - WasapiMixer = BassMix.CreateMixerStream(wasapiInfo.Frequency, wasapiInfo.Channels, BassFlags.MixerNonStop | BassFlags.Decode | BassFlags.Float); + globalMixerHandle.Value = BassMix.CreateMixerStream(wasapiInfo.Frequency, wasapiInfo.Channels, BassFlags.MixerNonStop | BassFlags.Decode | BassFlags.Float); BassWasapi.Start(); } } @@ -219,5 +230,7 @@ internal static void PreloadBass() Library.Load("libbass.so", Library.LoadFlags.RTLD_LAZY | Library.LoadFlags.RTLD_GLOBAL); } } + + #endregion } } From 6813e3b89901aa23d5385f418b23a710c0c1eae5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 15:49:18 +0900 Subject: [PATCH 10/18] Add safety to wasapi callback --- osu.Framework/Threading/AudioThread.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Framework/Threading/AudioThread.cs b/osu.Framework/Threading/AudioThread.cs index 8c14293fa8..c345f82af2 100644 --- a/osu.Framework/Threading/AudioThread.cs +++ b/osu.Framework/Threading/AudioThread.cs @@ -187,7 +187,13 @@ private void attemptWasapiInitialisation() // This is intentionally initialised inline and stored to a field. // If we don't do this, it gets GC'd away. - wasapiProcedure = (buffer, length, _) => Bass.ChannelGetData(globalMixerHandle.Value!.Value, buffer, length); + wasapiProcedure = (buffer, length, _) => + { + if (globalMixerHandle.Value == null) + return 0; + + return Bass.ChannelGetData(globalMixerHandle.Value!.Value, buffer, length); + }; if (BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Buffer: 0.02f, Period: 0.005f)) { From 64360a8bac0cb4ad3a0ebba8800739b4063f6fd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 15:54:38 +0900 Subject: [PATCH 11/18] Expose `GlobalMixerHandle` as `IBindable` only --- osu.Framework/Audio/AudioManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Audio/AudioManager.cs b/osu.Framework/Audio/AudioManager.cs index b5395868a5..517067ed80 100644 --- a/osu.Framework/Audio/AudioManager.cs +++ b/osu.Framework/Audio/AudioManager.cs @@ -112,7 +112,7 @@ public class AudioManager : AudioCollectionManager /// 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. /// - internal readonly Bindable GlobalMixerHandle = new Bindable(); + internal readonly IBindable GlobalMixerHandle = new Bindable(); public override bool IsLoaded => base.IsLoaded && // bass default device is a null device (-1), not the actual system default. From a0b2071c069fbce03d00ada342956ac958dd439a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 17:28:53 +0900 Subject: [PATCH 12/18] Fix typo Co-authored-by: Bastian Pedersen --- osu.Framework/Threading/AudioThread.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Threading/AudioThread.cs b/osu.Framework/Threading/AudioThread.cs index c345f82af2..82791adc16 100644 --- a/osu.Framework/Threading/AudioThread.cs +++ b/osu.Framework/Threading/AudioThread.cs @@ -118,7 +118,7 @@ protected override void OnExit() #region BASS Initialisation - // TODO: All this bass init stuff should proably not be in this class. + // TODO: All this bass init stuff should probably not be in this class. private WasapiProcedure? wasapiProcedure; From 7aafe275293f78490b809fa1a171fc3441f1f292 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 23:43:22 +0900 Subject: [PATCH 13/18] Free wasapi on exit --- osu.Framework/Threading/AudioThread.cs | 78 ++++++++++++++------------ 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/osu.Framework/Threading/AudioThread.cs b/osu.Framework/Threading/AudioThread.cs index 82791adc16..439cf4d16b 100644 --- a/osu.Framework/Threading/AudioThread.cs +++ b/osu.Framework/Threading/AudioThread.cs @@ -143,6 +143,40 @@ internal bool InitDevice(int deviceId) return true; } + internal void FreeDevice(int deviceId) + { + Debug.Assert(ThreadSafety.IsAudioThread); + + int selectedDevice = Bass.CurrentDevice; + + if (canSelectDevice(deviceId)) + { + Bass.CurrentDevice = deviceId; + Bass.Free(); + } + + freeWasapi(); + + if (selectedDevice != deviceId && canSelectDevice(selectedDevice)) + Bass.CurrentDevice = selectedDevice; + + initialised_devices.Remove(deviceId); + + static bool canSelectDevice(int deviceId) => Bass.GetDeviceInfo(deviceId, out var deviceInfo) && deviceInfo.IsInitialized; + } + + /// + /// Makes BASS available to be consumed. + /// + internal static void PreloadBass() + { + if (RuntimeInfo.OS == RuntimeInfo.Platform.Linux) + { + // required for the time being to address libbass_fx.so load failures (see https://github.com/ppy/osu/issues/2852) + Library.Load("libbass.so", Library.LoadFlags.RTLD_LAZY | Library.LoadFlags.RTLD_GLOBAL); + } + } + private void attemptWasapiInitialisation() { if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) @@ -177,13 +211,7 @@ private void attemptWasapiInitialisation() } // To keep things in a sane state let's only keep one device initialised via wasapi. - // TODO: The mixer probably doesn't need to be recycled. Just keeping things sane for now. - if (globalMixerHandle.Value != null) - { - Bass.StreamFree(globalMixerHandle.Value.Value); - BassWasapi.Free(); - globalMixerHandle.Value = null; - } + freeWasapi(); // This is intentionally initialised inline and stored to a field. // If we don't do this, it gets GC'd away. @@ -203,38 +231,14 @@ private void attemptWasapiInitialisation() } } - internal static void FreeDevice(int deviceId) + private void freeWasapi() { - Debug.Assert(ThreadSafety.IsAudioThread); - - int selectedDevice = Bass.CurrentDevice; - - if (canSelectDevice(deviceId)) - { - Bass.CurrentDevice = deviceId; - Bass.Free(); - } - - // TODO: wasapi free? - - if (selectedDevice != deviceId && canSelectDevice(selectedDevice)) - Bass.CurrentDevice = selectedDevice; + if (globalMixerHandle.Value == null) return; - initialised_devices.Remove(deviceId); - - static bool canSelectDevice(int deviceId) => Bass.GetDeviceInfo(deviceId, out var deviceInfo) && deviceInfo.IsInitialized; - } - - /// - /// Makes BASS available to be consumed. - /// - internal static void PreloadBass() - { - if (RuntimeInfo.OS == RuntimeInfo.Platform.Linux) - { - // required for the time being to address libbass_fx.so load failures (see https://github.com/ppy/osu/issues/2852) - Library.Load("libbass.so", Library.LoadFlags.RTLD_LAZY | Library.LoadFlags.RTLD_GLOBAL); - } + // The mixer probably doesn't need to be recycled. Just keeping things sane for now. + Bass.StreamFree(globalMixerHandle.Value.Value); + BassWasapi.Free(); + globalMixerHandle.Value = null; } #endregion From fd52277f695b9de11877cd6323b1fcc8035c226d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Dec 2023 23:47:39 +0900 Subject: [PATCH 14/18] Add extra commentary about global mixer --- osu.Framework/Audio/AudioManager.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Framework/Audio/AudioManager.cs b/osu.Framework/Audio/AudioManager.cs index 517067ed80..07e63484e7 100644 --- a/osu.Framework/Audio/AudioManager.cs +++ b/osu.Framework/Audio/AudioManager.cs @@ -112,6 +112,16 @@ public class AudioManager : AudioCollectionManager /// 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. /// + /// + /// When this is non-null, all mixers created via + /// 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 + /// 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. + /// internal readonly IBindable GlobalMixerHandle = new Bindable(); public override bool IsLoaded => base.IsLoaded && From 71c9fa34703c86eca0c27754dc86e66af2e244b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Dec 2023 20:24:26 +0100 Subject: [PATCH 15/18] Handle WASAPI audio output switches Co-authored-by: Dean Herbert --- osu.Framework/Audio/AudioManager.cs | 7 +++--- osu.Framework/Threading/AudioThread.cs | 30 ++++++++++++++++++++------ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/osu.Framework/Audio/AudioManager.cs b/osu.Framework/Audio/AudioManager.cs index 07e63484e7..fd8f87f404 100644 --- a/osu.Framework/Audio/AudioManager.cs +++ b/osu.Framework/Audio/AudioManager.cs @@ -162,7 +162,8 @@ public AudioManager(AudioThread audioThread, ResourceStore trackStore, R thread.RegisterManager(this); - AudioDevice.ValueChanged += onDeviceChanged; + AudioDevice.ValueChanged += _ => onDeviceChanged(); + GlobalMixerHandle.ValueChanged += _ => onDeviceChanged(); AddItem(TrackMixer = createAudioMixer(null, nameof(TrackMixer))); AddItem(SampleMixer = createAudioMixer(null, nameof(SampleMixer))); @@ -221,9 +222,9 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - private void onDeviceChanged(ValueChangedEvent args) + private void onDeviceChanged() { - scheduler.Add(() => setAudioDevice(args.NewValue)); + scheduler.Add(() => setAudioDevice(AudioDevice.Value)); } private void onDevicesChanged() diff --git a/osu.Framework/Threading/AudioThread.cs b/osu.Framework/Threading/AudioThread.cs index 439cf4d16b..1021829f66 100644 --- a/osu.Framework/Threading/AudioThread.cs +++ b/osu.Framework/Threading/AudioThread.cs @@ -121,6 +121,7 @@ protected override void OnExit() // TODO: All this bass init stuff should probably not be in this class. private WasapiProcedure? wasapiProcedure; + private WasapiNotifyProcedure? wasapiNotifyProcedure; /// /// If a global mixer is being used, this will be the BASS handle for it. @@ -212,7 +213,11 @@ private void attemptWasapiInitialisation() // 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, _) => @@ -222,13 +227,25 @@ private void attemptWasapiInitialisation() return Bass.ChannelGetData(globalMixerHandle.Value!.Value, buffer, length); }; - - if (BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Buffer: 0.02f, Period: 0.005f)) + wasapiNotifyProcedure = (notify, device, _) => Scheduler.Add(() => { - BassWasapi.GetInfo(out var wasapiInfo); - globalMixerHandle.Value = BassMix.CreateMixerStream(wasapiInfo.Frequency, wasapiInfo.Channels, BassFlags.MixerNonStop | BassFlags.Decode | BassFlags.Float); - BassWasapi.Start(); - } + if (notify == WasapiNotificationType.DefaultOutput) + { + freeWasapi(); + initWasapi(device); + } + }); + + bool initialised = BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Buffer: 0.02f, Period: 0.005f); + + 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() @@ -237,6 +254,7 @@ private void freeWasapi() // 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; } From 7798dcaecf69420e1fe3084b6cae6b31aa1d4043 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 02:07:31 +0900 Subject: [PATCH 16/18] Expose state of global mixer usage --- osu.Framework/Audio/AudioManager.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Framework/Audio/AudioManager.cs b/osu.Framework/Audio/AudioManager.cs index fd8f87f404..f7fe849fee 100644 --- a/osu.Framework/Audio/AudioManager.cs +++ b/osu.Framework/Audio/AudioManager.cs @@ -108,6 +108,14 @@ public class AudioManager : AudioCollectionManager MaxValue = 1 }; + /// + /// 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. + /// + public IBindable UsingGlobalMixer => usingGlobalMixer; + + private readonly Bindable usingGlobalMixer = new BindableBool(); + /// /// 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. @@ -163,7 +171,11 @@ public AudioManager(AudioThread audioThread, ResourceStore trackStore, R thread.RegisterManager(this); AudioDevice.ValueChanged += _ => onDeviceChanged(); - GlobalMixerHandle.ValueChanged += _ => onDeviceChanged(); + GlobalMixerHandle.ValueChanged += handle => + { + onDeviceChanged(); + usingGlobalMixer.Value = handle.NewValue.HasValue; + }; AddItem(TrackMixer = createAudioMixer(null, nameof(TrackMixer))); AddItem(SampleMixer = createAudioMixer(null, nameof(SampleMixer))); From 28eb2298963fa0aa6a757b7ca2fcf434ba1f7a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Dec 2023 18:12:17 +0100 Subject: [PATCH 17/18] Fix mixer visualiser stealing audio output from global mixer --- .../Visualisation/Audio/AudioChannelDisplay.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Framework/Graphics/Visualisation/Audio/AudioChannelDisplay.cs b/osu.Framework/Graphics/Visualisation/Audio/AudioChannelDisplay.cs index e4ba45fc36..f2e7a75c0f 100644 --- a/osu.Framework/Graphics/Visualisation/Audio/AudioChannelDisplay.cs +++ b/osu.Framework/Graphics/Visualisation/Audio/AudioChannelDisplay.cs @@ -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; @@ -28,6 +30,7 @@ public partial class AudioChannelDisplay : CompositeDrawable private float maxPeak = float.MinValue; private double lastMaxPeakTime; private readonly bool isOutputChannel; + private IBindable usingGlobalMixer = null!; public AudioChannelDisplay(int channelHandle, bool isOutputChannel = false) { @@ -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); From b328590ecb8cf19ae10fbe41d7e4c1de740526c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Dec 2023 03:14:39 +0900 Subject: [PATCH 18/18] Adjust buffer length and update period down to minimums --- osu.Framework/Audio/AudioManager.cs | 2 +- osu.Framework/Threading/AudioThread.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Framework/Audio/AudioManager.cs b/osu.Framework/Audio/AudioManager.cs index f7fe849fee..10c6329884 100644 --- a/osu.Framework/Audio/AudioManager.cs +++ b/osu.Framework/Audio/AudioManager.cs @@ -394,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; diff --git a/osu.Framework/Threading/AudioThread.cs b/osu.Framework/Threading/AudioThread.cs index 1021829f66..b73ddf8088 100644 --- a/osu.Framework/Threading/AudioThread.cs +++ b/osu.Framework/Threading/AudioThread.cs @@ -236,7 +236,7 @@ private void initWasapi(int wasapiDevice) } }); - bool initialised = BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Buffer: 0.02f, Period: 0.005f); + bool initialised = BassWasapi.Init(wasapiDevice, Procedure: wasapiProcedure, Buffer: 0.001f, Period: 0.001f); if (!initialised) return;