1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-15 11:47:18 +08:00

Merge pull request #10234 from peppy/editor-load-audio

Add audio track selection to editor setup screen
This commit is contained in:
Dan Balasescu 2020-09-25 19:58:23 +09:00 committed by GitHub
commit 93a137ed84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 290 additions and 21 deletions

View File

@ -0,0 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Tests.Resources;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorBeatmapCreation : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override bool EditorComponentsReady => Editor.ChildrenOfType<SetupScreen>().SingleOrDefault()?.IsLoaded == true;
public override void SetUpSteps()
{
AddStep("set dummy", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null));
base.SetUpSteps();
// if we save a beatmap with a hash collision, things fall over.
// probably needs a more solid resolution in the future but this will do for now.
AddStep("make new beatmap unique", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString());
}
[Test]
public void TestCreateNewBeatmap()
{
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0);
}
[Test]
public void TestAddAudioTrack()
{
AddAssert("switch track to real track", () =>
{
var setup = Editor.ChildrenOfType<SetupScreen>().First();
var temp = TestResources.GetTestBeatmapForImport();
string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder);
bool success = setup.ChangeAudioTrack(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3"));
File.Delete(temp);
Directory.Delete(extractedFolder, true);
return success;
});
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
}
}
}

View File

@ -260,7 +260,7 @@ namespace osu.Game.Beatmaps
fileInfo.Filename = beatmapInfo.Path;
stream.Seek(0, SeekOrigin.Begin);
UpdateFile(setInfo, fileInfo, stream);
ReplaceFile(setInfo, fileInfo, stream);
}
}

View File

@ -401,12 +401,27 @@ namespace osu.Game.Database
}
/// <summary>
/// Update an existing file, or create a new entry if not already part of the <paramref name="model"/>'s files.
/// Replace an existing file with a new version.
/// </summary>
/// <param name="model">The item to operate on.</param>
/// <param name="file">The file model to be updated or added.</param>
/// <param name="file">The existing file to be replaced.</param>
/// <param name="contents">The new file contents.</param>
public void UpdateFile(TModel model, TFileModel file, Stream contents)
/// <param name="filename">An optional filename for the new file. Will use the previous filename if not specified.</param>
public void ReplaceFile(TModel model, TFileModel file, Stream contents, string filename = null)
{
using (ContextFactory.GetForWrite())
{
DeleteFile(model, file);
AddFile(model, contents, filename ?? file.Filename);
}
}
/// <summary>
/// Delete new file.
/// </summary>
/// <param name="model">The item to operate on.</param>
/// <param name="file">The existing file to be deleted.</param>
public void DeleteFile(TModel model, TFileModel file)
{
using (var usage = ContextFactory.GetForWrite())
{
@ -415,15 +430,28 @@ namespace osu.Game.Database
{
Files.Dereference(file.FileInfo);
// Remove the file model.
// This shouldn't be required, but here for safety in case the provided TModel is not being change tracked
// Definitely can be removed once we rework the database backend.
usage.Context.Set<TFileModel>().Remove(file);
}
// Add the new file info and containing file model.
model.Files.Remove(file);
}
}
/// <summary>
/// Add a new file.
/// </summary>
/// <param name="model">The item to operate on.</param>
/// <param name="contents">The new file contents.</param>
/// <param name="filename">The filename for the new file.</param>
public void AddFile(TModel model, Stream contents, string filename)
{
using (ContextFactory.GetForWrite())
{
model.Files.Add(new TFileModel
{
Filename = file.Filename,
Filename = filename,
FileInfo = Files.Add(contents)
});

View File

@ -44,13 +44,18 @@ namespace osu.Game.Graphics.UserInterfaceV2
Component.BorderColour = colours.Blue;
}
protected override OsuTextBox CreateComponent() => new OsuTextBox
protected virtual OsuTextBox CreateTextBox() => new OsuTextBox
{
CommitOnFocusLost = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
CornerRadius = CORNER_RADIUS,
}.With(t => t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText));
};
protected override OsuTextBox CreateComponent() => CreateTextBox().With(t =>
{
t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText);
});
}
}

View File

@ -81,6 +81,11 @@ namespace osu.Game.Overlays
mods.BindValueChanged(_ => ResetTrackAdjustments(), true);
}
/// <summary>
/// Forcefully reload the current <see cref="WorkingBeatmap"/>'s track from disk.
/// </summary>
public void ReloadCurrentTrack() => changeTrack();
/// <summary>
/// Change the position of a <see cref="BeatmapSetInfo"/> in the current playlist.
/// </summary>

View File

@ -18,7 +18,8 @@ namespace osu.Game.Screens.Edit.Components
private const float contents_padding = 15;
protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
protected Track Track => Beatmap.Value.Track;
protected readonly IBindable<Track> Track = new Bindable<Track>();
private readonly Drawable background;
private readonly Container content;
@ -42,9 +43,11 @@ namespace osu.Game.Screens.Edit.Components
}
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours)
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, EditorClock clock)
{
Beatmap.BindTo(beatmap);
Track.BindTo(clock.Track);
background.Colour = colours.Gray1;
}
}

View File

@ -62,12 +62,12 @@ namespace osu.Game.Screens.Edit.Components
}
};
Track?.AddAdjustment(AdjustableProperty.Tempo, tempo);
Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempo), true);
}
protected override void Dispose(bool isDisposing)
{
Track?.RemoveAdjustment(AdjustableProperty.Tempo, tempo);
Track.Value?.RemoveAdjustment(AdjustableProperty.Tempo, tempo);
base.Dispose(isDisposing);
}

View File

@ -3,6 +3,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osuTK;
using osu.Framework.Graphics;
@ -22,6 +23,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
protected readonly IBindable<Track> Track = new Bindable<Track>();
private readonly Container<T> content;
protected override Container<T> Content => content;
@ -35,12 +38,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
updateRelativeChildSize();
LoadBeatmap(b.NewValue);
};
Track.ValueChanged += _ => updateRelativeChildSize();
}
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap)
private void load(IBindable<WorkingBeatmap> beatmap, EditorClock clock)
{
Beatmap.BindTo(beatmap);
Track.BindTo(clock.Track);
}
private void updateRelativeChildSize()

View File

@ -43,6 +43,7 @@ using osuTK.Input;
namespace osu.Game.Screens.Edit
{
[Cached(typeof(IBeatSnapProvider))]
[Cached]
public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>, IKeyBindingHandler<PlatformAction>, IBeatSnapProvider
{
public override float BackgroundParallaxAmount => 0.1f;
@ -91,6 +92,9 @@ namespace osu.Game.Screens.Edit
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private MusicController music { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours, GameHost host)
{
@ -98,9 +102,9 @@ namespace osu.Game.Screens.Edit
beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue);
// Todo: should probably be done at a DrawableRuleset level to share logic with Player.
var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock();
clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false };
clock.ChangeSource(sourceClock);
UpdateClockSource();
dependencies.CacheAs(clock);
AddInternal(clock);
@ -271,6 +275,15 @@ namespace osu.Game.Screens.Edit
bottomBackground.Colour = colours.Gray2;
}
/// <summary>
/// If the beatmap's track has changed, this method must be called to keep the editor in a valid state.
/// </summary>
public void UpdateClockSource()
{
var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock();
clock.ChangeSource(sourceClock);
}
protected void Save()
{
// apply any set-level metadata changes.

View File

@ -3,6 +3,8 @@
using System;
using System.Linq;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Utils;
@ -17,7 +19,11 @@ namespace osu.Game.Screens.Edit
/// </summary>
public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
{
public readonly double TrackLength;
public IBindable<Track> Track => track;
private readonly Bindable<Track> track = new Bindable<Track>();
public double TrackLength => track.Value?.Length ?? 60000;
public ControlPointInfo ControlPointInfo;
@ -35,7 +41,6 @@ namespace osu.Game.Screens.Edit
this.beatDivisor = beatDivisor;
ControlPointInfo = controlPointInfo;
TrackLength = trackLength;
underlyingClock = new DecoupleableInterpolatingFramedClock();
}
@ -190,7 +195,11 @@ namespace osu.Game.Screens.Edit
public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo;
public void ChangeSource(IClock source) => underlyingClock.ChangeSource(source);
public void ChangeSource(IClock source)
{
track.Value = source as Track;
underlyingClock.ChangeSource(source);
}
public IClock Source => underlyingClock.Source;

View File

@ -1,17 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.IO;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.Edit.Setup
@ -23,6 +30,16 @@ namespace osu.Game.Screens.Edit.Setup
private LabelledTextBox titleTextBox;
private LabelledTextBox creatorTextBox;
private LabelledTextBox difficultyTextBox;
private LabelledTextBox audioTrackTextBox;
[Resolved]
private MusicController music { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
[Resolved(canBeNull: true)]
private Editor editor { get; set; }
public SetupScreen()
: base(EditorScreenMode.SongSetup)
@ -32,6 +49,12 @@ namespace osu.Game.Screens.Edit.Setup
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Container audioTrackFileChooserContainer = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
};
Child = new Container
{
RelativeSizeAxes = Axes.Both,
@ -75,6 +98,18 @@ namespace osu.Game.Screens.Edit.Setup
},
},
new OsuSpriteText
{
Text = "Resources"
},
audioTrackTextBox = new FileChooserLabelledTextBox
{
Label = "Audio Track",
Current = { Value = Beatmap.Value.Metadata.AudioFile ?? "Click to select a track" },
Target = audioTrackFileChooserContainer,
TabbableContentContainer = this
},
audioTrackFileChooserContainer,
new OsuSpriteText
{
Text = "Beatmap metadata"
},
@ -109,10 +144,47 @@ namespace osu.Game.Screens.Edit.Setup
}
};
audioTrackTextBox.Current.BindValueChanged(audioTrackChanged);
foreach (var item in flow.OfType<LabelledTextBox>())
item.OnCommit += onCommit;
}
public bool ChangeAudioTrack(string path)
{
var info = new FileInfo(path);
if (!info.Exists)
return false;
var set = Beatmap.Value.BeatmapSetInfo;
// remove the previous audio track for now.
// in the future we probably want to check if this is being used elsewhere (other difficulties?)
var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.AudioFile);
using (var stream = info.OpenRead())
{
if (oldFile != null)
beatmaps.ReplaceFile(set, oldFile, stream, info.Name);
else
beatmaps.AddFile(set, stream, info.Name);
}
Beatmap.Value.Metadata.AudioFile = info.Name;
music.ReloadCurrentTrack();
editor?.UpdateClockSource();
return true;
}
private void audioTrackChanged(ValueChangedEvent<string> filePath)
{
if (!ChangeAudioTrack(filePath.NewValue))
audioTrackTextBox.Current.Value = filePath.OldValue;
}
private void onCommit(TextBox sender, bool newText)
{
if (!newText) return;
@ -125,4 +197,60 @@ namespace osu.Game.Screens.Edit.Setup
Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value;
}
}
internal class FileChooserLabelledTextBox : LabelledTextBox
{
public Container Target;
private readonly IBindable<FileInfo> currentFile = new Bindable<FileInfo>();
public FileChooserLabelledTextBox()
{
currentFile.BindValueChanged(onFileSelected);
}
private void onFileSelected(ValueChangedEvent<FileInfo> file)
{
if (file.NewValue == null)
return;
Target.Clear();
Current.Value = file.NewValue.FullName;
}
protected override OsuTextBox CreateTextBox() =>
new FileChooserOsuTextBox
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
CornerRadius = CORNER_RADIUS,
OnFocused = DisplayFileChooser
};
public void DisplayFileChooser()
{
Target.Child = new FileSelector(validFileExtensions: new[] { ".mp3", ".ogg" })
{
RelativeSizeAxes = Axes.X,
Height = 400,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
CurrentFile = { BindTarget = currentFile }
};
}
internal class FileChooserOsuTextBox : OsuTextBox
{
public Action OnFocused;
protected override void OnFocus(FocusEvent e)
{
OnFocused?.Invoke();
base.OnFocus(e);
GetContainingInputManager().TriggerFocusContention(this);
}
}
}
}

View File

@ -26,13 +26,15 @@ namespace osu.Game.Tests.Visual
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
}
protected virtual bool EditorComponentsReady => Editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
&& Editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("load editor", () => LoadScreen(Editor = CreateEditor()));
AddUntilStep("wait for editor to load", () => Editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
&& Editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for editor to load", () => EditorComponentsReady);
AddStep("get beatmap", () => EditorBeatmap = Editor.ChildrenOfType<EditorBeatmap>().Single());
AddStep("get clock", () => EditorClock = Editor.ChildrenOfType<EditorClock>().Single());
}

View File

@ -909,6 +909,7 @@ private void load()
<s:Boolean x:Key="/Default/UserDictionary/Words/=beatmaps/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=beatmap_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=bindable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=bindables/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Catmull/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Drawables/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=gameplay/@EntryIndexedValue">True</s:Boolean>