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

Merge branch 'master' into song-select-key-repeat

This commit is contained in:
Dan Balasescu 2020-06-26 20:33:13 +09:00 committed by GitHub
commit 77258659e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 332 additions and 16 deletions

View File

@ -0,0 +1,10 @@
osu file format v14
[General]
Mode: 3
[TimingPoints]
0,300,4,0,2,100,1,0
[HitObjects]
444,320,1000,5,2,0:0:0:0:

View File

@ -0,0 +1,10 @@
osu file format v14
[General]
Mode: 3
[TimingPoints]
0,300,4,0,2,100,1,0
[HitObjects]
444,320,1000,5,1,0:0:0:0:

View File

@ -0,0 +1,49 @@
// 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.Reflection;
using NUnit.Framework;
using osu.Framework.IO.Stores;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
{
public class TestSceneManiaHitObjectSamples : HitObjectSampleTest
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
protected override IResourceStore<byte[]> Resources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneManiaHitObjectSamples)));
/// <summary>
/// Tests that when a normal sample bank is used, the normal hitsound will be looked up.
/// </summary>
[Test]
public void TestManiaHitObjectNormalSampleBank()
{
const string expected_sample = "normal-hitnormal2";
SetupSkins(expected_sample, expected_sample);
CreateTestWithBeatmap("mania-hitobject-beatmap-normal-sample-bank.osu");
AssertBeatmapLookup(expected_sample);
}
/// <summary>
/// Tests that when a custom sample bank is used, layered hitsounds are not played
/// (only the sample from the custom bank is looked up).
/// </summary>
[Test]
public void TestManiaHitObjectCustomSampleBank()
{
const string expected_sample = "normal-hitwhistle2";
const string unwanted_sample = "normal-hitnormal2";
SetupSkins(expected_sample, unwanted_sample);
CreateTestWithBeatmap("mania-hitobject-beatmap-custom-sample-bank.osu");
AssertBeatmapLookup(expected_sample);
AssertNoLookup(unwanted_sample);
}
}
}

View File

@ -9,6 +9,9 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Skinning;
using System.Collections.Generic;
using osu.Framework.Audio.Sample;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Legacy;
namespace osu.Game.Rulesets.Mania.Skinning
{
@ -129,6 +132,15 @@ namespace osu.Game.Rulesets.Mania.Skinning
return this.GetAnimation(filename, true, true);
}
public override SampleChannel GetSample(ISampleInfo sampleInfo)
{
// layered hit sounds never play in mania
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
return new SampleChannelVirtual();
return Source.GetSample(sampleInfo);
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{
if (lookup is ManiaSkinConfigurationLookup maniaLookup)

View File

@ -6,6 +6,7 @@ using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
@ -167,5 +168,64 @@ namespace osu.Game.Tests.Gameplay
AssertBeatmapLookup(expected_sample);
}
/// <summary>
/// Tests that when a custom sample bank is used, both the normal and additional sounds will be looked up.
/// </summary>
[Test]
public void TestHitObjectCustomSampleBank()
{
string[] expectedSamples =
{
"normal-hitnormal2",
"normal-hitwhistle2"
};
SetupSkins(expectedSamples[0], expectedSamples[1]);
CreateTestWithBeatmap("hitobject-beatmap-custom-sample-bank.osu");
AssertBeatmapLookup(expectedSamples[0]);
AssertUserLookup(expectedSamples[1]);
}
/// <summary>
/// Tests that when a custom sample bank is used, but <see cref="GlobalSkinConfiguration.LayeredHitSounds"/> is disabled,
/// only the additional sound will be looked up.
/// </summary>
[Test]
public void TestHitObjectCustomSampleBankWithoutLayered()
{
const string expected_sample = "normal-hitwhistle2";
const string unwanted_sample = "normal-hitnormal2";
SetupSkins(expected_sample, unwanted_sample);
disableLayeredHitSounds();
CreateTestWithBeatmap("hitobject-beatmap-custom-sample-bank.osu");
AssertBeatmapLookup(expected_sample);
AssertNoLookup(unwanted_sample);
}
/// <summary>
/// Tests that when a normal sample bank is used and <see cref="GlobalSkinConfiguration.LayeredHitSounds"/> is disabled,
/// the normal sound will be looked up anyway.
/// </summary>
[Test]
public void TestHitObjectNormalSampleBankWithoutLayered()
{
const string expected_sample = "normal-hitnormal";
SetupSkins(expected_sample, expected_sample);
disableLayeredHitSounds();
CreateTestWithBeatmap("hitobject-beatmap-sample.osu");
AssertBeatmapLookup(expected_sample);
}
private void disableLayeredHitSounds()
=> AddStep("set LayeredHitSounds to false", () => Skin.Configuration.ConfigDictionary[GlobalSkinConfiguration.LayeredHitSounds.ToString()] = "0");
}
}

View File

@ -11,8 +11,10 @@ using osu.Framework.Audio.Sample;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osu.Game.Storyboards;
@ -70,6 +72,50 @@ namespace osu.Game.Tests.Gameplay
AddUntilStep("sample playback succeeded", () => sample.LifetimeEnd < double.MaxValue);
}
[TestCase(typeof(OsuModDoubleTime), 1.5)]
[TestCase(typeof(OsuModHalfTime), 0.75)]
[TestCase(typeof(ModWindUp), 1.5)]
[TestCase(typeof(ModWindDown), 0.75)]
[TestCase(typeof(OsuModDoubleTime), 2)]
[TestCase(typeof(OsuModHalfTime), 0.5)]
[TestCase(typeof(ModWindUp), 2)]
[TestCase(typeof(ModWindDown), 0.5)]
public void TestSamplePlaybackWithRateMods(Type expectedMod, double expectedRate)
{
GameplayClockContainer gameplayContainer = null;
TestDrawableStoryboardSample sample = null;
Mod testedMod = Activator.CreateInstance(expectedMod) as Mod;
switch (testedMod)
{
case ModRateAdjust m:
m.SpeedChange.Value = expectedRate;
break;
case ModTimeRamp m:
m.InitialRate.Value = m.FinalRate.Value = expectedRate;
break;
}
AddStep("setup storyboard sample", () =>
{
Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio);
SelectedMods.Value = new[] { testedMod };
Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, SelectedMods.Value, 0));
gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
{
Clock = gameplayContainer.GameplayClock
});
});
AddStep("start", () => gameplayContainer.Start());
AddAssert("sample playback rate matches mod rates", () => sample.Channel.AggregateFrequency.Value == expectedRate);
}
private class TestSkin : LegacySkin
{
public TestSkin(string resourceName, AudioManager audioManager)
@ -99,5 +145,28 @@ namespace osu.Game.Tests.Gameplay
{
}
}
private class TestCustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap
{
private readonly AudioManager audio;
public TestCustomSkinWorkingBeatmap(RulesetInfo ruleset, AudioManager audio)
: base(ruleset, null, audio)
{
this.audio = audio;
}
protected override ISkin GetSkin() => new TestSkin("test-sample", audio);
}
private class TestDrawableStoryboardSample : DrawableStoryboardSample
{
public TestDrawableStoryboardSample(StoryboardSampleInfo sampleInfo)
: base(sampleInfo)
{
}
public new SampleChannel Channel => base.Channel;
}
}
}

View File

@ -0,0 +1,7 @@
osu file format v14
[TimingPoints]
0,300,4,0,2,100,1,0
[HitObjects]
444,320,1000,5,2,0:0:0:0:

View File

@ -0,0 +1,5 @@
[General]
Version: latest
[Colours]
Combo1: 255,255,255,0

View File

@ -108,5 +108,15 @@ namespace osu.Game.Tests.Skins
using (var stream = new LineBufferedReader(resStream))
Assert.That(decoder.Decode(stream).LegacyVersion, Is.EqualTo(1.0m));
}
[Test]
public void TestDecodeColourWithZeroAlpha()
{
var decoder = new LegacySkinDecoder();
using (var resStream = TestResources.OpenResource("skin-zero-alpha-colour.ini"))
using (var stream = new LineBufferedReader(resStream))
Assert.That(decoder.Decode(stream).ComboColours[0].A, Is.EqualTo(1.0f));
}
}
}

View File

@ -103,7 +103,12 @@ namespace osu.Game.Beatmaps.Formats
try
{
colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), split.Length == 4 ? byte.Parse(split[3]) : (byte)255);
byte alpha = split.Length == 4 ? byte.Parse(split[3]) : (byte)255;
if (alpha == 0)
alpha = 255;
colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), alpha);
}
catch
{

View File

@ -0,0 +1,9 @@
// 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.
namespace osu.Game.Rulesets.Mods
{
public interface IApplicableToAudio : IApplicableToTrack, IApplicableToSample
{
}
}

View File

@ -0,0 +1,15 @@
// 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.Audio.Sample;
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// An interface for mods that make adjustments to a sample.
/// </summary>
public interface IApplicableToSample : IApplicableMod
{
void ApplyToSample(SampleChannel sample);
}
}

View File

@ -2,12 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModRateAdjust : Mod, IApplicableToTrack
public abstract class ModRateAdjust : Mod, IApplicableToAudio
{
public abstract BindableNumber<double> SpeedChange { get; }
@ -16,6 +17,11 @@ namespace osu.Game.Rulesets.Mods
track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange);
}
public virtual void ApplyToSample(SampleChannel sample)
{
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
}
public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x";
}
}

View File

@ -10,10 +10,11 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Objects;
using osu.Framework.Audio.Sample;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToTrack
public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToAudio
{
/// <summary>
/// The point in the beatmap at which the final ramping rate should be reached.
@ -58,6 +59,11 @@ namespace osu.Game.Rulesets.Mods
AdjustPitch.TriggerChange();
}
public void ApplyToSample(SampleChannel sample)
{
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
}
public virtual void ApplyToBeatmap(IBeatmap beatmap)
{
HitObject lastObject = beatmap.HitObjects.LastOrDefault();

View File

@ -12,6 +12,7 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Utils;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Objects.Legacy
{
@ -356,7 +357,10 @@ namespace osu.Game.Rulesets.Objects.Legacy
Bank = bankInfo.Normal,
Name = HitSampleInfo.HIT_NORMAL,
Volume = bankInfo.Volume,
CustomSampleBank = bankInfo.CustomSampleBank
CustomSampleBank = bankInfo.CustomSampleBank,
// if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
// None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
IsLayered = type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal)
}
};
@ -409,7 +413,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
}
internal class LegacyHitSampleInfo : HitSampleInfo
public class LegacyHitSampleInfo : HitSampleInfo
{
private int customSampleBank;
@ -424,6 +428,15 @@ namespace osu.Game.Rulesets.Objects.Legacy
Suffix = value.ToString();
}
}
/// <summary>
/// Whether this hit sample is layered.
/// </summary>
/// <remarks>
/// Layered hit samples are automatically added in all modes (except osu!mania), but can be disabled
/// using the <see cref="GlobalSkinConfiguration.LayeredHitSounds"/> skin config option.
/// </remarks>
public bool IsLayered { get; set; }
}
private class FileHitSampleInfo : LegacyHitSampleInfo

View File

@ -5,6 +5,7 @@ namespace osu.Game.Skinning
{
public enum GlobalSkinConfiguration
{
AnimationFramerate
AnimationFramerate,
LayeredHitSounds,
}
}

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Legacy;
namespace osu.Game.Skinning
{
@ -28,7 +29,17 @@ namespace osu.Game.Skinning
public Texture GetTexture(string componentName) => Source.GetTexture(componentName);
public virtual SampleChannel GetSample(ISampleInfo sampleInfo) => Source.GetSample(sampleInfo);
public virtual SampleChannel GetSample(ISampleInfo sampleInfo)
{
if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample))
return Source.GetSample(sampleInfo);
var playLayeredHitSounds = GetConfig<GlobalSkinConfiguration, bool>(GlobalSkinConfiguration.LayeredHitSounds);
if (legacySample.IsLayered && playLayeredHitSounds?.Value == false)
return new SampleChannelVirtual();
return Source.GetSample(sampleInfo);
}
public abstract IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup);
}

View File

@ -1,11 +1,14 @@
// 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.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Storyboards.Drawables
{
@ -17,7 +20,8 @@ namespace osu.Game.Storyboards.Drawables
private const double allowable_late_start = 100;
private readonly StoryboardSampleInfo sampleInfo;
private SampleChannel channel;
protected SampleChannel Channel { get; private set; }
public override bool RemoveWhenNotAlive => false;
@ -28,12 +32,16 @@ namespace osu.Game.Storyboards.Drawables
}
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap)
private void load(IBindable<WorkingBeatmap> beatmap, IBindable<IReadOnlyList<Mod>> mods)
{
channel = beatmap.Value.Skin.GetSample(sampleInfo);
Channel = beatmap.Value.Skin.GetSample(sampleInfo);
if (Channel == null)
return;
if (channel != null)
channel.Volume.Value = sampleInfo.Volume / 100.0;
Channel.Volume.Value = sampleInfo.Volume / 100.0;
foreach (var mod in mods.Value.OfType<IApplicableToSample>())
mod.ApplyToSample(Channel);
}
protected override void Update()
@ -44,7 +52,7 @@ namespace osu.Game.Storyboards.Drawables
if (Time.Current < sampleInfo.StartTime)
{
// We've rewound before the start time of the sample
channel?.Stop();
Channel?.Stop();
// In the case that the user fast-forwards to a point far beyond the start time of the sample,
// we want to be able to fall into the if-conditional below (therefore we must not have a life time end)
@ -56,7 +64,7 @@ namespace osu.Game.Storyboards.Drawables
// We've passed the start time of the sample. We only play the sample if we're within an allowable range
// from the sample's start, to reduce layering if we've been fast-forwarded far into the future
if (Time.Current - sampleInfo.StartTime < allowable_late_start)
channel?.Play();
Channel?.Play();
// In the case that the user rewinds to a point far behind the start time of the sample,
// we want to be able to fall into the if-conditional above (therefore we must not have a life time start)
@ -67,7 +75,7 @@ namespace osu.Game.Storyboards.Drawables
protected override void Dispose(bool isDisposing)
{
channel?.Stop();
Channel?.Stop();
base.Dispose(isDisposing);
}
}

View File

@ -24,6 +24,10 @@ namespace osu.Game.Tests.Beatmaps
public abstract class HitObjectSampleTest : PlayerTestScene
{
protected abstract IResourceStore<byte[]> Resources { get; }
protected LegacySkin Skin { get; private set; }
[Resolved]
private RulesetStore rulesetStore { get; set; }
private readonly SkinInfo userSkinInfo = new SkinInfo();
@ -64,6 +68,9 @@ namespace osu.Game.Tests.Beatmaps
{
using (var reader = new LineBufferedReader(Resources.GetStream($"Resources/SampleLookups/{filename}")))
currentTestBeatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
// populate ruleset for beatmap converters that require it to be present.
currentTestBeatmap.BeatmapInfo.Ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.RulesetID);
});
});
}
@ -91,7 +98,7 @@ namespace osu.Game.Tests.Beatmaps
};
// Need to refresh the cached skin source to refresh the skin resource store.
dependencies.SkinSource = new SkinProvidingContainer(new LegacySkin(userSkinInfo, userSkinResourceStore, Audio));
dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, userSkinResourceStore, Audio));
});
}
@ -101,6 +108,9 @@ namespace osu.Game.Tests.Beatmaps
protected void AssertUserLookup(string name) => AddAssert($"\"{name}\" looked up from user skin",
() => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && userSkinResourceStore.PerformedLookups.Contains(name));
protected void AssertNoLookup(string name) => AddAssert($"\"{name}\" not looked up",
() => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && !userSkinResourceStore.PerformedLookups.Contains(name));
private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer
{
public ISkinSource SkinSource;