Skip to content

Commit

Permalink
feat: DT/NC/HT audio preview (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
rushiiMachine authored Apr 3, 2024
1 parent 79487d9 commit 33f6d0e
Show file tree
Hide file tree
Showing 12 changed files with 545 additions and 7 deletions.
121 changes: 121 additions & 0 deletions Osu.Patcher.Hook/Patches/Mods/AudioPreview/FixUpdatePlaybackRate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using JetBrains.Annotations;
using Osu.Stubs.Audio;
using Osu.Utils.Extensions;
using static System.Reflection.Emit.OpCodes;

// ReSharper disable InconsistentNaming

namespace Osu.Patcher.Hook.Patches.Mods.AudioPreview;

/// <summary>
/// Apply various fixes to <c>AudioTrackBass</c> to make <c>BASS_ATTRIB_TEMPO</c> work again on
/// "Preview" audio streams. We use this in combination with <see cref="ModSelectAudioPreview" />
/// to reapply the constant pitch speed modifier (for DoubleTime).
/// </summary>
[OsuPatch]
[HarmonyPatch]
[UsedImplicitly]
internal class FixUpdatePlaybackRate
{
[UsedImplicitly]
[HarmonyTargetMethod]
private static MethodBase Target() => AudioTrackBass.Constructor.Reference;

[UsedImplicitly]
[HarmonyTranspiler]
private static IEnumerable<CodeInstruction> Transpiler(
IEnumerable<CodeInstruction> instructions,
ILGenerator generator)
{
// Remove the conditional & block that checks for this.Preview and always do the full initialization
// Otherwise, the "quick" initialization never uses BASS_FX_TempoCreate, which makes setting
// BASSAttribute.BASS_ATTRIB_TEMPO impossible (what's used for DoubleTime).
// instructions = instructions.NoopSignature(
instructions = instructions.NoopSignature(
// if (Preview) { audioStream = audioStreamForwards = audioStreamPrefilter; }
[
Ldarg_0,
Callvirt,
Brfalse_S,
Ldarg_0,
Ldarg_0,
Ldarg_0,
Ldfld,
Dup,
Stloc_1,
Stfld,
Ldloc_1,
Call,
Br_S,
]
);

// Change this "BASSFlag flags = Preview ? 0 : (BASSFlag.BASS_STREAM_DECODE | BASSFlag.BASS_STREAM_PRESCAN);"
// into "BASSFlag flags = Preview ? BASSFlag.BASS_STREAM_DECODE : (BASSFlag.BASS_STREAM_DECODE | BASSFlag.BASS_STREAM_PRESCAN);"
// This is so BASS_FX_TempoCreate still works when called later
var foundFlags = false;
const int BASS_STREAM_DECODE = 0x200000;
const int BASS_STREAM_PRESCAN = 0x20000;
instructions = instructions.Manipulator(
inst => foundFlags || inst.Is(Ldc_I4, BASS_STREAM_DECODE | BASS_STREAM_PRESCAN),
inst =>
{
if (inst.OperandIs(BASS_STREAM_DECODE | BASS_STREAM_PRESCAN))
{
foundFlags = true;
return;
}

// This is the "? 0"
if (inst.opcode != Ldc_I4_0)
return;

inst.opcode = Ldc_I4;
inst.operand = BASS_STREAM_DECODE;
foundFlags = false;
}
);

// Load speed optimization: disable BASS_FX_ReverseCreate when the "quick" parameter is true (aka. Preview)
var found = false;
instructions = instructions.Reverse().ManipulatorReplace(
inst => found || inst.Is(Stfld, AudioTrackBass.AudioStreamBackwardsHandle.Reference),
inst =>
{
// Only targeting the Call instruction before Stfld
if (inst.opcode != Call)
{
found = true;
return [inst];
}

found = false;
var labelTrue = generator.DefineLabel();
var labelFalse = generator.DefineLabel();

return new[]
{
new(Ldarg_2), // "bool quick"
new(Brfalse_S, labelFalse),

// Clean up the 3 values to the Call on stack
new(Pop),
new(Pop),
new(Pop),
new(Ldc_I4_0), // Load a "0" to be used for Stfld AudioStreamBackwardsHandle
new(Br_S, labelTrue),

inst.WithLabels([labelFalse]),
new(Nop) { labels = [labelTrue] },
}.Reverse();
}
).Reverse();

return instructions;
}
}
61 changes: 61 additions & 0 deletions Osu.Patcher.Hook/Patches/Mods/AudioPreview/ModAudioEffects.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using Osu.Stubs.Audio;
using Osu.Stubs.Scoring;
using static Osu.Stubs.Other.Mods;

namespace Osu.Patcher.Hook.Patches.Mods.AudioPreview;

/// <summary>
/// Handles applying and resetting the audio effects on the AudioEngine.
/// </summary>
internal static class ModAudioEffects
{
/// <summary>
/// Applies the audio changes to the AudioEngine based on the current global mods.
/// </summary>
internal static void ApplyModEffects()
{
ResetChanges();

var mods = ModManager.ModStatus.Get();

// NC always comes with DT
if ((mods & Nightcore) > None)
mods &= ~DoubleTime;

switch (mods & (DoubleTime | Nightcore | HalfTime))
{
case DoubleTime:
UpdateAudioRate(rate => rate * 1.5);
break;
case Nightcore:
AudioEngine.Nightcore.Set(true);
UpdateAudioRate(rate => rate * 1.5);
break;
case HalfTime:
UpdateAudioRate(rate => rate * 0.75);
break;
}
}

/// <summary>
/// Resets the audio stream effects back to default.
/// </summary>
private static void ResetChanges()
{
AudioEngine.Nightcore.Set(false);
UpdateAudioRate(_ => 100);
}

/// <summary>
/// Gets, modifies, and writes back the CurrentPlaybackRate on the AudioEngine.
/// </summary>
/// <param name="onModify">Rate transformer.</param>
private static void UpdateAudioRate(Func<double, double> onModify)
{
var currentRate = AudioEngine.GetCurrentPlaybackRate.Invoke();
var newRate = onModify.Invoke(currentRate);

AudioEngine.SetCurrentPlaybackRate.Invoke(parameters: [newRate]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Reflection;
using System.Threading.Tasks;
using HarmonyLib;
using JetBrains.Annotations;
using Osu.Stubs.SongSelect;

namespace Osu.Patcher.Hook.Patches.Mods.AudioPreview;

/// <summary>
/// Hooks the place where ModButtons get updates in the mod selection menu to apply audio effects.
/// </summary>
[OsuPatch]
[HarmonyPatch]
[UsedImplicitly]
internal class ModSelectAudioPreview
{
[UsedImplicitly]
[HarmonyTargetMethod]
private static MethodBase Target() => ModSelection.UpdateMods.Reference;

[UsedImplicitly]
[HarmonyPostfix]
private static void After() => Task.Run(ModAudioEffects.ApplyModEffects);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Reflection;
using System.Threading.Tasks;
using HarmonyLib;
using JetBrains.Annotations;
using Osu.Stubs.Audio;

namespace Osu.Patcher.Hook.Patches.Mods.AudioPreview;

/// <summary>
/// Hooks the place where preview audio gets loaded to apply our mod audio effects.
/// </summary>
[OsuPatch]
[HarmonyPatch]
[UsedImplicitly]
public class TrackUpdatePreviewMusic
{
[UsedImplicitly]
[HarmonyTargetMethod]
private static MethodBase Target() => AudioEngine.LoadAudioForPreview.Reference;

[HarmonyPostfix]
[UsedImplicitly]
private static void After() => Task.Run(ModAudioEffects.ApplyModEffects);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using JetBrains.Annotations;
using Osu.Stubs.Rulesets;
using static System.Reflection.Emit.OpCodes;
using static Osu.Stubs.Other.Mods;

namespace Osu.Patcher.Hook.Patches.Mods;

Expand All @@ -24,11 +25,8 @@ namespace Osu.Patcher.Hook.Patches.Mods;
[OsuPatch]
[HarmonyPatch]
[UsedImplicitly]
internal static class PatchSuddenDeathAutoRetry
internal static class SuddenDeathAutoRetry
{
private const int ModPerfect = 1 << 14;
private const int ModSuddenDeath = 1 << 5;

[UsedImplicitly]
[HarmonyTargetMethod]
private static MethodBase Target() => Ruleset.Fail.Reference;
Expand All @@ -42,8 +40,8 @@ internal static class PatchSuddenDeathAutoRetry
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
instructions = instructions.Manipulator(
inst => inst.opcode == Ldc_I4 && inst.OperandIs(ModPerfect),
inst => inst.operand = ModPerfect | ModSuddenDeath
inst => inst.opcode == Ldc_I4 && inst.OperandIs(Perfect),
inst => inst.operand = Perfect | SuddenDeath
);

return instructions;
Expand Down
92 changes: 92 additions & 0 deletions Osu.Stubs/Audio/AudioEngine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Osu.Utils.IL;
using Osu.Utils.Lazy;
using static System.Reflection.Emit.OpCodes;

namespace Osu.Stubs.Audio;

/// <summary>
/// Original: <c>osu.Audio.AudioEngine</c>
/// b20240123: <c></c>
/// </summary>
[PublicAPI]
public class AudioEngine
{
/// <summary>
/// Original: <c>LoadAudioForPreview(Beatmap beatmap, bool continuePlayback, bool previewPoint, bool quick)</c>
/// b20240123: <c></c>
/// </summary>
[Stub]
public static readonly LazyMethod<bool> LoadAudioForPreview = LazyMethod<bool>.ByPartialSignature(
"osu.Audio.AudioEngine::LoadAudioForPreview(Beatmap, bool, bool, bool)",
[
Call,
Ldc_I4_0,
Ceq,
Stloc_0,
Leave_S,
Pop,
Ldc_I4,
Call,
]
);

/// <summary>
/// Original: <c>get_CurrentPlaybackRate()</c> (property getter)
/// b20240123: <c></c>
/// </summary>
[Stub]
public static readonly LazyMethod<double> GetCurrentPlaybackRate = LazyMethod<double>.BySignature(
"osu.Audio.AudioEngine::get_CurrentPlaybackRate()",
[
Ldsfld,
Dup,
Brtrue_S,
Pop,
Ldc_R8,
Ret,
Callvirt,
Ret,
]
);

/// <summary>
/// Original: <c>set_CurrentPlaybackRate()</c> (property setter)
/// b20240123: <c></c>
/// </summary>
[Stub]
public static readonly LazyMethod SetCurrentPlaybackRate = LazyMethod.ByPartialSignature(
"osu.Audio.AudioEngine::set_CurrentPlaybackRate()",
[
Ldsfld, // Reference to AudioEngine::Nightcore
Ldc_I4_0,
Ceq,
Callvirt,
Ldloc_0,
Ldarg_0,
Callvirt,
Ret,
]
);

/// <summary>
/// Original: <c>Nightcore</c>
/// b20240123: <c></c>
/// </summary>
[Stub]
public static readonly LazyField<bool> Nightcore = new(
"osu.Audio.AudioEngine::Nightcore",
() =>
{
// Last Ldsfld in get_CurrentPlaybackRate() is a reference to AudioEngine::Nightcore
var instruction = MethodReader
.GetInstructions(SetCurrentPlaybackRate.Reference)
.Reverse()
.First(inst => inst.Opcode == Ldsfld);

return (FieldInfo)instruction.Operand;
}
);
}
Loading

0 comments on commit 33f6d0e

Please sign in to comment.