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 @@
-
+