1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-13 15:27:30 +08:00

Merge pull request #24119 from peppy/taiko-hitsounding-final-attempt

Implement new argon osu!taiko hitsounds
This commit is contained in:
Bartłomiej Dach 2023-07-08 20:19:45 +02:00 committed by GitHub
commit b9eee29442
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 463 additions and 85 deletions

View File

@ -72,13 +72,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
seekTo(200);
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
}
[Test]
@ -100,13 +100,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
seekTo(200);
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
}
[Test]
@ -145,23 +145,23 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
seekTo(120);
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
seekTo(480);
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
seekTo(700);
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
}
[Test]
@ -184,13 +184,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit.StrongNestedHit>);
checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
seekTo(200);
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
}
[Test]
@ -213,18 +213,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
seekTo(600);
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
seekTo(1200);
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
}
[Test]
@ -247,18 +247,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
seekTo(600);
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
seekTo(1200);
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
}
[Test]
@ -282,18 +282,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
seekTo(600);
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
seekTo(1200);
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Centre, true, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, true, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
}
[Test]
@ -319,18 +319,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
// But for sample playback purposes they can be ignored as noise.
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
seekTo(600);
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
seekTo(1200);
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
}
[Test]
@ -356,23 +356,23 @@ namespace osu.Game.Rulesets.Taiko.Tests
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
// But for sample playback purposes they can be ignored as noise.
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
seekTo(600);
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
seekTo(1200);
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
}
private void checkSamples(HitType hitType, string expectedSamplesCsv, string expectedBank)
private void checkSamples(HitType hitType, bool strong, string expectedSamplesCsv, string expectedBank)
{
AddStep($"hit {hitType}", () => triggerSource.Play(hitType));
AddStep($"hit {hitType}", () => triggerSource.Play(hitType, strong));
AddAssert($"last played sample is {expectedSamplesCsv}", () => string.Join(',', triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Select(s => s.Name)),
() => Is.EqualTo(expectedSamplesCsv));
AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().First().Bank, () => Is.EqualTo(expectedBank));

View File

@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
/// The lenience for the second key press.
/// This does not adjust by map difficulty in ScoreV2 yet.
/// </summary>
private const double second_hit_window = 30;
public const double SECOND_HIT_WINDOW = 30;
public StrongNestedHit()
: this(null)
@ -223,12 +223,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (!userTriggered)
{
if (timeOffset - ParentHitObject.Result.TimeOffset > second_hit_window)
if (timeOffset - ParentHitObject.Result.TimeOffset > SECOND_HIT_WINDOW)
ApplyResult(r => r.Type = r.Judgement.MinResult);
return;
}
if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= second_hit_window)
if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= SECOND_HIT_WINDOW)
ApplyResult(r => r.Type = r.Judgement.MaxResult);
}

View File

@ -0,0 +1,42 @@
// 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 osu.Framework.Allocation;
using osu.Game.Audio;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{
internal partial class ArgonDrumSamplePlayer : DrumSamplePlayer
{
private ArgonFlourishTriggerSource argonFlourishTrigger = null!;
[BackgroundDependencyLoader]
private void load(Playfield playfield, IPooledSampleProvider sampleProvider)
{
var hitObjectContainer = playfield.HitObjectContainer;
// Warm up pools for non-standard samples.
sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_NORMAL), true));
sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_CLAP), true));
sampleProvider.GetPooledSample(new VolumeAwareHitSampleInfo(new HitSampleInfo(HitSampleInfo.HIT_FLOURISH), true));
// We want to play back flourishes in an isolated source as to not have them cancelled.
AddInternal(argonFlourishTrigger = new ArgonFlourishTriggerSource(hitObjectContainer));
}
protected override DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance) =>
new ArgonDrumSampleTriggerSource(hitObjectContainer, balance);
protected override void Play(DrumSampleTriggerSource triggerSource, HitType hitType, bool strong)
{
base.Play(triggerSource, hitType, strong);
// This won't always play something, but the logic for flourish playback is contained within.
argonFlourishTrigger.Play(hitType, strong);
}
}
}

View File

@ -0,0 +1,46 @@
// 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 osu.Framework.Allocation;
using osu.Game.Audio;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{
public partial class ArgonDrumSampleTriggerSource : DrumSampleTriggerSource
{
[Resolved]
private ISkinSource skinSource { get; set; } = null!;
public ArgonDrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance)
: base(hitObjectContainer, balance)
{
}
public override void Play(HitType hitType, bool strong)
{
TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject;
if (hitObject == null)
return;
var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL);
// If the sample is provided by a legacy skin, we should not try and do anything special.
if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer)
{
base.Play(hitType, strong);
return;
}
// let the magic begin...
var samplesToPlay = new List<ISampleInfo> { new VolumeAwareHitSampleInfo(originalSample, strong) };
PlaySamples(samplesToPlay.ToArray());
}
}
}

View File

@ -0,0 +1,79 @@
// 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.Linq;
using osu.Framework.Allocation;
using osu.Game.Audio;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{
internal partial class ArgonFlourishTriggerSource : DrumSampleTriggerSource
{
private readonly HitObjectContainer hitObjectContainer;
[Resolved]
private ISkinSource skinSource { get; set; } = null!;
/// <summary>
/// The minimum time to leave between flourishes that are added to strong rim hits.
/// </summary>
private const double time_between_flourishes = 2000;
public ArgonFlourishTriggerSource(HitObjectContainer hitObjectContainer)
: base(hitObjectContainer)
{
this.hitObjectContainer = hitObjectContainer;
}
public override void Play(HitType hitType, bool strong)
{
TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject;
if (hitObject == null)
return;
var originalSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL);
// If the sample is provided by a legacy skin, we should not try and do anything special.
if (skinSource.FindProvider(s => s.GetSample(originalSample) != null) is LegacySkinTransformer)
return;
if (strong && hitType == HitType.Rim && canPlayFlourish(hitObject))
PlaySamples(new ISampleInfo[] { new VolumeAwareHitSampleInfo(hitObject.CreateHitSampleInfo(HitSampleInfo.HIT_FLOURISH), true) });
}
private bool canPlayFlourish(TaikoHitObject hitObject)
{
double? lastFlourish = null;
var hitObjects = hitObjectContainer.AliveObjects
.Reverse()
.Select(d => d.HitObject)
.OfType<Hit>()
.Where(h => h.IsStrong && h.Type == HitType.Rim);
// Add an additional 'flourish' sample to strong rim hits (that are at least `time_between_flourishes` apart).
// This is applied to hitobjects in reverse order, as to sound more musically coherent by biasing towards to
// end of groups/combos of strong rim hits instead of the start.
foreach (var h in hitObjects)
{
bool canFlourish = lastFlourish == null || lastFlourish - h.StartTime >= time_between_flourishes;
if (canFlourish)
lastFlourish = h.StartTime;
// hitObject can be either the strong hit itself (if hit late), or its nested strong object (if hit early)
// due to `GetMostValidObject()` idiosyncrasies.
// whichever it is, if we encounter it during iteration, stop looking.
if (h == hitObject || h.NestedHitObjects.Contains(hitObject))
return canFlourish;
}
return false;
}
}
}

View File

@ -60,6 +60,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
// the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield.
return Drawable.Empty().With(d => d.Expire());
case TaikoSkinComponents.DrumSamplePlayer:
return new ArgonDrumSamplePlayer();
case TaikoSkinComponents.TaikoExplosionGreat:
case TaikoSkinComponents.TaikoExplosionMiss:
case TaikoSkinComponents.TaikoExplosionOk:

View File

@ -0,0 +1,52 @@
// 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 osu.Game.Audio;
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{
public class VolumeAwareHitSampleInfo : HitSampleInfo
{
public const int SAMPLE_VOLUME_THRESHOLD_HARD = 90;
public const int SAMPLE_VOLUME_THRESHOLD_MEDIUM = 60;
public VolumeAwareHitSampleInfo(HitSampleInfo sampleInfo, bool isStrong = false)
: base(sampleInfo.Name, isStrong ? BANK_STRONG : getBank(sampleInfo.Bank, sampleInfo.Name, sampleInfo.Volume), sampleInfo.Suffix, sampleInfo.Volume)
{
}
public override IEnumerable<string> LookupNames
{
get
{
foreach (string name in base.LookupNames)
yield return name.Insert(name.LastIndexOf('/') + 1, "Argon/taiko-");
}
}
private static string getBank(string originalBank, string sampleName, int volume)
{
// So basically we're overwriting mapper's bank intentions here.
// The rationale is that most taiko beatmaps only use a single bank, but regularly adjust volume.
switch (sampleName)
{
case HIT_NORMAL:
case HIT_CLAP:
{
if (volume >= SAMPLE_VOLUME_THRESHOLD_HARD)
return BANK_DRUM;
if (volume >= SAMPLE_VOLUME_THRESHOLD_MEDIUM)
return BANK_NORMAL;
return BANK_SOFT;
}
default:
return originalBank;
}
}
}
}

View File

@ -52,6 +52,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
return null;
case TaikoSkinComponents.DrumSamplePlayer:
return null;
case TaikoSkinComponents.CentreHit:
case TaikoSkinComponents.RimHit:
if (hasHitCircle)

View File

@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Taiko
TaikoExplosionKiai,
Scroller,
Mascot,
KiaiGlow
KiaiGlow,
DrumSamplePlayer
}
}

View File

@ -1,57 +1,151 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Taiko.UI
{
internal partial class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler<TaikoAction>
{
private readonly DrumSampleTriggerSource leftRimSampleTriggerSource;
private readonly DrumSampleTriggerSource leftCentreSampleTriggerSource;
private readonly DrumSampleTriggerSource rightCentreSampleTriggerSource;
private readonly DrumSampleTriggerSource rightRimSampleTriggerSource;
private DrumSampleTriggerSource leftCentreTrigger = null!;
private DrumSampleTriggerSource rightCentreTrigger = null!;
private DrumSampleTriggerSource leftRimTrigger = null!;
private DrumSampleTriggerSource rightRimTrigger = null!;
private DrumSampleTriggerSource strongCentreTrigger = null!;
private DrumSampleTriggerSource strongRimTrigger = null!;
public DrumSamplePlayer(HitObjectContainer hitObjectContainer)
private double lastHitTime;
private TaikoAction? lastAction;
[BackgroundDependencyLoader]
private void load(Playfield playfield)
{
var hitObjectContainer = playfield.HitObjectContainer;
InternalChildren = new Drawable[]
{
leftRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
leftCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
rightCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
rightRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
leftCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Left),
rightCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Right),
leftRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Left),
rightRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Right),
strongCentreTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Centre),
strongRimTrigger = CreateTriggerSource(hitObjectContainer, SampleBalance.Centre)
};
}
protected virtual DrumSampleTriggerSource CreateTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance)
=> new DrumSampleTriggerSource(hitObjectContainer);
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
{
if ((Clock as IGameplayClock)?.IsRewinding == true)
return false;
HitType hitType;
DrumSampleTriggerSource triggerSource;
bool strong = checkStrongValidity(e.Action, lastAction, Time.Current - lastHitTime);
switch (e.Action)
{
case TaikoAction.LeftRim:
leftRimSampleTriggerSource.Play(HitType.Rim);
break;
case TaikoAction.LeftCentre:
leftCentreSampleTriggerSource.Play(HitType.Centre);
hitType = HitType.Centre;
triggerSource = strong ? strongCentreTrigger : leftCentreTrigger;
break;
case TaikoAction.RightCentre:
rightCentreSampleTriggerSource.Play(HitType.Centre);
hitType = HitType.Centre;
triggerSource = strong ? strongCentreTrigger : rightCentreTrigger;
break;
case TaikoAction.LeftRim:
hitType = HitType.Rim;
triggerSource = strong ? strongRimTrigger : leftRimTrigger;
break;
case TaikoAction.RightRim:
rightRimSampleTriggerSource.Play(HitType.Rim);
hitType = HitType.Rim;
triggerSource = strong ? strongRimTrigger : rightRimTrigger;
break;
default:
return false;
}
if (strong)
{
switch (hitType)
{
case HitType.Centre:
flushCenterTriggerSources();
break;
case HitType.Rim:
flushRimTriggerSources();
break;
}
}
Play(triggerSource, hitType, strong);
lastHitTime = Time.Current;
lastAction = e.Action;
return false;
}
protected virtual void Play(DrumSampleTriggerSource triggerSource, HitType hitType, bool strong) =>
triggerSource.Play(hitType, strong);
private bool checkStrongValidity(TaikoAction newAction, TaikoAction? lastAction, double timeBetweenActions)
{
if (lastAction == null)
return false;
if (timeBetweenActions < 0 || timeBetweenActions > DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW)
return false;
switch (newAction)
{
case TaikoAction.LeftCentre:
return lastAction == TaikoAction.RightCentre;
case TaikoAction.RightCentre:
return lastAction == TaikoAction.LeftCentre;
case TaikoAction.LeftRim:
return lastAction == TaikoAction.RightRim;
case TaikoAction.RightRim:
return lastAction == TaikoAction.LeftRim;
default:
return false;
}
}
private void flushCenterTriggerSources()
{
leftCentreTrigger.StopAllPlayback();
rightCentreTrigger.StopAllPlayback();
strongCentreTrigger.StopAllPlayback();
}
private void flushRimTriggerSources()
{
leftRimTrigger.StopAllPlayback();
rightRimTrigger.StopAllPlayback();
strongRimTrigger.StopAllPlayback();
}
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e)
{
}

View File

@ -2,20 +2,38 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.UI
{
public partial class DrumSampleTriggerSource : GameplaySampleTriggerSource
{
public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer)
private const double stereo_separation = 0.2;
public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer, SampleBalance balance = SampleBalance.Centre)
: base(hitObjectContainer)
{
switch (balance)
{
case SampleBalance.Left:
AudioContainer.Balance.Value = -stereo_separation;
break;
case SampleBalance.Centre:
AudioContainer.Balance.Value = 0;
break;
case SampleBalance.Right:
AudioContainer.Balance.Value = stereo_separation;
break;
}
}
public void Play(HitType hitType)
public virtual void Play(HitType hitType, bool strong)
{
TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject;
@ -24,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI
var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL);
if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject)
if (strong)
{
PlaySamples(new ISampleInfo[]
{
@ -39,5 +57,19 @@ namespace osu.Game.Rulesets.Taiko.UI
}
public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");
protected override void ApplySampleInfo(SkinnableSound hitSound, ISampleInfo[] samples)
{
base.ApplySampleInfo(hitSound, samples);
hitSound.Balance.Value = -0.05 + RNG.NextDouble(0.1);
}
}
public enum SampleBalance
{
Left,
Centre,
Right
}
}

View File

@ -170,7 +170,10 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Both,
},
drumRollHitContainer.CreateProxy(),
new DrumSamplePlayer(HitObjectContainer),
new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumSamplePlayer), _ => new DrumSamplePlayer())
{
RelativeSizeAxes = Axes.Both,
},
// this is added at the end of the hierarchy to receive input before taiko objects.
// but is proxied below everything to not cover visual effects such as hit explosions.
inputDrum,

View File

@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
private void checkValidObjectIndex(int index) =>
AddAssert($"check valid object is {index}", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[index]));
AddAssert($"check object at index {index} is correct", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[index]));
private DrawableHitObject? getNextAliveObject() =>
Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault();

View File

@ -24,6 +24,12 @@ namespace osu.Game.Audio
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>

View File

@ -34,14 +34,19 @@ namespace osu.Game.Rulesets.UI
[Resolved]
private IGameplayClock? gameplayClock { get; set; }
protected readonly AudioContainer AudioContainer;
public GameplaySampleTriggerSource(HitObjectContainer hitObjectContainer)
{
this.hitObjectContainer = hitObjectContainer;
InternalChild = hitSounds = new Container<SkinnableSound>
InternalChild = AudioContainer = new AudioContainer
{
Name = "concurrent sample pool",
ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound())
Child = hitSounds = new Container<SkinnableSound>
{
Name = "concurrent sample pool",
ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound())
}
};
}
@ -64,11 +69,22 @@ namespace osu.Game.Rulesets.UI
protected virtual void PlaySamples(ISampleInfo[] samples) => Schedule(() =>
{
var hitSound = getNextSample();
hitSound.Samples = samples;
var hitSound = GetNextSample();
ApplySampleInfo(hitSound, samples);
hitSound.Play();
});
protected virtual void ApplySampleInfo(SkinnableSound hitSound, ISampleInfo[] samples)
{
hitSound.Samples = samples;
}
public void StopAllPlayback() => Schedule(() =>
{
foreach (var sound in hitSounds)
sound.Stop();
});
protected override void Update()
{
base.Update();
@ -118,7 +134,7 @@ namespace osu.Game.Rulesets.UI
return getAllNested(mostValidObject.HitObject).OrderBy(h => h.GetEndTime()).SkipWhile(h => h.GetEndTime() <= getReferenceTime()).FirstOrDefault() ?? mostValidObject.HitObject;
}
private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.Result?.HasResult == true;
private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.AllJudged;
private bool isCloseEnoughToCurrentTime(HitObject h) => getReferenceTime() >= h.StartTime - h.HitWindows.WindowFor(HitResult.Miss) * 2;
private double getReferenceTime() => gameplayClock?.CurrentTime ?? Clock.CurrentTime;
@ -134,7 +150,7 @@ namespace osu.Game.Rulesets.UI
}
}
private SkinnableSound getNextSample()
protected SkinnableSound GetNextSample()
{
SkinnableSound hitSound = hitSounds[nextHitSoundIndex];

View File

@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.UI
{
[Cached(typeof(IPooledHitObjectProvider))]
[Cached(typeof(IPooledSampleProvider))]
[Cached]
public abstract partial class Playfield : CompositeDrawable, IPooledHitObjectProvider, IPooledSampleProvider
{
/// <summary>

View File

@ -8,7 +8,7 @@ namespace osu.Game.Skinning
/// <summary>
/// Provides pooled samples to be used by <see cref="SkinnableSound"/>s.
/// </summary>
internal interface IPooledSampleProvider
public interface IPooledSampleProvider
{
/// <summary>
/// Retrieves a <see cref="PoolableSkinnableSample"/> from a pool.