Skip to content

Commit 5acb295

Browse files
authored
Merge pull request #6660 from smoogipoo/fix-wasapi-init
Fix WASAPI initialisation and BASS logging
2 parents 8387643 + 331096c commit 5acb295

File tree

5 files changed

+98
-100
lines changed

5 files changed

+98
-100
lines changed

osu.Framework.Tests/Audio/AudioManagerWithDeviceLoss.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace osu.Framework.Tests.Audio
1717
internal class AudioManagerWithDeviceLoss : AudioManager
1818
{
1919
public AudioManagerWithDeviceLoss(AudioThread audioThread, ResourceStore<byte[]> trackStore, ResourceStore<byte[]> sampleStore)
20-
: base(audioThread, trackStore, sampleStore)
20+
: base(audioThread, trackStore, sampleStore, null)
2121
{
2222
}
2323

osu.Framework/Audio/AudioManager.cs

Lines changed: 89 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Diagnostics;
1010
using System.Linq;
1111
using System.Threading;
12+
using JetBrains.Annotations;
1213
using ManagedBass;
1314
using ManagedBass.Fx;
1415
using ManagedBass.Mix;
@@ -17,6 +18,7 @@
1718
using osu.Framework.Audio.Sample;
1819
using osu.Framework.Audio.Track;
1920
using osu.Framework.Bindables;
21+
using osu.Framework.Configuration;
2022
using osu.Framework.Development;
2123
using osu.Framework.Extensions.TypeExtensions;
2224
using osu.Framework.IO.Stores;
@@ -176,19 +178,27 @@ public class AudioManager : AudioCollectionManager<AudioComponent>
176178
/// <param name="audioThread">The host's audio thread.</param>
177179
/// <param name="trackStore">The resource store containing all audio tracks to be used in the future.</param>
178180
/// <param name="sampleStore">The sample store containing all audio samples to be used in the future.</param>
179-
public AudioManager(AudioThread audioThread, ResourceStore<byte[]> trackStore, ResourceStore<byte[]> sampleStore)
181+
/// <param name="config"></param>
182+
public AudioManager(AudioThread audioThread, ResourceStore<byte[]> trackStore, ResourceStore<byte[]> sampleStore, [CanBeNull] FrameworkConfigManager config)
180183
{
181184
thread = audioThread;
182185

183186
thread.RegisterManager(this);
184187

185-
AudioDevice.ValueChanged += _ => onDeviceChanged();
186-
UseExperimentalWasapi.ValueChanged += _ => onDeviceChanged();
187-
GlobalMixerHandle.ValueChanged += handle =>
188+
if (config != null)
188189
{
189-
onDeviceChanged();
190-
usingGlobalMixer.Value = handle.NewValue.HasValue;
191-
};
190+
// attach config bindables
191+
config.BindWith(FrameworkSetting.AudioDevice, AudioDevice);
192+
config.BindWith(FrameworkSetting.AudioUseExperimentalWasapi, UseExperimentalWasapi);
193+
config.BindWith(FrameworkSetting.VolumeUniversal, Volume);
194+
config.BindWith(FrameworkSetting.VolumeEffect, VolumeSample);
195+
config.BindWith(FrameworkSetting.VolumeMusic, VolumeTrack);
196+
}
197+
198+
AudioDevice.ValueChanged += _ => scheduler.AddOnce(initCurrentDevice);
199+
UseExperimentalWasapi.ValueChanged += _ => scheduler.AddOnce(initCurrentDevice);
200+
// initCurrentDevice not required for changes to `GlobalMixerHandle` as it is only changed when experimental wasapi is toggled (handled above).
201+
GlobalMixerHandle.ValueChanged += handle => usingGlobalMixer.Value = handle.NewValue.HasValue;
192202

193203
AddItem(TrackMixer = createAudioMixer(null, nameof(TrackMixer)));
194204
AddItem(SampleMixer = createAudioMixer(null, nameof(SampleMixer)));
@@ -209,12 +219,12 @@ public AudioManager(AudioThread audioThread, ResourceStore<byte[]> trackStore, R
209219
return store;
210220
});
211221

212-
CancellationToken token = cancelSource.Token;
213-
214222
syncAudioDevices();
223+
224+
// check for changes in any audio devices every 1000ms (slightly expensive operation)
225+
CancellationToken token = cancelSource.Token;
215226
scheduler.AddDelayed(() =>
216227
{
217-
// sync audioDevices every 1000ms
218228
new Thread(() =>
219229
{
220230
while (!token.IsCancellationRequested)
@@ -248,23 +258,6 @@ protected override void Dispose(bool disposing)
248258
base.Dispose(disposing);
249259
}
250260

251-
private void onDeviceChanged()
252-
{
253-
scheduler.Add(() => setAudioDevice(AudioDevice.Value));
254-
}
255-
256-
private void onDevicesChanged()
257-
{
258-
scheduler.Add(() =>
259-
{
260-
if (cancelSource.IsCancellationRequested)
261-
return;
262-
263-
if (!IsCurrentDeviceValid())
264-
setAudioDevice();
265-
});
266-
}
267-
268261
private static int userMixerID;
269262

270263
/// <summary>
@@ -334,81 +327,58 @@ public ISampleStore GetSampleStore(IResourceStore<byte[]> store = null, AudioMix
334327
}
335328

336329
/// <summary>
337-
/// Sets the output audio device by its name.
330+
/// (Re-)Initialises BASS for the current <see cref="AudioDevice"/>.
338331
/// This will automatically fall back to the system default device on failure.
339332
/// </summary>
340-
/// <param name="deviceName">Name of the audio device, or null to use the configured device preference <see cref="AudioDevice"/>.</param>
341-
private bool setAudioDevice(string deviceName = null)
333+
private void initCurrentDevice()
342334
{
343-
deviceName ??= AudioDevice.Value;
335+
string deviceName = AudioDevice.Value;
344336

345337
// try using the specified device
346338
int deviceIndex = audioDeviceNames.FindIndex(d => d == deviceName);
347-
if (deviceIndex >= 0 && setAudioDevice(BASS_INTERNAL_DEVICE_COUNT + deviceIndex))
348-
return true;
339+
if (deviceIndex >= 0 && trySetDevice(BASS_INTERNAL_DEVICE_COUNT + deviceIndex)) return;
349340

350341
// try using the system default if there is any device present.
351342
// mobiles are an exception as the built-in speakers may not be provided as an audio device name,
352343
// but they are still provided by BASS under the internal device name "Default".
353-
if ((audioDeviceNames.Count > 0 || RuntimeInfo.IsMobile) && setAudioDevice(bass_default_device))
354-
return true;
344+
if ((audioDeviceNames.Count > 0 || RuntimeInfo.IsMobile) && trySetDevice(bass_default_device)) return;
355345

356346
// no audio devices can be used, so try using Bass-provided "No sound" device as last resort.
357-
if (setAudioDevice(Bass.NoSoundDevice))
358-
return true;
347+
trySetDevice(Bass.NoSoundDevice);
359348

360349
// we're boned. even "No sound" device won't initialise.
361-
return false;
362-
}
350+
return;
363351

364-
private bool setAudioDevice(int deviceIndex)
365-
{
366-
var device = audioDevices.ElementAtOrDefault(deviceIndex);
352+
bool trySetDevice(int deviceId)
353+
{
354+
var device = audioDevices.ElementAtOrDefault(deviceId);
367355

368-
// device is invalid
369-
if (!device.IsEnabled)
370-
return false;
356+
// device is invalid
357+
if (!device.IsEnabled)
358+
return false;
371359

372-
// we don't want bass initializing with real audio device on headless test runs.
373-
if (deviceIndex != Bass.NoSoundDevice && DebugUtils.IsNUnitRunning)
374-
return false;
360+
// we don't want bass initializing with real audio device on headless test runs.
361+
if (deviceId != Bass.NoSoundDevice && DebugUtils.IsNUnitRunning)
362+
return false;
375363

376-
// initialize new device
377-
bool initSuccess = InitBass(deviceIndex);
378-
if (Bass.LastError != Errors.Already && BassUtils.CheckFaulted(false))
379-
return false;
364+
// initialize new device
365+
if (!InitBass(deviceId))
366+
return false;
380367

381-
if (!initSuccess)
382-
{
383-
Logger.Log("BASS failed to initialize but did not provide an error code", level: LogLevel.Error);
384-
return false;
385-
}
368+
//we have successfully initialised a new device.
369+
UpdateDevice(deviceId);
386370

387-
Logger.Log($@"🔈 BASS initialised
388-
BASS version: {Bass.Version}
389-
BASS FX version: {BassFx.Version}
390-
BASS MIX version: {BassMix.Version}
391-
Device: {device.Name}
392-
Driver: {device.Driver}
393-
Update period: {Bass.UpdatePeriod} ms
394-
Device buffer length: {Bass.DeviceBufferLength} ms
395-
Playback buffer length: {Bass.PlaybackBufferLength} ms");
396-
397-
//we have successfully initialised a new device.
398-
UpdateDevice(deviceIndex);
399-
400-
return true;
371+
return true;
372+
}
401373
}
402374

403375
/// <summary>
404376
/// This method calls <see cref="Bass.Init(int, int, DeviceInitFlags, IntPtr, IntPtr)"/>.
405377
/// It can be overridden for unit testing.
406378
/// </summary>
379+
/// <param name="device">The device to initialise.</param>
407380
protected virtual bool InitBass(int device)
408381
{
409-
if (Bass.CurrentDevice == device)
410-
return true;
411-
412382
// this likely doesn't help us but also doesn't seem to cause any issues or any cpu increase.
413383
Bass.UpdatePeriod = 5;
414384

@@ -438,16 +408,47 @@ protected virtual bool InitBass(int device)
438408
// See https://www.un4seen.com/forum/?topic=19601 for more information.
439409
Bass.Configure((ManagedBass.Configuration)70, false);
440410

441-
if (UseExperimentalWasapi.Value)
411+
bool success = attemptInit();
412+
413+
if (success || !UseExperimentalWasapi.Value)
414+
return success;
415+
416+
// in the case we're using experimental WASAPI, give a second chance of initialisation by forcefully disabling it.
417+
Logger.Log($"BASS device {device} failed to initialise with experimental WASAPI, disabling", level: LogLevel.Error);
418+
UseExperimentalWasapi.Value = false;
419+
return attemptInit();
420+
421+
bool attemptInit()
442422
{
443-
if (thread.InitDevice(device, true))
423+
bool innerSuccess = thread.InitDevice(device, UseExperimentalWasapi.Value);
424+
bool alreadyInitialised = Bass.LastError == Errors.Already;
425+
426+
if (alreadyInitialised)
444427
return true;
445428

446-
Logger.Log($"BASS device {device} failed to initialise with experimental WASAPI, disabling", level: LogLevel.Error);
447-
UseExperimentalWasapi.Value = false;
448-
}
429+
if (BassUtils.CheckFaulted(false))
430+
return false;
431+
432+
if (!innerSuccess)
433+
{
434+
Logger.Log("BASS failed to initialize but did not provide an error code", level: LogLevel.Error);
435+
return false;
436+
}
437+
438+
var deviceInfo = audioDevices.ElementAtOrDefault(device);
449439

450-
return thread.InitDevice(device, false);
440+
Logger.Log($@"🔈 BASS initialised
441+
BASS version: {Bass.Version}
442+
BASS FX version: {BassFx.Version}
443+
BASS MIX version: {BassMix.Version}
444+
Device: {deviceInfo.Name}
445+
Driver: {deviceInfo.Driver}
446+
Update period: {Bass.UpdatePeriod} ms
447+
Device buffer length: {Bass.DeviceBufferLength} ms
448+
Playback buffer length: {Bass.PlaybackBufferLength} ms");
449+
450+
return true;
451+
}
451452
}
452453

453454
private void syncAudioDevices()
@@ -460,7 +461,14 @@ private void syncAudioDevices()
460461
var oldDeviceNames = audioDeviceNames;
461462
var newDeviceNames = audioDeviceNames = audioDevices.Skip(BASS_INTERNAL_DEVICE_COUNT).Where(d => d.IsEnabled).Select(d => d.Name).ToImmutableList();
462463

463-
onDevicesChanged();
464+
scheduler.Add(() =>
465+
{
466+
if (cancelSource.IsCancellationRequested)
467+
return;
468+
469+
if (!IsCurrentDeviceValid())
470+
initCurrentDevice();
471+
}, false);
464472

465473
var newDevices = newDeviceNames.Except(oldDeviceNames).ToList();
466474
var lostDevices = oldDeviceNames.Except(newDeviceNames).ToList();

osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -261,15 +261,9 @@ public bool StreamFree(IBassAudioChannel channel)
261261

262262
public void UpdateDevice(int deviceIndex)
263263
{
264-
if (Handle == 0)
265-
createMixer();
266-
else
267-
{
268-
ManagedBass.Bass.ChannelSetDevice(Handle, deviceIndex);
269-
270-
if (manager?.GlobalMixerHandle.Value != null)
271-
BassMix.MixerAddChannel(manager.GlobalMixerHandle.Value.Value, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin);
272-
}
264+
// It's important that we re-create the mixer after a device change.
265+
// The mixer may need to be initialised with different flags depending on the state of global mixer usage.
266+
createMixer();
273267
}
274268

275269
protected override void UpdateState()
@@ -291,14 +285,15 @@ protected override void UpdateState()
291285
private void createMixer()
292286
{
293287
if (Handle != 0)
294-
return;
288+
ManagedBass.Bass.StreamFree(Handle);
295289

296290
// Make sure that bass is initialised before trying to create a mixer.
297291
// If not, this will be called again when the device is initialised via UpdateDevice().
298292
if (!ManagedBass.Bass.GetDeviceInfo(ManagedBass.Bass.CurrentDevice, out var deviceInfo) || !deviceInfo.IsInitialized)
299293
return;
300294

301295
Handle = manager?.GlobalMixerHandle.Value != null
296+
// The decode flag here is super important to maintain the lowest latency audio output.
302297
? BassMix.CreateMixerStream(frequency, 2, BassFlags.MixerNonStop | BassFlags.Decode)
303298
: BassMix.CreateMixerStream(frequency, 2, BassFlags.MixerNonStop);
304299

osu.Framework/Game.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -188,19 +188,12 @@ private void load(FrameworkConfigManager config)
188188
samples.AddStore(new NamespacedResourceStore<byte[]>(Resources, @"Samples"));
189189
samples.AddStore(CreateOnlineStore());
190190

191-
Audio = new AudioManager(Host.AudioThread, tracks, samples) { EventScheduler = Scheduler };
191+
Audio = new AudioManager(Host.AudioThread, tracks, samples, config) { EventScheduler = Scheduler };
192192
dependencies.Cache(Audio);
193193

194194
dependencies.CacheAs(Audio.Tracks);
195195
dependencies.CacheAs(Audio.Samples);
196196

197-
// attach our bindables to the audio subsystem.
198-
config.BindWith(FrameworkSetting.AudioDevice, Audio.AudioDevice);
199-
config.BindWith(FrameworkSetting.AudioUseExperimentalWasapi, Audio.UseExperimentalWasapi);
200-
config.BindWith(FrameworkSetting.VolumeUniversal, Audio.Volume);
201-
config.BindWith(FrameworkSetting.VolumeEffect, Audio.VolumeSample);
202-
config.BindWith(FrameworkSetting.VolumeMusic, Audio.VolumeTrack);
203-
204197
Shaders = new ShaderManager(Host.Renderer, new NamespacedResourceStore<byte[]>(Resources, @"Shaders"));
205198
dependencies.Cache(Shaders);
206199

osu.Framework/Threading/AudioThread.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ internal bool InitDevice(int deviceId, bool useExperimentalWasapi)
141141

142142
if (useExperimentalWasapi)
143143
attemptWasapiInitialisation();
144+
else
145+
freeWasapi();
144146

145147
initialised_devices.Add(deviceId);
146148
return true;

0 commit comments

Comments
 (0)