diff --git a/osu.Android.props b/osu.Android.props
index aaea784852..d5a77c6349 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs
index de441995b5..46b45979ea 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs
@@ -4,15 +4,15 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
-using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing;
using osuTK;
@@ -22,9 +22,9 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture]
public class TestSceneTapTimingControl : EditorClockTestScene
{
- [Cached(typeof(EditorBeatmap))]
- [Cached(typeof(IBeatSnapProvider))]
- private readonly EditorBeatmap editorBeatmap;
+ private EditorBeatmap editorBeatmap => editorBeatmapContainer?.EditorBeatmap;
+
+ private TestSceneHitObjectComposer.EditorBeatmapContainer editorBeatmapContainer;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
@@ -33,38 +33,48 @@ namespace osu.Game.Tests.Visual.Editing
private Bindable selectedGroup = new Bindable();
private TapTimingControl control;
+ private OsuSpriteText timingInfo;
- public TestSceneTapTimingControl()
+ [Resolved]
+ private AudioManager audio { get; set; }
+
+ [SetUpSteps]
+ public void SetUpSteps()
{
- var playableBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
+ AddStep("create beatmap", () =>
+ {
+ Beatmap.Value = new WaveformTestBeatmap(audio);
+ });
- // Ensure time doesn't end while testing
- playableBeatmap.BeatmapInfo.Length = 1200000;
+ AddStep("Create component", () =>
+ {
+ Child = editorBeatmapContainer = new TestSceneHitObjectComposer.EditorBeatmapContainer(Beatmap.Value)
+ {
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Y,
+ Width = 400,
+ Scale = new Vector2(1.5f),
+ Child = control = new TapTimingControl(),
+ },
+ timingInfo = new OsuSpriteText(),
+ }
+ };
- editorBeatmap = new EditorBeatmap(playableBeatmap);
-
- selectedGroup.Value = editorBeatmap.ControlPointInfo.Groups.First();
+ selectedGroup.Value = editorBeatmap.ControlPointInfo.Groups.First();
+ });
}
- protected override void LoadComplete()
+ protected override void Update()
{
- base.LoadComplete();
+ base.Update();
- Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
- Beatmap.Disabled = true;
-
- Children = new Drawable[]
- {
- new Container
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- AutoSizeAxes = Axes.Y,
- Width = 400,
- Scale = new Vector2(1.5f),
- Child = control = new TapTimingControl(),
- }
- };
+ if (selectedGroup.Value != null)
+ timingInfo.Text = $"offset: {selectedGroup.Value.Time:N2} bpm: {selectedGroup.Value.ControlPoints.OfType().First().BPM:N2}";
}
[Test]
@@ -104,7 +114,13 @@ namespace osu.Game.Tests.Visual.Editing
.TriggerClick();
});
- AddSliderStep("BPM", 30, 400, 60, bpm => editorBeatmap.ControlPointInfo.TimingPoints.First().BeatLength = 60000f / bpm);
+ AddSliderStep("BPM", 30, 400, 128, bpm =>
+ {
+ if (editorBeatmap == null)
+ return;
+
+ editorBeatmap.ControlPointInfo.TimingPoints.First().BeatLength = 60000f / bpm;
+ });
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
index 922439fcb8..3a7c8b2ec0 100644
--- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.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 osu.Framework.Bindables;
using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics;
diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs
index 1b0f0a3f5e..d0ab4d1f98 100644
--- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs
+++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs
@@ -24,8 +24,8 @@ namespace osu.Game.Screens.Edit.Timing
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuColour colours)
{
- Height = 200;
RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
CornerRadius = LabelledDrawable.CORNER_RADIUS;
Masking = true;
@@ -39,20 +39,44 @@ namespace osu.Game.Screens.Edit.Timing
},
new GridContainer
{
- RelativeSizeAxes = Axes.Both,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
RowDimensions = new[]
{
- new Dimension(),
+ new Dimension(GridSizeMode.Absolute, 200),
new Dimension(GridSizeMode.Absolute, 60),
},
Content = new[]
{
new Drawable[]
{
- new MetronomeDisplay
+ new Container
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ColumnDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension()
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new MetronomeDisplay
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ },
+ new WaveformComparisonDisplay(),
+ }
+ },
+ }
+ }
}
},
new Drawable[]
diff --git a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs
new file mode 100644
index 0000000000..c80d3c4261
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs
@@ -0,0 +1,218 @@
+// 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.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Audio;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Overlays;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ internal class WaveformComparisonDisplay : CompositeDrawable
+ {
+ private const int total_waveforms = 8;
+
+ private readonly BindableNumber beatLength = new BindableDouble();
+
+ [Resolved]
+ private IBindable beatmap { get; set; } = null!;
+
+ [Resolved]
+ private EditorBeatmap editorBeatmap { get; set; } = null!;
+
+ [Resolved]
+ private Bindable selectedGroup { get; set; } = null!;
+
+ [Resolved]
+ private EditorClock editorClock { get; set; } = null!;
+
+ private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT;
+
+ private int lastDisplayedBeatIndex;
+
+ private double selectedGroupStartTime;
+ private double selectedGroupEndTime;
+
+ private readonly IBindableList controlPointGroups = new BindableList();
+
+ public WaveformComparisonDisplay()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ CornerRadius = LabelledDrawable.CORNER_RADIUS;
+ Masking = true;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ for (int i = 0; i < total_waveforms; i++)
+ {
+ AddInternal(new WaveformRow
+ {
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.Both,
+ Height = 1f / total_waveforms,
+ Y = (float)i / total_waveforms,
+ });
+ }
+
+ AddInternal(new Circle
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = Color4.White,
+ RelativeSizeAxes = Axes.Y,
+ Width = 3,
+ });
+
+ selectedGroup.BindValueChanged(_ => updateTimingGroup(), true);
+
+ controlPointGroups.BindTo(editorBeatmap.ControlPointInfo.Groups);
+ controlPointGroups.BindCollectionChanged((_, __) => updateTimingGroup());
+
+ beatLength.BindValueChanged(_ => showFrom(lastDisplayedBeatIndex), true);
+ }
+
+ private void updateTimingGroup()
+ {
+ beatLength.UnbindBindings();
+
+ selectedGroupStartTime = 0;
+ selectedGroupEndTime = beatmap.Value.Track.Length;
+
+ var tcp = selectedGroup.Value?.ControlPoints.OfType().FirstOrDefault();
+
+ if (tcp == null)
+ {
+ timingPoint = new TimingControlPoint();
+ return;
+ }
+
+ timingPoint = tcp;
+ beatLength.BindTo(timingPoint.BeatLengthBindable);
+
+ selectedGroupStartTime = selectedGroup.Value?.Time ?? 0;
+
+ var nextGroup = editorBeatmap.ControlPointInfo.TimingPoints
+ .SkipWhile(g => g != tcp)
+ .Skip(1)
+ .FirstOrDefault();
+
+ if (nextGroup != null)
+ selectedGroupEndTime = nextGroup.Time;
+ }
+
+ protected override bool OnHover(HoverEvent e) => true;
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ float trackLength = (float)beatmap.Value.Track.Length;
+ int totalBeatsAvailable = (int)(trackLength / timingPoint.BeatLength);
+
+ Scheduler.AddOnce(showFrom, (int)(e.MousePosition.X / DrawWidth * totalBeatsAvailable));
+
+ return base.OnMouseMove(e);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!IsHovered)
+ {
+ int currentBeat = (int)Math.Floor((editorClock.CurrentTimeAccurate - selectedGroupStartTime) / timingPoint.BeatLength);
+
+ showFrom(currentBeat);
+ }
+ }
+
+ private void showFrom(int beatIndex)
+ {
+ if (lastDisplayedBeatIndex == beatIndex)
+ return;
+
+ // Chosen as a pretty usable number across all BPMs.
+ // Optimally we'd want this to scale with the BPM in question, but performing
+ // scaling of the display is both expensive in resampling, and decreases usability
+ // (as it is harder to track the waveform when making realtime adjustments).
+ const float visible_width = 300;
+
+ float trackLength = (float)beatmap.Value.Track.Length;
+ float scale = trackLength / visible_width;
+
+ // Start displaying from before the current beat
+ beatIndex -= total_waveforms / 2;
+
+ foreach (var row in InternalChildren.OfType())
+ {
+ // offset to the required beat index.
+ double time = selectedGroupStartTime + beatIndex * timingPoint.BeatLength;
+
+ float offset = (float)(time - visible_width / 2) / trackLength * scale;
+
+ row.Alpha = time < selectedGroupStartTime || time > selectedGroupEndTime ? 0.2f : 1;
+ row.WaveformOffset = -offset;
+ row.WaveformScale = new Vector2(scale, 1);
+ row.BeatIndex = beatIndex++;
+ }
+
+ lastDisplayedBeatIndex = beatIndex;
+ }
+
+ internal class WaveformRow : CompositeDrawable
+ {
+ private OsuSpriteText beatIndexText = null!;
+ private WaveformGraph waveformGraph = null!;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(IBindable beatmap)
+ {
+ InternalChildren = new Drawable[]
+ {
+ waveformGraph = new WaveformGraph
+ {
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.Both,
+ Waveform = beatmap.Value.Waveform,
+ Resolution = 1,
+
+ BaseColour = colourProvider.Colour0,
+ LowColour = colourProvider.Colour1,
+ MidColour = colourProvider.Colour2,
+ HighColour = colourProvider.Colour4,
+ },
+ beatIndexText = new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Padding = new MarginPadding(5),
+ Colour = colourProvider.Content2
+ }
+ };
+ }
+
+ public int BeatIndex { set => beatIndexText.Text = value.ToString(); }
+ public Vector2 WaveformScale { set => waveformGraph.Scale = value; }
+ public float WaveformOffset { set => waveformGraph.X = value; }
+ }
+ }
+}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 5bc65ca507..32a0adb859 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -35,7 +35,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 3597e7e5c0..112b5b4615 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,7 +61,7 @@
-
+
@@ -84,7 +84,7 @@
-
+