A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using osu.Framework.Statistics;
5using System;
6using System.Collections.Generic;
7using System.Diagnostics;
8using System.Linq;
9using ManagedBass;
10using osu.Framework.Audio;
11using osu.Framework.Development;
12using osu.Framework.Platform.Linux.Native;
13
14namespace osu.Framework.Threading
15{
16 public class AudioThread : GameThread
17 {
18 public AudioThread()
19 : base(name: "Audio")
20 {
21 OnNewFrame += onNewFrame;
22 PreloadBass();
23 }
24
25 public override bool IsCurrent => ThreadSafety.IsAudioThread;
26
27 internal sealed override void MakeCurrent()
28 {
29 base.MakeCurrent();
30
31 ThreadSafety.IsAudioThread = true;
32 }
33
34 internal override IEnumerable<StatisticsCounterType> StatisticsCounters => new[]
35 {
36 StatisticsCounterType.TasksRun,
37 StatisticsCounterType.Tracks,
38 StatisticsCounterType.Samples,
39 StatisticsCounterType.SChannels,
40 StatisticsCounterType.Components,
41 StatisticsCounterType.MixChannels,
42 };
43
44 private readonly List<AudioManager> managers = new List<AudioManager>();
45
46 private static readonly HashSet<int> initialised_devices = new HashSet<int>();
47
48 private static readonly GlobalStatistic<double> cpu_usage = GlobalStatistics.Get<double>("Audio", "Bass CPU%");
49
50 private void onNewFrame()
51 {
52 cpu_usage.Value = Bass.CPUUsage;
53
54 lock (managers)
55 {
56 for (var i = 0; i < managers.Count; i++)
57 {
58 var m = managers[i];
59 m.Update();
60 }
61 }
62 }
63
64 internal void RegisterManager(AudioManager manager)
65 {
66 lock (managers)
67 {
68 if (managers.Contains(manager))
69 throw new InvalidOperationException($"{manager} was already registered");
70
71 managers.Add(manager);
72 }
73 }
74
75 internal void UnregisterManager(AudioManager manager)
76 {
77 lock (managers)
78 managers.Remove(manager);
79 }
80
81 internal void RegisterInitialisedDevice(int deviceId)
82 {
83 Debug.Assert(ThreadSafety.IsAudioThread);
84
85 initialised_devices.Add(deviceId);
86 }
87
88 protected override void OnExit()
89 {
90 base.OnExit();
91
92 lock (managers)
93 {
94 // AudioManagers are iterated over backwards since disposal will unregister and remove them from the list.
95 for (int i = managers.Count - 1; i >= 0; i--)
96 {
97 var m = managers[i];
98
99 m.Dispose();
100
101 // Audio component disposal (including the AudioManager itself) is scheduled and only runs when the AudioThread updates.
102 // 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.
103 m.Update();
104 }
105
106 managers.Clear();
107 }
108
109 // Safety net to ensure we have freed all devices before exiting.
110 // This is mainly required for device-lost scenarios.
111 // See https://github.com/ppy/osu-framework/pull/3378 for further discussion.
112 foreach (var d in initialised_devices.ToArray())
113 FreeDevice(d);
114 }
115
116 internal static void FreeDevice(int deviceId)
117 {
118 Debug.Assert(ThreadSafety.IsAudioThread);
119
120 int lastDevice = Bass.CurrentDevice;
121
122 // Freeing the 0 device on linux can cause deadlocks. This doesn't always happen immediately.
123 // Todo: Reproduce in native code and report to BASS at some point.
124 if (deviceId != 0 || RuntimeInfo.OS != RuntimeInfo.Platform.Linux)
125 {
126 Bass.CurrentDevice = deviceId;
127 Bass.Free();
128 }
129
130 if (lastDevice != deviceId)
131 Bass.CurrentDevice = lastDevice;
132
133 initialised_devices.Remove(deviceId);
134 }
135
136 /// <summary>
137 /// Makes BASS available to be consumed.
138 /// </summary>
139 internal static void PreloadBass()
140 {
141 if (RuntimeInfo.OS == RuntimeInfo.Platform.Linux)
142 {
143 // required for the time being to address libbass_fx.so load failures (see https://github.com/ppy/osu/issues/2852)
144 Library.Load("libbass.so", Library.LoadFlags.RTLD_LAZY | Library.LoadFlags.RTLD_GLOBAL);
145 }
146 }
147 }
148}