diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 1c9696901c..616ba132fd 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Beatmaps.IO [TestFixture] public class ImportBeatmapTest { - private const string osz_path = @"../../../../osu-resources/osu.Game.Resources/Beatmaps/241526 Soleily - Renatus.osz"; + public const string TEST_OSZ_PATH = @"../../../../osu-resources/osu.Game.Resources/Beatmaps/241526 Soleily - Renatus.osz"; [Test] public void TestImportWhenClosed() @@ -265,7 +265,7 @@ namespace osu.Game.Tests.Beatmaps.IO private string createTemporaryBeatmap() { var temp = Path.GetTempFileName() + ".osz"; - File.Copy(osz_path, temp, true); + File.Copy(TEST_OSZ_PATH, temp, true); Assert.IsTrue(File.Exists(temp)); return temp; } diff --git a/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs b/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs index 6c74876e81..5aa17bca7d 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs @@ -4,28 +4,42 @@ using System; using System.Collections.Generic; using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Configuration; using OpenTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Overlays; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Timing; +using osu.Game.Beatmaps; using osu.Game.Screens.Edit.Screens.Compose.Timeline; +using OpenTK.Graphics; namespace osu.Game.Tests.Visual { [TestFixture] - public class TestCaseEditorComposeTimeline : OsuTestCase + public class TestCaseEditorComposeTimeline : EditorClockTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(TimelineArea), typeof(Timeline), typeof(TimelineButton) }; - public TestCaseEditorComposeTimeline() + [BackgroundDependencyLoader] + private void load() { + Beatmap.Value = new WaveformTestBeatmap(); + Children = new Drawable[] { - new MusicController + new FillFlowContainer { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - State = Visibility.Visible + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new Drawable[] + { + new StartStopButton(), + new AudioVisualiser(), + } }, new TimelineArea { @@ -36,5 +50,85 @@ namespace osu.Game.Tests.Visual } }; } + + private class AudioVisualiser : CompositeDrawable + { + private readonly Drawable marker; + + private readonly IBindable beatmap = new Bindable(); + private IAdjustableClock adjustableClock; + + public AudioVisualiser() + { + Size = new Vector2(250, 25); + + InternalChildren = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.25f, + }, + marker = new Box + { + RelativePositionAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Width = 2, + } + }; + } + + [BackgroundDependencyLoader] + private void load(IAdjustableClock adjustableClock, IBindableBeatmap beatmap) + { + this.adjustableClock = adjustableClock; + this.beatmap.BindTo(beatmap); + } + + protected override void Update() + { + base.Update(); + + if (beatmap.Value.Track.IsLoaded) + marker.X = (float)(adjustableClock.CurrentTime / beatmap.Value.Track.Length); + } + } + + private class StartStopButton : Button + { + private IAdjustableClock adjustableClock; + private bool started; + + public StartStopButton() + { + BackgroundColour = Color4.SlateGray; + Size = new Vector2(100, 50); + Text = "Start"; + + Action = onClick; + } + + [BackgroundDependencyLoader] + private void load(IAdjustableClock adjustableClock) + { + this.adjustableClock = adjustableClock; + } + + private void onClick() + { + if (started) + { + adjustableClock.Stop(); + Text = "Start"; + } + else + { + adjustableClock.Start(); + Text = "Stop"; + } + + started = !started; + } + } } } diff --git a/osu.Game.Tests/Visual/TestCaseWaveform.cs b/osu.Game.Tests/Visual/TestCaseWaveform.cs index 983b98016e..46d46863ad 100644 --- a/osu.Game.Tests/Visual/TestCaseWaveform.cs +++ b/osu.Game.Tests/Visual/TestCaseWaveform.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Sprites; -using osu.Game.Overlays; namespace osu.Game.Tests.Visual { @@ -20,22 +19,14 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load() { + Beatmap.Value = new WaveformTestBeatmap(); + FillFlowContainer flow; Child = flow = new FillFlowContainer { RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 10), - Children = new Drawable[] - { - new MusicController - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Y = 100, - State = Visibility.Visible - }, - } }; for (int i = 1; i <= 16; i *= 2) @@ -44,10 +35,9 @@ namespace osu.Game.Tests.Visual { RelativeSizeAxes = Axes.Both, Resolution = 1f / i, + Waveform = Beatmap.Value.Waveform, }; - Beatmap.ValueChanged += b => newDisplay.Waveform = b.Waveform; - flow.Add(new Container { RelativeSizeAxes = Axes.X, diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs new file mode 100644 index 0000000000..17aa7db14d --- /dev/null +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.IO; +using System.Linq; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO.Archives; +using osu.Game.Tests.Beatmaps.IO; + +namespace osu.Game.Tests +{ + /// + /// A that is used for testcases that include waveforms. + /// + public class WaveformTestBeatmap : WorkingBeatmap + { + private readonly ZipArchiveReader reader; + private readonly FileStream stream; + + public WaveformTestBeatmap() + : base(new BeatmapInfo()) + { + stream = File.OpenRead(ImportBeatmapTest.TEST_OSZ_PATH); + reader = new ZipArchiveReader(stream); + } + + public override void Dispose() + { + base.Dispose(); + stream?.Dispose(); + reader?.Dispose(); + } + + protected override IBeatmap GetBeatmap() => createTestBeatmap(); + + protected override Texture GetBackground() => null; + + protected override Waveform GetWaveform() => new Waveform(getAudioStream()); + + protected override Track GetTrack() => new TrackBass(getAudioStream()); + + private Stream getAudioStream() => reader.GetStream(reader.Filenames.First(f => f.EndsWith(".mp3"))); + private Stream getBeatmapStream() => reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu"))); + + private Beatmap createTestBeatmap() + { + using (var beatmapStream = getBeatmapStream()) + using (var beatmapReader = new StreamReader(beatmapStream)) + return Decoder.GetDecoder(beatmapReader).Decode(beatmapReader); + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/Timeline.cs index 3649b24cd0..daf67ed7f0 100644 --- a/osu.Game/Screens/Edit/Screens/Compose/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Screens/Compose/Timeline/Timeline.cs @@ -2,9 +2,12 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; +using osu.Framework.Input; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -15,6 +18,8 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Timeline public readonly Bindable WaveformVisible = new Bindable(); public readonly IBindable Beatmap = new Bindable(); + private IAdjustableClock adjustableClock; + public Timeline() { ZoomDuration = 200; @@ -25,8 +30,10 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Timeline private WaveformGraph waveform; [BackgroundDependencyLoader] - private void load(IBindableBeatmap beatmap) + private void load(IBindableBeatmap beatmap, IAdjustableClock adjustableClock) { + this.adjustableClock = adjustableClock; + Child = waveform = new WaveformGraph { RelativeSizeAxes = Axes.Both, @@ -46,12 +53,132 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Timeline waveform.Waveform = Beatmap.Value.Waveform; } + /// + /// The track's time in the previous frame. + /// + private double lastTrackTime; + + /// + /// Whether the user is currently dragging the timeline. + /// + private bool handlingDragInput; + + /// + /// Whether the track was playing before a user drag event. + /// + private bool trackWasPlaying; + protected override void Update() { base.Update(); - // We want time = 0 to be at the centre of the container when scrolled to the start + // The extrema of track time should be positioned at the centre of the container when scrolled to the start or end Content.Margin = new MarginPadding { Horizontal = DrawWidth / 2 }; + + if (handlingDragInput) + { + // The user is dragging - the track should always follow the timeline + seekTrackToCurrent(); + } + else if (adjustableClock.IsRunning) + { + // If the user hasn't provided mouse input but the track is running, always follow the track + scrollToTrackTime(); + } + else + { + // The track isn't playing, so we want to smooth-scroll once more, and re-enable wheel scrolling + // There are two cases we have to be wary of: + // 1) The user scrolls on this timeline: We want the track to follow us + // 2) The user changes the track time through some other means (scrolling in the editor or overview timeline): We want to follow the track time + + // The simplest way to cover both cases is by checking that inter-frame track times are identical + if (adjustableClock.CurrentTime == lastTrackTime) + { + // The track hasn't been seeked externally + seekTrackToCurrent(); + } + else + { + // The track has been seeked externally + scrollToTrackTime(); + } + } + + lastTrackTime = adjustableClock.CurrentTime; + + void seekTrackToCurrent() + { + if (!(Beatmap.Value.Track is TrackVirtual)) + adjustableClock.Seek(Current / Content.DrawWidth * Beatmap.Value.Track.Length); + } + + void scrollToTrackTime() + { + if (!(Beatmap.Value.Track is TrackVirtual)) + ScrollTo((float)(adjustableClock.CurrentTime / Beatmap.Value.Track.Length) * Content.DrawWidth, false); + } + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + if (base.OnMouseDown(state, args)) + { + beginUserDrag(); + return true; + } + + return false; + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + endUserDrag(); + return base.OnMouseUp(state, args); + } + + private void beginUserDrag() + { + handlingDragInput = true; + trackWasPlaying = adjustableClock.IsRunning; + adjustableClock.Stop(); + } + + private void endUserDrag() + { + handlingDragInput = false; + if (trackWasPlaying) + adjustableClock.Start(); + } + + protected override ScrollbarContainer CreateScrollbar(Direction direction) => new TimelineScrollbar(this, direction); + + private class TimelineScrollbar : ScrollbarContainer + { + private readonly Timeline timeline; + + public TimelineScrollbar(Timeline timeline, Direction scrollDir) + : base(scrollDir) + { + this.timeline = timeline; + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + if (base.OnMouseDown(state, args)) + { + timeline.beginUserDrag(); + return true; + } + + return false; + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + timeline.endUserDrag(); + return base.OnMouseUp(state, args); + } } } } diff --git a/osu.Game/Tests/Visual/EditorClockTestCase.cs b/osu.Game/Tests/Visual/EditorClockTestCase.cs index 08dc6a3bbd..521b51529e 100644 --- a/osu.Game/Tests/Visual/EditorClockTestCase.cs +++ b/osu.Game/Tests/Visual/EditorClockTestCase.cs @@ -25,13 +25,20 @@ namespace osu.Game.Tests.Visual Clock = new EditorClock(new ControlPointInfo(), 5000, BeatDivisor) { IsCoupled = false }; } + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + + dependencies.Cache(BeatDivisor); + dependencies.CacheAs(Clock); + dependencies.CacheAs(Clock); + + return dependencies; + } + [BackgroundDependencyLoader] private void load() { - Dependencies.Cache(BeatDivisor); - Dependencies.CacheAs(Clock); - Dependencies.CacheAs(Clock); - Beatmap.BindValueChanged(beatmapChanged, true); }