mirror of
https://github.com/ppy/osu.git
synced 2024-11-15 22:37:26 +08:00
e8a394f894
Closes https://github.com/ppy/osu/issues/29832. The underlying reason for the incorrect sample playback was an equality comparer failure. Samples are contained in several pools which are managed by the playfield. In particular, the pools are keyed by `ISampleInfo` instances. This means that for correct operation, `ISampleInfo` has to implement `IEquatable<ISampleInfo>` and also provide an appropriately correct `GetHashCode()` implementation. Different audible samples must not compare equal to each other when represented by `ISampleInfo`. As it turns out, `VolumeAwareHitSampleInfo` failed on this, due to not overriding equality members. Therefore, a `new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL, volume: 70)` was allowed to compare equal to a `VolumeAwareHitSampleInfo` wrapping it, *even though they correspond to completely different sounds and go through entirely different lookup path sequences*. Therefore, to fix, provide more proper equality implementations for `VolumeAwareHitSampleInfo`. When testing note that this issue *only occurs immediately after placing an object*. Saving and re-entering editor makes this issue go away. I haven't looked too long into why, but the general gist of it is ordering; it appears that a `normal-hitnormal` pool exists at point of query of a new object placement, but does not seem to exist when entering editor afresh. That said I'm not sure that ordering aspect of this bug matters much if at all, since the two `IHitSampleInfo`s should never be allowed to alias with each other at all wrt equality.
108 lines
3.9 KiB
C#
108 lines
3.9 KiB
C#
// 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 Newtonsoft.Json;
|
|
using osu.Game.Beatmaps.ControlPoints;
|
|
using osu.Game.Utils;
|
|
|
|
namespace osu.Game.Audio
|
|
{
|
|
/// <summary>
|
|
/// Describes a gameplay hit sample.
|
|
/// </summary>
|
|
[Serializable]
|
|
public class HitSampleInfo : ISampleInfo, IEquatable<HitSampleInfo>
|
|
{
|
|
public const string HIT_NORMAL = @"hitnormal";
|
|
public const string HIT_WHISTLE = @"hitwhistle";
|
|
public const string HIT_FINISH = @"hitfinish";
|
|
public const string HIT_CLAP = @"hitclap";
|
|
|
|
public const string BANK_NORMAL = @"normal";
|
|
public const string BANK_SOFT = @"soft";
|
|
public const string BANK_DRUM = @"drum";
|
|
|
|
// new sample used exclusively by taiko for now.
|
|
public const string HIT_FLOURISH = "hitflourish";
|
|
|
|
// new bank used exclusively by taiko for now.
|
|
public const string BANK_STRONG = @"strong";
|
|
|
|
/// <summary>
|
|
/// All valid sample addition constants.
|
|
/// </summary>
|
|
public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_FINISH, HIT_CLAP };
|
|
|
|
/// <summary>
|
|
/// All valid bank constants.
|
|
/// </summary>
|
|
public static IEnumerable<string> AllBanks => new[] { BANK_NORMAL, BANK_SOFT, BANK_DRUM };
|
|
|
|
/// <summary>
|
|
/// The name of the sample to load.
|
|
/// </summary>
|
|
public readonly string Name;
|
|
|
|
/// <summary>
|
|
/// The bank to load the sample from.
|
|
/// </summary>
|
|
public readonly string Bank;
|
|
|
|
/// <summary>
|
|
/// An optional suffix to provide priority lookup. Falls back to non-suffixed <see cref="Name"/>.
|
|
/// </summary>
|
|
public readonly string? Suffix;
|
|
|
|
/// <summary>
|
|
/// The sample volume.
|
|
/// </summary>
|
|
public int Volume { get; }
|
|
|
|
public HitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100)
|
|
{
|
|
Name = name;
|
|
Bank = bank;
|
|
Suffix = suffix;
|
|
Volume = volume;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieve all possible filenames that can be used as a source, returned in order of preference (highest first).
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public virtual IEnumerable<string> LookupNames
|
|
{
|
|
get
|
|
{
|
|
if (!string.IsNullOrEmpty(Suffix))
|
|
yield return $"Gameplay/{Bank}-{Name}{Suffix}";
|
|
|
|
yield return $"Gameplay/{Bank}-{Name}";
|
|
|
|
yield return $"Gameplay/{Name}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new <see cref="HitSampleInfo"/> with overridden values.
|
|
/// </summary>
|
|
/// <param name="newName">An optional new sample name.</param>
|
|
/// <param name="newBank">An optional new sample bank.</param>
|
|
/// <param name="newSuffix">An optional new lookup suffix.</param>
|
|
/// <param name="newVolume">An optional new volume.</param>
|
|
/// <returns>The new <see cref="HitSampleInfo"/>.</returns>
|
|
public virtual HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default)
|
|
=> new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume));
|
|
|
|
public virtual bool Equals(HitSampleInfo? other)
|
|
=> other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix;
|
|
|
|
public override bool Equals(object? obj)
|
|
=> obj is HitSampleInfo other && Equals(other);
|
|
|
|
public override int GetHashCode() => HashCode.Combine(Name, Bank, Suffix);
|
|
}
|
|
}
|