// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Statistics; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using ManagedBass; using osu.Framework.Audio; using osu.Framework.Development; using osu.Framework.Platform.Linux.Native; namespace osu.Framework.Threading { public class AudioThread : GameThread { public AudioThread() : base(name: "Audio") { OnNewFrame += onNewFrame; PreloadBass(); } public override bool IsCurrent => ThreadSafety.IsAudioThread; internal sealed override void MakeCurrent() { base.MakeCurrent(); ThreadSafety.IsAudioThread = true; } internal override IEnumerable StatisticsCounters => new[] { StatisticsCounterType.TasksRun, StatisticsCounterType.Tracks, StatisticsCounterType.Samples, StatisticsCounterType.SChannels, StatisticsCounterType.Components, StatisticsCounterType.MixChannels, }; private readonly List managers = new List(); private static readonly HashSet initialised_devices = new HashSet(); private static readonly GlobalStatistic cpu_usage = GlobalStatistics.Get("Audio", "Bass CPU%"); private void onNewFrame() { cpu_usage.Value = Bass.CPUUsage; lock (managers) { for (var i = 0; i < managers.Count; i++) { var m = managers[i]; m.Update(); } } } internal void RegisterManager(AudioManager manager) { lock (managers) { if (managers.Contains(manager)) throw new InvalidOperationException($"{manager} was already registered"); managers.Add(manager); } } internal void UnregisterManager(AudioManager manager) { lock (managers) managers.Remove(manager); } internal void RegisterInitialisedDevice(int deviceId) { Debug.Assert(ThreadSafety.IsAudioThread); initialised_devices.Add(deviceId); } protected override void OnExit() { base.OnExit(); lock (managers) { // AudioManagers are iterated over backwards since disposal will unregister and remove them from the list. for (int i = managers.Count - 1; i >= 0; i--) { var m = managers[i]; m.Dispose(); // Audio component disposal (including the AudioManager itself) is scheduled and only runs when the AudioThread updates. // But the AudioThread won't run another update since it's exiting, so an update must be performed manually in order to finish the disposal. m.Update(); } managers.Clear(); } // Safety net to ensure we have freed all devices before exiting. // This is mainly required for device-lost scenarios. // See https://github.com/ppy/osu-framework/pull/3378 for further discussion. foreach (var d in initialised_devices.ToArray()) FreeDevice(d); } internal static void FreeDevice(int deviceId) { Debug.Assert(ThreadSafety.IsAudioThread); int lastDevice = Bass.CurrentDevice; // Freeing the 0 device on linux can cause deadlocks. This doesn't always happen immediately. // Todo: Reproduce in native code and report to BASS at some point. if (deviceId != 0 || RuntimeInfo.OS != RuntimeInfo.Platform.Linux) { Bass.CurrentDevice = deviceId; Bass.Free(); } if (lastDevice != deviceId) Bass.CurrentDevice = lastDevice; initialised_devices.Remove(deviceId); } /// /// 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); } } } }