diff --git a/osu.Android.props b/osu.Android.props index 9d99218f88..9a3d42d6b7 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index 7734ebed12..a274f25200 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Collections.Generic; using osu.Framework.Utils; @@ -8,6 +10,7 @@ using osu.Game.Audio; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Utils; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects @@ -53,19 +56,22 @@ namespace osu.Game.Rulesets.Catch.Objects private class BananaHitSampleInfo : HitSampleInfo, IEquatable { - private static readonly string[] lookup_names = { "metronomelow", "catch-banana" }; + private static readonly string[] lookup_names = { "Gameplay/metronomelow", "Gameplay/catch-banana" }; public override IEnumerable LookupNames => lookup_names; - public BananaHitSampleInfo() - : base(string.Empty) + public BananaHitSampleInfo(int volume = 0) + : base(string.Empty, volume: volume) { } - public bool Equals(BananaHitSampleInfo other) + public sealed override HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default) + => new BananaHitSampleInfo(newVolume.GetOr(Volume)); + + public bool Equals(BananaHitSampleInfo? other) => other != null; - public override bool Equals(object obj) + public override bool Equals(object? obj) => obj is BananaHitSampleInfo other && Equals(other); public override int GetHashCode() => lookup_names.GetHashCode(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index d3787585e6..af5b609ec8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; +using osu.Game.Audio; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.UI; @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container tailContainer; private Container tickContainer; private Container repeatContainer; - private Container samplesContainer; + private PausableSkinnableSound slidingSample; public DrawableSlider() : this(null) @@ -69,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Alpha = 0 }, headContainer = new Container { RelativeSizeAxes = Axes.Both }, - samplesContainer = new Container { RelativeSizeAxes = Axes.Both } + slidingSample = new PausableSkinnableSound { Looping = true } }; PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); @@ -100,27 +101,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.OnFree(); PathVersion.UnbindFrom(HitObject.Path.Version); - } - private PausableSkinnableSound slidingSample; + slidingSample.Samples = null; + } protected override void LoadSamples() { base.LoadSamples(); - samplesContainer.Clear(); - slidingSample = null; - var firstSample = HitObject.Samples.FirstOrDefault(); if (firstSample != null) { var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderslide"); - samplesContainer.Add(slidingSample = new PausableSkinnableSound(clone) - { - Looping = true - }); + slidingSample.Samples = new ISampleInfo[] { clone }; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 5a11265a47..aea37acf6f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -9,6 +9,7 @@ using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container ticks; private SpinnerBonusDisplay bonusDisplay; - private Container samplesContainer; + private PausableSkinnableSound spinningSample; private Bindable isSpinning; private bool spinnerFrequencyModulate; @@ -81,7 +82,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre, Y = -120, }, - samplesContainer = new Container { RelativeSizeAxes = Axes.Both } + spinningSample = new PausableSkinnableSound + { + Volume = { Value = 0 }, + Looping = true, + Frequency = { Value = spinning_sample_initial_frequency } + } }; PositionBindable.BindValueChanged(pos => Position = pos.NewValue); @@ -95,29 +101,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables isSpinning.BindValueChanged(updateSpinningSample); } - private PausableSkinnableSound spinningSample; private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; + protected override void OnFree() + { + base.OnFree(); + + spinningSample.Samples = null; + } + protected override void LoadSamples() { base.LoadSamples(); - samplesContainer.Clear(); - spinningSample = null; - var firstSample = HitObject.Samples.FirstOrDefault(); if (firstSample != null) { var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("spinnerspin"); - samplesContainer.Add(spinningSample = new PausableSkinnableSound(clone) - { - Volume = { Value = 0 }, - Looping = true, - Frequency = { Value = spinning_sample_initial_frequency } - }); + spinningSample.Samples = new ISampleInfo[] { clone }; + spinningSample.Frequency.Value = spinning_sample_initial_frequency; } } diff --git a/osu.Game.Tests/Audio/SampleInfoEqualityTest.cs b/osu.Game.Tests/Audio/SampleInfoEqualityTest.cs new file mode 100644 index 0000000000..149096608f --- /dev/null +++ b/osu.Game.Tests/Audio/SampleInfoEqualityTest.cs @@ -0,0 +1,78 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Audio; + +namespace osu.Game.Tests.Audio +{ + [TestFixture] + public class SampleInfoEqualityTest + { + [Test] + public void TestSameSingleSamplesAreEqual() + { + var first = new SampleInfo("sample"); + var second = new SampleInfo("sample"); + + assertEquality(first, second); + } + + [Test] + public void TestDifferentSingleSamplesAreNotEqual() + { + var first = new SampleInfo("first"); + var second = new SampleInfo("second"); + + assertNonEquality(first, second); + } + + [Test] + public void TestDifferentCountSampleSetsAreNotEqual() + { + var first = new SampleInfo("sample", "extra"); + var second = new SampleInfo("sample"); + + assertNonEquality(first, second); + } + + [Test] + public void TestDifferentSampleSetsOfSameCountAreNotEqual() + { + var first = new SampleInfo("first", "common"); + var second = new SampleInfo("common", "second"); + + assertNonEquality(first, second); + } + + [Test] + public void TestSameOrderSameSampleSetsAreEqual() + { + var first = new SampleInfo("first", "second"); + var second = new SampleInfo("first", "second"); + + assertEquality(first, second); + } + + [Test] + public void TestDifferentOrderSameSampleSetsAreEqual() + { + var first = new SampleInfo("first", "second"); + var second = new SampleInfo("second", "first"); + + assertEquality(first, second); + } + + private void assertEquality(SampleInfo first, SampleInfo second) + { + Assert.That(first.Equals(second), Is.True); + Assert.That(first.GetHashCode(), Is.EqualTo(second.GetHashCode())); + } + + private void assertNonEquality(SampleInfo first, SampleInfo second) + { + Assert.That(first.Equals(second), Is.False); + Assert.That(first.GetHashCode(), Is.Not.EqualTo(second.GetHashCode())); + } + } +} diff --git a/osu.Game.Tournament/Models/TournamentMatch.cs b/osu.Game.Tournament/Models/TournamentMatch.cs index 8ebcbf4e15..bdfb1728f3 100644 --- a/osu.Game.Tournament/Models/TournamentMatch.cs +++ b/osu.Game.Tournament/Models/TournamentMatch.cs @@ -4,10 +4,10 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Drawing; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Game.Tournament.Screens.Ladder.Components; -using SixLabors.Primitives; namespace osu.Game.Tournament.Models { diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index efec4cffdd..ca46c3b050 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Drawing; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -16,7 +17,6 @@ using osu.Game.Tournament.Screens.Ladder; using osu.Game.Tournament.Screens.Ladder.Components; using osuTK; using osuTK.Graphics; -using SixLabors.Primitives; namespace osu.Game.Tournament.Screens.Editors { diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs index f2065e7e88..1c805bb42e 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Drawing; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -13,7 +14,6 @@ using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; using osuTK.Input; -using SixLabors.Primitives; namespace osu.Game.Tournament.Screens.Ladder.Components { diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs index 240d70c418..5d8240204e 100644 --- a/osu.Game/Audio/SampleInfo.cs +++ b/osu.Game/Audio/SampleInfo.cs @@ -1,24 +1,41 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections; using System.Collections.Generic; +using System.Linq; namespace osu.Game.Audio { /// /// Describes a gameplay sample. /// - public class SampleInfo : ISampleInfo + public class SampleInfo : ISampleInfo, IEquatable { private readonly string[] sampleNames; public SampleInfo(params string[] sampleNames) { this.sampleNames = sampleNames; + Array.Sort(sampleNames); } public IEnumerable LookupNames => sampleNames; public int Volume { get; } = 100; + + public override int GetHashCode() + { + return HashCode.Combine( + StructuralComparisons.StructuralEqualityComparer.GetHashCode(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); } } diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index d1f6fd445e..53ee711626 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -116,13 +116,13 @@ namespace osu.Game.Graphics switch (screenshotFormat.Value) { case ScreenshotFormat.Png: - image.SaveAsPng(stream); + await image.SaveAsPngAsync(stream); break; case ScreenshotFormat.Jpg: const int jpeg_quality = 92; - image.SaveAsJpeg(stream, new JpegEncoder { Quality = jpeg_quality }); + await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }); break; default: diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index ae65ac09b2..7343870dbc 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -75,6 +75,7 @@ namespace osu.Game.Online.API.Requests.Responses StarDifficulty = starDifficulty, OnlineBeatmapID = OnlineBeatmapID, Version = version, + // this is actually an incorrect mapping (Length is calculated as drain length in lazer's import process, see BeatmapManager.calculateLength). Length = TimeSpan.FromSeconds(length).TotalMilliseconds, Status = Status, BeatmapSet = set, diff --git a/osu.Game/Online/Multiplayer/PlaylistExtensions.cs b/osu.Game/Online/Multiplayer/PlaylistExtensions.cs new file mode 100644 index 0000000000..fe3d96e295 --- /dev/null +++ b/osu.Game/Online/Multiplayer/PlaylistExtensions.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using Humanizer; +using Humanizer.Localisation; +using osu.Framework.Bindables; + +namespace osu.Game.Online.Multiplayer +{ + public static class PlaylistExtensions + { + public static string GetTotalDuration(this BindableList playlist) => + playlist.Select(p => p.Beatmap.Value.Length).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); + } +} diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 94d63e4e68..d800758cc1 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -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; @@ -156,8 +155,6 @@ namespace osu.Game.Rulesets.Objects.Drawables [Resolved(CanBeNull = true)] private IPooledHitObjectProvider pooledObjectProvider { get; set; } - private Container samplesContainer; - /// /// Whether the initialization logic in has applied. /// @@ -181,7 +178,7 @@ namespace osu.Game.Rulesets.Objects.Drawables config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds); // Explicit non-virtual function call. - base.AddInternal(samplesContainer = new Container { RelativeSizeAxes = Axes.Both }); + base.AddInternal(Samples = new PausableSkinnableSound()); } protected override void LoadAsyncComplete() @@ -305,6 +302,9 @@ 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; + // Release the samples for other hitobjects to use. + Samples.Samples = null; + if (nestedHitObjects.IsValueCreated) { foreach (var obj in nestedHitObjects.Value) @@ -362,9 +362,6 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected virtual void LoadSamples() { - samplesContainer.Clear(); - Samples = null; - var samples = GetSamples().ToArray(); if (samples.Length <= 0) @@ -376,7 +373,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().ToArray(); } private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index cbf3362ea7..b4e0025351 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -8,20 +8,24 @@ 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; using System.Diagnostics; namespace osu.Game.Rulesets.UI { [Cached(typeof(IPooledHitObjectProvider))] - public abstract class Playfield : CompositeDrawable, IPooledHitObjectProvider + [Cached(typeof(IPooledSampleProvider))] + public abstract class Playfield : CompositeDrawable, IPooledHitObjectProvider, IPooledSampleProvider { /// /// Invoked when a is judged. @@ -81,6 +85,12 @@ namespace osu.Game.Rulesets.UI /// public readonly BindableBool DisplayJudgements = new BindableBool(true); + [Resolved(CanBeNull = true)] + private IReadOnlyList mods { get; set; } + + [Resolved] + private ISampleStore sampleStore { get; set; } + /// /// Creates a new . /// @@ -97,9 +107,6 @@ namespace osu.Game.Rulesets.UI })); } - [Resolved(CanBeNull = true)] - private IReadOnlyList mods { get; set; } - [BackgroundDependencyLoader] private void load() { @@ -364,6 +371,29 @@ namespace osu.Game.Rulesets.UI }); } + private readonly Dictionary> samplePools = new Dictionary>(); + + public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo) + { + if (!samplePools.TryGetValue(sampleInfo, out var existingPool)) + AddInternal(samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 1)); + + return existingPool.Get(); + } + + private class DrawableSamplePool : DrawablePool + { + 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 diff --git a/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs b/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs new file mode 100644 index 0000000000..5552c1cb72 --- /dev/null +++ b/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Screens.Multi.Components +{ + public class OverlinedPlaylistHeader : OverlinedHeader + { + public OverlinedPlaylistHeader() + : base("Playlist") + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Playlist.BindCollectionChanged((_, __) => Details.Value = Playlist.GetTotalDuration(), true); + } + } +} diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs index 77fbd606f4..dfee278e87 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components } } }, - new Drawable[] { new OverlinedHeader("Playlist"), }, + new Drawable[] { new OverlinedPlaylistHeader(), }, new Drawable[] { new DrawableRoomPlaylist(false, false) diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index caefc194b1..668a373d80 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Specialized; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -69,6 +70,7 @@ namespace osu.Game.Screens.Multi.Match.Components private OsuSpriteText typeLabel; private LoadingLayer loadingLayer; private DrawableRoomPlaylist playlist; + private OsuSpriteText playlistLength; [Resolved(CanBeNull = true)] private IRoomManager manager { get; set; } @@ -229,6 +231,15 @@ namespace osu.Game.Screens.Multi.Match.Components playlist = new DrawableRoomPlaylist(true, true) { RelativeSizeAxes = Axes.Both } }, new Drawable[] + { + playlistLength = new OsuSpriteText + { + Margin = new MarginPadding { Vertical = 5 }, + Colour = colours.Yellow, + Font = OsuFont.GetFont(size: 12), + } + }, + new Drawable[] { new PurpleTriangleButton { @@ -243,6 +254,7 @@ namespace osu.Game.Screens.Multi.Match.Components { new Dimension(), new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), } } }, @@ -315,6 +327,7 @@ namespace osu.Game.Screens.Multi.Match.Components Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue, true); playlist.Items.BindTo(Playlist); + Playlist.BindCollectionChanged(onPlaylistChanged, true); } protected override void Update() @@ -324,6 +337,9 @@ namespace osu.Game.Screens.Multi.Match.Components ApplyButton.Enabled.Value = hasValidSettings; } + private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => + playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}"; + private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0; private void apply() diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 2cbe215a39..2f8aad4e65 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -135,7 +135,7 @@ namespace osu.Game.Screens.Multi.Match RelativeSizeAxes = Axes.Both, Content = new[] { - new Drawable[] { new OverlinedHeader("Playlist"), }, + new Drawable[] { new OverlinedPlaylistHeader(), }, new Drawable[] { new DrawableRoomPlaylistWithResults diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 4ce87927a1..d76f0abb9e 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -914,6 +914,9 @@ namespace osu.Game.Screens.Select { // size is determined by the carousel itself, due to not all content necessarily being loaded. ScrollContent.AutoSizeAxes = Axes.None; + + // the scroll container may get pushed off-screen by global screen changes, but we still want panels to display outside of the bounds. + Masking = false; } // ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index e25c6932cf..b3c5d458d6 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -100,8 +100,14 @@ namespace osu.Game.Screens.Select.Carousel background = new DelayedLoadWrapper(() => new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) { RelativeSizeAxes = Axes.Both, - }, 300), - mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 100), + }, 300) + { + RelativeSizeAxes = Axes.Both + }, + mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 100) + { + RelativeSizeAxes = Axes.Both + }, }; background.DelayedLoadComplete += fadeContentIn; diff --git a/osu.Game/Skinning/IPooledSampleProvider.cs b/osu.Game/Skinning/IPooledSampleProvider.cs new file mode 100644 index 0000000000..40193d1a1a --- /dev/null +++ b/osu.Game/Skinning/IPooledSampleProvider.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Game.Audio; + +namespace osu.Game.Skinning +{ + /// + /// Provides pooled samples to be used by s. + /// + internal interface IPooledSampleProvider + { + /// + /// Retrieves a from a pool. + /// + /// The describing the sample to retrieve. + /// The . + [CanBeNull] + PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo); + } +} diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index 4f09aec0b6..be4664356d 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Threading; @@ -14,13 +15,17 @@ namespace osu.Game.Skinning { protected bool RequestedPlaying { get; private set; } - public PausableSkinnableSound(ISampleInfo hitSamples) - : base(hitSamples) + public PausableSkinnableSound() { } - public PausableSkinnableSound(IEnumerable hitSamples) - : base(hitSamples) + public PausableSkinnableSound([NotNull] IEnumerable samples) + : base(samples) + { + } + + public PausableSkinnableSound([NotNull] ISampleInfo sample) + : base(sample) { } diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs new file mode 100644 index 0000000000..19b96d6c60 --- /dev/null +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -0,0 +1,168 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; +using osu.Framework.Graphics.Containers; +using osu.Game.Audio; + +namespace osu.Game.Skinning +{ + /// + /// A sample corresponding to an that supports being pooled and responding to skin changes. + /// + public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent + { + /// + /// The currently-loaded . + /// + [CanBeNull] + public DrawableSample Sample { get; private set; } + + private readonly AudioContainer sampleContainer; + private ISampleInfo sampleInfo; + + [Resolved] + private ISampleStore sampleStore { get; set; } + + /// + /// Creates a new with no applied . + /// An can be applied later via . + /// + public PoolableSkinnableSample() + { + InternalChild = sampleContainer = new AudioContainer { RelativeSizeAxes = Axes.Both }; + } + + /// + /// Creates a new with an applied . + /// + /// The to attach. + public PoolableSkinnableSample(ISampleInfo sampleInfo) + : this() + { + Apply(sampleInfo); + } + + /// + /// Applies an that describes the sample to retrieve. + /// Only one can ever be applied to a . + /// + /// The to apply. + /// If an has already been applied to this . + 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; + + Volume.Value = sampleInfo.Volume / 100.0; + + if (LoadState >= LoadState.Ready) + updateSample(); + } + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + updateSample(); + } + + private void updateSample() + { + if (sampleInfo == null) + return; + + bool wasPlaying = Playing; + + sampleContainer.Clear(); + Sample = null; + + 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; + + sampleContainer.Add(Sample = new DrawableSample(ch) { Looping = Looping }); + + // Start playback internally for the new sample if the previous one was playing beforehand. + if (wasPlaying) + Play(); + } + + /// + /// Plays the sample. + /// + /// Whether to play the sample from the beginning. + public void Play(bool restart = true) => Sample?.Play(restart); + + /// + /// Stops the sample. + /// + public void Stop() => Sample?.Stop(); + + /// + /// Whether the sample is currently playing. + /// + public bool Playing => Sample?.Playing ?? false; + + private bool looping; + + /// + /// Whether the sample should loop on completion. + /// + public bool Looping + { + get => looping; + set + { + looping = value; + + if (Sample != null) + Sample.Looping = value; + } + } + + #region Re-expose AudioContainer + + public BindableNumber Volume => sampleContainer.Volume; + + public BindableNumber Balance => sampleContainer.Balance; + + public BindableNumber Frequency => sampleContainer.Frequency; + + public BindableNumber Tempo => sampleContainer.Tempo; + + public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable); + + public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable); + + public void RemoveAllAdjustments(AdjustableProperty type) => sampleContainer.RemoveAllAdjustments(type); + + public IBindable AggregateVolume => sampleContainer.AggregateVolume; + + public IBindable AggregateBalance => sampleContainer.AggregateBalance; + + public IBindable AggregateFrequency => sampleContainer.AggregateFrequency; + + public IBindable AggregateTempo => sampleContainer.AggregateTempo; + + #endregion + } +} diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs index cc9cbf7b59..50b4143375 100644 --- a/osu.Game/Skinning/SkinReloadableDrawable.cs +++ b/osu.Game/Skinning/SkinReloadableDrawable.cs @@ -27,7 +27,7 @@ namespace osu.Game.Skinning /// /// Whether fallback to default skin should be allowed if the custom skin is missing this resource. /// - private bool allowDefaultFallback => allowFallback == null || allowFallback.Invoke(CurrentSkin); + protected bool AllowDefaultFallback => allowFallback == null || allowFallback.Invoke(CurrentSkin); /// /// Create a new @@ -58,7 +58,7 @@ namespace osu.Game.Skinning private void skinChanged() { - SkinChanged(CurrentSkin, allowDefaultFallback); + SkinChanged(CurrentSkin, AllowDefaultFallback); OnSkinChanged?.Invoke(); } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index ffa0a963ce..23159e4fe1 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -1,26 +1,27 @@ // Copyright (c) ppy Pty Ltd . 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 { + /// + /// A sound consisting of one or more samples to be played. + /// public class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent { - private readonly ISampleInfo[] hitSamples; - - [Resolved] - private ISampleStore samples { get; set; } - public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; @@ -34,21 +35,74 @@ namespace osu.Game.Skinning /// protected bool PlayWhenZeroVolume => Looping; - protected readonly AudioContainer SamplesContainer; + /// + /// All raw s contained in this . + /// + [NotNull, ItemNotNull] + protected IEnumerable DrawableSamples => samplesContainer.Select(c => c.Sample).Where(s => s != null); - public SkinnableSound(ISampleInfo hitSamples) - : this(new[] { hitSamples }) + private readonly AudioContainer samplesContainer; + + [Resolved] + private ISampleStore sampleStore { get; set; } + + [Resolved(CanBeNull = true)] + private IPooledSampleProvider samplePool { get; set; } + + /// + /// Creates a new . + /// + public SkinnableSound() + { + InternalChild = samplesContainer = new AudioContainer(); + } + + /// + /// Creates a new with some initial samples. + /// + /// The initial samples. + public SkinnableSound([NotNull] IEnumerable samples) + : this() + { + this.samples = samples.ToArray(); + } + + /// + /// Creates a new with an initial sample. + /// + /// The initial sample. + public SkinnableSound([NotNull] ISampleInfo sample) + : this(new[] { sample }) { } - public SkinnableSound(IEnumerable hitSamples) + private ISampleInfo[] samples = Array.Empty(); + + /// + /// The samples that should be played. + /// + public ISampleInfo[] Samples { - this.hitSamples = hitSamples.ToArray(); - InternalChild = SamplesContainer = new AudioContainer(); + get => samples; + set + { + value ??= Array.Empty(); + + if (samples == value) + return; + + samples = value; + + if (LoadState >= LoadState.Ready) + updateSamples(); + } } private bool looping; + /// + /// Whether the samples should loop on completion. + /// public bool Looping { get => looping; @@ -58,77 +112,80 @@ namespace osu.Game.Skinning looping = value; - SamplesContainer.ForEach(c => c.Looping = looping); + samplesContainer.ForEach(c => c.Looping = looping); } } + /// + /// Plays the samples. + /// public virtual void Play() { - SamplesContainer.ForEach(c => + samplesContainer.ForEach(c => { if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) c.Play(); }); } + /// + /// Stops the samples. + /// public virtual void Stop() { - SamplesContainer.ForEach(c => c.Stop()); + samplesContainer.ForEach(c => c.Stop()); } protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + updateSamples(); + } + + private void updateSamples() { bool wasPlaying = IsPlaying; - var channels = hitSamples.Select(s => + // Remove all pooled samples (return them to the pool), and dispose the rest. + samplesContainer.RemoveAll(s => s.IsInPool); + samplesContainer.Clear(); + + foreach (var s in samples) { - var ch = skin.GetSample(s); + var sample = samplePool?.GetPooledSample(s) ?? new PoolableSkinnableSample(s); + sample.Looping = Looping; + sample.Volume.Value = s.Volume / 100.0; - if (ch == null && allowFallback) - { - foreach (var lookup in s.LookupNames) - { - if ((ch = samples.Get(lookup)) != null) - break; - } - } + samplesContainer.Add(sample); + } - 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(); } #region Re-expose AudioContainer - public BindableNumber Volume => SamplesContainer.Volume; + public BindableNumber Volume => samplesContainer.Volume; - public BindableNumber Balance => SamplesContainer.Balance; + public BindableNumber Balance => samplesContainer.Balance; - public BindableNumber Frequency => SamplesContainer.Frequency; + public BindableNumber Frequency => samplesContainer.Frequency; - public BindableNumber Tempo => SamplesContainer.Tempo; + public BindableNumber Tempo => samplesContainer.Tempo; public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => SamplesContainer.AddAdjustment(type, adjustBindable); + => samplesContainer.AddAdjustment(type, adjustBindable); public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => SamplesContainer.RemoveAdjustment(type, adjustBindable); + => samplesContainer.RemoveAdjustment(type, adjustBindable); public void RemoveAllAdjustments(AdjustableProperty type) - => SamplesContainer.RemoveAllAdjustments(type); + => samplesContainer.RemoveAllAdjustments(type); - public bool IsPlaying => SamplesContainer.Any(s => s.Playing); + /// + /// Whether any samples are currently playing. + /// + public bool IsPlaying => samplesContainer.Any(s => s.Playing); #endregion } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 08811b9b8c..218f051bf0 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -37,7 +37,7 @@ namespace osu.Game.Storyboards.Drawables foreach (var mod in mods.Value.OfType()) { - foreach (var sample in SamplesContainer) + foreach (var sample in DrawableSamples) mod.ApplyToSample(sample); } } diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index 87b77f4616..035cb64099 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -31,6 +31,7 @@ namespace osu.Game.Tests.Beatmaps BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata; BeatmapInfo.BeatmapSet.Files = new List(); BeatmapInfo.BeatmapSet.Beatmaps = new List { BeatmapInfo }; + BeatmapInfo.Length = 75000; BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo { Status = BeatmapSetOnlineStatus.Ranked, diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4b931726e0..9d37ceee6c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 3a47b77820..ab03393836 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - +