mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 15:30:06 +08:00
de97660fa4
Probably closes https://github.com/ppy/osu/issues/36492. This is dumb but it's in large part my stupidity. To begin with, the immediate direct offending call that causes the observed symptoms is https://github.com/ppy/osu/blob/a401c7d5e9d6d8b05b2ec293145ad308dfe9d6d0/osu.Game/Screens/Edit/Components/FormSampleSet.cs#L296 The reason why this "invalidation" affects sample volume is that in the framework implementation, the call [removes the relevant sample factory from the sample store which is an audio component](https://github.com/ppy/osu-framework/blob/5b716dcbef6f99e03188a7a7706361fa8445c754/osu.Framework/Audio/Sample/SampleStore.cs#L65-L72). In the process it also [unbinds audio adjustments](https://github.com/ppy/osu-framework/blob/5b716dcbef6f99e03188a7a7706361fa8445c754/osu.Framework/Audio/AudioCollectionManager.cs#L37-L38), which *would* have the effect of resetting the sample volume to 100%, effectively (and I've pretty much confirmed that that's what happens). Now for why this call sometimes does the right thing and sometimes doesn't: Sometimes the call is made in response to an *actual* change to the beatmap skin, which is correct and expected, if very indirect, but sometimes it is made *over-eagerly* when there is no reason to recycle anything yet. One such circumstance is entering the setup screen, which will still "invalidate" (read: remove) the samples, but the compose tab hasn't seen any change to the beatmap skin, so when it is returned to, it has no reason to retrieve the sample again, and as such it will try to play samples which are for better or worse in a completely undefined state because they're not supposed to be *in use* anymore. Therefore, the right thing here would seem to be to take the responsibility of invalidation from a random component, and move it to a place that's *actually* correlated to every other component needing to recycle samples, e.g. `EditorBeatmapSkin` responding to changes in the beatmap resources via raising `BeatmapSkinChanged`. Unfortunately, because of the structure of everything, this recycle needs to go from targeted to individual samples, to nuking the entire store. The reason for this is that `RealmBackedResourceStore` does not provide information as to *what* resources changed, it just says that *the set of them* did. For the recycle to be smarter, `EditorBeatmapSkin` would need to know not only which samples were added or replaced, but also which ones were *removed*, so that users don't hear phantom samples that no longer exist in the editor later. That would however be a lot of hassle for nothing, so I just recycle everything here and hope it won't matter. As to why I could only reproduce this on this one beatmap - I'm not super sure. The failure does not seem to be specific to beatmaps, but it may be exacerbated by certain patterns of accessing samples which means that beatmaps with high BPM like the one I managed to reproduce this on may just be more susceptible to this. As a final note, I do realise that this is not fundamentally improving the surrounding systems and it's still a pretty rickety thing to do. It's still on the consumers to know and respond to the sample store recycle and this is likely to fail if a consumer ever doesn't. That said, I have no brighter ideas at this point in time that won't involve me spending a week refactoring audio.
343 lines
12 KiB
C#
343 lines
12 KiB
C#
// 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.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Audio.Sample;
|
|
using osu.Framework.Bindables;
|
|
using osu.Framework.Extensions;
|
|
using osu.Framework.Extensions.Color4Extensions;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Graphics.Colour;
|
|
using osu.Framework.Graphics.Containers;
|
|
using osu.Framework.Graphics.Cursor;
|
|
using osu.Framework.Graphics.Sprites;
|
|
using osu.Framework.Graphics.UserInterface;
|
|
using osu.Framework.Input.Events;
|
|
using osu.Framework.Localisation;
|
|
using osu.Game.Audio;
|
|
using osu.Game.Graphics;
|
|
using osu.Game.Graphics.Backgrounds;
|
|
using osu.Game.Graphics.Sprites;
|
|
using osu.Game.Graphics.UserInterface;
|
|
using osu.Game.Graphics.UserInterfaceV2;
|
|
using osu.Game.Overlays;
|
|
using osu.Game.Resources.Localisation.Web;
|
|
using osu.Game.Utils;
|
|
using osuTK;
|
|
using osuTK.Graphics;
|
|
|
|
namespace osu.Game.Screens.Edit.Components
|
|
{
|
|
public partial class FormSampleSet : CompositeDrawable, IHasCurrentValue<EditorBeatmapSkin.SampleSet?>
|
|
{
|
|
public Bindable<EditorBeatmapSkin.SampleSet?> Current
|
|
{
|
|
get => current.Current;
|
|
set => current.Current = value;
|
|
}
|
|
|
|
public Func<FileInfo, string, string>? SampleAddRequested { get; init; }
|
|
public Action<string>? SampleRemoveRequested { get; init; }
|
|
|
|
private readonly BindableWithCurrent<EditorBeatmapSkin.SampleSet?> current = new BindableWithCurrent<EditorBeatmapSkin.SampleSet?>();
|
|
private readonly Dictionary<(string name, string bank), SampleButton> buttons = new Dictionary<(string, string), SampleButton>();
|
|
|
|
private FormControlBackground background = null!;
|
|
private FormFieldCaption caption = null!;
|
|
|
|
[Resolved]
|
|
private OverlayColourProvider colourProvider { get; set; } = null!;
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load()
|
|
{
|
|
RelativeSizeAxes = Axes.X;
|
|
AutoSizeAxes = Axes.Y;
|
|
|
|
Masking = true;
|
|
CornerRadius = 5;
|
|
CornerExponent = 2.5f;
|
|
|
|
InternalChildren = new Drawable[]
|
|
{
|
|
background = new FormControlBackground
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
},
|
|
new FillFlowContainer
|
|
{
|
|
RelativeSizeAxes = Axes.X,
|
|
AutoSizeAxes = Axes.Y,
|
|
Padding = new MarginPadding(9),
|
|
Spacing = new Vector2(7),
|
|
Direction = FillDirection.Vertical,
|
|
Children = new Drawable[]
|
|
{
|
|
caption = new FormFieldCaption(),
|
|
new GridContainer
|
|
{
|
|
AutoSizeAxes = Axes.Both,
|
|
RowDimensions = Enumerable.Repeat(new Dimension(GridSizeMode.AutoSize), 4).ToArray(),
|
|
ColumnDimensions = Enumerable.Repeat(new Dimension(GridSizeMode.AutoSize), 5).ToArray(),
|
|
Content = createTableContent().ToArray(),
|
|
}
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
private IEnumerable<Drawable[]> createTableContent()
|
|
{
|
|
string[] columns = HitSampleInfo.ALL_ADDITIONS.Prepend(HitSampleInfo.HIT_NORMAL).ToArray();
|
|
string[] rows = HitSampleInfo.ALL_BANKS;
|
|
|
|
yield return columns.Select(makeTableHeading).Prepend(Empty()).ToArray();
|
|
|
|
foreach (string row in rows)
|
|
{
|
|
List<Drawable> drawables = [makeTableHeading(row)];
|
|
|
|
foreach (string col in columns)
|
|
drawables.Add(buttons[(col, row)] = makeButton());
|
|
|
|
yield return drawables.ToArray();
|
|
}
|
|
}
|
|
|
|
private OsuSpriteText makeTableHeading(string text) => new OsuSpriteText
|
|
{
|
|
Text = text,
|
|
Font = OsuFont.Style.Caption1,
|
|
Anchor = Anchor.Centre,
|
|
Origin = Anchor.Centre,
|
|
};
|
|
|
|
private SampleButton makeButton() => new SampleButton
|
|
{
|
|
Width = 60,
|
|
Anchor = Anchor.Centre,
|
|
Origin = Anchor.Centre,
|
|
Margin = new MarginPadding(5),
|
|
SampleAddRequested = SampleAddRequested,
|
|
SampleRemoveRequested = SampleRemoveRequested,
|
|
};
|
|
|
|
protected override void LoadComplete()
|
|
{
|
|
base.LoadComplete();
|
|
|
|
updateState();
|
|
Current.BindValueChanged(setChanged, true);
|
|
}
|
|
|
|
private void setChanged(ValueChangedEvent<EditorBeatmapSkin.SampleSet?> valueChangedEvent)
|
|
{
|
|
var set = valueChangedEvent.NewValue;
|
|
|
|
caption.Caption = set?.Name ?? default(LocalisableString);
|
|
Alpha = set != null && set.SampleSetIndex > 0 ? 1 : 0;
|
|
|
|
if (set != null)
|
|
{
|
|
foreach (var (sample, button) in buttons)
|
|
{
|
|
button.ExpectedFilename.Value = $@"{sample.bank}-{sample.name}{(set.SampleSetIndex > 1 ? set.SampleSetIndex : null)}";
|
|
button.ActualFilename.Value = set.FindSampleIfExists(sample.name, sample.bank);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override bool OnHover(HoverEvent e)
|
|
{
|
|
updateState();
|
|
return true;
|
|
}
|
|
|
|
protected override void OnHoverLost(HoverLostEvent e)
|
|
{
|
|
updateState();
|
|
base.OnHoverLost(e);
|
|
}
|
|
|
|
private void updateState()
|
|
{
|
|
caption.Colour = colourProvider.Content2;
|
|
|
|
background.VisualStyle = IsHovered ? VisualStyle.Hovered : VisualStyle.Normal;
|
|
}
|
|
|
|
public partial class SampleButton : OsuButton, IHasPopover, IHasContextMenu
|
|
{
|
|
/// <summary>
|
|
/// The expected filename for the sample that this button represents.
|
|
/// Does not contain extension.
|
|
/// </summary>
|
|
public Bindable<string> ExpectedFilename { get; } = new Bindable<string>();
|
|
|
|
/// <summary>
|
|
/// The actual chosen filename for the sample that this button represent.
|
|
/// Can be <see langword="null"/> if the sample is omitted / missing.
|
|
/// Does contain extension.
|
|
/// </summary>
|
|
public Bindable<string?> ActualFilename { get; } = new Bindable<string?>();
|
|
|
|
/// <summary>
|
|
/// Invoked when a new sample is selected via this button.
|
|
/// </summary>
|
|
public Func<FileInfo, string, string>? SampleAddRequested { get; init; }
|
|
|
|
/// <summary>
|
|
/// Invoked when a sample removal is selected via this button.
|
|
/// </summary>
|
|
public Action<string>? SampleRemoveRequested { get; init; }
|
|
|
|
private Bindable<FileInfo?> selectedFile { get; } = new Bindable<FileInfo?>();
|
|
|
|
private TrianglesV2? triangles { get; set; }
|
|
|
|
protected override float HoverLayerFinalAlpha => 0;
|
|
|
|
private Color4? triangleGradientSecondColour;
|
|
private SpriteIcon icon = null!;
|
|
|
|
[Resolved]
|
|
private OverlayColourProvider overlayColourProvider { get; set; } = null!;
|
|
|
|
[Resolved]
|
|
private EditorBeatmap? editorBeatmap { get; set; }
|
|
|
|
private HoverSounds? hoverSounds;
|
|
|
|
private ISample? sample;
|
|
|
|
public SampleButton()
|
|
: base(null)
|
|
{
|
|
}
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load()
|
|
{
|
|
Add(icon = new SpriteIcon
|
|
{
|
|
Icon = FontAwesome.Solid.Plus,
|
|
Size = new Vector2(16),
|
|
Shadow = true,
|
|
Anchor = Anchor.Centre,
|
|
Origin = Anchor.Centre,
|
|
});
|
|
|
|
Action = () =>
|
|
{
|
|
if (ActualFilename.Value == null)
|
|
{
|
|
selectedFile.Value = null;
|
|
this.ShowPopover();
|
|
}
|
|
else
|
|
sample?.Play();
|
|
};
|
|
|
|
if (editorBeatmap?.BeatmapSkin != null)
|
|
editorBeatmap.BeatmapSkin.BeatmapSkinChanged += recycleSamples;
|
|
}
|
|
|
|
protected override void LoadComplete()
|
|
{
|
|
base.LoadComplete();
|
|
|
|
Content.CornerRadius = 4;
|
|
|
|
Add(triangles = new TrianglesV2
|
|
{
|
|
Thickness = 0.02f,
|
|
SpawnRatio = 0.6f,
|
|
RelativeSizeAxes = Axes.Both,
|
|
Depth = float.MaxValue,
|
|
});
|
|
|
|
ActualFilename.BindValueChanged(_ => updateState(), true);
|
|
selectedFile.BindValueChanged(_ => addSample());
|
|
}
|
|
|
|
private void updateState()
|
|
{
|
|
BackgroundColour = ActualFilename.Value == null ? overlayColourProvider.Background3 : overlayColourProvider.Colour3;
|
|
triangleGradientSecondColour = BackgroundColour.Lighten(0.2f);
|
|
icon.Icon = ActualFilename.Value == null ? FontAwesome.Solid.Plus : FontAwesome.Solid.Play;
|
|
|
|
recycleSamples();
|
|
|
|
if (triangles == null)
|
|
return;
|
|
|
|
triangles.Colour = ColourInfo.GradientVertical(triangleGradientSecondColour.Value, BackgroundColour);
|
|
}
|
|
|
|
private void recycleSamples() => Schedule(() =>
|
|
{
|
|
if (hoverSounds?.Parent == this)
|
|
{
|
|
RemoveInternal(hoverSounds, true);
|
|
hoverSounds = null;
|
|
}
|
|
|
|
AddInternal(hoverSounds = (ActualFilename.Value == null ? new HoverClickSounds(HoverSampleSet.Button) : new HoverSounds(HoverSampleSet.Button)));
|
|
|
|
sample = ActualFilename.Value != null ? editorBeatmap?.BeatmapSkin?.Skin.Samples?.Get(ActualFilename.Value) : null;
|
|
});
|
|
|
|
protected override bool OnHover(HoverEvent e)
|
|
{
|
|
Debug.Assert(triangleGradientSecondColour != null);
|
|
|
|
Background.FadeColour(triangleGradientSecondColour.Value, 300, Easing.OutQuint);
|
|
return base.OnHover(e);
|
|
}
|
|
|
|
protected override void OnHoverLost(HoverLostEvent e)
|
|
{
|
|
Background.FadeColour(BackgroundColour, 300, Easing.OutQuint);
|
|
base.OnHoverLost(e);
|
|
}
|
|
|
|
private void addSample()
|
|
{
|
|
if (selectedFile.Value == null)
|
|
return;
|
|
|
|
this.HidePopover();
|
|
ActualFilename.Value = SampleAddRequested?.Invoke(selectedFile.Value, ExpectedFilename.Value) ?? selectedFile.Value.ToString();
|
|
}
|
|
|
|
private void deleteSample()
|
|
{
|
|
if (ActualFilename.Value == null)
|
|
return;
|
|
|
|
SampleRemoveRequested?.Invoke(ActualFilename.Value);
|
|
ActualFilename.Value = null;
|
|
}
|
|
|
|
public Popover? GetPopover() => ActualFilename.Value == null ? new FormFileSelector.FileChooserPopover(SupportedExtensions.AUDIO_EXTENSIONS, selectedFile, null) : null;
|
|
|
|
public MenuItem[]? ContextMenuItems =>
|
|
ActualFilename.Value != null
|
|
? [new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, deleteSample)]
|
|
: null;
|
|
|
|
protected override void Dispose(bool isDisposing)
|
|
{
|
|
if (editorBeatmap?.BeatmapSkin != null)
|
|
editorBeatmap.BeatmapSkin.BeatmapSkinChanged -= recycleSamples;
|
|
base.Dispose(isDisposing);
|
|
}
|
|
}
|
|
}
|
|
}
|