// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Game.Audio; namespace osu.Game.Skinning { /// /// A sound consisting of one or more samples to be played. /// public partial class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent { /// /// The minimum allowable volume for . /// that specify a lower will be forcibly pulled up to this volume. /// public int MinimumSampleVolume { get; set; } public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; /// /// Whether to play the underlying sample when aggregate volume is zero. /// Note that this is checked at the point of calling ; changing the volume post-play will not begin playback. /// Defaults to false unless . /// /// /// Can serve as an optimisation if it is known ahead-of-time that this behaviour is allowed in a given use case. /// protected bool PlayWhenZeroVolume => Looping; /// /// All raw s contained in this . /// protected IEnumerable DrawableSamples => samplesContainer.Select(c => c.Sample).Where(s => s != null); private readonly AudioContainer samplesContainer; [Resolved] private IPooledSampleProvider? samplePool { get; set; } /// /// Creates a new . /// public SkinnableSound() { InternalChild = samplesContainer = new AudioContainer(); } /// /// Creates a new with some initial samples. /// /// The initial samples. public SkinnableSound(IEnumerable samples) : this() { this.samples = samples.ToArray(); } /// /// Creates a new with an initial sample. /// /// The initial sample. public SkinnableSound(ISampleInfo sample) : this(new[] { sample }) { } private ISampleInfo[] samples = Array.Empty(); /// /// The samples that should be played. /// public ISampleInfo[] Samples { get => samples; set { if (samples == value) return; samples = value; if (LoadState >= LoadState.Ready) updateSamples(); } } public void ClearSamples() => Samples = Array.Empty(); private bool looping; /// /// Whether the samples should loop on completion. /// public bool Looping { get => looping; set { if (value == looping) return; looping = value; samplesContainer.ForEach(c => c.Looping = looping); } } /// /// Plays the samples. /// public virtual void Play() { FlushPendingSkinChanges(); samplesContainer.ForEach(c => { if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) { c.Stop(); c.Play(); } }); } protected override void LoadAsyncComplete() { // ensure samples are constructed before SkinChanged() is called via base.LoadAsyncComplete(). if (!samplesContainer.Any()) updateSamples(); base.LoadAsyncComplete(); } /// /// Stops the samples. /// public virtual void Stop() { samplesContainer.ForEach(c => c.Stop()); } private void updateSamples() { bool wasPlaying = IsPlaying; if (wasPlaying && Looping) Stop(); // Remove all pooled samples (return them to the pool), and dispose the rest. samplesContainer.RemoveAll(s => s.IsInPool, false); samplesContainer.Clear(); foreach (var s in samples) { var sample = samplePool?.GetPooledSample(s) ?? new PoolableSkinnableSample(s); sample.Looping = Looping; sample.Volume.Value = Math.Max(s.Volume, MinimumSampleVolume) / 100.0; samplesContainer.Add(sample); } if (wasPlaying && Looping) Play(); } #region Re-expose AudioContainer public BindableNumber Volume => samplesContainer.Volume; public BindableNumber Balance => samplesContainer.Balance; public BindableNumber Frequency => samplesContainer.Frequency; public BindableNumber Tempo => samplesContainer.Tempo; public void BindAdjustments(IAggregateAudioAdjustment component) => samplesContainer.BindAdjustments(component); public void UnbindAdjustments(IAggregateAudioAdjustment component) => samplesContainer.UnbindAdjustments(component); public void AddAdjustment(AdjustableProperty type, IBindable adjustBindable) => samplesContainer.AddAdjustment(type, adjustBindable); public void RemoveAdjustment(AdjustableProperty type, IBindable adjustBindable) => samplesContainer.RemoveAdjustment(type, adjustBindable); public void RemoveAllAdjustments(AdjustableProperty type) => samplesContainer.RemoveAllAdjustments(type); /// /// Whether any samples are currently playing. /// public bool IsPlaying { get { foreach (PoolableSkinnableSample s in samplesContainer) { if (s.Playing) return true; } return false; } } public bool IsPlayed { get { foreach (PoolableSkinnableSample s in samplesContainer) { if (s.Played) return true; } return false; } } public IBindable AggregateVolume => samplesContainer.AggregateVolume; public IBindable AggregateBalance => samplesContainer.AggregateBalance; public IBindable AggregateFrequency => samplesContainer.AggregateFrequency; public IBindable AggregateTempo => samplesContainer.AggregateTempo; #endregion } }