Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an "Adjust pitch" switch to DT/HT #24640

Merged
merged 8 commits into from
Oct 18, 2023
21 changes: 7 additions & 14 deletions osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ public class ModAdaptiveSpeed : Mod, IApplicableToRate, IApplicableToDrawableHit
// Apply a fixed rate change when missing, allowing the player to catch up when the rate is too fast.
private const double rate_change_on_miss = 0.95d;

private IAdjustableAudioComponent? track;
private double targetRate = 1d;

/// <summary>
Expand Down Expand Up @@ -123,24 +122,27 @@ public class ModAdaptiveSpeed : Mod, IApplicableToRate, IApplicableToDrawableHit
/// </summary>
private readonly Dictionary<HitObject, double> ratesForRewinding = new Dictionary<HitObject, double>();

private readonly RateAdjustModHelper rateAdjustHelper;

public ModAdaptiveSpeed()
{
rateAdjustHelper = new RateAdjustModHelper(SpeedChange);
rateAdjustHelper.HandleAudioAdjustments(AdjustPitch);

InitialRate.BindValueChanged(val =>
{
SpeedChange.Value = val.NewValue;
targetRate = val.NewValue;
});
AdjustPitch.BindValueChanged(adjustPitchChanged);
}

public void ApplyToTrack(IAdjustableAudioComponent track)
{
this.track = track;

InitialRate.TriggerChange();
AdjustPitch.TriggerChange();
recentRates.Clear();
recentRates.AddRange(Enumerable.Repeat(InitialRate.Value, recent_rate_count));

rateAdjustHelper.ApplyToTrack(track);
}

public void ApplyToSample(IAdjustableAudioComponent sample)
Expand Down Expand Up @@ -199,15 +201,6 @@ public void ApplyToBeatmap(IBeatmap beatmap)
}
}

private void adjustPitchChanged(ValueChangedEvent<bool> adjustPitchSetting)
{
track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange);
track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange);
}

private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue)
=> adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo;

private IEnumerable<HitObject> getAllApplicableHitObjects(IEnumerable<HitObject> hitObjects)
{
foreach (var hitObject in hitObjects)
Expand Down
20 changes: 18 additions & 2 deletions osu.Game/Rulesets/Mods/ModDaycore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,36 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;

namespace osu.Game.Rulesets.Mods
{
public abstract class ModDaycore : ModHalfTime
public abstract class ModDaycore : ModRateAdjust
{
public override string Name => "Daycore";
public override string Acronym => "DC";
public override IconUsage? Icon => null;
public override ModType Type => ModType.DifficultyReduction;
public override LocalisableString Description => "Whoaaaaa...";

[SettingSource("Speed decrease", "The actual decrease to apply")]
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(0.75)
{
MinValue = 0.5,
MaxValue = 0.99,
Precision = 0.01,
};

private readonly BindableNumber<double> tempoAdjust = new BindableDouble(1);
private readonly BindableNumber<double> freqAdjust = new BindableDouble(1);
private readonly RateAdjustModHelper rateAdjustHelper;

protected ModDaycore()
{
rateAdjustHelper = new RateAdjustModHelper(SpeedChange);

// intentionally not deferring the speed change handling to `RateAdjustModHelper`
// as the expected result of operation is not the same (daycore should preserve constant pitch).
SpeedChange.BindValueChanged(val =>
{
freqAdjust.Value = SpeedChange.Default;
Expand All @@ -29,9 +44,10 @@ protected ModDaycore()

public override void ApplyToTrack(IAdjustableAudioComponent track)
{
// base.ApplyToTrack() intentionally not called (different tempo adjustment is applied)
track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust);
track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust);
}

public override double ScoreMultiplier => rateAdjustHelper.ScoreMultiplier;
}
}
26 changes: 14 additions & 12 deletions osu.Game/Rulesets/Mods/ModDoubleTime.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
Expand All @@ -26,21 +27,22 @@ public abstract class ModDoubleTime : ModRateAdjust
Precision = 0.01,
};

public override double ScoreMultiplier
{
get
{
// Round to the nearest multiple of 0.1.
double value = (int)(SpeedChange.Value * 10) / 10.0;
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public virtual BindableBool AdjustPitch { get; } = new BindableBool();

// Offset back to 0.
value -= 1;
private readonly RateAdjustModHelper rateAdjustHelper;

// Each 0.1 multiple changes score multiplier by 0.02.
value /= 5;
protected ModDoubleTime()
{
rateAdjustHelper = new RateAdjustModHelper(SpeedChange);
rateAdjustHelper.HandleAudioAdjustments(AdjustPitch);
}

return 1 + value;
}
public override void ApplyToTrack(IAdjustableAudioComponent track)
{
rateAdjustHelper.ApplyToTrack(track);
}

public override double ScoreMultiplier => rateAdjustHelper.ScoreMultiplier;
}
}
25 changes: 15 additions & 10 deletions osu.Game/Rulesets/Mods/ModHalfTime.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
Expand All @@ -26,18 +27,22 @@ public abstract class ModHalfTime : ModRateAdjust
Precision = 0.01,
};

public override double ScoreMultiplier
{
get
{
// Round to the nearest multiple of 0.1.
double value = (int)(SpeedChange.Value * 10) / 10.0;
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public virtual BindableBool AdjustPitch { get; } = new BindableBool();

private readonly RateAdjustModHelper rateAdjustHelper;

// Offset back to 0.
value -= 1;
protected ModHalfTime()
{
rateAdjustHelper = new RateAdjustModHelper(SpeedChange);
rateAdjustHelper.HandleAudioAdjustments(AdjustPitch);
}

return 1 + value;
}
public override void ApplyToTrack(IAdjustableAudioComponent track)
{
rateAdjustHelper.ApplyToTrack(track);
}

public override double ScoreMultiplier => rateAdjustHelper.ScoreMultiplier;
}
}
29 changes: 23 additions & 6 deletions osu.Game/Rulesets/Mods/ModNightcore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
Expand All @@ -19,22 +20,33 @@

namespace osu.Game.Rulesets.Mods
{
public abstract class ModNightcore : ModDoubleTime
public abstract class ModNightcore : ModRateAdjust
{
public override string Name => "Nightcore";
public override string Acronym => "NC";
public override IconUsage? Icon => OsuIcon.ModNightcore;
public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Uguuuuuuuu...";
}

public abstract partial class ModNightcore<TObject> : ModNightcore, IApplicableToDrawableRuleset<TObject>
where TObject : HitObject
{
[SettingSource("Speed increase", "The actual increase to apply")]
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(1.5)
{
MinValue = 1.01,
MaxValue = 2,
Precision = 0.01,
};

private readonly BindableNumber<double> tempoAdjust = new BindableDouble(1);
private readonly BindableNumber<double> freqAdjust = new BindableDouble(1);

private readonly RateAdjustModHelper rateAdjustHelper;

protected ModNightcore()
{
rateAdjustHelper = new RateAdjustModHelper(SpeedChange);

// intentionally not deferring the speed change handling to `RateAdjustModHelper`
// as the expected result of operation is not the same (nightcore should preserve constant pitch).
SpeedChange.BindValueChanged(val =>
{
freqAdjust.Value = SpeedChange.Default;
Expand All @@ -44,11 +56,16 @@ protected ModNightcore()

public override void ApplyToTrack(IAdjustableAudioComponent track)
{
// base.ApplyToTrack() intentionally not called (different tempo adjustment is applied)
track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust);
track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust);
}

public override double ScoreMultiplier => rateAdjustHelper.ScoreMultiplier;
}

public abstract partial class ModNightcore<TObject> : ModNightcore, IApplicableToDrawableRuleset<TObject>
where TObject : HitObject
{
public void ApplyToDrawableRuleset(DrawableRuleset<TObject> drawableRuleset)
{
drawableRuleset.Overlays.Add(new NightcoreBeatContainer());
Expand Down
5 changes: 1 addition & 4 deletions osu.Game/Rulesets/Mods/ModRateAdjust.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ public abstract class ModRateAdjust : Mod, IApplicableToRate

public abstract BindableNumber<double> SpeedChange { get; }

public virtual void ApplyToTrack(IAdjustableAudioComponent track)
{
track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange);
}
public abstract void ApplyToTrack(IAdjustableAudioComponent track);

public virtual void ApplyToSample(IAdjustableAudioComponent sample)
{
Expand Down
21 changes: 5 additions & 16 deletions osu.Game/Rulesets/Mods/ModTimeRamp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,21 @@ public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBea
Precision = 0.01,
};

private IAdjustableAudioComponent? track;
private readonly RateAdjustModHelper rateAdjustHelper;

protected ModTimeRamp()
{
rateAdjustHelper = new RateAdjustModHelper(SpeedChange);
rateAdjustHelper.HandleAudioAdjustments(AdjustPitch);

// for preview purpose at song select. eventually we'll want to be able to update every frame.
FinalRate.BindValueChanged(_ => applyRateAdjustment(double.PositiveInfinity), true);
AdjustPitch.BindValueChanged(applyPitchAdjustment);
}

public void ApplyToTrack(IAdjustableAudioComponent track)
{
this.track = track;

rateAdjustHelper.ApplyToTrack(track);
FinalRate.TriggerChange();
AdjustPitch.TriggerChange();
}

public void ApplyToSample(IAdjustableAudioComponent sample)
Expand Down Expand Up @@ -95,16 +95,5 @@ public virtual void Update(Playfield playfield)
/// Adjust the rate along the specified ramp.
/// </summary>
private void applyRateAdjustment(double time) => SpeedChange.Value = ApplyToRate(time);

private void applyPitchAdjustment(ValueChangedEvent<bool> adjustPitchSetting)
{
// remove existing old adjustment
track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange);

track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange);
}

private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue)
=> adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo;
}
}
84 changes: 84 additions & 0 deletions osu.Game/Rulesets/Mods/RateAdjustModHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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 osu.Framework.Audio;
using osu.Framework.Bindables;

namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// Provides common functionality shared across various rate adjust mods.
/// </summary>
public class RateAdjustModHelper : IApplicableToTrack
{
public readonly IBindableNumber<double> SpeedChange;

private IAdjustableAudioComponent? track;

private BindableBool? adjustPitch;

/// <summary>
/// The score multiplier for the current <see cref="SpeedChange"/>.
/// </summary>
public double ScoreMultiplier
{
get
{
// Round to the nearest multiple of 0.1.
double value = (int)(SpeedChange.Value * 10) / 10.0;

// Offset back to 0.
value -= 1;

if (SpeedChange.Value >= 1)
value /= 5;

return 1 + value;
}
}

/// <summary>
/// Construct a new <see cref="RateAdjustModHelper"/>.
/// </summary>
/// <param name="speedChange">The main speed adjust parameter which is exposed to the user.</param>
public RateAdjustModHelper(IBindableNumber<double> speedChange)
{
SpeedChange = speedChange;
}

/// <summary>
/// Setup audio track adjustments for a rate adjust mod.
/// Importantly, <see cref="ApplyToTrack"/> must be called when a track is obtained/changed for this to work.
/// </summary>
/// <param name="adjustPitch">The "adjust pitch" setting as exposed to the user.</param>
public void HandleAudioAdjustments(BindableBool adjustPitch)
{
this.adjustPitch = adjustPitch;

// When switching between pitch adjust, we need to update adjustments to time-shift or frequency-scale.
adjustPitch.BindValueChanged(adjustPitchSetting =>
{
track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange);
track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange);

AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue)
=> adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo;
});
}

/// <summary>
/// Should be invoked when a track is obtained / changed.
/// </summary>
/// <param name="track">The new track.</param>
/// <exception cref="InvalidOperationException">If this method is called before <see cref="HandleAudioAdjustments"/>.</exception>
public void ApplyToTrack(IAdjustableAudioComponent track)
{
if (adjustPitch == null)
throw new InvalidOperationException($"Must call {nameof(HandleAudioAdjustments)} first");

this.track = track;
adjustPitch.TriggerChange();
}
}
}