diff --git a/Osu.Patcher.Hook/Patches/Mods/AudioPreview/FixUpdatePlaybackRate.cs b/Osu.Patcher.Hook/Patches/Mods/AudioPreview/FixUpdatePlaybackRate.cs index 0064117..eb9825d 100644 --- a/Osu.Patcher.Hook/Patches/Mods/AudioPreview/FixUpdatePlaybackRate.cs +++ b/Osu.Patcher.Hook/Patches/Mods/AudioPreview/FixUpdatePlaybackRate.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using System.Linq; using System.Reflection; +using System.Reflection.Emit; using HarmonyLib; using JetBrains.Annotations; using Osu.Stubs.Audio; @@ -9,58 +11,109 @@ namespace Osu.Patcher.Hook.Patches.Mods.AudioPreview; /// -/// AudioTrackBass::updatePlaybackRate() forcibly resets the tempo to -/// normal if a pitch isn't applied. This forces the tempo to be set if pitch isn't applied, -/// to be used with the patch +/// Apply various fixes to AudioTrackBass to make BASS_ATTRIB_TEMPO work again on +/// "Preview" audio streams. We use this in combination with +/// to reapply the constant pitch speed modifier (for DoubleTime). /// [OsuPatch] [HarmonyPatch] [UsedImplicitly] -internal static class FixUpdatePlaybackRate +internal class FixUpdatePlaybackRate { - // static FixUpdatePlaybackRate() - // { - // Task.Run(async () => - // { - // await Task.Delay(2000); - // try - // { - // var mtd = AccessTools.Method(AudioTrackBass.Class.Reference.Name + ":" + - // AudioTrackBass.UpdatePlaybackRate.Reference.Name); - // foreach (var instruction in MethodReader.GetInstructions(mtd)) - // { - // Console.WriteLine($"{instruction.Opcode} {instruction.Operand}"); - // } - // } - // catch (Exception e) - // { - // Console.WriteLine(e); - // } - // }); - // } - [UsedImplicitly] [HarmonyTargetMethod] - private static MethodBase Target() => AudioTrackBass.UpdatePlaybackRate.Reference; + private static MethodBase Target() => AudioTrackBass.Constructor.Reference; - // TODO: WHY THE FUCK ISN'T THIS TRANSPILER APPLYING CHANGES????? PRE/POST PATCHES WORK FINE BUT THIS DOESN'T???????? [UsedImplicitly] [HarmonyTranspiler] - private static IEnumerable Transpiler(IEnumerable instructions) + private static IEnumerable Transpiler( + IEnumerable instructions, + ILGenerator generator) { - instructions = instructions.ManipulatorReplace( - // Find inst that loads 0f as the parameter "value" to BASS_ChannelSetAttribute - inst => inst.Is(Ldc_R4, 0f), - inst => new CodeInstruction[] + // 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 => { - new(Ldarg_0) { labels = inst.labels }, // Load "this" - new(Ldfld, AudioTrackBass.PlaybackRate.Reference), // Load the float64 "playbackRate" - new(Ldc_R8, 100.0), // Load the float64 "100.0" - new(Sub), // Subtract 100.0 from playbackRate - new(Conv_R4), // Convert to float32 + 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; } } \ No newline at end of file diff --git a/Osu.Patcher.Hook/Patches/Mods/AudioPreview/ModSelectAudioPreview.cs b/Osu.Patcher.Hook/Patches/Mods/AudioPreview/ModSelectAudioPreview.cs index 02e3e6e..bf5d051 100644 --- a/Osu.Patcher.Hook/Patches/Mods/AudioPreview/ModSelectAudioPreview.cs +++ b/Osu.Patcher.Hook/Patches/Mods/AudioPreview/ModSelectAudioPreview.cs @@ -26,7 +26,8 @@ private static void After( [HarmonyArgument(1)] int mod, [HarmonyArgument(2)] bool playSound) { - // These calls happen for all mods on any mod update and don't actually do anything + // These calls happen for all mods on any mod update + // and don't actually indicate a ModButton being pressed if (!playSound) return; // var availableModStates = ModButton.AvailableStates.Get(__instance); diff --git a/Osu.Stubs/Audio/AudioTrackBass.cs b/Osu.Stubs/Audio/AudioTrackBass.cs index 2933136..798d7b2 100644 --- a/Osu.Stubs/Audio/AudioTrackBass.cs +++ b/Osu.Stubs/Audio/AudioTrackBass.cs @@ -16,38 +16,91 @@ public class AudioTrackBass [Stub] public static readonly LazyType Class = new( "osu.Audio.AudioTrackBass", - () => UpdatePlaybackRate!.Reference.DeclaringType! + () => Constructor!.Reference.DeclaringType! ); /// - /// Original: updatePlaybackRate() + /// Original: AudioTrackBass(Stream data, bool quick = false, bool loop = false) /// b20240123: /// [Stub] - public static readonly LazyMethod UpdatePlaybackRate = LazyMethod.ByPartialSignature( - "osu.Audio.AudioTrackBass::updatePlaybackRate()", + public static readonly LazyConstructor Constructor = LazyConstructor.ByPartialSignature( + "osu.Audio.AudioTrackBass::AudioTrackBass(Stream, bool, bool)", [ - Conv_R8, + Newobj, + Throw, + Ldarg_0, + Ldarg_1, + Isinst, + Stfld, Ldarg_0, Ldfld, - Mul, - Ldc_R8, - Div, - Conv_R4, - Call, - Pop, ] ); /// - /// Original: playbackRate + /// Original: audioStreamBackwards /// b20240123: /// [Stub] - public static readonly LazyField PlaybackRate = new( - "osu.Audio.AudioTrackBass::playbackRate", + public static readonly LazyField AudioStreamBackwardsHandle = new( + "osu.Audio.AudioTrackBass::audioStreamBackwards", () => Class.Reference .GetDeclaredFields() - .Single(field => field.FieldType == typeof(double)) + .Where(field => field.FieldType == typeof(int)) + .Skip(1) + .First() ); + + // /// + // /// Original: updatePlaybackRate() + // /// b20240123: + // /// + // [Stub] + // public static readonly LazyMethod UpdatePlaybackRate = LazyMethod.ByPartialSignature( + // "osu.Audio.AudioTrackBass::updatePlaybackRate()", + // [ + // Conv_R8, + // Ldarg_0, + // Ldfld, + // Mul, + // Ldc_R8, + // Div, + // Conv_R4, + // Call, + // Pop, + // ] + // ); + + // /// + // /// Original: Play(bool restart = true) + // /// b20240123: + // /// + // [Stub] + // public static readonly LazyMethod Play = LazyMethod.BySignature( + // "osu.Audio.AudioTrackBass::Play(bool)", + // [ + // Ldarg_0, + // Ldc_I4_1, + // Stfld, + // Ldarg_0, + // Call, + // Ldarg_1, + // Call, + // Pop, + // Ret, + // ] + // ); + + // /// + // /// Original: playbackRate + // /// b20240123: + // /// + // [Stub] + // public static readonly LazyField PlaybackRate = new( + // "osu.Audio.AudioTrackBass::playbackRate", + // () => Class.Reference + // .GetDeclaredFields() + // .Single(field => field.FieldType == typeof(double)) + // ); } \ No newline at end of file