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 System;
5using JetBrains.Annotations;
6using osu.Framework.Allocation;
7using osu.Framework.Audio;
8using osu.Framework.Audio.Mixing;
9using osu.Framework.Bindables;
10using osu.Framework.Graphics.Containers;
11using osu.Framework.Layout;
12
13namespace osu.Framework.Graphics.Audio
14{
15 /// <summary>
16 /// A wrapper which allows audio components (or adjustments) to exist in the draw hierarchy.
17 /// </summary>
18 [Cached(typeof(IAggregateAudioAdjustment))]
19 public abstract class DrawableAudioWrapper : CompositeDrawable, IAdjustableAudioComponent
20 {
21 /// <summary>
22 /// The volume of this component.
23 /// </summary>
24 public BindableNumber<double> Volume => adjustments.Volume;
25
26 /// <summary>
27 /// The playback balance of this sample (-1 .. 1 where 0 is centered)
28 /// </summary>
29 public BindableNumber<double> Balance => adjustments.Balance;
30
31 /// <summary>
32 /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency.
33 /// </summary>
34 public BindableNumber<double> Frequency => adjustments.Frequency;
35
36 /// <summary>
37 /// Rate at which the component is played back (does not affect pitch). 1 is 100% playback speed.
38 /// </summary>
39 public BindableNumber<double> Tempo => adjustments.Tempo;
40
41 public void BindAdjustments(IAggregateAudioAdjustment component) => adjustments.BindAdjustments(component);
42
43 public void UnbindAdjustments(IAggregateAudioAdjustment component) => adjustments.UnbindAdjustments(component);
44
45 private readonly IAdjustableAudioComponent component;
46
47 private readonly bool disposeUnderlyingComponentOnDispose;
48
49 private readonly AudioAdjustments adjustments = new AudioAdjustments();
50
51 private IAggregateAudioAdjustment parentAdjustment;
52 private IAudioMixer parentMixer;
53
54 private readonly LayoutValue fromParentLayout = new LayoutValue(Invalidation.Parent);
55
56 private DrawableAudioWrapper()
57 {
58 AddLayout(fromParentLayout);
59 }
60
61 /// <summary>
62 /// Creates a <see cref="DrawableAudioWrapper"/> that will contain a drawable child.
63 /// Generally used to add adjustments to a hierarchy without adding an audio component.
64 /// </summary>
65 /// <param name="content">The <see cref="Drawable"/> to be wrapped.</param>
66 protected DrawableAudioWrapper(Drawable content)
67 : this()
68 {
69 AddInternal(content);
70 }
71
72 /// <summary>
73 /// Creates a <see cref="DrawableAudioWrapper"/> that will wrap an audio component (and contain no drawable content).
74 /// </summary>
75 /// <param name="component">The audio component to wrap.</param>
76 /// <param name="disposeUnderlyingComponentOnDispose">Whether the component should be automatically disposed on drawable disposal/expiry.</param>
77 protected DrawableAudioWrapper([NotNull] IAdjustableAudioComponent component, bool disposeUnderlyingComponentOnDispose = true)
78 : this()
79 {
80 this.component = component ?? throw new ArgumentNullException(nameof(component));
81 this.disposeUnderlyingComponentOnDispose = disposeUnderlyingComponentOnDispose;
82
83 component.BindAdjustments(adjustments);
84 }
85
86 protected override void Update()
87 {
88 base.Update();
89
90 if (!fromParentLayout.IsValid)
91 {
92 refreshLayoutFromParent();
93 fromParentLayout.Validate();
94 }
95 }
96
97 private void refreshLayoutFromParent()
98 {
99 // because these components may be pooled, relying on DI is not feasible.
100 // in the majority of cases the traversal should be quite short. may require later attention if a use case comes up which this is not true for.
101 Drawable cursor = this;
102 IAggregateAudioAdjustment newAdjustments = null;
103 IAudioMixer newMixer = null;
104
105 while ((cursor = cursor.Parent) != null)
106 {
107 if (newAdjustments == null && cursor is IAggregateAudioAdjustment candidateAdjustment)
108 {
109 // components may be delegating the aggregates of a contained child.
110 // to avoid binding to one's self, check reference equality on an arbitrary bindable.
111 if (candidateAdjustment.AggregateVolume != adjustments.AggregateVolume)
112 newAdjustments = candidateAdjustment;
113 }
114
115 if (newMixer == null && cursor is IAudioMixer candidateMixer)
116 newMixer = candidateMixer;
117
118 if (newAdjustments != null && newMixer != null)
119 break;
120 }
121
122 if (newAdjustments != parentAdjustment)
123 {
124 if (parentAdjustment != null) adjustments.UnbindAdjustments(parentAdjustment);
125 parentAdjustment = newAdjustments;
126 if (parentAdjustment != null) adjustments.BindAdjustments(parentAdjustment);
127 }
128
129 if (parentMixer != newMixer)
130 OnMixerChanged(new ValueChangedEvent<IAudioMixer>(parentMixer, newMixer));
131
132 parentMixer = newMixer;
133 }
134
135 protected virtual void OnMixerChanged(ValueChangedEvent<IAudioMixer> mixer)
136 {
137 }
138
139 protected override void Dispose(bool isDisposing)
140 {
141 base.Dispose(isDisposing);
142 component?.UnbindAdjustments(adjustments);
143
144 if (disposeUnderlyingComponentOnDispose)
145 (component as IDisposable)?.Dispose();
146
147 parentAdjustment = null;
148 parentMixer = null;
149 }
150
151 public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable)
152 => adjustments.AddAdjustment(type, adjustBindable);
153
154 public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable)
155 => adjustments.RemoveAdjustment(type, adjustBindable);
156
157 public void RemoveAllAdjustments(AdjustableProperty type) => adjustments.RemoveAllAdjustments(type);
158
159 public IBindable<double> AggregateVolume => adjustments.AggregateVolume;
160
161 public IBindable<double> AggregateBalance => adjustments.AggregateBalance;
162
163 public IBindable<double> AggregateFrequency => adjustments.AggregateFrequency;
164
165 public IBindable<double> AggregateTempo => adjustments.AggregateTempo;
166 }
167}