mirror of
https://github.com/ppy/osu.git
synced 2024-11-08 10:47:24 +08:00
134 lines
5.5 KiB
C#
134 lines
5.5 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.Collections.Generic;
|
|
using System.Linq;
|
|
using osu.Framework.Utils;
|
|
using osu.Game.Rulesets.Edit.Checks.Components;
|
|
using osu.Game.Rulesets.Objects;
|
|
using osu.Game.Rulesets.Objects.Types;
|
|
|
|
namespace osu.Game.Rulesets.Edit.Checks
|
|
{
|
|
public class CheckMutedObjects : ICheck
|
|
{
|
|
/// <summary>
|
|
/// Volume percentages lower than this are typically inaudible.
|
|
/// </summary>
|
|
private const int muted_threshold = 5;
|
|
|
|
/// <summary>
|
|
/// Volume percentages lower than this can sometimes be inaudible depending on sample used and music volume.
|
|
/// </summary>
|
|
private const int low_volume_threshold = 20;
|
|
|
|
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Low volume hitobjects");
|
|
|
|
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
|
{
|
|
new IssueTemplateMutedActive(this),
|
|
new IssueTemplateLowVolumeActive(this),
|
|
new IssueTemplateMutedPassive(this)
|
|
};
|
|
|
|
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
|
{
|
|
foreach (var hitObject in context.Beatmap.HitObjects)
|
|
{
|
|
// Worth keeping in mind: The samples of an object always play at its end time.
|
|
// Objects like spinners have no sound at its start because of this, while hold notes have nested objects to accomplish this.
|
|
foreach (var nestedHitObject in hitObject.NestedHitObjects)
|
|
{
|
|
foreach (var issue in getVolumeIssues(hitObject, nestedHitObject))
|
|
yield return issue;
|
|
}
|
|
|
|
foreach (var issue in getVolumeIssues(hitObject))
|
|
yield return issue;
|
|
}
|
|
}
|
|
|
|
private IEnumerable<Issue> getVolumeIssues(HitObject hitObject, HitObject sampledHitObject = null)
|
|
{
|
|
sampledHitObject ??= hitObject;
|
|
if (!sampledHitObject.Samples.Any())
|
|
yield break;
|
|
|
|
// Samples that allow themselves to be overridden by control points have a volume of 0.
|
|
int maxVolume = sampledHitObject.Samples.Max(sample => sample.Volume > 0 ? sample.Volume : sampledHitObject.SampleControlPoint.SampleVolume);
|
|
double samplePlayTime = sampledHitObject.GetEndTime();
|
|
|
|
bool head = Precision.AlmostEquals(samplePlayTime, hitObject.StartTime, 1f);
|
|
bool tail = Precision.AlmostEquals(samplePlayTime, hitObject.GetEndTime(), 1f);
|
|
bool repeat = false;
|
|
|
|
if (hitObject is IHasRepeats hasRepeats && !head && !tail)
|
|
{
|
|
double spanDuration = hasRepeats.Duration / hasRepeats.SpanCount();
|
|
repeat = Precision.AlmostEquals((samplePlayTime - hitObject.StartTime) % spanDuration, 0f, 1f);
|
|
}
|
|
|
|
// We only care about samples played on the edges of objects, not ones like spinnerspin or slidertick.
|
|
if (!head && !tail && !repeat)
|
|
yield break;
|
|
|
|
string postfix = null;
|
|
if (hitObject is IHasDuration)
|
|
postfix = head ? "head" : tail ? "tail" : "repeat";
|
|
|
|
if (maxVolume <= muted_threshold)
|
|
{
|
|
if (head)
|
|
yield return new IssueTemplateMutedActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
|
|
else
|
|
yield return new IssueTemplateMutedPassive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
|
|
}
|
|
else if (maxVolume <= low_volume_threshold && head)
|
|
{
|
|
yield return new IssueTemplateLowVolumeActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
|
|
}
|
|
}
|
|
|
|
public abstract class IssueTemplateMuted : IssueTemplate
|
|
{
|
|
protected IssueTemplateMuted(ICheck check, IssueType type, string unformattedMessage)
|
|
: base(check, type, unformattedMessage)
|
|
{
|
|
}
|
|
|
|
public Issue Create(HitObject hitobject, double volume, double time, string postfix = "")
|
|
{
|
|
string objectName = hitobject.GetType().Name;
|
|
if (!string.IsNullOrEmpty(postfix))
|
|
objectName += " " + postfix;
|
|
|
|
return new Issue(hitobject, this, objectName, volume) { Time = time };
|
|
}
|
|
}
|
|
|
|
public class IssueTemplateMutedActive : IssueTemplateMuted
|
|
{
|
|
public IssueTemplateMutedActive(ICheck check)
|
|
: base(check, IssueType.Problem, "{0} has a volume of {1:0%}. Clickable objects must have clearly audible feedback.")
|
|
{
|
|
}
|
|
}
|
|
|
|
public class IssueTemplateLowVolumeActive : IssueTemplateMuted
|
|
{
|
|
public IssueTemplateLowVolumeActive(ICheck check)
|
|
: base(check, IssueType.Warning, "{0} has a volume of {1:0%}, ensure this is audible.")
|
|
{
|
|
}
|
|
}
|
|
|
|
public class IssueTemplateMutedPassive : IssueTemplateMuted
|
|
{
|
|
public IssueTemplateMutedPassive(ICheck check)
|
|
: base(check, IssueType.Negligible, "{0} has a volume of {1:0%}, ensure there is no distinct sound here in the song if inaudible.")
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|