diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 86920927dc..3490d50871 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -108,7 +108,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables RelativeSizeAxes = Axes.X }, tailContainer = new Container { RelativeSizeAxes = Axes.Both }, - slidingSample = new PausableSkinnableSound { Looping = true } + slidingSample = new PausableSkinnableSound + { + Looping = true, + MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME, + } }); maskedContents.AddRange(new[] diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 09973ce5fd..b682d14879 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -107,7 +107,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables headContainer = new Container { RelativeSizeAxes = Axes.Both }, OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, }, Ball, - slidingSample = new PausableSkinnableSound { Looping = true } + slidingSample = new PausableSkinnableSound + { + Looping = true, + MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME, + } }); PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index d4e257d754..c092b4dd4b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -106,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables spinningSample = new PausableSkinnableSound { Volume = { Value = 0 }, + MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME, Looping = true, Frequency = { Value = spinning_sample_initial_frequency } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 3bb0e3dfb8..ce6475d3ce 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -159,6 +159,26 @@ namespace osu.Game.Rulesets.Objects.Drawables /// internal bool IsInitialized; + /// + /// The minimum allowable volume for sample playback. + /// quieter than that will be forcibly played at this volume instead. + /// + /// + /// + /// Drawable hitobjects adding their own custom samples, or other sample playback sources + /// (i.e. ) must enforce this themselves. + /// + /// + /// This sample volume floor is present in stable, although it is set at 8% rather than 5%. + /// See: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Audio/AudioEngine.cs#L1070, + /// https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Audio/AudioEngine.cs#L1404-L1405. + /// The reason why it is 5% here is that the 8% cap was enforced in a silent manner + /// (i.e. the minimum selectable volume in the editor was 5%, but it would be played at 8% anyways), + /// which is confusing and arbitrary, so we're just doing 5% here at the cost of sacrificing strict parity. + /// + /// + public const int MINIMUM_SAMPLE_VOLUME = 5; + /// /// Creates a new . /// @@ -181,7 +201,10 @@ namespace osu.Game.Rulesets.Objects.Drawables comboColourBrightness.BindTo(gameplaySettings.ComboColourNormalisationAmount); // Explicit non-virtual function call in case a DrawableHitObject overrides AddInternal. - base.AddInternal(Samples = new PausableSkinnableSound()); + base.AddInternal(Samples = new PausableSkinnableSound + { + MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME + }); CurrentSkin = skinSource; CurrentSkin.SourceChanged += skinSourceChanged; diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs index fed262868d..b61e8d9674 100644 --- a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs +++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Skinning; @@ -45,7 +46,10 @@ namespace osu.Game.Rulesets.UI Child = hitSounds = new Container { Name = "concurrent sample pool", - ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound()) + ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound + { + MinimumSampleVolume = DrawableHitObject.MINIMUM_SAMPLE_VOLUME + }) } }; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index b02cfb505e..28841fc9e5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -16,6 +16,7 @@ using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Screens.Edit.Timing; using osuTK; using osuTK.Graphics; @@ -101,7 +102,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, volume = new IndeterminateSliderWithTextBoxInput("Volume", new BindableInt(100) { - MinValue = 0, + MinValue = DrawableHitObject.MINIMUM_SAMPLE_VOLUME, MaxValue = 100, }) } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 59b3799e0a..f866a4f8ec 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -20,6 +20,12 @@ namespace osu.Game.Skinning /// public partial class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent { + /// + /// The minimum allowable volume for . + /// that specify a lower will be forcibly pulled up to this volume. + /// + public int MinimumSampleVolume { get; set; } + public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; @@ -156,7 +162,7 @@ namespace osu.Game.Skinning { var sample = samplePool?.GetPooledSample(s) ?? new PoolableSkinnableSample(s); sample.Looping = Looping; - sample.Volume.Value = s.Volume / 100.0; + sample.Volume.Value = Math.Max(s.Volume, MinimumSampleVolume) / 100.0; samplesContainer.Add(sample); }