1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-23 00:07:24 +08:00

Merge pull request #10282 from peppy/fix-editor-silence-on-seek

Fix hit samples playing while paused / seeking in the editor
This commit is contained in:
Dan Balasescu 2020-09-30 21:33:25 +09:00 committed by GitHub
commit db78a0edf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 155 additions and 81 deletions

View File

@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.BindValueChanged(updateSlidingSample);
}
private SkinnableSound slidingSample;
private PausableSkinnableSound slidingSample;
protected override void LoadSamples()
{
@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
clone.Name = "sliderslide";
AddInternal(slidingSample = new SkinnableSound(clone)
AddInternal(slidingSample = new PausableSkinnableSound(clone)
{
Looping = true
});
@ -112,10 +112,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void updateSlidingSample(ValueChangedEvent<bool> tracking)
{
// note that samples will not start playing if exiting a seek operation in the middle of a slider.
// may be something we want to address at a later point, but not so easy to make happen right now
// (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update).
if (tracking.NewValue && ShouldPlaySamples)
if (tracking.NewValue)
slidingSample?.Play();
else
slidingSample?.Stop();

View File

@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
isSpinning.BindValueChanged(updateSpinningSample);
}
private SkinnableSound spinningSample;
private PausableSkinnableSound spinningSample;
private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;
@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
clone.Name = "spinnerspin";
AddInternal(spinningSample = new SkinnableSound(clone)
AddInternal(spinningSample = new PausableSkinnableSound(clone)
{
Volume = { Value = 0 },
Looping = true,
@ -113,10 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void updateSpinningSample(ValueChangedEvent<bool> tracking)
{
// note that samples will not start playing if exiting a seek operation in the middle of a spinner.
// may be something we want to address at a later point, but not so easy to make happen right now
// (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update).
if (tracking.NewValue && ShouldPlaySamples)
if (tracking.NewValue)
{
spinningSample?.Play();
spinningSample?.VolumeTo(1, 200);

View File

@ -42,9 +42,9 @@ namespace osu.Game.Rulesets.Taiko.Audio
}
}
private SkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd)
private PausableSkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd)
{
var drawable = new SkinnableSound(hitSampleInfo)
var drawable = new PausableSkinnableSound(hitSampleInfo)
{
LifetimeStart = lifetimeStart,
LifetimeEnd = lifetimeEnd
@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Taiko.Audio
public class DrumSample
{
public SkinnableSound Centre;
public SkinnableSound Rim;
public PausableSkinnableSound Centre;
public PausableSkinnableSound Rim;
}
}
}

View File

@ -22,11 +22,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinnableSound : OsuTestScene
{
[Cached]
[Cached(typeof(ISamplePlaybackDisabler))]
private GameplayClock gameplayClock = new GameplayClock(new FramedClock());
private TestSkinSourceContainer skinSource;
private SkinnableSound skinnableSound;
private PausableSkinnableSound skinnableSound;
[SetUp]
public void SetUp() => Schedule(() =>
@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
Clock = gameplayClock,
RelativeSizeAxes = Axes.Both,
Child = skinnableSound = new SkinnableSound(new SampleInfo("normal-sliderslide"))
Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide"))
},
};
});

View File

@ -52,10 +52,10 @@ namespace osu.Game.Rulesets.Mods
public class NightcoreBeatContainer : BeatSyncedContainer
{
private SkinnableSound hatSample;
private SkinnableSound clapSample;
private SkinnableSound kickSample;
private SkinnableSound finishSample;
private PausableSkinnableSound hatSample;
private PausableSkinnableSound clapSample;
private PausableSkinnableSound kickSample;
private PausableSkinnableSound finishSample;
private int? firstBeat;
@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Mods
{
InternalChildren = new Drawable[]
{
hatSample = new SkinnableSound(new SampleInfo("nightcore-hat")),
clapSample = new SkinnableSound(new SampleInfo("nightcore-clap")),
kickSample = new SkinnableSound(new SampleInfo("nightcore-kick")),
finishSample = new SkinnableSound(new SampleInfo("nightcore-finish")),
hatSample = new PausableSkinnableSound(new SampleInfo("nightcore-hat")),
clapSample = new PausableSkinnableSound(new SampleInfo("nightcore-clap")),
kickSample = new PausableSkinnableSound(new SampleInfo("nightcore-kick")),
finishSample = new PausableSkinnableSound(new SampleInfo("nightcore-finish")),
};
}

View File

@ -17,7 +17,6 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osu.Game.Configuration;
using osu.Game.Screens.Play;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Objects.Drawables
@ -34,7 +33,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// </summary>
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>(Color4.Gray);
protected SkinnableSound Samples { get; private set; }
protected PausableSkinnableSound Samples { get; private set; }
public virtual IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples;
@ -179,7 +178,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)));
Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)));
AddInternal(Samples);
}
@ -359,9 +358,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
{
}
[Resolved(canBeNull: true)]
private GameplayClock gameplayClock { get; set; }
/// <summary>
/// Calculate the position to be used for sample playback at a specified X position (0..1).
/// </summary>
@ -374,18 +370,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
return balance_adjust_amount * (userPositionalHitSounds.Value ? position - 0.5f : 0);
}
/// <summary>
/// Whether samples should currently be playing. Will be false during seek operations.
/// </summary>
protected bool ShouldPlaySamples => gameplayClock?.IsSeeking != true;
/// <summary>
/// Plays all the hit sounds for this <see cref="DrawableHitObject"/>.
/// This is invoked automatically when this <see cref="DrawableHitObject"/> is hit.
/// </summary>
public virtual void PlaySamples()
{
if (Samples != null && ShouldPlaySamples)
if (Samples != null)
{
Samples.Balance.Value = CalculateSamplePlaybackBalance(SamplePlaybackPosition);
Samples.Play();

View File

@ -107,6 +107,7 @@ namespace osu.Game.Screens.Edit
UpdateClockSource();
dependencies.CacheAs(clock);
dependencies.CacheAs<ISamplePlaybackDisabler>(clock);
AddInternal(clock);
// todo: remove caching of this and consume via editorBeatmap?

View File

@ -7,17 +7,18 @@ using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Utils;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.Edit
{
/// <summary>
/// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor.
/// </summary>
public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock, ISamplePlaybackDisabler
{
public IBindable<Track> Track => track;
@ -31,6 +32,10 @@ namespace osu.Game.Screens.Edit
private readonly DecoupleableInterpolatingFramedClock underlyingClock;
public IBindable<bool> SamplePlaybackDisabled => samplePlaybackDisabled;
private readonly Bindable<bool> samplePlaybackDisabled = new Bindable<bool>();
public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor)
: this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor)
{
@ -166,11 +171,14 @@ namespace osu.Game.Screens.Edit
public void Stop()
{
samplePlaybackDisabled.Value = true;
underlyingClock.Stop();
}
public bool Seek(double position)
{
samplePlaybackDisabled.Value = true;
ClearTransforms();
return underlyingClock.Seek(position);
}
@ -211,8 +219,35 @@ namespace osu.Game.Screens.Edit
private const double transform_time = 300;
protected override void Update()
{
base.Update();
updateSeekingState();
}
private void updateSeekingState()
{
if (samplePlaybackDisabled.Value)
{
if (track.Value?.IsRunning != true)
{
// seeking in the editor can happen while the track isn't running.
// in this case we always want to expose ourselves as seeking (to avoid sample playback).
return;
}
// we are either running a seek tween or doing an immediate seek.
// in the case of an immediate seek the seeking bool will be set to false after one update.
// this allows for silencing hit sounds and the likes.
samplePlaybackDisabled.Value = Transforms.Any();
}
}
public void SeekTo(double seekDestination)
{
samplePlaybackDisabled.Value = true;
if (IsRunning)
Seek(seekDestination);
else

View File

@ -16,7 +16,7 @@ namespace osu.Game.Screens.Play
/// <see cref="IFrameBasedClock"/>, as this should only be done once to ensure accuracy.
/// </remarks>
/// </summary>
public class GameplayClock : IFrameBasedClock
public class GameplayClock : IFrameBasedClock, ISamplePlaybackDisabler
{
private readonly IFrameBasedClock underlyingClock;
@ -72,5 +72,7 @@ namespace osu.Game.Screens.Play
public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo;
public IClock Source => underlyingClock;
public IBindable<bool> SamplePlaybackDisabled => IsPaused;
}
}

View File

@ -54,6 +54,7 @@ namespace osu.Game.Screens.Play
public GameplayClock GameplayClock => localGameplayClock;
[Cached(typeof(GameplayClock))]
[Cached(typeof(ISamplePlaybackDisabler))]
private readonly LocalGameplayClock localGameplayClock;
private Bindable<double> userAudioOffset;

View File

@ -0,0 +1,20 @@
// 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 osu.Framework.Bindables;
using osu.Game.Skinning;
namespace osu.Game.Screens.Play
{
/// <summary>
/// Allows a component to disable sample playback dynamically as required.
/// Handled by <see cref="PausableSkinnableSound"/>.
/// </summary>
public interface ISamplePlaybackDisabler
{
/// <summary>
/// Whether sample playback should be disabled (or paused for looping samples).
/// </summary>
IBindable<bool> SamplePlaybackDisabled { get; }
}
}

View File

@ -0,0 +1,66 @@
// 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.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Screens.Play;
namespace osu.Game.Skinning
{
public class PausableSkinnableSound : SkinnableSound
{
protected bool RequestedPlaying { get; private set; }
public PausableSkinnableSound(ISampleInfo hitSamples)
: base(hitSamples)
{
}
public PausableSkinnableSound(IEnumerable<ISampleInfo> hitSamples)
: base(hitSamples)
{
}
private readonly IBindable<bool> samplePlaybackDisabled = new Bindable<bool>();
[BackgroundDependencyLoader(true)]
private void load(ISamplePlaybackDisabler samplePlaybackDisabler)
{
// if in a gameplay context, pause sample playback when gameplay is paused.
if (samplePlaybackDisabler != null)
{
samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
samplePlaybackDisabled.BindValueChanged(disabled =>
{
if (RequestedPlaying)
{
if (disabled.NewValue)
base.Stop();
// it's not easy to know if a sample has finished playing (to end).
// to keep things simple only resume playing looping samples.
else if (Looping)
base.Play();
}
});
}
}
public override void Play()
{
RequestedPlaying = true;
if (samplePlaybackDisabled.Value)
return;
base.Play();
}
public override void Stop()
{
RequestedPlaying = false;
base.Stop();
}
}
}

View File

@ -11,7 +11,6 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Screens.Play;
namespace osu.Game.Skinning
{
@ -22,8 +21,6 @@ namespace osu.Game.Skinning
[Resolved]
private ISampleStore samples { get; set; }
private bool requestedPlaying;
public override bool RemoveWhenNotAlive => false;
public override bool RemoveCompletedTransforms => false;
@ -50,27 +47,6 @@ namespace osu.Game.Skinning
InternalChild = samplesContainer = new AudioContainer<DrawableSample>();
}
private Bindable<bool> gameplayClockPaused;
[BackgroundDependencyLoader(true)]
private void load(GameplayClock gameplayClock)
{
// if in a gameplay context, pause sample playback when gameplay is paused.
gameplayClockPaused = gameplayClock?.IsPaused.GetBoundCopy();
gameplayClockPaused?.BindValueChanged(paused =>
{
if (requestedPlaying)
{
if (paused.NewValue)
stop();
// it's not easy to know if a sample has finished playing (to end).
// to keep things simple only resume playing looping samples.
else if (Looping)
play();
}
});
}
private bool looping;
public bool Looping
@ -86,13 +62,7 @@ namespace osu.Game.Skinning
}
}
public void Play()
{
requestedPlaying = true;
play();
}
private void play()
public virtual void Play()
{
samplesContainer.ForEach(c =>
{
@ -101,13 +71,7 @@ namespace osu.Game.Skinning
});
}
public void Stop()
{
requestedPlaying = false;
stop();
}
private void stop()
public virtual void Stop()
{
samplesContainer.ForEach(c => c.Stop());
}
@ -142,7 +106,7 @@ namespace osu.Game.Skinning
// Start playback internally for the new samples if the previous ones were playing beforehand.
if (wasPlaying)
play();
Play();
}
#region Re-expose AudioContainer