Skip to content
Draft
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
56 changes: 56 additions & 0 deletions osu.Framework.Tests/Audio/BassAudioMixerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
#nullable disable

using System;
using System.Reflection;
using System.Threading;
using ManagedBass;
using ManagedBass.Mix;
using NUnit.Framework;
using osu.Framework.Audio;
using osu.Framework.Audio.Mixing.Bass;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
Expand Down Expand Up @@ -266,6 +268,60 @@ public void TestChannelDoesNotPlayIfReachedEndAndMovedMixers()
Assert.That(secondMixer.ChannelIsActive(track), Is.Not.EqualTo(PlaybackState.Playing));
}

/// Tests a race condition where mixer.Add(track) is called before createMixer.
/// Artificially forces the wrong order by manually modifying AudioComponent.pendingActions.
/// This actually happens quite frequently when initializing TestSceneAudioMixer.cs.
[Test]
public void TestRaceConditionAddBeforeCreateMixer()
{
int trackHandle = getHandle();
Assert.That(trackHandle, Is.Not.Zero, "Track should have a BASS handle");

// Create a mixer but don't register it with the component manager yet
var newMixer = new BassAudioMixer(null, bass.Mixer, "Race test mixer");
bass.Add(newMixer);

var enqueueActionMethod = typeof(AudioComponent).GetMethod("EnqueueAction",
BindingFlags.NonPublic | BindingFlags.Instance);
var pendingActionsField = typeof(AudioComponent).GetField("PendingActions",
BindingFlags.NonPublic | BindingFlags.Instance);
var createMixerMethod = typeof(BassAudioMixer).GetMethod("createMixer",
BindingFlags.NonPublic | BindingFlags.Instance);

Assert.That(enqueueActionMethod, Is.Not.Null, "Should be able to find EnqueueAction method");
Assert.That(pendingActionsField, Is.Not.Null, "Should be able to find PendingActions field");
Assert.That(createMixerMethod, Is.Not.Null, "Should be able to find createMixer method");

// Clear the pending action queue to remove the auto-queued createMixer from constructor
object pendingActions = pendingActionsField!.GetValue(newMixer);
Assert.That(pendingActions, Is.Not.Null, "PendingActions should not be null");
object newQueue = Activator.CreateInstance(pendingActions.GetType());
pendingActionsField.SetValue(newMixer, newQueue);

// Reconstruct the pending action queue to simulate what happens when mixer.Add(track) is called before the mixer is initialized
// 1. track channel added to the BassAudioMixer.pendingChannels, since mixer handle is still null
enqueueActionMethod.Invoke(newMixer,
[
new Action(() =>
{
newMixer.Add(track);
})
]);
// 2. create mixer is called, after which mixer handle will be valid
enqueueActionMethod.Invoke(newMixer,
[
new Action(() => createMixerMethod!.Invoke(newMixer, null))
]);
// 3. track channel salvaged from pending channels and added to the mixer by BassAudioMixer.UpdateState
bass.Update();

Assert.That(newMixer.Handle, Is.Not.Zero, "Mixer should be initialized");

// Now the track should be in the new mixer
Assert.That(BassMix.ChannelGetMixer(trackHandle), Is.EqualTo(newMixer.Handle),
"Track should be successfully added to the mixer even when Add was called before createMixer");
}

private int getHandle() => ((IBassAudioChannel)track).Handle;
}
}
76 changes: 62 additions & 14 deletions osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using ManagedBass;
using ManagedBass.Mix;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Logging;
using osu.Framework.Statistics;

namespace osu.Framework.Audio.Mixing.Bass
Expand All @@ -29,6 +30,12 @@ internal class BassAudioMixer : AudioMixer, IBassAudio
/// </summary>
private readonly List<IBassAudioChannel> activeChannels = new List<IBassAudioChannel>();

/// <summary>
/// The list of channels which are pending addition to the BASS mix.
/// These channels are waiting for either the mixer or the channel to be initialized.
/// </summary>
private readonly List<IBassAudioChannel> pendingChannels = new List<IBassAudioChannel>();

private readonly Dictionary<IEffectParameter, int> activeEffects = new Dictionary<IEffectParameter, int>();

private const int frequency = 44100;
Expand Down Expand Up @@ -77,41 +84,46 @@ protected override void AddInternal(IAudioChannel channel)
{
Debug.Assert(CanPerformInline);

if (!(channel is IBassAudioChannel bassChannel))
if (channel is not IBassAudioChannel bassChannel)
return;

if (Handle == 0 || bassChannel.Handle == 0)
{
// Channel will be added to the BASS mix when both the mixer and channel are ready.
if (!pendingChannels.Contains(bassChannel))
pendingChannels.Add(bassChannel);
return;
}

if (!bassChannel.MixerChannelPaused)
ChannelPlay(bassChannel);
AddToBassMixAndPlay(bassChannel);
}

protected override void RemoveInternal(IAudioChannel channel)
{
Debug.Assert(CanPerformInline);

if (!(channel is IBassAudioChannel bassChannel))
if (channel is not IBassAudioChannel bassChannel)
return;

if (Handle == 0 || bassChannel.Handle == 0)
return;
// Remove from pending channels if it's there
pendingChannels.Remove(bassChannel);

if (activeChannels.Remove(bassChannel))
// Remove from active channels and BASS mix if mixer is valid
if (Handle != 0 && bassChannel.Handle != 0 && activeChannels.Remove(bassChannel))
removeChannelFromBassMix(bassChannel);
}

/// <summary>
/// Plays a channel.
/// Adds a channel to the BASS mix and plays it.
/// </summary>
/// <remarks>See: <see cref="ManagedBass.Bass.ChannelPlay"/>.</remarks>
/// <param name="channel">The channel to play.</param>
/// <param name="restart">Restart playback from the beginning?</param>
/// <param name="channel">The channel to add and play.</param>
/// <returns>
/// If successful, <see langword="true"/> is returned, else <see langword="false"/> is returned.
/// Use <see cref="ManagedBass.Bass.LastError"/> to get the error code.
/// </returns>
public bool ChannelPlay(IBassAudioChannel channel, bool restart = false)
public bool AddToBassMixAndPlay(IBassAudioChannel channel)
{
if (Handle == 0 || channel.Handle == 0)
return false;
Expand Down Expand Up @@ -268,6 +280,27 @@ public void UpdateDevice(int deviceIndex)

protected override void UpdateState()
{
// Process pending channels if mixer is ready
if (Handle != 0 && pendingChannels.Count > 0)
{
var toProcess = pendingChannels.ToArray();
pendingChannels.Clear();

foreach (var channel in toProcess)
{
if (channel.Handle == 0)
{
// Channel still not ready, keep pending
pendingChannels.Add(channel);
}
else
{
// Channel is ready, add to BASS mix and play
AddToBassMixAndPlay(channel);
}
}
}

for (int i = 0; i < activeChannels.Count; i++)
{
var channel = activeChannels[i];
Expand Down Expand Up @@ -303,11 +336,26 @@ private void createMixer()
// Lower latency is valued more for the time since we are not using complex DSP effects. Disable buffering on the mixer channel in order for data to be produced immediately.
ManagedBass.Bass.ChannelSetAttribute(Handle, ChannelAttribute.Buffer, 0);

// Register all channels that were previously played prior to the mixer being loaded.
var toAdd = activeChannels.ToArray();
activeChannels.Clear();
// Register all channels that were previously added prior to the mixer being loaded.
// These channels were queued in pendingChannels but not yet added to the BASS mix.
var toAdd = pendingChannels.ToArray();
pendingChannels.Clear();

foreach (var channel in toAdd)
AddChannelToBassMix(channel);
{
if (channel.Handle == 0)
{
// Channel is not yet initialized, keep it pending for later processing
pendingChannels.Add(channel);
}
else
{
AddToBassMixAndPlay(channel);
}
}

if (pendingChannels.Count > 0)
Logger.Log($"BassAudioMixer '{Identifier}' has {pendingChannels.Count} pending channels after mixer creation", LoggingTarget.Runtime, LogLevel.Important);

if (manager?.GlobalMixerHandle.Value != null)
BassMix.MixerAddChannel(manager.GlobalMixerHandle.Value.Value, Handle, BassFlags.MixerChanBuffer | BassFlags.MixerChanNoRampin);
Expand Down
2 changes: 1 addition & 1 deletion osu.Framework/Audio/Sample/SampleChannelBass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ private bool playInternal()
if (relativeFrequencyHandler.IsFrequencyZero)
return true;

return bassMixer.ChannelPlay(this);
return bassMixer.AddToBassMixAndPlay(this);
}

private void stopInternal()
Expand Down
2 changes: 1 addition & 1 deletion osu.Framework/Audio/Track/TrackBass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ private bool startInternal()

setLoopFlag(Looping);

return bassMixer.ChannelPlay(this);
return bassMixer.AddToBassMixAndPlay(this);
}

public override bool Looping
Expand Down
Loading