1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-15 12:47:18 +08:00

Merge pull request #10905 from smoogipoo/hit-sample-pooling

Implement hitsample pooling
This commit is contained in:
Dean Herbert 2020-12-03 18:09:53 +09:00 committed by GitHub
commit 78417b8015
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 379 additions and 87 deletions

View File

@ -11,6 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI;
@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Container<DrawableSliderTail> tailContainer;
private Container<DrawableSliderTick> tickContainer;
private Container<DrawableSliderRepeat> repeatContainer;
private Container<PausableSkinnableSound> samplesContainer;
private PausableSkinnableSound slidingSample;
public DrawableSlider()
: this(null)
@ -69,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Alpha = 0
},
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
samplesContainer = new Container<PausableSkinnableSound> { RelativeSizeAxes = Axes.Both }
slidingSample = new PausableSkinnableSound { Looping = true }
};
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
@ -100,27 +101,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.OnFree();
PathVersion.UnbindFrom(HitObject.Path.Version);
}
private PausableSkinnableSound slidingSample;
slidingSample.Samples = null;
}
protected override void LoadSamples()
{
base.LoadSamples();
samplesContainer.Clear();
slidingSample = null;
var firstSample = HitObject.Samples.FirstOrDefault();
if (firstSample != null)
{
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderslide");
samplesContainer.Add(slidingSample = new PausableSkinnableSound(clone)
{
Looping = true
});
slidingSample.Samples = new ISampleInfo[] { clone };
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Container<DrawableSpinnerTick> ticks;
private SpinnerBonusDisplay bonusDisplay;
private Container<PausableSkinnableSound> samplesContainer;
private PausableSkinnableSound spinningSample;
private Bindable<bool> isSpinning;
private bool spinnerFrequencyModulate;
@ -81,7 +82,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre,
Y = -120,
},
samplesContainer = new Container<PausableSkinnableSound> { RelativeSizeAxes = Axes.Both }
spinningSample = new PausableSkinnableSound
{
Volume = { Value = 0 },
Looping = true,
Frequency = { Value = spinning_sample_initial_frequency }
}
};
PositionBindable.BindValueChanged(pos => Position = pos.NewValue);
@ -95,29 +101,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
isSpinning.BindValueChanged(updateSpinningSample);
}
private PausableSkinnableSound spinningSample;
private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;
protected override void OnFree()
{
base.OnFree();
spinningSample.Samples = null;
}
protected override void LoadSamples()
{
base.LoadSamples();
samplesContainer.Clear();
spinningSample = null;
var firstSample = HitObject.Samples.FirstOrDefault();
if (firstSample != null)
{
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("spinnerspin");
samplesContainer.Add(spinningSample = new PausableSkinnableSound(clone)
{
Volume = { Value = 0 },
Looping = true,
Frequency = { Value = spinning_sample_initial_frequency }
});
spinningSample.Samples = new ISampleInfo[] { clone };
spinningSample.Frequency.Value = spinning_sample_initial_frequency;
}
}

View File

@ -1,14 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
namespace osu.Game.Audio
{
/// <summary>
/// Describes a gameplay sample.
/// </summary>
public class SampleInfo : ISampleInfo
public class SampleInfo : ISampleInfo, IEquatable<SampleInfo>
{
private readonly string[] sampleNames;
@ -20,5 +22,16 @@ namespace osu.Game.Audio
public IEnumerable<string> LookupNames => sampleNames;
public int Volume { get; } = 100;
public override int GetHashCode()
{
return HashCode.Combine(sampleNames, Volume);
}
public bool Equals(SampleInfo other)
=> other != null && sampleNames.SequenceEqual(other.sampleNames);
public override bool Equals(object obj)
=> obj is SampleInfo other && Equals(other);
}
}

View File

@ -10,7 +10,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Logging;
using osu.Framework.Threading;
@ -150,8 +149,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
[Resolved(CanBeNull = true)]
private IPooledHitObjectProvider pooledObjectProvider { get; set; }
private Container<PausableSkinnableSound> samplesContainer;
/// <summary>
/// Whether the initialization logic in <see cref="Playfield" /> has applied.
/// </summary>
@ -175,7 +172,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds);
// Explicit non-virtual function call.
base.AddInternal(samplesContainer = new Container<PausableSkinnableSound> { RelativeSizeAxes = Axes.Both });
base.AddInternal(Samples = new PausableSkinnableSound());
}
protected override void LoadAsyncComplete()
@ -297,6 +294,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
// In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply().
samplesBindable.CollectionChanged -= onSamplesChanged;
// Release the samples for other hitobjects to use.
Samples.Samples = null;
if (nestedHitObjects.IsValueCreated)
{
foreach (var obj in nestedHitObjects.Value)
@ -361,9 +361,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// </summary>
protected virtual void LoadSamples()
{
samplesContainer.Clear();
Samples = null;
var samples = GetSamples().ToArray();
if (samples.Length <= 0)
@ -375,7 +372,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
samplesContainer.Add(Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))));
Samples.Samples = samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
}
private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples();

View File

@ -8,20 +8,24 @@ using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Skinning;
using osuTK;
using System.Diagnostics;
namespace osu.Game.Rulesets.UI
{
[Cached(typeof(IPooledHitObjectProvider))]
public abstract class Playfield : CompositeDrawable, IPooledHitObjectProvider
[Cached(typeof(IPooledSampleProvider))]
public abstract class Playfield : CompositeDrawable, IPooledHitObjectProvider, IPooledSampleProvider
{
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is judged.
@ -81,6 +85,12 @@ namespace osu.Game.Rulesets.UI
/// </summary>
public readonly BindableBool DisplayJudgements = new BindableBool(true);
[Resolved(CanBeNull = true)]
private IReadOnlyList<Mod> mods { get; set; }
[Resolved]
private ISampleStore sampleStore { get; set; }
/// <summary>
/// Creates a new <see cref="Playfield"/>.
/// </summary>
@ -97,9 +107,6 @@ namespace osu.Game.Rulesets.UI
}));
}
[Resolved(CanBeNull = true)]
private IReadOnlyList<Mod> mods { get; set; }
[BackgroundDependencyLoader]
private void load()
{
@ -363,6 +370,29 @@ namespace osu.Game.Rulesets.UI
});
}
private readonly Dictionary<ISampleInfo, DrawablePool<PoolableSkinnableSample>> samplePools = new Dictionary<ISampleInfo, DrawablePool<PoolableSkinnableSample>>();
public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo)
{
if (!samplePools.TryGetValue(sampleInfo, out var existingPool))
AddInternal(samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 1));
return existingPool.Get();
}
private class DrawableSamplePool : DrawablePool<PoolableSkinnableSample>
{
private readonly ISampleInfo sampleInfo;
public DrawableSamplePool(ISampleInfo sampleInfo, int initialSize, int? maximumSize = null)
: base(initialSize, maximumSize)
{
this.sampleInfo = sampleInfo;
}
protected override PoolableSkinnableSample CreateNewDrawable() => base.CreateNewDrawable().With(d => d.Apply(sampleInfo));
}
#endregion
#region Editor logic

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Game.Audio;
namespace osu.Game.Skinning
{
/// <summary>
/// Provides pooled samples to be used by <see cref="SkinnableSound"/>s.
/// </summary>
internal interface IPooledSampleProvider
{
/// <summary>
/// Retrieves a <see cref="PoolableSkinnableSample"/> from a pool.
/// </summary>
/// <param name="sampleInfo">The <see cref="SampleInfo"/> describing the sample to retrieve.</param>
/// <returns>The <see cref="PoolableSkinnableSample"/>.</returns>
[CanBeNull]
PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo);
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Threading;
@ -14,13 +15,17 @@ namespace osu.Game.Skinning
{
protected bool RequestedPlaying { get; private set; }
public PausableSkinnableSound(ISampleInfo hitSamples)
: base(hitSamples)
public PausableSkinnableSound()
{
}
public PausableSkinnableSound(IEnumerable<ISampleInfo> hitSamples)
: base(hitSamples)
public PausableSkinnableSound([NotNull] IEnumerable<ISampleInfo> samples)
: base(samples)
{
}
public PausableSkinnableSound([NotNull] ISampleInfo sample)
: base(sample)
{
}

View File

@ -0,0 +1,168 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
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
{
/// <summary>
/// A sample corresponding to an <see cref="ISampleInfo"/> that supports being pooled and responding to skin changes.
/// </summary>
public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent
{
/// <summary>
/// The currently-loaded <see cref="DrawableSample"/>.
/// </summary>
[CanBeNull]
public DrawableSample Sample { get; private set; }
private readonly AudioContainer<DrawableSample> sampleContainer;
private ISampleInfo sampleInfo;
[Resolved]
private ISampleStore sampleStore { get; set; }
/// <summary>
/// Creates a new <see cref="PoolableSkinnableSample"/> with no applied <see cref="ISampleInfo"/>.
/// An <see cref="ISampleInfo"/> can be applied later via <see cref="Apply"/>.
/// </summary>
public PoolableSkinnableSample()
{
InternalChild = sampleContainer = new AudioContainer<DrawableSample> { RelativeSizeAxes = Axes.Both };
}
/// <summary>
/// Creates a new <see cref="PoolableSkinnableSample"/> with an applied <see cref="ISampleInfo"/>.
/// </summary>
/// <param name="sampleInfo">The <see cref="ISampleInfo"/> to attach.</param>
public PoolableSkinnableSample(ISampleInfo sampleInfo)
: this()
{
Apply(sampleInfo);
}
/// <summary>
/// Applies an <see cref="ISampleInfo"/> that describes the sample to retrieve.
/// Only one <see cref="ISampleInfo"/> can ever be applied to a <see cref="PoolableSkinnableSample"/>.
/// </summary>
/// <param name="sampleInfo">The <see cref="ISampleInfo"/> to apply.</param>
/// <exception cref="InvalidOperationException">If an <see cref="ISampleInfo"/> has already been applied to this <see cref="PoolableSkinnableSample"/>.</exception>
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, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
updateSample();
}
private void updateSample()
{
if (sampleInfo == null)
return;
bool wasPlaying = Playing;
sampleContainer.Clear();
Sample = null;
var ch = CurrentSkin.GetSample(sampleInfo);
if (ch == null && AllowDefaultFallback)
{
foreach (var lookup in sampleInfo.LookupNames)
{
if ((ch = sampleStore.Get(lookup)) != null)
break;
}
}
if (ch == null)
return;
sampleContainer.Add(Sample = new DrawableSample(ch) { Looping = Looping });
// Start playback internally for the new sample if the previous one was playing beforehand.
if (wasPlaying)
Play();
}
/// <summary>
/// Plays the sample.
/// </summary>
/// <param name="restart">Whether to play the sample from the beginning.</param>
public void Play(bool restart = true) => Sample?.Play(restart);
/// <summary>
/// Stops the sample.
/// </summary>
public void Stop() => Sample?.Stop();
/// <summary>
/// Whether the sample is currently playing.
/// </summary>
public bool Playing => Sample?.Playing ?? false;
private bool looping;
/// <summary>
/// Whether the sample should loop on completion.
/// </summary>
public bool Looping
{
get => looping;
set
{
looping = value;
if (Sample != null)
Sample.Looping = value;
}
}
#region Re-expose AudioContainer
public BindableNumber<double> Volume => sampleContainer.Volume;
public BindableNumber<double> Balance => sampleContainer.Balance;
public BindableNumber<double> Frequency => sampleContainer.Frequency;
public BindableNumber<double> Tempo => sampleContainer.Tempo;
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable);
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable);
public void RemoveAllAdjustments(AdjustableProperty type) => sampleContainer.RemoveAllAdjustments(type);
public IBindable<double> AggregateVolume => sampleContainer.AggregateVolume;
public IBindable<double> AggregateBalance => sampleContainer.AggregateBalance;
public IBindable<double> AggregateFrequency => sampleContainer.AggregateFrequency;
public IBindable<double> AggregateTempo => sampleContainer.AggregateTempo;
#endregion
}
}

View File

@ -27,7 +27,7 @@ namespace osu.Game.Skinning
/// <summary>
/// Whether fallback to default skin should be allowed if the custom skin is missing this resource.
/// </summary>
private bool allowDefaultFallback => allowFallback == null || allowFallback.Invoke(CurrentSkin);
protected bool AllowDefaultFallback => allowFallback == null || allowFallback.Invoke(CurrentSkin);
/// <summary>
/// Create a new <see cref="SkinReloadableDrawable"/>
@ -58,7 +58,7 @@ namespace osu.Game.Skinning
private void skinChanged()
{
SkinChanged(CurrentSkin, allowDefaultFallback);
SkinChanged(CurrentSkin, AllowDefaultFallback);
OnSkinChanged?.Invoke();
}

View File

@ -1,26 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
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
{
/// <summary>
/// A sound consisting of one or more samples to be played.
/// </summary>
public class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent
{
private readonly ISampleInfo[] hitSamples;
[Resolved]
private ISampleStore samples { get; set; }
public override bool RemoveWhenNotAlive => false;
public override bool RemoveCompletedTransforms => false;
@ -34,21 +35,74 @@ namespace osu.Game.Skinning
/// </remarks>
protected bool PlayWhenZeroVolume => Looping;
protected readonly AudioContainer<DrawableSample> SamplesContainer;
/// <summary>
/// All raw <see cref="DrawableSamples"/>s contained in this <see cref="SkinnableSound"/>.
/// </summary>
[NotNull, ItemNotNull]
protected IEnumerable<DrawableSample> DrawableSamples => samplesContainer.Select(c => c.Sample).Where(s => s != null);
public SkinnableSound(ISampleInfo hitSamples)
: this(new[] { hitSamples })
private readonly AudioContainer<PoolableSkinnableSample> samplesContainer;
[Resolved]
private ISampleStore sampleStore { get; set; }
[Resolved(CanBeNull = true)]
private IPooledSampleProvider samplePool { get; set; }
/// <summary>
/// Creates a new <see cref="SkinnableSound"/>.
/// </summary>
public SkinnableSound()
{
InternalChild = samplesContainer = new AudioContainer<PoolableSkinnableSample>();
}
/// <summary>
/// Creates a new <see cref="SkinnableSound"/> with some initial samples.
/// </summary>
/// <param name="samples">The initial samples.</param>
public SkinnableSound([NotNull] IEnumerable<ISampleInfo> samples)
: this()
{
this.samples = samples.ToArray();
}
/// <summary>
/// Creates a new <see cref="SkinnableSound"/> with an initial sample.
/// </summary>
/// <param name="sample">The initial sample.</param>
public SkinnableSound([NotNull] ISampleInfo sample)
: this(new[] { sample })
{
}
public SkinnableSound(IEnumerable<ISampleInfo> hitSamples)
private ISampleInfo[] samples = Array.Empty<ISampleInfo>();
/// <summary>
/// The samples that should be played.
/// </summary>
public ISampleInfo[] Samples
{
this.hitSamples = hitSamples.ToArray();
InternalChild = SamplesContainer = new AudioContainer<DrawableSample>();
get => samples;
set
{
value ??= Array.Empty<ISampleInfo>();
if (samples == value)
return;
samples = value;
if (LoadState >= LoadState.Ready)
updateSamples();
}
}
private bool looping;
/// <summary>
/// Whether the samples should loop on completion.
/// </summary>
public bool Looping
{
get => looping;
@ -58,77 +112,80 @@ namespace osu.Game.Skinning
looping = value;
SamplesContainer.ForEach(c => c.Looping = looping);
samplesContainer.ForEach(c => c.Looping = looping);
}
}
/// <summary>
/// Plays the samples.
/// </summary>
public virtual void Play()
{
SamplesContainer.ForEach(c =>
samplesContainer.ForEach(c =>
{
if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0)
c.Play();
});
}
/// <summary>
/// Stops the samples.
/// </summary>
public virtual void Stop()
{
SamplesContainer.ForEach(c => c.Stop());
samplesContainer.ForEach(c => c.Stop());
}
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
updateSamples();
}
private void updateSamples()
{
bool wasPlaying = IsPlaying;
var channels = hitSamples.Select(s =>
// Remove all pooled samples (return them to the pool), and dispose the rest.
samplesContainer.RemoveAll(s => s.IsInPool);
samplesContainer.Clear();
foreach (var s in samples)
{
var ch = skin.GetSample(s);
var sample = samplePool?.GetPooledSample(s) ?? new PoolableSkinnableSample(s);
sample.Looping = Looping;
sample.Volume.Value = s.Volume / 100.0;
if (ch == null && allowFallback)
{
foreach (var lookup in s.LookupNames)
{
if ((ch = samples.Get(lookup)) != null)
break;
}
}
samplesContainer.Add(sample);
}
if (ch != null)
{
ch.Looping = looping;
ch.Volume.Value = s.Volume / 100.0;
}
return ch;
}).Where(c => c != null);
SamplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c));
// Start playback internally for the new samples if the previous ones were playing beforehand.
if (wasPlaying)
Play();
}
#region Re-expose AudioContainer
public BindableNumber<double> Volume => SamplesContainer.Volume;
public BindableNumber<double> Volume => samplesContainer.Volume;
public BindableNumber<double> Balance => SamplesContainer.Balance;
public BindableNumber<double> Balance => samplesContainer.Balance;
public BindableNumber<double> Frequency => SamplesContainer.Frequency;
public BindableNumber<double> Frequency => samplesContainer.Frequency;
public BindableNumber<double> Tempo => SamplesContainer.Tempo;
public BindableNumber<double> Tempo => samplesContainer.Tempo;
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable)
=> SamplesContainer.AddAdjustment(type, adjustBindable);
=> samplesContainer.AddAdjustment(type, adjustBindable);
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable)
=> SamplesContainer.RemoveAdjustment(type, adjustBindable);
=> samplesContainer.RemoveAdjustment(type, adjustBindable);
public void RemoveAllAdjustments(AdjustableProperty type)
=> SamplesContainer.RemoveAllAdjustments(type);
=> samplesContainer.RemoveAllAdjustments(type);
public bool IsPlaying => SamplesContainer.Any(s => s.Playing);
/// <summary>
/// Whether any samples are currently playing.
/// </summary>
public bool IsPlaying => samplesContainer.Any(s => s.Playing);
#endregion
}

View File

@ -37,7 +37,7 @@ namespace osu.Game.Storyboards.Drawables
foreach (var mod in mods.Value.OfType<IApplicableToSample>())
{
foreach (var sample in SamplesContainer)
foreach (var sample in DrawableSamples)
mod.ApplyToSample(sample);
}
}