From 2c4a60e4818480a8725ef2c212e50bd30ab07884 Mon Sep 17 00:00:00 2001 From: ShibuyaCyana <39703247+ShibuyaCyana@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:58:11 +0800 Subject: [PATCH 1/3] Fix broken AudioMixer vtest --- osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs b/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs index 8b703b486e..e01c37b718 100644 --- a/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs +++ b/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs @@ -77,11 +77,17 @@ 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 the mixer is created. + // See createMixer() which processes all channels in activeChannels. + if (!activeChannels.Contains(bassChannel)) + activeChannels.Add(bassChannel); return; + } if (!bassChannel.MixerChannelPaused) ChannelPlay(bassChannel); @@ -91,7 +97,7 @@ 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) @@ -303,7 +309,8 @@ 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. + // Register all channels that were previously added prior to the mixer being loaded. + // These channels were queued in activeChannels but not yet added to the BASS mix. var toAdd = activeChannels.ToArray(); activeChannels.Clear(); foreach (var channel in toAdd) From 8d8684a5578a3a91e4d288c787356ab15f152960 Mon Sep 17 00:00:00 2001 From: ShibuyaCyana <39703247+ShibuyaCyana@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:59:29 +0800 Subject: [PATCH 2/3] Tidy up mixer initialization procedure --- .../Audio/Mixing/Bass/BassAudioMixer.cs | 73 +++++++++++++++---- .../Audio/Sample/SampleChannelBass.cs | 2 +- osu.Framework/Audio/Track/TrackBass.cs | 2 +- 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs b/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs index e01c37b718..9b00c0f771 100644 --- a/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs +++ b/osu.Framework/Audio/Mixing/Bass/BassAudioMixer.cs @@ -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 @@ -29,6 +30,12 @@ internal class BassAudioMixer : AudioMixer, IBassAudio /// private readonly List activeChannels = new List(); + /// + /// 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. + /// + private readonly List pendingChannels = new List(); + private readonly Dictionary activeEffects = new Dictionary(); private const int frequency = 44100; @@ -82,15 +89,14 @@ protected override void AddInternal(IAudioChannel channel) if (Handle == 0 || bassChannel.Handle == 0) { - // Channel will be added to the BASS mix when the mixer is created. - // See createMixer() which processes all channels in activeChannels. - if (!activeChannels.Contains(bassChannel)) - activeChannels.Add(bassChannel); + // 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) @@ -100,24 +106,24 @@ protected override void RemoveInternal(IAudioChannel channel) 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); } /// - /// Plays a channel. + /// Adds a channel to the BASS mix and plays it. /// /// See: . - /// The channel to play. - /// Restart playback from the beginning? + /// The channel to add and play. /// /// If successful, is returned, else is returned. /// Use to get the error code. /// - public bool ChannelPlay(IBassAudioChannel channel, bool restart = false) + public bool AddToBassMixAndPlay(IBassAudioChannel channel) { if (Handle == 0 || channel.Handle == 0) return false; @@ -274,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]; @@ -310,11 +337,25 @@ private void createMixer() ManagedBass.Bass.ChannelSetAttribute(Handle, ChannelAttribute.Buffer, 0); // Register all channels that were previously added prior to the mixer being loaded. - // These channels were queued in activeChannels but not yet added to the BASS mix. - var toAdd = activeChannels.ToArray(); - activeChannels.Clear(); + // 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); diff --git a/osu.Framework/Audio/Sample/SampleChannelBass.cs b/osu.Framework/Audio/Sample/SampleChannelBass.cs index 38d9387411..b14d8cfbf8 100644 --- a/osu.Framework/Audio/Sample/SampleChannelBass.cs +++ b/osu.Framework/Audio/Sample/SampleChannelBass.cs @@ -183,7 +183,7 @@ private bool playInternal() if (relativeFrequencyHandler.IsFrequencyZero) return true; - return bassMixer.ChannelPlay(this); + return bassMixer.AddToBassMixAndPlay(this); } private void stopInternal() diff --git a/osu.Framework/Audio/Track/TrackBass.cs b/osu.Framework/Audio/Track/TrackBass.cs index 3148ffa9b1..2de8c0cfc1 100644 --- a/osu.Framework/Audio/Track/TrackBass.cs +++ b/osu.Framework/Audio/Track/TrackBass.cs @@ -284,7 +284,7 @@ private bool startInternal() setLoopFlag(Looping); - return bassMixer.ChannelPlay(this); + return bassMixer.AddToBassMixAndPlay(this); } public override bool Looping From 5c5436f90100fcd21c05c1930d447003f05fa3b2 Mon Sep 17 00:00:00 2001 From: ShibuyaCyana <39703247+ShibuyaCyana@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:42:44 +0800 Subject: [PATCH 3/3] Headless test for channel added before mixer ready --- .../Audio/BassAudioMixerTest.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/osu.Framework.Tests/Audio/BassAudioMixerTest.cs b/osu.Framework.Tests/Audio/BassAudioMixerTest.cs index 23b5a10a75..7fceca19da 100644 --- a/osu.Framework.Tests/Audio/BassAudioMixerTest.cs +++ b/osu.Framework.Tests/Audio/BassAudioMixerTest.cs @@ -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; @@ -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; } }