1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-06 08:22:56 +08:00

Make DrawableStoryboardSample a SkinnableSound

Allows sharing pause logic with gameplay samples.
This commit is contained in:
Dean Herbert 2020-09-29 14:09:51 +09:00
parent 74e74e1c31
commit 136843c8e4
7 changed files with 94 additions and 95 deletions

View File

@ -4,10 +4,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Graphics.Audio;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
@ -106,9 +108,14 @@ namespace osu.Game.Tests.Gameplay
Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio); Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio);
SelectedMods.Value = new[] { testedMod }; SelectedMods.Value = new[] { testedMod };
Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0)); var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0)
{
Child = beatmapSkinSourceContainer
});
beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
{ {
Clock = gameplayContainer.GameplayClock Clock = gameplayContainer.GameplayClock
}); });
@ -116,7 +123,7 @@ namespace osu.Game.Tests.Gameplay
AddStep("start", () => gameplayContainer.Start()); AddStep("start", () => gameplayContainer.Start());
AddAssert("sample playback rate matches mod rates", () => sample.Channel.AggregateFrequency.Value == expectedRate); AddAssert("sample playback rate matches mod rates", () => sample.ChildrenOfType<DrawableSample>().First().AggregateFrequency.Value == expectedRate);
} }
private class TestSkin : LegacySkin private class TestSkin : LegacySkin
@ -168,8 +175,6 @@ namespace osu.Game.Tests.Gameplay
: base(sampleInfo) : base(sampleInfo)
{ {
} }
public new SampleChannel Channel => base.Channel;
} }
} }
} }

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio.Sample; using osu.Framework.Graphics.Audio;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Mods
/// </summary> /// </summary>
public interface IApplicableToSample : IApplicableMod public interface IApplicableToSample : IApplicableMod
{ {
void ApplyToSample(SampleChannel sample); void ApplyToSample(DrawableSample sample);
} }
} }

View File

@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Audio;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mods
track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange);
} }
public virtual void ApplyToSample(SampleChannel sample) public virtual void ApplyToSample(DrawableSample sample)
{ {
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
} }

View File

@ -6,11 +6,11 @@ using System.Linq;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Framework.Audio.Sample; using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Mods
AdjustPitch.TriggerChange(); AdjustPitch.TriggerChange();
} }
public void ApplyToSample(SampleChannel sample) public void ApplyToSample(DrawableSample sample)
{ {
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
} }

View File

@ -191,9 +191,25 @@ namespace osu.Game.Screens.Play
dependencies.CacheAs(gameplayBeatmap); dependencies.CacheAs(gameplayBeatmap);
addUnderlayComponents(GameplayClockContainer); var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
addGameplayComponents(GameplayClockContainer, Beatmap.Value, playableBeatmap);
addOverlayComponents(GameplayClockContainer, Beatmap.Value); // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
// full access to all skin sources.
var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap));
// load the skinning hierarchy first.
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
GameplayClockContainer.Add(beatmapSkinProvider.WithChild(rulesetSkinProvider));
rulesetSkinProvider.AddRange(new[]
{
// underlay and gameplay should have access the to skinning sources.
createUnderlayComponents(),
createGameplayComponents(Beatmap.Value, playableBeatmap)
});
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
GameplayClockContainer.Add(createOverlayComponents(Beatmap.Value));
if (!DrawableRuleset.AllowGameplayOverlays) if (!DrawableRuleset.AllowGameplayOverlays)
{ {
@ -238,45 +254,31 @@ namespace osu.Game.Screens.Play
breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
} }
private void addUnderlayComponents(Container target) private Drawable createUnderlayComponents() =>
DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both };
private Drawable createGameplayComponents(WorkingBeatmap working, IBeatmap playableBeatmap) => new ScalingContainer(ScalingMode.Gameplay)
{ {
target.Add(DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }); Children = new Drawable[]
}
private void addGameplayComponents(Container target, WorkingBeatmap working, IBeatmap playableBeatmap)
{
var beatmapSkinProvider = new BeatmapSkinProvidingContainer(working.Skin);
// the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
// full access to all skin sources.
var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap));
// load the skinning hierarchy first.
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
target.Add(new ScalingContainer(ScalingMode.Gameplay)
.WithChild(beatmapSkinProvider
.WithChild(target = rulesetSkinProvider)));
target.AddRange(new Drawable[]
{ {
DrawableRuleset, DrawableRuleset.With(r =>
r.FrameStableComponents.Children = new Drawable[]
{
ScoreProcessor,
HealthProcessor,
breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor)
{
Breaks = working.Beatmap.Breaks
}
}),
new ComboEffects(ScoreProcessor) new ComboEffects(ScoreProcessor)
}); }
};
DrawableRuleset.FrameStableComponents.AddRange(new Drawable[] private Drawable createOverlayComponents(WorkingBeatmap working) => new Container
{
ScoreProcessor,
HealthProcessor,
breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor)
{
Breaks = working.Beatmap.Breaks
}
});
}
private void addOverlayComponents(Container target, WorkingBeatmap working)
{ {
target.AddRange(new[] RelativeSizeAxes = Axes.Both,
Children = new[]
{ {
DimmableStoryboard.OverlayLayerContainer.CreateProxy(), DimmableStoryboard.OverlayLayerContainer.CreateProxy(),
BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
@ -342,8 +344,8 @@ namespace osu.Game.Screens.Play
}, },
}, },
failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, },
}); }
} };
private void onBreakTimeChanged(ValueChangedEvent<bool> isBreakTime) private void onBreakTimeChanged(ValueChangedEvent<bool> isBreakTime)
{ {

View File

@ -22,7 +22,10 @@ namespace osu.Game.Skinning
[Resolved] [Resolved]
private ISampleStore samples { get; set; } private ISampleStore samples { get; set; }
private bool requestedPlaying; /// <summary>
/// Whether playback of this sound has been requested, regardless of whether it could be played or not (due to being paused, for instance).
/// </summary>
protected bool PlaybackRequested;
public override bool RemoveWhenNotAlive => false; public override bool RemoveWhenNotAlive => false;
public override bool RemoveCompletedTransforms => false; public override bool RemoveCompletedTransforms => false;
@ -39,7 +42,7 @@ namespace osu.Game.Skinning
protected virtual bool PlayWhenPaused => false; protected virtual bool PlayWhenPaused => false;
private readonly AudioContainer<DrawableSample> samplesContainer; protected readonly AudioContainer<DrawableSample> SamplesContainer;
public SkinnableSound(ISampleInfo hitSamples) public SkinnableSound(ISampleInfo hitSamples)
: this(new[] { hitSamples }) : this(new[] { hitSamples })
@ -49,7 +52,7 @@ namespace osu.Game.Skinning
public SkinnableSound(IEnumerable<ISampleInfo> hitSamples) public SkinnableSound(IEnumerable<ISampleInfo> hitSamples)
{ {
this.hitSamples = hitSamples.ToArray(); this.hitSamples = hitSamples.ToArray();
InternalChild = samplesContainer = new AudioContainer<DrawableSample>(); InternalChild = SamplesContainer = new AudioContainer<DrawableSample>();
} }
private readonly IBindable<bool> samplePlaybackDisabled = new Bindable<bool>(); private readonly IBindable<bool> samplePlaybackDisabled = new Bindable<bool>();
@ -63,7 +66,7 @@ namespace osu.Game.Skinning
samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
samplePlaybackDisabled.BindValueChanged(disabled => samplePlaybackDisabled.BindValueChanged(disabled =>
{ {
if (requestedPlaying) if (PlaybackRequested)
{ {
if (disabled.NewValue && !PlayWhenPaused) if (disabled.NewValue && !PlayWhenPaused)
stop(); stop();
@ -87,13 +90,13 @@ namespace osu.Game.Skinning
looping = value; looping = value;
samplesContainer.ForEach(c => c.Looping = looping); SamplesContainer.ForEach(c => c.Looping = looping);
} }
} }
public void Play() public void Play()
{ {
requestedPlaying = true; PlaybackRequested = true;
play(); play();
} }
@ -102,7 +105,7 @@ namespace osu.Game.Skinning
if (samplePlaybackDisabled.Value && !PlayWhenPaused) if (samplePlaybackDisabled.Value && !PlayWhenPaused)
return; return;
samplesContainer.ForEach(c => SamplesContainer.ForEach(c =>
{ {
if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0)
c.Play(); c.Play();
@ -111,13 +114,13 @@ namespace osu.Game.Skinning
public void Stop() public void Stop()
{ {
requestedPlaying = false; PlaybackRequested = false;
stop(); stop();
} }
private void stop() private void stop()
{ {
samplesContainer.ForEach(c => c.Stop()); SamplesContainer.ForEach(c => c.Stop());
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin, bool allowFallback)
@ -146,7 +149,7 @@ namespace osu.Game.Skinning
return ch; return ch;
}).Where(c => c != null); }).Where(c => c != null);
samplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c)); SamplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c));
// Start playback internally for the new samples if the previous ones were playing beforehand. // Start playback internally for the new samples if the previous ones were playing beforehand.
if (wasPlaying) if (wasPlaying)
@ -155,24 +158,24 @@ namespace osu.Game.Skinning
#region Re-expose AudioContainer #region Re-expose AudioContainer
public BindableNumber<double> Volume => samplesContainer.Volume; public BindableNumber<double> Volume => SamplesContainer.Volume;
public BindableNumber<double> Balance => samplesContainer.Balance; public BindableNumber<double> Balance => SamplesContainer.Balance;
public BindableNumber<double> Frequency => samplesContainer.Frequency; public BindableNumber<double> Frequency => SamplesContainer.Frequency;
public BindableNumber<double> Tempo => samplesContainer.Tempo; public BindableNumber<double> Tempo => SamplesContainer.Tempo;
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable)
=> samplesContainer.AddAdjustment(type, adjustBindable); => SamplesContainer.AddAdjustment(type, adjustBindable);
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable)
=> samplesContainer.RemoveAdjustment(type, adjustBindable); => SamplesContainer.RemoveAdjustment(type, adjustBindable);
public void RemoveAllAdjustments(AdjustableProperty type) public void RemoveAllAdjustments(AdjustableProperty type)
=> samplesContainer.RemoveAllAdjustments(type); => SamplesContainer.RemoveAllAdjustments(type);
public bool IsPlaying => samplesContainer.Any(s => s.Playing); public bool IsPlaying => SamplesContainer.Any(s => s.Playing);
#endregion #endregion
} }

View File

@ -4,15 +4,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Skinning;
namespace osu.Game.Storyboards.Drawables namespace osu.Game.Storyboards.Drawables
{ {
public class DrawableStoryboardSample : Component public class DrawableStoryboardSample : SkinnableSound
{ {
/// <summary> /// <summary>
/// The amount of time allowable beyond the start time of the sample, for the sample to start. /// The amount of time allowable beyond the start time of the sample, for the sample to start.
@ -21,38 +19,37 @@ namespace osu.Game.Storyboards.Drawables
private readonly StoryboardSampleInfo sampleInfo; private readonly StoryboardSampleInfo sampleInfo;
protected SampleChannel Channel { get; private set; }
public override bool RemoveWhenNotAlive => false; public override bool RemoveWhenNotAlive => false;
public DrawableStoryboardSample(StoryboardSampleInfo sampleInfo) public DrawableStoryboardSample(StoryboardSampleInfo sampleInfo)
: base(sampleInfo)
{ {
this.sampleInfo = sampleInfo; this.sampleInfo = sampleInfo;
LifetimeStart = sampleInfo.StartTime; LifetimeStart = sampleInfo.StartTime;
} }
[BackgroundDependencyLoader] [Resolved]
private void load(IBindable<WorkingBeatmap> beatmap, IBindable<IReadOnlyList<Mod>> mods) private IBindable<IReadOnlyList<Mod>> mods { get; set; }
{
Channel = beatmap.Value.Skin.GetSample(sampleInfo);
if (Channel == null)
return;
Channel.Volume.Value = sampleInfo.Volume / 100.0; protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
foreach (var mod in mods.Value.OfType<IApplicableToSample>()) foreach (var mod in mods.Value.OfType<IApplicableToSample>())
mod.ApplyToSample(Channel); {
foreach (var sample in SamplesContainer)
mod.ApplyToSample(sample);
}
} }
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
// TODO: this logic will need to be consolidated with other game samples like hit sounds.
if (Time.Current < sampleInfo.StartTime) if (Time.Current < sampleInfo.StartTime)
{ {
// We've rewound before the start time of the sample // We've rewound before the start time of the sample
Channel?.Stop(); Stop();
// In the case that the user fast-forwards to a point far beyond the start time of the sample, // In the case that the user fast-forwards to a point far beyond the start time of the sample,
// we want to be able to fall into the if-conditional below (therefore we must not have a life time end) // we want to be able to fall into the if-conditional below (therefore we must not have a life time end)
@ -63,8 +60,8 @@ namespace osu.Game.Storyboards.Drawables
{ {
// We've passed the start time of the sample. We only play the sample if we're within an allowable range // We've passed the start time of the sample. We only play the sample if we're within an allowable range
// from the sample's start, to reduce layering if we've been fast-forwarded far into the future // from the sample's start, to reduce layering if we've been fast-forwarded far into the future
if (Time.Current - sampleInfo.StartTime < allowable_late_start) if (!PlaybackRequested && Time.Current - sampleInfo.StartTime < allowable_late_start)
Channel?.Play(); Play();
// In the case that the user rewinds to a point far behind the start time of the sample, // In the case that the user rewinds to a point far behind the start time of the sample,
// we want to be able to fall into the if-conditional above (therefore we must not have a life time start) // we want to be able to fall into the if-conditional above (therefore we must not have a life time start)
@ -72,13 +69,5 @@ namespace osu.Game.Storyboards.Drawables
LifetimeEnd = sampleInfo.StartTime; LifetimeEnd = sampleInfo.StartTime;
} }
} }
protected override void Dispose(bool isDisposing)
{
Channel?.Stop();
Channel = null;
base.Dispose(isDisposing);
}
} }
} }