1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-06 09:07:25 +08:00

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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Game.Audio;
using osu.Game.Rulesets.Catch.Judgements;
@ -26,11 +27,17 @@ namespace osu.Game.Rulesets.Catch.Objects
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.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace osu.Game.Audio
{
@ -10,7 +11,7 @@ namespace osu.Game.Audio
/// Describes a gameplay hit sample.
/// </summary>
[Serializable]
public class HitSampleInfo : ISampleInfo
public class HitSampleInfo : ISampleInfo, IEquatable<HitSampleInfo>
{
public const string HIT_WHISTLE = @"hitwhistle";
public const string HIT_FINISH = @"hitfinish";
@ -57,5 +58,17 @@ namespace osu.Game.Audio
}
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.
// 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;
@ -139,8 +138,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
[Resolved(CanBeNull = true)]
private IPooledHitObjectProvider pooledObjectProvider { get; set; }
private Container<PausableSkinnableSound> samplesContainer;
/// <summary>
/// Creates a new <see cref="DrawableHitObject"/>.
/// </summary>
@ -159,7 +156,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(Array.Empty<ISampleInfo>()));
}
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().
samplesBindable.CollectionChanged -= onSamplesChanged;
Samples.Samples = Array.Empty<ISampleInfo>();
if (nestedHitObjects.IsValueCreated)
{
foreach (var obj in nestedHitObjects.Value)
@ -335,8 +334,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// </summary>
protected virtual void LoadSamples()
{
samplesContainer.Clear();
Samples = null;
Samples.Samples = Array.Empty<ISampleInfo>();
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}.");
}
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

@ -5,6 +5,7 @@ using osuTK;
using osu.Game.Rulesets.Objects.Types;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using osu.Game.Beatmaps.Formats;
using osu.Game.Audio;
@ -500,7 +501,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
}
public class LegacyHitSampleInfo : HitSampleInfo
public class LegacyHitSampleInfo : HitSampleInfo, IEquatable<LegacyHitSampleInfo>
{
private int customSampleBank;
@ -524,9 +525,21 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// using the <see cref="LegacySkinConfiguration.LegacySetting.LayeredHitSounds"/> skin config option.
/// </remarks>
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;
@ -542,6 +555,18 @@ namespace osu.Game.Rulesets.Objects.Legacy
Filename,
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.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;
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.
@ -80,6 +84,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>
@ -96,9 +106,6 @@ namespace osu.Game.Rulesets.UI
}));
}
[Resolved(CanBeNull = true)]
private IReadOnlyList<Mod> mods { get; set; }
[BackgroundDependencyLoader]
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
#region Editor logic

View File

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

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,149 @@
// 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
{
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]
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 RemoveCompletedTransforms => false;
@ -34,17 +157,44 @@ namespace osu.Game.Skinning
/// </remarks>
protected bool PlayWhenZeroVolume => Looping;
protected readonly AudioContainer<DrawableSample> SamplesContainer;
protected readonly AudioContainer<PoolableSkinnableSample> SamplesContainer;
public SkinnableSound(ISampleInfo hitSamples)
: this(new[] { hitSamples })
[Resolved]
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();
InternalChild = SamplesContainer = new AudioContainer<DrawableSample>();
this.samples = samples.ToArray();
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;
@ -77,34 +227,23 @@ namespace osu.Game.Skinning
}
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;
var channels = hitSamples.Select(s =>
{
var ch = skin.GetSample(s);
// Remove all pooled samples (return them to the pool), and dispose the rest.
SamplesContainer.RemoveAll(s => s.IsInPool);
SamplesContainer.Clear();
if (ch == null && allowFallback)
{
foreach (var lookup in s.LookupNames)
{
if ((ch = samples.Get(lookup)) != null)
break;
}
}
foreach (var s in samples)
SamplesContainer.Add(pooledProvider?.GetPooledSample(s) ?? new PoolableSkinnableSample(s));
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();
}

View File

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