From 1e7dcd508abcafa7e084586f089a1a43c75eb2a163f33db90d6b5e9b778580cf Mon Sep 17 00:00:00 2001 From: copecone Date: Thu, 9 Jul 2026 01:44:59 +0900 Subject: [PATCH] Add Sync Between AudioSource and SoundEvent --- Core/RenderManager.cs | 1 + Patch/Engine/AudioPatches.cs | 63 ++++++++- Patch/Engine/RenderEnvironmentPatches.cs | 1 + RenderMod.cs | 1 + Util/Audio/AudioResampler.cs | 101 ++++++++++++++ Util/{ => Audio}/AudioSpectrumGenerator.cs | 2 +- Util/{ => Audio}/OfflineAudioMixer.cs | 154 +++++++-------------- 7 files changed, 213 insertions(+), 110 deletions(-) create mode 100644 Util/Audio/AudioResampler.cs rename Util/{ => Audio}/AudioSpectrumGenerator.cs (99%) rename Util/{ => Audio}/OfflineAudioMixer.cs (69%) diff --git a/Core/RenderManager.cs b/Core/RenderManager.cs index 24a2731..d59fb16 100644 --- a/Core/RenderManager.cs +++ b/Core/RenderManager.cs @@ -7,6 +7,7 @@ using System.Buffers; using ADOFAIRenderer.Data; using ADOFAIRenderer.UI; using ADOFAIRenderer.Util; +using ADOFAIRenderer.Util.Audio; using ADOFAIRenderer.Util.FFmpeg; using FFmpeg.AutoGen; using HarmonyLib; diff --git a/Patch/Engine/AudioPatches.cs b/Patch/Engine/AudioPatches.cs index c780e9d..d7c73fa 100644 --- a/Patch/Engine/AudioPatches.cs +++ b/Patch/Engine/AudioPatches.cs @@ -1,6 +1,9 @@ +using System; using ADOFAIRenderer.Core; using ADOFAIRenderer.Util; +using ADOFAIRenderer.Util.Audio; using HarmonyLib; +using UnityEngine; using UnityEngine.Audio; namespace ADOFAIRenderer.Patch.Engine @@ -15,14 +18,21 @@ namespace ADOFAIRenderer.Patch.Engine { // Cache the clip on the main thread AudioClipCache.GetOrCache(snd, RenderManager.Instance.Configuration.AudioChannels, RenderManager.Instance.Configuration.AudioSampleRate, allowLoad: true); + } + return true; // Let the original method execute to prevent NullReferenceException + } + [HarmonyPostfix] + public static void Postfix(AudioSource __result, string snd, double time, AudioMixerGroup group, float volume, int priority) + { + if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.RenderAudio && __result != null) + { // Convert absolute DSP time to relative time in seconds from render start double relativeStartTime = time - RenderManager.Instance.dspTimeBase; - // Register event to the offline mixer (default pitch = 1.0f) - OfflineAudioMixer.RegisterSoundEvent(snd, relativeStartTime, -1.0, volume, 1.0f); + // Register event to the offline mixer (default pitch = 1.0f) and map it to the AudioSource + OfflineAudioMixer.RegisterSoundEvent(__result, snd, relativeStartTime, -1.0, volume, 1.0f); } - return true; // Let the original method execute to prevent NullReferenceException } } @@ -47,4 +57,51 @@ namespace ADOFAIRenderer.Patch.Engine return true; // Let the original method execute } } + + [HarmonyPatch(typeof(AudioSource), "pitch", MethodType.Setter)] + public static class AudioSourcePitchPatch + { + [HarmonyPostfix] + public static void Postfix(AudioSource __instance, float value) + { + if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.RenderAudio) + { + if (OfflineAudioMixer.TryGetSoundEvent(__instance, out var soundEvent)) + { + soundEvent.Pitch = value; + } + } + } + } + + + + [HarmonyPatch(typeof(AudioSource), "SetScheduledEndTime", typeof(double))] + public static class AudioSourceSetScheduledEndTimePatch + { + [HarmonyPostfix] + public static void Postfix(AudioSource __instance, double time) + { + if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.RenderAudio) + { + if (OfflineAudioMixer.TryGetSoundEvent(__instance, out var soundEvent)) + { + soundEvent.EndTime = time - RenderManager.Instance.dspTimeBase; + } + } + } + } + + [HarmonyPatch(typeof(AudioSource), "Stop", new Type[0])] + public static class AudioSourceStopPatch + { + [HarmonyPostfix] + public static void Postfix(AudioSource __instance) + { + if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.RenderAudio) + { + OfflineAudioMixer.RemoveSourceMapping(__instance); + } + } + } } diff --git a/Patch/Engine/RenderEnvironmentPatches.cs b/Patch/Engine/RenderEnvironmentPatches.cs index 703d32b..0fe972a 100644 --- a/Patch/Engine/RenderEnvironmentPatches.cs +++ b/Patch/Engine/RenderEnvironmentPatches.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using ADOFAIRenderer.Core; using ADOFAIRenderer.Util; +using ADOFAIRenderer.Util.Audio; using HarmonyLib; using UnityEngine; diff --git a/RenderMod.cs b/RenderMod.cs index 3c04fc0..272921f 100644 --- a/RenderMod.cs +++ b/RenderMod.cs @@ -1,5 +1,6 @@ using ADOFAIRenderer; using ADOFAIRenderer.Core; +using ADOFAIRenderer.Patch.Engine; using ADOFAIRenderer.Util; using FFmpeg.AutoGen; using MelonLoader; diff --git a/Util/Audio/AudioResampler.cs b/Util/Audio/AudioResampler.cs new file mode 100644 index 0000000..b2df1d4 --- /dev/null +++ b/Util/Audio/AudioResampler.cs @@ -0,0 +1,101 @@ +using System; + +namespace ADOFAIRenderer.Util.Audio +{ + public static class AudioResampler + { + public static float[] Resample(float[] inputSamples, int inputChannels, int inputFrequency, int targetChannels, int targetFrequency) + { + // ── Safety Validation ── + if (inputSamples.Length == 0 || + inputChannels <= 0 || targetChannels <= 0 || + inputFrequency <= 0 || targetFrequency <= 0) + { + return Array.Empty(); + } + + // If format is identical, return original samples + if (inputFrequency == targetFrequency && inputChannels == targetChannels) + return inputSamples; + + int inputLength = inputSamples.Length / inputChannels; + int targetLength = (int)Math.Floor((double)inputLength * targetFrequency / inputFrequency); + float[] outputSamples = new float[targetLength * targetChannels]; + + for (int i = 0; i < targetLength; i++) + { + double srcIndex = (double)i * inputFrequency / targetFrequency; + long idxLow = (long)Math.Floor(srcIndex); + long idxHigh = idxLow + 1; + float t = (float)(srcIndex - idxLow); + + if (idxHigh >= inputLength) idxHigh = inputLength - 1; + if (idxLow >= inputLength) idxLow = inputLength - 1; + + if (targetChannels == 1) // 1. Mono Output + { + float valLow, valHigh; + if (inputChannels == 1) // Mono -> Mono + { + valLow = inputSamples[idxLow]; + valHigh = inputSamples[idxHigh]; + } + else // Multi -> Mono (Downmix: Average all channels) + { + float sumLow = 0f; + float sumHigh = 0f; + for (int c = 0; c < inputChannels; c++) + { + sumLow += inputSamples[idxLow * inputChannels + c]; + sumHigh += inputSamples[idxHigh * inputChannels + c]; + } + valLow = sumLow / inputChannels; + valHigh = sumHigh / inputChannels; + } + outputSamples[i] = valLow + t * (valHigh - valLow); + } + else if (targetChannels == 2) // 2. Stereo Output + { + if (inputChannels == 1) // Mono -> Stereo (Duplicate to Left/Right) + { + float valLow = inputSamples[idxLow]; + float valHigh = inputSamples[idxHigh]; + float val = valLow + t * (valHigh - valLow); + outputSamples[i * 2] = val; + outputSamples[i * 2 + 1] = val; + } + else // Stereo/Multi -> Stereo (Map Left/Right, ignore extra channels) + { + float valLowL = inputSamples[idxLow * inputChannels]; + float valHighL = inputSamples[idxHigh * inputChannels]; + outputSamples[i * 2] = valLowL + t * (valHighL - valLowL); + + float valLowR = inputSamples[idxLow * inputChannels + 1]; + float valHighR = inputSamples[idxHigh * inputChannels + 1]; + outputSamples[i * 2 + 1] = valLowR + t * (valHighR - valLowR); + } + } + else // 3. Multi-channel Output (e.g., 5.1 Surround) + { + for (int c = 0; c < targetChannels; c++) + { + float valLow, valHigh; + if (c < inputChannels) + { + valLow = inputSamples[idxLow * inputChannels + c]; + valHigh = inputSamples[idxHigh * inputChannels + c]; + } + else + { + valLow = inputSamples[idxLow * inputChannels]; + valHigh = inputSamples[idxHigh * inputChannels]; + } + outputSamples[i * targetChannels + c] = valLow + t * (valHigh - valLow); + } + } + } + + return outputSamples; + } + } +} \ No newline at end of file diff --git a/Util/AudioSpectrumGenerator.cs b/Util/Audio/AudioSpectrumGenerator.cs similarity index 99% rename from Util/AudioSpectrumGenerator.cs rename to Util/Audio/AudioSpectrumGenerator.cs index 9e27c31..b6e223c 100644 --- a/Util/AudioSpectrumGenerator.cs +++ b/Util/Audio/AudioSpectrumGenerator.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using ADOFAIRenderer.Core; using UnityEngine; -namespace ADOFAIRenderer.Util +namespace ADOFAIRenderer.Util.Audio { public static class AudioSpectrumGenerator { diff --git a/Util/OfflineAudioMixer.cs b/Util/Audio/OfflineAudioMixer.cs similarity index 69% rename from Util/OfflineAudioMixer.cs rename to Util/Audio/OfflineAudioMixer.cs index 85eefd6..ddea03d 100644 --- a/Util/OfflineAudioMixer.cs +++ b/Util/Audio/OfflineAudioMixer.cs @@ -2,105 +2,8 @@ using System; using System.Collections.Generic; using UnityEngine; -namespace ADOFAIRenderer.Util +namespace ADOFAIRenderer.Util.Audio { - public static class AudioResampler - { - public static float[] Resample(float[] inputSamples, int inputChannels, int inputFrequency, int targetChannels, int targetFrequency) - { - // ── Safety Validation ── - if (inputSamples.Length == 0 || - inputChannels <= 0 || targetChannels <= 0 || - inputFrequency <= 0 || targetFrequency <= 0) - { - return Array.Empty(); - } - - // If format is identical, return original samples - if (inputFrequency == targetFrequency && inputChannels == targetChannels) - return inputSamples; - - int inputLength = inputSamples.Length / inputChannels; - int targetLength = (int)Math.Floor((double)inputLength * targetFrequency / inputFrequency); - float[] outputSamples = new float[targetLength * targetChannels]; - - for (int i = 0; i < targetLength; i++) - { - double srcIndex = (double)i * inputFrequency / targetFrequency; - long idxLow = (long)Math.Floor(srcIndex); - long idxHigh = idxLow + 1; - float t = (float)(srcIndex - idxLow); - - if (idxHigh >= inputLength) idxHigh = inputLength - 1; - if (idxLow >= inputLength) idxLow = inputLength - 1; - - if (targetChannels == 1) // 1. Mono Output - { - float valLow, valHigh; - if (inputChannels == 1) // Mono -> Mono - { - valLow = inputSamples[idxLow]; - valHigh = inputSamples[idxHigh]; - } - else // Multi -> Mono (Downmix: Average all channels) - { - float sumLow = 0f; - float sumHigh = 0f; - for (int c = 0; c < inputChannels; c++) - { - sumLow += inputSamples[idxLow * inputChannels + c]; - sumHigh += inputSamples[idxHigh * inputChannels + c]; - } - valLow = sumLow / inputChannels; - valHigh = sumHigh / inputChannels; - } - outputSamples[i] = valLow + t * (valHigh - valLow); - } - else if (targetChannels == 2) // 2. Stereo Output - { - if (inputChannels == 1) // Mono -> Stereo (Duplicate to Left/Right) - { - float valLow = inputSamples[idxLow]; - float valHigh = inputSamples[idxHigh]; - float val = valLow + t * (valHigh - valLow); - outputSamples[i * 2] = val; - outputSamples[i * 2 + 1] = val; - } - else // Stereo/Multi -> Stereo (Map Left/Right, ignore extra channels) - { - float valLowL = inputSamples[idxLow * inputChannels]; - float valHighL = inputSamples[idxHigh * inputChannels]; - outputSamples[i * 2] = valLowL + t * (valHighL - valLowL); - - float valLowR = inputSamples[idxLow * inputChannels + 1]; - float valHighR = inputSamples[idxHigh * inputChannels + 1]; - outputSamples[i * 2 + 1] = valLowR + t * (valHighR - valLowR); - } - } - else // 3. Multi-channel Output (e.g., 5.1 Surround) - { - for (int c = 0; c < targetChannels; c++) - { - float valLow, valHigh; - if (c < inputChannels) - { - valLow = inputSamples[idxLow * inputChannels + c]; - valHigh = inputSamples[idxHigh * inputChannels + c]; - } - else - { - valLow = inputSamples[idxLow * inputChannels]; - valHigh = inputSamples[idxHigh * inputChannels]; - } - outputSamples[i * targetChannels + c] = valLow + t * (valHigh - valLow); - } - } - } - - return outputSamples; - } - } - public static class AudioClipCache { private static readonly Dictionary Cache = new(); @@ -165,9 +68,9 @@ namespace ADOFAIRenderer.Util public static class OfflineAudioMixer { - public struct SoundEvent + public class SoundEvent { - public string ClipName; + public string ClipName = null!; public double StartTime; // In seconds relative to dspTimeBase public double EndTime; // In seconds relative to dspTimeBase (-1 if one-shot) public float Volume; @@ -175,11 +78,13 @@ namespace ADOFAIRenderer.Util } private static readonly List ActiveEvents = new(); + private static System.Runtime.CompilerServices.ConditionalWeakTable _sourceMappings = new(); private static readonly object Lock = new(); // Target Audio Settings private static int _targetChannels = 2; private static int _targetSampleRate = 44100; + private static readonly float MasterVolume = 0.2f; // BGM Cached Samples (Pre-resampled) private static float[]? _bgmSamples; @@ -221,18 +126,50 @@ namespace ADOFAIRenderer.Util } } - public static void RegisterSoundEvent(string clipName, double startTime, double endTime, float volume, float pitch) + public static void RegisterSoundEvent(AudioSource? source, string clipName, double startTime, double endTime, float volume, float pitch) { lock (Lock) { - ActiveEvents.Add(new SoundEvent + var ev = new SoundEvent { ClipName = clipName, StartTime = startTime, EndTime = endTime, Volume = volume, Pitch = pitch - }); + }; + ActiveEvents.Add(ev); + if (source != null) + { + _sourceMappings.Remove(source); + _sourceMappings.Add(source, ev); + } + } + } + + public static void RegisterSoundEvent(string clipName, double startTime, double endTime, float volume, float pitch) + { + RegisterSoundEvent(null, clipName, startTime, endTime, volume, pitch); + } + + public static bool TryGetSoundEvent(AudioSource? source, out SoundEvent ev) + { + if (source == null) + { + ev = null!; + return false; + } + lock (Lock) + { + return _sourceMappings.TryGetValue(source, out ev); + } + } + + public static bool RemoveSourceMapping(AudioSource source) + { + lock (Lock) + { + return _sourceMappings.Remove(source); } } @@ -241,6 +178,7 @@ namespace ADOFAIRenderer.Util lock (Lock) { ActiveEvents.Clear(); + _sourceMappings = new System.Runtime.CompilerServices.ConditionalWeakTable(); _bgmSamples = null; _bgmDuration = 0; AudioClipCache.Clear(); @@ -307,7 +245,7 @@ namespace ADOFAIRenderer.Util { float valLow = _bgmSamples[idxLow * _targetChannels + c]; float valHigh = _bgmSamples[idxHigh * _targetChannels + c]; - mixedBuffer[s * _targetChannels + c] += valLow + t * (valHigh - valLow); + mixedBuffer[s * _targetChannels + c] += (valLow + t * (valHigh - valLow)) * MasterVolume; } } } @@ -319,7 +257,11 @@ namespace ADOFAIRenderer.Util { var ev = ActiveEvents[i]; var clipData = AudioClipCache.GetOrCache(ev.ClipName, _targetChannels, _targetSampleRate, allowLoad: false); - if (clipData == null) continue; + if (clipData == null) + { + ActiveEvents.RemoveAt(i); + continue; + } bool isSoundFinished = false; int totalSamples = clipData.Samples.Length / _targetChannels; @@ -365,7 +307,7 @@ namespace ADOFAIRenderer.Util { float valLow = clipData.Samples[idxLow * _targetChannels + c]; float valHigh = clipData.Samples[idxHigh * _targetChannels + c]; - mixedBuffer[s * _targetChannels + c] += (valLow + t * (valHigh - valLow)) * ev.Volume; + mixedBuffer[s * _targetChannels + c] += (valLow + t * (valHigh - valLow)) * ev.Volume * MasterVolume; } }