A game framework written with osu! in mind.
at master 167 lines 6.7 kB view raw
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}