Add initial hit sample pooling

This commit is contained in:
smoogipoo
2020-11-19 19:51:09 +09:00
parent 7f3c8ad744
commit 730b14b5bb
10 changed files with 283 additions and 58 deletions

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Judgements;
@ -26,11 +27,17 @@ namespace osu.Game.Rulesets.Catch.Objects
Samples = samples; Samples = samples;
} }
private class BananaHitSampleInfo : HitSampleInfo private class BananaHitSampleInfo : HitSampleInfo, IEquatable<BananaHitSampleInfo>
{ {
private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" }; private static readonly string[] lookup_names = { "metronomelow", "catch-banana" };
public override IEnumerable<string> LookupNames => lookupNames; public override IEnumerable<string> LookupNames => lookup_names;
public bool Equals(BananaHitSampleInfo other) => true;
public override bool Equals(object obj) => obj is BananaHitSampleInfo other && Equals(other);
public override int GetHashCode() => lookup_names.GetHashCode();
} }
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace osu.Game.Audio namespace osu.Game.Audio
{ {
@ -10,7 +11,7 @@ namespace osu.Game.Audio
/// Describes a gameplay hit sample. /// Describes a gameplay hit sample.
/// </summary> /// </summary>
[Serializable] [Serializable]
public class HitSampleInfo : ISampleInfo public class HitSampleInfo : ISampleInfo, IEquatable<HitSampleInfo>
{ {
public const string HIT_WHISTLE = @"hitwhistle"; public const string HIT_WHISTLE = @"hitwhistle";
public const string HIT_FINISH = @"hitfinish"; public const string HIT_FINISH = @"hitfinish";
@ -57,5 +58,17 @@ namespace osu.Game.Audio
} }
public HitSampleInfo Clone() => (HitSampleInfo)MemberwiseClone(); public HitSampleInfo Clone() => (HitSampleInfo)MemberwiseClone();
public bool Equals(HitSampleInfo other)
=> other != null && Bank == other.Bank && Name == other.Name && Suffix == other.Suffix;
public override bool Equals(object obj)
=> obj is HitSampleInfo other && Equals(other);
[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] // This will have to be addressed eventually
public override int GetHashCode()
{
return HashCode.Combine(Bank, Name, Suffix);
}
} }
} }

View File

@ -1,14 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Audio namespace osu.Game.Audio
{ {
/// <summary> /// <summary>
/// Describes a gameplay sample. /// Describes a gameplay sample.
/// </summary> /// </summary>
public class SampleInfo : ISampleInfo public class SampleInfo : ISampleInfo, IEquatable<SampleInfo>
{ {
private readonly string[] sampleNames; private readonly string[] sampleNames;
@ -20,5 +22,16 @@ namespace osu.Game.Audio
public IEnumerable<string> LookupNames => sampleNames; public IEnumerable<string> LookupNames => sampleNames;
public int Volume { get; } = 100; 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.Bindables;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Threading; using osu.Framework.Threading;
@ -139,8 +138,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IPooledHitObjectProvider pooledObjectProvider { get; set; } private IPooledHitObjectProvider pooledObjectProvider { get; set; }
private Container<PausableSkinnableSound> samplesContainer;
/// <summary> /// <summary>
/// Creates a new <see cref="DrawableHitObject"/>. /// Creates a new <see cref="DrawableHitObject"/>.
/// </summary> /// </summary>
@ -159,7 +156,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds); config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds);
// Explicit non-virtual function call. // Explicit non-virtual function call.
base.AddInternal(samplesContainer = new Container<PausableSkinnableSound> { RelativeSizeAxes = Axes.Both }); base.AddInternal(Samples = new PausableSkinnableSound(Array.Empty<ISampleInfo>()));
} }
protected override void LoadAsyncComplete() protected override void LoadAsyncComplete()
@ -269,6 +266,8 @@ 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(). // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply().
samplesBindable.CollectionChanged -= onSamplesChanged; samplesBindable.CollectionChanged -= onSamplesChanged;
Samples.Samples = Array.Empty<ISampleInfo>();
if (nestedHitObjects.IsValueCreated) if (nestedHitObjects.IsValueCreated)
{ {
foreach (var obj in nestedHitObjects.Value) foreach (var obj in nestedHitObjects.Value)
@ -335,8 +334,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// </summary> /// </summary>
protected virtual void LoadSamples() protected virtual void LoadSamples()
{ {
samplesContainer.Clear(); Samples.Samples = Array.Empty<ISampleInfo>();
Samples = null;
var samples = GetSamples().ToArray(); var samples = GetSamples().ToArray();
@ -349,7 +347,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); + $" 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(); private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples();

View File

@ -5,6 +5,7 @@ using osuTK;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Audio; using osu.Game.Audio;
@ -500,7 +501,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
} }
public class LegacyHitSampleInfo : HitSampleInfo public class LegacyHitSampleInfo : HitSampleInfo, IEquatable<LegacyHitSampleInfo>
{ {
private int customSampleBank; private int customSampleBank;
@ -524,9 +525,21 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// using the <see cref="LegacySkinConfiguration.LegacySetting.LayeredHitSounds"/> skin config option. /// using the <see cref="LegacySkinConfiguration.LegacySetting.LayeredHitSounds"/> skin config option.
/// </remarks> /// </remarks>
public bool IsLayered { get; set; } public bool IsLayered { get; set; }
public bool Equals(LegacyHitSampleInfo other)
=> other != null && base.Equals(other) && CustomSampleBank == other.CustomSampleBank;
public override bool Equals(object obj)
=> obj is LegacyHitSampleInfo other && Equals(other);
[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] // This will have to be addressed eventually
public override int GetHashCode()
{
return HashCode.Combine(base.GetHashCode(), customSampleBank);
}
} }
private class FileHitSampleInfo : LegacyHitSampleInfo private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable<FileHitSampleInfo>
{ {
public string Filename; public string Filename;
@ -542,6 +555,18 @@ namespace osu.Game.Rulesets.Objects.Legacy
Filename, Filename,
Path.ChangeExtension(Filename, null) Path.ChangeExtension(Filename, null)
}; };
public bool Equals(FileHitSampleInfo other)
=> other != null && Filename == other.Filename;
public override bool Equals(object obj)
=> obj is FileHitSampleInfo other && Equals(other);
[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] // This will have to be addressed eventually
public override int GetHashCode()
{
return HashCode.Combine(Filename);
}
} }
} }
} }

View File

@ -8,19 +8,23 @@ using JetBrains.Annotations;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
{ {
[Cached(typeof(IPooledHitObjectProvider))] [Cached(typeof(IPooledHitObjectProvider))]
public abstract class Playfield : CompositeDrawable, IPooledHitObjectProvider [Cached(typeof(IPooledSampleProvider))]
public abstract class Playfield : CompositeDrawable, IPooledHitObjectProvider, IPooledSampleProvider
{ {
/// <summary> /// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is judged. /// Invoked when a <see cref="DrawableHitObject"/> is judged.
@ -80,6 +84,12 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
public readonly BindableBool DisplayJudgements = new BindableBool(true); public readonly BindableBool DisplayJudgements = new BindableBool(true);
[Resolved(CanBeNull = true)]
private IReadOnlyList<Mod> mods { get; set; }
[Resolved]
private ISampleStore sampleStore { get; set; }
/// <summary> /// <summary>
/// Creates a new <see cref="Playfield"/>. /// Creates a new <see cref="Playfield"/>.
/// </summary> /// </summary>
@ -96,9 +106,6 @@ namespace osu.Game.Rulesets.UI
})); }));
} }
[Resolved(CanBeNull = true)]
private IReadOnlyList<Mod> mods { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -336,6 +343,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))
samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 5);
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 #endregion
#region Editor logic #region Editor logic

View File

@ -14,13 +14,13 @@ namespace osu.Game.Skinning
{ {
protected bool RequestedPlaying { get; private set; } protected bool RequestedPlaying { get; private set; }
public PausableSkinnableSound(ISampleInfo hitSamples) public PausableSkinnableSound(ISampleInfo sample)
: base(hitSamples) : base(sample)
{ {
} }
public PausableSkinnableSound(IEnumerable<ISampleInfo> hitSamples) public PausableSkinnableSound(IEnumerable<ISampleInfo> samples)
: base(hitSamples) : base(samples)
{ {
} }

View File

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

View File

@ -1,26 +1,149 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Audio; using osu.Game.Audio;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
public class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent public interface IPooledSampleProvider
{ {
private readonly ISampleInfo[] hitSamples; [CanBeNull]
PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo);
}
public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent
{
private ISampleInfo sampleInfo;
private DrawableSample sample;
[Resolved] [Resolved]
private ISampleStore samples { get; set; } private ISampleStore sampleStore { get; set; }
[Cached]
private readonly AudioAdjustments adjustments = new AudioAdjustments();
public PoolableSkinnableSample()
{
}
public PoolableSkinnableSample(ISampleInfo sampleInfo)
{
Apply(sampleInfo);
}
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;
if (LoadState >= LoadState.Ready)
updateSample();
}
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
updateSample();
}
private void updateSample()
{
ClearInternal();
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;
AddInternal(sample = new DrawableSample(ch)
{
Looping = Looping,
Volume = { Value = sampleInfo.Volume / 100.0 }
});
}
public void Play(bool restart = true) => sample?.Play(restart);
public void Stop() => sample?.Stop();
public bool Playing => sample?.Playing ?? false;
private bool looping;
public bool Looping
{
get => looping;
set
{
looping = value;
if (sample != null)
sample.Looping = value;
}
}
/// <summary>
/// The volume of this component.
/// </summary>
public BindableNumber<double> Volume => adjustments.Volume;
/// <summary>
/// The playback balance of this sample (-1 .. 1 where 0 is centered)
/// </summary>
public BindableNumber<double> Balance => adjustments.Balance;
/// <summary>
/// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency.
/// </summary>
public BindableNumber<double> Frequency => adjustments.Frequency;
/// <summary>
/// Rate at which the component is played back (does not affect pitch). 1 is 100% playback speed.
/// </summary>
public BindableNumber<double> Tempo => adjustments.Tempo;
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable)
=> adjustments.AddAdjustment(type, adjustBindable);
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable)
=> adjustments.RemoveAdjustment(type, adjustBindable);
public void RemoveAllAdjustments(AdjustableProperty type) => adjustments.RemoveAllAdjustments(type);
public IBindable<double> AggregateVolume => adjustments.AggregateVolume;
public IBindable<double> AggregateBalance => adjustments.AggregateBalance;
public IBindable<double> AggregateFrequency => adjustments.AggregateFrequency;
public IBindable<double> AggregateTempo => adjustments.AggregateTempo;
}
public class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent
{
public override bool RemoveWhenNotAlive => false; public override bool RemoveWhenNotAlive => false;
public override bool RemoveCompletedTransforms => false; public override bool RemoveCompletedTransforms => false;
@ -34,17 +157,44 @@ namespace osu.Game.Skinning
/// </remarks> /// </remarks>
protected bool PlayWhenZeroVolume => Looping; protected bool PlayWhenZeroVolume => Looping;
protected readonly AudioContainer<DrawableSample> SamplesContainer; protected readonly AudioContainer<PoolableSkinnableSample> SamplesContainer;
public SkinnableSound(ISampleInfo hitSamples) [Resolved]
: this(new[] { hitSamples }) private ISampleStore sampleStore { get; set; }
[Resolved(CanBeNull = true)]
private IPooledSampleProvider pooledProvider { get; set; }
public SkinnableSound(ISampleInfo sample)
: this(new[] { sample })
{ {
} }
public SkinnableSound(IEnumerable<ISampleInfo> hitSamples) public SkinnableSound(IEnumerable<ISampleInfo> samples)
{ {
this.hitSamples = hitSamples.ToArray(); this.samples = samples.ToArray();
InternalChild = SamplesContainer = new AudioContainer<DrawableSample>();
InternalChild = SamplesContainer = new AudioContainer<PoolableSkinnableSample>();
}
private ISampleInfo[] samples;
public ISampleInfo[] Samples
{
get => samples;
set
{
if (value == null)
throw new ArgumentNullException(nameof(value));
if (samples == value)
return;
samples = value;
if (LoadState >= LoadState.Ready)
updateSamples();
}
} }
private bool looping; private bool looping;
@ -77,34 +227,23 @@ namespace osu.Game.Skinning
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
// Start playback internally for the new samples if the previous ones were playing beforehand.
if (IsPlaying)
Play();
}
private void updateSamples()
{ {
bool wasPlaying = IsPlaying; 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);
var ch = skin.GetSample(s); SamplesContainer.Clear();
if (ch == null && allowFallback) foreach (var s in samples)
{ SamplesContainer.Add(pooledProvider?.GetPooledSample(s) ?? new PoolableSkinnableSample(s));
foreach (var lookup in s.LookupNames)
{
if ((ch = samples.Get(lookup)) != null)
break;
}
}
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) if (wasPlaying)
Play(); Play();
} }

View File

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