// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable using System; using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Game.Audio; namespace osu.Game.Skinning { /// /// A sample corresponding to an that supports being pooled and responding to skin changes. /// public partial class PoolableSkinnableSample : SkinReloadableDrawable, IAdjustableAudioComponent { /// /// The currently-loaded . /// [CanBeNull] public DrawableSample Sample { get; private set; } private readonly AudioContainer sampleContainer; private ISampleInfo sampleInfo; private SampleChannel activeChannel; /// /// Creates a new with no applied . /// An can be applied later via . /// public PoolableSkinnableSample() { InternalChild = sampleContainer = new AudioContainer { RelativeSizeAxes = Axes.Both }; } /// /// Creates a new with an applied . /// /// The to attach. public PoolableSkinnableSample(ISampleInfo sampleInfo) : this() { Apply(sampleInfo); } /// /// Applies an that describes the sample to retrieve. /// Only one can ever be applied to a . /// /// The to apply. /// If an has already been applied to this . public void Apply(ISampleInfo sampleInfo) { if (this.sampleInfo != null) throw new InvalidOperationException($"A {nameof(PoolableSkinnableSample)} cannot be applied multiple {nameof(ISampleInfo)}s."); this.sampleInfo = sampleInfo; Volume.Value = sampleInfo.Volume / 100.0; if (LoadState >= LoadState.Ready) updateSample(); } protected override void SkinChanged(ISkinSource skin) { base.SkinChanged(skin); updateSample(); } private void updateSample() { if (sampleInfo == null) return; bool wasPlaying = Playing; sampleContainer.Clear(); var sample = CurrentSkin.GetSample(sampleInfo); if (sample == null) return; sampleContainer.Add(Sample = new DrawableSample(sample)); // Start playback internally for the new sample if the previous one was playing beforehand. if (wasPlaying && Looping) Play(); } /// /// Plays the sample. /// public void Play() { FlushPendingSkinChanges(); if (Sample == null) return; activeChannel = Sample.GetChannel(); activeChannel.Looping = Looping; activeChannel.Play(); Played = true; } /// /// Stops the sample. /// public void Stop() { activeChannel?.Stop(); activeChannel = null; } /// /// Whether the sample is currently playing. /// public bool Playing => activeChannel?.Playing ?? false; public bool Played { get; private set; } private bool looping; /// /// Whether the sample should loop on completion. /// public bool Looping { get => looping; set { looping = value; if (activeChannel != null) activeChannel.Looping = value; } } #region Re-expose AudioContainer public BindableNumber Volume => sampleContainer.Volume; public BindableNumber Balance => sampleContainer.Balance; public BindableNumber Frequency => sampleContainer.Frequency; public BindableNumber Tempo => sampleContainer.Tempo; public void BindAdjustments(IAggregateAudioAdjustment component) => sampleContainer.BindAdjustments(component); public void UnbindAdjustments(IAggregateAudioAdjustment component) => sampleContainer.UnbindAdjustments(component); public void AddAdjustment(AdjustableProperty type, IBindable adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable); public void RemoveAdjustment(AdjustableProperty type, IBindable adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable); public void RemoveAllAdjustments(AdjustableProperty type) => sampleContainer.RemoveAllAdjustments(type); public IBindable AggregateVolume => sampleContainer.AggregateVolume; public IBindable AggregateBalance => sampleContainer.AggregateBalance; public IBindable AggregateFrequency => sampleContainer.AggregateFrequency; public IBindable AggregateTempo => sampleContainer.AggregateTempo; #endregion } }