SHA256
Refactor Every namespace Keyword Usage
This commit is contained in:
1 parent
4e67cb9b09
commit
27be2141f7
44 files changed
+4619
-4545
No files matched your search
@@ -1,101 +1,100 @@
|
||||
using System;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio
|
||||
namespace ADOFAIRenderer.Util.Audio;
|
||||
|
||||
internal static class AudioResampler
|
||||
{
|
||||
internal static class AudioResampler
|
||||
public static float[] Resample(float[] inputSamples, int inputChannels, int inputFrequency, int targetChannels, int targetFrequency)
|
||||
{
|
||||
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)
|
||||
{
|
||||
// ── Safety Validation ──
|
||||
if (inputSamples.Length == 0 ||
|
||||
inputChannels <= 0 || targetChannels <= 0 ||
|
||||
inputFrequency <= 0 || targetFrequency <= 0)
|
||||
return Array.Empty<float>();
|
||||
}
|
||||
|
||||
// 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
|
||||
{
|
||||
return Array.Empty<float>();
|
||||
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);
|
||||
}
|
||||
|
||||
// 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++)
|
||||
else if (targetChannels == 2) // 2. Stereo Output
|
||||
{
|
||||
double srcIndex = (double)i * inputFrequency / targetFrequency;
|
||||
long idxLow = (long)Math.Floor(srcIndex);
|
||||
long idxHigh = idxLow + 1;
|
||||
float t = (float)(srcIndex - idxLow);
|
||||
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);
|
||||
|
||||
if (idxHigh >= inputLength) idxHigh = inputLength - 1;
|
||||
if (idxLow >= inputLength) idxLow = inputLength - 1;
|
||||
|
||||
if (targetChannels == 1) // 1. Mono Output
|
||||
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 (inputChannels == 1) // Mono -> Mono
|
||||
if (c < inputChannels)
|
||||
{
|
||||
valLow = inputSamples[idxLow];
|
||||
valHigh = inputSamples[idxHigh];
|
||||
valLow = inputSamples[idxLow * inputChannels + c];
|
||||
valHigh = inputSamples[idxHigh * inputChannels + c];
|
||||
}
|
||||
else // Multi -> Mono (Downmix: Average all channels)
|
||||
else
|
||||
{
|
||||
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);
|
||||
valLow = inputSamples[idxLow * inputChannels];
|
||||
valHigh = inputSamples[idxHigh * inputChannels];
|
||||
}
|
||||
outputSamples[i * targetChannels + c] = valLow + t * (valHigh - valLow);
|
||||
}
|
||||
}
|
||||
|
||||
return outputSamples;
|
||||
}
|
||||
|
||||
return outputSamples;
|
||||
}
|
||||
}
|
||||
@@ -3,238 +3,239 @@ using System.Collections.Generic;
|
||||
using ADOFAIRenderer.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio
|
||||
namespace ADOFAIRenderer.Util.Audio;
|
||||
|
||||
internal static class AudioSpectrumGenerator
|
||||
{
|
||||
internal static class AudioSpectrumGenerator
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
public static float[]? AudioClipData { get; private set; }
|
||||
public static int Channels { get; private set; }
|
||||
public static int Frequency { get; private set; }
|
||||
public static int TotalSamples { get; private set; }
|
||||
// ReSharper restore MemberCanBePrivate.Global
|
||||
|
||||
private static long _cachedFrame = -1;
|
||||
private static readonly Dictionary<int, float[]> CachedSpectrums = new();
|
||||
|
||||
public static void Initialize(AudioClip? clip)
|
||||
{
|
||||
public static float[]? AudioClipData { get; private set; }
|
||||
public static int Channels { get; private set; }
|
||||
public static int Frequency { get; private set; }
|
||||
public static int TotalSamples { get; private set; }
|
||||
|
||||
private static long _cachedFrame = -1;
|
||||
private static readonly Dictionary<int, float[]> CachedSpectrums = new();
|
||||
|
||||
public static void Initialize(AudioClip? clip)
|
||||
if (!clip)
|
||||
{
|
||||
if (!clip)
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Channels = clip.channels;
|
||||
Frequency = clip.frequency;
|
||||
int samplesCount = clip.samples;
|
||||
|
||||
float[] rawSamples = new float[samplesCount * Channels];
|
||||
clip.GetData(rawSamples, 0);
|
||||
|
||||
if (Channels == 1)
|
||||
{
|
||||
Clear();
|
||||
return;
|
||||
AudioClipData = rawSamples;
|
||||
TotalSamples = samplesCount;
|
||||
}
|
||||
|
||||
try
|
||||
else
|
||||
{
|
||||
Channels = clip.channels;
|
||||
Frequency = clip.frequency;
|
||||
int samplesCount = clip.samples;
|
||||
|
||||
float[] rawSamples = new float[samplesCount * Channels];
|
||||
clip.GetData(rawSamples, 0);
|
||||
|
||||
if (Channels == 1)
|
||||
AudioClipData = new float[samplesCount];
|
||||
TotalSamples = samplesCount;
|
||||
for (int i = 0; i < samplesCount; i++)
|
||||
{
|
||||
AudioClipData = rawSamples;
|
||||
TotalSamples = samplesCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
AudioClipData = new float[samplesCount];
|
||||
TotalSamples = samplesCount;
|
||||
for (int i = 0; i < samplesCount; i++)
|
||||
float sum = 0f;
|
||||
for (int c = 0; c < Channels; c++)
|
||||
{
|
||||
float sum = 0f;
|
||||
for (int c = 0; c < Channels; c++)
|
||||
{
|
||||
sum += rawSamples[i * Channels + c];
|
||||
}
|
||||
AudioClipData[i] = sum / Channels;
|
||||
sum += rawSamples[i * Channels + c];
|
||||
}
|
||||
AudioClipData[i] = sum / Channels;
|
||||
}
|
||||
MelonLoader.MelonLogger.Msg($"[AudioSpectrumGenerator] Cached audio clip: {clip.name}, Samples: {TotalSamples}, Frequency: {Frequency}Hz, Channels: {Channels}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
MelonLoader.MelonLogger.Error($"[AudioSpectrumGenerator] Failed to initialize audio cache: {e}");
|
||||
Clear();
|
||||
}
|
||||
MelonLoader.MelonLogger.Msg($"[AudioSpectrumGenerator] Cached audio clip: {clip.name}, Samples: {TotalSamples}, Frequency: {Frequency}Hz, Channels: {Channels}");
|
||||
}
|
||||
|
||||
public static void Clear()
|
||||
catch (Exception e)
|
||||
{
|
||||
AudioClipData = null;
|
||||
TotalSamples = 0;
|
||||
Channels = 0;
|
||||
Frequency = 0;
|
||||
lock (CachedSpectrums)
|
||||
{
|
||||
_cachedFrame = -1;
|
||||
CachedSpectrums.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static void GetSpectrumData(float[] samples, int channel, FFTWindow window)
|
||||
{
|
||||
if (AudioClipData == null || AudioClipData.Length == 0 || !scrConductor.instance || !scrConductor.instance.hasSongStarted || !scrConductor.instance.song)
|
||||
{
|
||||
Array.Clear(samples, 0, samples.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
var conductor = scrConductor.instance;
|
||||
var pitch = conductor.song.pitch;
|
||||
var separateCountdownOffset = conductor.separateCountdownTime ?
|
||||
conductor.crotchetAtStart * conductor.adjustedCountdownTicks : 0.0;
|
||||
|
||||
// Calculate the actual scheduled start time of the song
|
||||
var songScheduledStartTime = conductor.skipOffset ?
|
||||
conductor.dspTimeSong + conductor.addoffset / pitch :
|
||||
conductor.dspTimeSong + separateCountdownOffset / pitch;
|
||||
|
||||
if (conductor.dspTime < songScheduledStartTime)
|
||||
{
|
||||
Array.Clear(samples, 0, samples.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
var songTime = (conductor.dspTime - conductor.dspTimeSong) * pitch - separateCountdownOffset;
|
||||
|
||||
var n = samples.Length;
|
||||
if (n == 0 || (n & (n - 1)) != 0) // Ensure it's a power of 2
|
||||
{
|
||||
Array.Clear(samples, 0, samples.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
var currentFrame = (RenderManager.Instance != null) ? RenderManager.Instance.frameCount : -1;
|
||||
|
||||
lock (CachedSpectrums)
|
||||
{
|
||||
if (currentFrame != _cachedFrame)
|
||||
{
|
||||
_cachedFrame = currentFrame;
|
||||
CachedSpectrums.Clear();
|
||||
}
|
||||
|
||||
if (CachedSpectrums.TryGetValue(n, out var cached))
|
||||
{
|
||||
Array.Copy(cached, samples, n);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var fftSize = 2 * n;
|
||||
var frequency = Frequency;
|
||||
var totalSamples = TotalSamples;
|
||||
var currentSampleIndex = (long)(songTime * frequency);
|
||||
|
||||
var windowSamples = new float[fftSize];
|
||||
for (var i = 0; i < fftSize; i++)
|
||||
{
|
||||
var idx = currentSampleIndex - fftSize / 2 + i;
|
||||
if (idx >= 0 && idx < totalSamples)
|
||||
{
|
||||
windowSamples[i] = AudioClipData[idx];
|
||||
}
|
||||
else
|
||||
{
|
||||
windowSamples[i] = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
var spectrum = new float[n];
|
||||
CalculateFFT(windowSamples, spectrum);
|
||||
|
||||
lock (CachedSpectrums)
|
||||
{
|
||||
CachedSpectrums[n] = spectrum;
|
||||
Array.Copy(spectrum, samples, n);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CalculateFFT(float[] samples, float[] spectrum)
|
||||
{
|
||||
var n = samples.Length;
|
||||
|
||||
// Apply Blackman-Harris window
|
||||
const float a0 = 0.35875f;
|
||||
const float a1 = 0.48829f;
|
||||
const float a2 = 0.14128f;
|
||||
const float a3 = 0.01168f;
|
||||
|
||||
var re = new float[n];
|
||||
var im = new float[n];
|
||||
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
var angle = 2.0 * Math.PI * i / (n - 1);
|
||||
var w = (float)(a0 - a1 * Math.Cos(angle) + a2 * Math.Cos(2.0 * angle) - a3 * Math.Cos(3.0 * angle));
|
||||
re[i] = samples[i] * w;
|
||||
im[i] = 0f;
|
||||
}
|
||||
|
||||
// Cooley-Tukey Radix-2 FFT
|
||||
var m = (int)Math.Round(Math.Log(n, 2));
|
||||
|
||||
// Bit-reversal
|
||||
var j = 0;
|
||||
for (var i = 0; i < n - 1; i++)
|
||||
{
|
||||
if (i < j)
|
||||
{
|
||||
var temp = re[i];
|
||||
re[i] = re[j];
|
||||
re[j] = temp;
|
||||
|
||||
temp = im[i];
|
||||
im[i] = im[j];
|
||||
im[j] = temp;
|
||||
}
|
||||
var k = n / 2;
|
||||
while (k <= j)
|
||||
{
|
||||
j -= k;
|
||||
k /= 2;
|
||||
}
|
||||
j += k;
|
||||
}
|
||||
|
||||
// FFT stages
|
||||
for (var stage = 1; stage <= m; stage++)
|
||||
{
|
||||
var le = 1 << stage;
|
||||
var le2 = le / 2;
|
||||
var ur = 1f;
|
||||
var ui = 0f;
|
||||
var wr = (float)Math.Cos(Math.PI / le2);
|
||||
var wi = -(float)Math.Sin(Math.PI / le2);
|
||||
|
||||
for (var sub = 0; sub < le2; sub++)
|
||||
{
|
||||
for (var item = sub; item < n; item += le)
|
||||
{
|
||||
var ip = item + le2;
|
||||
var tr = re[ip] * ur - im[ip] * ui;
|
||||
var ti = re[ip] * ui + im[ip] * ur;
|
||||
|
||||
re[ip] = re[item] - tr;
|
||||
im[ip] = im[item] - ti;
|
||||
re[item] += tr;
|
||||
im[item] += ti;
|
||||
}
|
||||
var tempUr = ur;
|
||||
ur = tempUr * wr - ui * wi;
|
||||
ui = tempUr * wi + ui * wr;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize and fill spectrum
|
||||
var factor = 2.0f / n;
|
||||
var outLen = Math.Min(spectrum.Length, n / 2);
|
||||
for (var i = 0; i < outLen; i++)
|
||||
{
|
||||
spectrum[i] = (float)Math.Sqrt(re[i] * re[i] + im[i] * im[i]) * factor;
|
||||
}
|
||||
MelonLoader.MelonLogger.Error($"[AudioSpectrumGenerator] Failed to initialize audio cache: {e}");
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
AudioClipData = null;
|
||||
TotalSamples = 0;
|
||||
Channels = 0;
|
||||
Frequency = 0;
|
||||
lock (CachedSpectrums)
|
||||
{
|
||||
_cachedFrame = -1;
|
||||
CachedSpectrums.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static void GetSpectrumData(float[] samples, int channel, FFTWindow window)
|
||||
{
|
||||
if (AudioClipData == null || AudioClipData.Length == 0 || !scrConductor.instance || !scrConductor.instance.hasSongStarted || !scrConductor.instance.song)
|
||||
{
|
||||
Array.Clear(samples, 0, samples.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
var conductor = scrConductor.instance;
|
||||
var pitch = conductor.song.pitch;
|
||||
var separateCountdownOffset = conductor.separateCountdownTime ?
|
||||
conductor.crotchetAtStart * conductor.adjustedCountdownTicks : 0.0;
|
||||
|
||||
// Calculate the actual scheduled start time of the song
|
||||
var songScheduledStartTime = conductor.skipOffset ?
|
||||
conductor.dspTimeSong + conductor.addoffset / pitch :
|
||||
conductor.dspTimeSong + separateCountdownOffset / pitch;
|
||||
|
||||
if (conductor.dspTime < songScheduledStartTime)
|
||||
{
|
||||
Array.Clear(samples, 0, samples.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
var songTime = (conductor.dspTime - conductor.dspTimeSong) * pitch - separateCountdownOffset;
|
||||
|
||||
var n = samples.Length;
|
||||
if (n == 0 || (n & (n - 1)) != 0) // Ensure it's a power of 2
|
||||
{
|
||||
Array.Clear(samples, 0, samples.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
var currentFrame = (RenderManager.Instance != null) ? RenderManager.Instance.frameCount : -1;
|
||||
|
||||
lock (CachedSpectrums)
|
||||
{
|
||||
if (currentFrame != _cachedFrame)
|
||||
{
|
||||
_cachedFrame = currentFrame;
|
||||
CachedSpectrums.Clear();
|
||||
}
|
||||
|
||||
if (CachedSpectrums.TryGetValue(n, out var cached))
|
||||
{
|
||||
Array.Copy(cached, samples, n);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var fftSize = 2 * n;
|
||||
var frequency = Frequency;
|
||||
var totalSamples = TotalSamples;
|
||||
var currentSampleIndex = (long)(songTime * frequency);
|
||||
|
||||
var windowSamples = new float[fftSize];
|
||||
for (var i = 0; i < fftSize; i++)
|
||||
{
|
||||
var idx = currentSampleIndex - fftSize / 2 + i;
|
||||
if (idx >= 0 && idx < totalSamples)
|
||||
{
|
||||
windowSamples[i] = AudioClipData[idx];
|
||||
}
|
||||
else
|
||||
{
|
||||
windowSamples[i] = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
var spectrum = new float[n];
|
||||
CalculateFFT(windowSamples, spectrum);
|
||||
|
||||
lock (CachedSpectrums)
|
||||
{
|
||||
CachedSpectrums[n] = spectrum;
|
||||
Array.Copy(spectrum, samples, n);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CalculateFFT(float[] samples, float[] spectrum)
|
||||
{
|
||||
var n = samples.Length;
|
||||
|
||||
// Apply Blackman-Harris window
|
||||
const float a0 = 0.35875f;
|
||||
const float a1 = 0.48829f;
|
||||
const float a2 = 0.14128f;
|
||||
const float a3 = 0.01168f;
|
||||
|
||||
var re = new float[n];
|
||||
var im = new float[n];
|
||||
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
var angle = 2.0 * Math.PI * i / (n - 1);
|
||||
var w = (float)(a0 - a1 * Math.Cos(angle) + a2 * Math.Cos(2.0 * angle) - a3 * Math.Cos(3.0 * angle));
|
||||
re[i] = samples[i] * w;
|
||||
im[i] = 0f;
|
||||
}
|
||||
|
||||
// Cooley-Tukey Radix-2 FFT
|
||||
var m = (int)Math.Round(Math.Log(n, 2));
|
||||
|
||||
// Bit-reversal
|
||||
var j = 0;
|
||||
for (var i = 0; i < n - 1; i++)
|
||||
{
|
||||
if (i < j)
|
||||
{
|
||||
var temp = re[i];
|
||||
re[i] = re[j];
|
||||
re[j] = temp;
|
||||
|
||||
temp = im[i];
|
||||
im[i] = im[j];
|
||||
im[j] = temp;
|
||||
}
|
||||
var k = n / 2;
|
||||
while (k <= j)
|
||||
{
|
||||
j -= k;
|
||||
k /= 2;
|
||||
}
|
||||
j += k;
|
||||
}
|
||||
|
||||
// FFT stages
|
||||
for (var stage = 1; stage <= m; stage++)
|
||||
{
|
||||
var le = 1 << stage;
|
||||
var le2 = le / 2;
|
||||
var ur = 1f;
|
||||
var ui = 0f;
|
||||
var wr = (float)Math.Cos(Math.PI / le2);
|
||||
var wi = -(float)Math.Sin(Math.PI / le2);
|
||||
|
||||
for (var sub = 0; sub < le2; sub++)
|
||||
{
|
||||
for (var item = sub; item < n; item += le)
|
||||
{
|
||||
var ip = item + le2;
|
||||
var tr = re[ip] * ur - im[ip] * ui;
|
||||
var ti = re[ip] * ui + im[ip] * ur;
|
||||
|
||||
re[ip] = re[item] - tr;
|
||||
im[ip] = im[item] - ti;
|
||||
re[item] += tr;
|
||||
im[item] += ti;
|
||||
}
|
||||
var tempUr = ur;
|
||||
ur = tempUr * wr - ui * wi;
|
||||
ui = tempUr * wi + ui * wr;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize and fill spectrum
|
||||
var factor = 2.0f / n;
|
||||
var outLen = Math.Min(spectrum.Length, n / 2);
|
||||
for (var i = 0; i < outLen; i++)
|
||||
{
|
||||
spectrum[i] = (float)Math.Sqrt(re[i] * re[i] + im[i] * im[i]) * factor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,40 @@
|
||||
using System;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio
|
||||
namespace ADOFAIRenderer.Util.Audio;
|
||||
|
||||
internal class AudioSyncCalculator
|
||||
{
|
||||
internal class AudioSyncCalculator
|
||||
{
|
||||
private readonly long _sampleRate;
|
||||
private readonly (long Numerator, long Denominator) _fpsRational;
|
||||
private readonly long _sampleRate;
|
||||
private readonly (long Numerator, long Denominator) _fpsRational;
|
||||
|
||||
// ReSharper disable once NotAccessedField.Local
|
||||
private long _currentSample;
|
||||
private long _remainder;
|
||||
// ReSharper disable once NotAccessedField.Local
|
||||
private long _currentSample;
|
||||
private long _remainder;
|
||||
|
||||
public AudioSyncCalculator(long sampleRate, double fps)
|
||||
{
|
||||
_sampleRate = sampleRate;
|
||||
_fpsRational = RationalUtil.Double2MaxPrecisionInt(fps);
|
||||
public AudioSyncCalculator(long sampleRate, double fps)
|
||||
{
|
||||
_sampleRate = sampleRate;
|
||||
_fpsRational = RationalUtil.Double2MaxPrecisionInt(fps);
|
||||
|
||||
// NTSC
|
||||
if (Math.Abs(fps - 29.97) < 0.001) _fpsRational = (30000, 1001);
|
||||
if (Math.Abs(fps - 59.94) < 0.001) _fpsRational = (60000, 1001);
|
||||
if (Math.Abs(fps - 23.976) < 0.001 || Math.Abs(fps - 23.98) < 0.001) _fpsRational = (24000, 1001);
|
||||
}
|
||||
// NTSC
|
||||
if (Math.Abs(fps - 29.97) < 0.001) _fpsRational = (30000, 1001);
|
||||
if (Math.Abs(fps - 59.94) < 0.001) _fpsRational = (60000, 1001);
|
||||
if (Math.Abs(fps - 23.976) < 0.001 || Math.Abs(fps - 23.98) < 0.001) _fpsRational = (24000, 1001);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_currentSample = 0;
|
||||
_remainder = 0;
|
||||
}
|
||||
public void Reset()
|
||||
{
|
||||
_currentSample = 0;
|
||||
_remainder = 0;
|
||||
}
|
||||
|
||||
public int GetNextFrameSamples()
|
||||
{
|
||||
var numerator = _sampleRate * _fpsRational.Denominator + _remainder;
|
||||
var samplesNeeded = numerator / _fpsRational.Numerator;
|
||||
public int GetNextFrameSamples()
|
||||
{
|
||||
var numerator = _sampleRate * _fpsRational.Denominator + _remainder;
|
||||
var samplesNeeded = numerator / _fpsRational.Numerator;
|
||||
|
||||
_remainder = numerator % _fpsRational.Numerator;
|
||||
_currentSample += samplesNeeded;
|
||||
return (int)samplesNeeded;
|
||||
}
|
||||
_remainder = numerator % _fpsRational.Numerator;
|
||||
_currentSample += samplesNeeded;
|
||||
return (int)samplesNeeded;
|
||||
}
|
||||
}
|
||||
+17
-18
@@ -1,24 +1,23 @@
|
||||
using System;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter;
|
||||
|
||||
internal class Gain : IAudioFilter
|
||||
{
|
||||
internal class Gain : IAudioFilter
|
||||
public float Level = 0f;
|
||||
|
||||
public void Initialize(int sampleRate, int channels) { }
|
||||
|
||||
public float ProcessSample(float sample, int channel) =>
|
||||
sample * (float)Math.Pow(10, Level / 20);
|
||||
|
||||
public void ProcessBlock(Span<float> buffer)
|
||||
{
|
||||
public float Level = 0f;
|
||||
|
||||
public void Initialize(int sampleRate, int channels) { }
|
||||
|
||||
public float ProcessSample(float sample, int channel) =>
|
||||
sample * (float)Math.Pow(10, Level / 20);
|
||||
|
||||
public void ProcessBlock(Span<float> buffer)
|
||||
{
|
||||
for (var i = 0; i < buffer.Length; i++)
|
||||
buffer[i] = ProcessSample(buffer[i], 0);
|
||||
}
|
||||
|
||||
public void Reset() { }
|
||||
|
||||
public double Latency => 0.0;
|
||||
for (var i = 0; i < buffer.Length; i++)
|
||||
buffer[i] = ProcessSample(buffer[i], 0);
|
||||
}
|
||||
|
||||
public void Reset() { }
|
||||
|
||||
public double Latency => 0.0;
|
||||
}
|
||||
@@ -1,33 +1,32 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter;
|
||||
|
||||
internal class HardClip : IAudioFilter
|
||||
{
|
||||
internal class HardClip : IAudioFilter
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
// ReSharper disable once FieldCanBeMadeReadOnly.Global
|
||||
// ReSharper disable once ConvertToConstant.Global
|
||||
public float Threshold = 1.0f;
|
||||
|
||||
public void Initialize(int sampleRate, int channels) { }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public float ProcessSample(float sample, int channel)
|
||||
{
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
// ReSharper disable once FieldCanBeMadeReadOnly.Global
|
||||
// ReSharper disable once ConvertToConstant.Global
|
||||
public float Threshold = 1.0f;
|
||||
|
||||
public void Initialize(int sampleRate, int channels) { }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public float ProcessSample(float sample, int channel)
|
||||
{
|
||||
if (sample > Threshold) return Threshold;
|
||||
if (sample < -Threshold) return -Threshold;
|
||||
return sample;
|
||||
}
|
||||
|
||||
public void ProcessBlock(Span<float> buffer)
|
||||
{
|
||||
for (var i = 0; i < buffer.Length; i++)
|
||||
buffer[i] = ProcessSample(buffer[i], 0);
|
||||
}
|
||||
|
||||
public void Reset() { }
|
||||
|
||||
public double Latency => 0.0;
|
||||
if (sample > Threshold) return Threshold;
|
||||
if (sample < -Threshold) return -Threshold;
|
||||
return sample;
|
||||
}
|
||||
|
||||
public void ProcessBlock(Span<float> buffer)
|
||||
{
|
||||
for (var i = 0; i < buffer.Length; i++)
|
||||
buffer[i] = ProcessSample(buffer[i], 0);
|
||||
}
|
||||
|
||||
public void Reset() { }
|
||||
|
||||
public double Latency => 0.0;
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter
|
||||
{
|
||||
internal interface IAudioFilter
|
||||
{
|
||||
void Initialize(int sampleRate, int channels);
|
||||
|
||||
float ProcessSample(float sample, int channel);
|
||||
void ProcessBlock(Span<float> buffer);
|
||||
|
||||
void Reset();
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter;
|
||||
|
||||
double Latency { get; }
|
||||
}
|
||||
internal interface IAudioFilter
|
||||
{
|
||||
void Initialize(int sampleRate, int channels);
|
||||
|
||||
float ProcessSample(float sample, int channel);
|
||||
void ProcessBlock(Span<float> buffer);
|
||||
|
||||
void Reset();
|
||||
|
||||
double Latency { get; }
|
||||
}
|
||||
+219
-220
@@ -1,229 +1,228 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter
|
||||
namespace ADOFAIRenderer.Util.Audio.Filter;
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
internal class RMSCompressor : IAudioFilter
|
||||
{
|
||||
// ReSharper disable once InconsistentNaming
|
||||
internal class RMSCompressor : IAudioFilter
|
||||
private float _thresholdDb = -20f;
|
||||
private float _ratio = 4f;
|
||||
private float _attackTime = 0.010f; // in seconds
|
||||
private float _releaseTime = 0.100f; // in seconds
|
||||
private float _kneeDb = 5f; // in dB
|
||||
private float _rmsWindowTime = 0.030f; // in seconds
|
||||
private float _lookAheadTime; // in seconds
|
||||
private int _lookAheadSamples;
|
||||
|
||||
private int _sampleRate = 44100;
|
||||
private int _channels = 2;
|
||||
|
||||
private float _attackCoef;
|
||||
private float _releaseCoef;
|
||||
private float _rmsCoef;
|
||||
|
||||
// Per-channel state
|
||||
private float[] _rmsState = Array.Empty<float>(); // running mean-square (MS) state
|
||||
private float[] _gainState = Array.Empty<float>(); // smoothed gain reduction in dB
|
||||
private float[][] _delayBuffers = Array.Empty<float[]>(); // circular delay buffers
|
||||
private int[] _delayWriteIndices = Array.Empty<int>();
|
||||
|
||||
public float LookAheadTime
|
||||
{
|
||||
private float _thresholdDb = -20f;
|
||||
private float _ratio = 4f;
|
||||
private float _attackTime = 0.010f; // in seconds
|
||||
private float _releaseTime = 0.100f; // in seconds
|
||||
private float _kneeDb = 5f; // in dB
|
||||
private float _rmsWindowTime = 0.030f; // in seconds
|
||||
private float _lookAheadTime; // in seconds
|
||||
private int _lookAheadSamples;
|
||||
|
||||
private int _sampleRate = 44100;
|
||||
private int _channels = 2;
|
||||
|
||||
private float _attackCoef;
|
||||
private float _releaseCoef;
|
||||
private float _rmsCoef;
|
||||
|
||||
// Per-channel state
|
||||
private float[] _rmsState = Array.Empty<float>(); // running mean-square (MS) state
|
||||
private float[] _gainState = Array.Empty<float>(); // smoothed gain reduction in dB
|
||||
private float[][] _delayBuffers = Array.Empty<float[]>(); // circular delay buffers
|
||||
private int[] _delayWriteIndices = Array.Empty<int>();
|
||||
|
||||
public float LookAheadTime
|
||||
get => _lookAheadTime;
|
||||
set
|
||||
{
|
||||
get => _lookAheadTime;
|
||||
set
|
||||
{
|
||||
_lookAheadTime = Math.Max(0f, Math.Min(1.0f, value));
|
||||
UpdateLookAheadSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public double Latency => _lookAheadTime;
|
||||
|
||||
public float ThresholdDb
|
||||
{
|
||||
get => _thresholdDb;
|
||||
set => _thresholdDb = value;
|
||||
}
|
||||
|
||||
public float Ratio
|
||||
{
|
||||
get => _ratio;
|
||||
set => _ratio = Math.Max(1f, value);
|
||||
}
|
||||
|
||||
public float AttackTime
|
||||
{
|
||||
get => _attackTime;
|
||||
set
|
||||
{
|
||||
_attackTime = Math.Max(0.0001f, value);
|
||||
UpdateAttackCoef();
|
||||
}
|
||||
}
|
||||
|
||||
public float ReleaseTime
|
||||
{
|
||||
get => _releaseTime;
|
||||
set
|
||||
{
|
||||
_releaseTime = Math.Max(0.0001f, value);
|
||||
UpdateReleaseCoef();
|
||||
}
|
||||
}
|
||||
|
||||
public float KneeDb
|
||||
{
|
||||
get => _kneeDb;
|
||||
set => _kneeDb = Math.Max(0f, value);
|
||||
}
|
||||
|
||||
public float RmsWindowTime
|
||||
{
|
||||
get => _rmsWindowTime;
|
||||
set
|
||||
{
|
||||
_rmsWindowTime = Math.Max(0.0001f, value);
|
||||
UpdateRmsCoef();
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize(int sampleRate, int channels)
|
||||
{
|
||||
_sampleRate = sampleRate;
|
||||
_channels = channels;
|
||||
|
||||
UpdateAttackCoef();
|
||||
UpdateReleaseCoef();
|
||||
UpdateRmsCoef();
|
||||
_lookAheadTime = Math.Max(0f, Math.Min(1.0f, value));
|
||||
UpdateLookAheadSettings();
|
||||
|
||||
_rmsState = new float[channels];
|
||||
_gainState = new float[channels];
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
private void UpdateLookAheadSettings()
|
||||
{
|
||||
_lookAheadSamples = (int)Math.Round(_sampleRate * _lookAheadTime);
|
||||
ReallocateDelayBuffers();
|
||||
}
|
||||
|
||||
private void ReallocateDelayBuffers()
|
||||
{
|
||||
if (_channels <= 0) return;
|
||||
|
||||
_delayBuffers = new float[_channels][];
|
||||
_delayWriteIndices = new int[_channels];
|
||||
|
||||
for (int i = 0; i < _channels; i++)
|
||||
{
|
||||
_delayBuffers[i] = _lookAheadSamples > 0 ? new float[_lookAheadSamples] : Array.Empty<float>();
|
||||
_delayWriteIndices[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAttackCoef()
|
||||
{
|
||||
_attackCoef = (float)Math.Exp(-1.0 / (_sampleRate * _attackTime));
|
||||
}
|
||||
|
||||
private void UpdateReleaseCoef()
|
||||
{
|
||||
_releaseCoef = (float)Math.Exp(-1.0 / (_sampleRate * _releaseTime));
|
||||
}
|
||||
|
||||
private void UpdateRmsCoef()
|
||||
{
|
||||
_rmsCoef = (float)Math.Exp(-1.0 / (_sampleRate * _rmsWindowTime));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public float ProcessSample(float sample, int channel)
|
||||
{
|
||||
if (channel < 0 || channel >= _channels)
|
||||
{
|
||||
return sample;
|
||||
}
|
||||
|
||||
// Calculate running Mean-Square (MS) level
|
||||
// v_c[n] = alpha * v_c[n-1] + (1 - alpha) * x^2[n]
|
||||
float sq = sample * sample;
|
||||
_rmsState[channel] = _rmsCoef * _rmsState[channel] + (1.0f - _rmsCoef) * sq;
|
||||
|
||||
float rmsSq = Math.Max(0f, _rmsState[channel]);
|
||||
// Convert to dB. 10 * log10(rmsSq) is equivalent to 20 * log10(sqrt(rmsSq))
|
||||
float rmsDb = 10.0f * (float)Math.Log10(rmsSq + 1e-12f);
|
||||
|
||||
// Calculate static envelope curve (gain reduction in dB)
|
||||
float gStatic = 0f;
|
||||
if (_kneeDb > 0f)
|
||||
{
|
||||
if (rmsDb > _thresholdDb + _kneeDb * 0.5f)
|
||||
{
|
||||
gStatic = (1f / _ratio - 1f) * (rmsDb - _thresholdDb);
|
||||
}
|
||||
else if (rmsDb >= _thresholdDb - _kneeDb * 0.5f)
|
||||
{
|
||||
float diff = rmsDb - _thresholdDb + _kneeDb * 0.5f;
|
||||
gStatic = (1f / _ratio - 1f) * diff * diff / (2f * _kneeDb);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rmsDb > _thresholdDb)
|
||||
{
|
||||
gStatic = (1f / _ratio - 1f) * (rmsDb - _thresholdDb);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply ballistics (attack and release) on the dB gain reduction
|
||||
float lastGain = _gainState[channel];
|
||||
float targetGain = gStatic;
|
||||
|
||||
// Note: Since targetGain and lastGain are <= 0 (gain reduction in dB),
|
||||
// targetGain < lastGain means target gain reduction is larger (more negative),
|
||||
// so we are in the attack phase.
|
||||
float coef = targetGain < lastGain ? _attackCoef : _releaseCoef;
|
||||
float currentGain = coef * lastGain + (1f - coef) * targetGain;
|
||||
_gainState[channel] = currentGain;
|
||||
|
||||
// Convert back to linear gain multiplier
|
||||
float linearGain = (float)Math.Pow(10.0, currentGain / 20.0);
|
||||
|
||||
if (_lookAheadSamples > 0 && _delayBuffers.Length > channel && _delayBuffers[channel].Length > 0)
|
||||
{
|
||||
var buffer = _delayBuffers[channel];
|
||||
int writeIdx = _delayWriteIndices[channel];
|
||||
float delayedSample = buffer[writeIdx];
|
||||
buffer[writeIdx] = sample;
|
||||
_delayWriteIndices[channel] = (writeIdx + 1) % _lookAheadSamples;
|
||||
return delayedSample * linearGain;
|
||||
}
|
||||
|
||||
return sample * linearGain;
|
||||
}
|
||||
|
||||
public void ProcessBlock(Span<float> buffer)
|
||||
{
|
||||
for (var i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
int channel = i % _channels;
|
||||
buffer[i] = ProcessSample(buffer[i], channel);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Array.Clear(_rmsState, 0, _rmsState.Length);
|
||||
Array.Clear(_gainState, 0, _gainState.Length);
|
||||
|
||||
for (int i = 0; i < _delayBuffers.Length; i++)
|
||||
{
|
||||
Array.Clear(_delayBuffers[i], 0, _delayBuffers[i].Length);
|
||||
}
|
||||
Array.Clear(_delayWriteIndices, 0, _delayWriteIndices.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double Latency => _lookAheadTime;
|
||||
|
||||
public float ThresholdDb
|
||||
{
|
||||
get => _thresholdDb;
|
||||
set => _thresholdDb = value;
|
||||
}
|
||||
|
||||
public float Ratio
|
||||
{
|
||||
get => _ratio;
|
||||
set => _ratio = Math.Max(1f, value);
|
||||
}
|
||||
|
||||
public float AttackTime
|
||||
{
|
||||
get => _attackTime;
|
||||
set
|
||||
{
|
||||
_attackTime = Math.Max(0.0001f, value);
|
||||
UpdateAttackCoef();
|
||||
}
|
||||
}
|
||||
|
||||
public float ReleaseTime
|
||||
{
|
||||
get => _releaseTime;
|
||||
set
|
||||
{
|
||||
_releaseTime = Math.Max(0.0001f, value);
|
||||
UpdateReleaseCoef();
|
||||
}
|
||||
}
|
||||
|
||||
public float KneeDb
|
||||
{
|
||||
get => _kneeDb;
|
||||
set => _kneeDb = Math.Max(0f, value);
|
||||
}
|
||||
|
||||
public float RmsWindowTime
|
||||
{
|
||||
get => _rmsWindowTime;
|
||||
set
|
||||
{
|
||||
_rmsWindowTime = Math.Max(0.0001f, value);
|
||||
UpdateRmsCoef();
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize(int sampleRate, int channels)
|
||||
{
|
||||
_sampleRate = sampleRate;
|
||||
_channels = channels;
|
||||
|
||||
UpdateAttackCoef();
|
||||
UpdateReleaseCoef();
|
||||
UpdateRmsCoef();
|
||||
UpdateLookAheadSettings();
|
||||
|
||||
_rmsState = new float[channels];
|
||||
_gainState = new float[channels];
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
private void UpdateLookAheadSettings()
|
||||
{
|
||||
_lookAheadSamples = (int)Math.Round(_sampleRate * _lookAheadTime);
|
||||
ReallocateDelayBuffers();
|
||||
}
|
||||
|
||||
private void ReallocateDelayBuffers()
|
||||
{
|
||||
if (_channels <= 0) return;
|
||||
|
||||
_delayBuffers = new float[_channels][];
|
||||
_delayWriteIndices = new int[_channels];
|
||||
|
||||
for (int i = 0; i < _channels; i++)
|
||||
{
|
||||
_delayBuffers[i] = _lookAheadSamples > 0 ? new float[_lookAheadSamples] : Array.Empty<float>();
|
||||
_delayWriteIndices[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAttackCoef()
|
||||
{
|
||||
_attackCoef = (float)Math.Exp(-1.0 / (_sampleRate * _attackTime));
|
||||
}
|
||||
|
||||
private void UpdateReleaseCoef()
|
||||
{
|
||||
_releaseCoef = (float)Math.Exp(-1.0 / (_sampleRate * _releaseTime));
|
||||
}
|
||||
|
||||
private void UpdateRmsCoef()
|
||||
{
|
||||
_rmsCoef = (float)Math.Exp(-1.0 / (_sampleRate * _rmsWindowTime));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public float ProcessSample(float sample, int channel)
|
||||
{
|
||||
if (channel < 0 || channel >= _channels)
|
||||
{
|
||||
return sample;
|
||||
}
|
||||
|
||||
// Calculate running Mean-Square (MS) level
|
||||
// v_c[n] = alpha * v_c[n-1] + (1 - alpha) * x^2[n]
|
||||
float sq = sample * sample;
|
||||
_rmsState[channel] = _rmsCoef * _rmsState[channel] + (1.0f - _rmsCoef) * sq;
|
||||
|
||||
float rmsSq = Math.Max(0f, _rmsState[channel]);
|
||||
// Convert to dB. 10 * log10(rmsSq) is equivalent to 20 * log10(sqrt(rmsSq))
|
||||
float rmsDb = 10.0f * (float)Math.Log10(rmsSq + 1e-12f);
|
||||
|
||||
// Calculate static envelope curve (gain reduction in dB)
|
||||
float gStatic = 0f;
|
||||
if (_kneeDb > 0f)
|
||||
{
|
||||
if (rmsDb > _thresholdDb + _kneeDb * 0.5f)
|
||||
{
|
||||
gStatic = (1f / _ratio - 1f) * (rmsDb - _thresholdDb);
|
||||
}
|
||||
else if (rmsDb >= _thresholdDb - _kneeDb * 0.5f)
|
||||
{
|
||||
float diff = rmsDb - _thresholdDb + _kneeDb * 0.5f;
|
||||
gStatic = (1f / _ratio - 1f) * diff * diff / (2f * _kneeDb);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rmsDb > _thresholdDb)
|
||||
{
|
||||
gStatic = (1f / _ratio - 1f) * (rmsDb - _thresholdDb);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply ballistics (attack and release) on the dB gain reduction
|
||||
float lastGain = _gainState[channel];
|
||||
float targetGain = gStatic;
|
||||
|
||||
// Note: Since targetGain and lastGain are <= 0 (gain reduction in dB),
|
||||
// targetGain < lastGain means target gain reduction is larger (more negative),
|
||||
// so we are in the attack phase.
|
||||
float coef = targetGain < lastGain ? _attackCoef : _releaseCoef;
|
||||
float currentGain = coef * lastGain + (1f - coef) * targetGain;
|
||||
_gainState[channel] = currentGain;
|
||||
|
||||
// Convert back to linear gain multiplier
|
||||
float linearGain = (float)Math.Pow(10.0, currentGain / 20.0);
|
||||
|
||||
if (_lookAheadSamples > 0 && _delayBuffers.Length > channel && _delayBuffers[channel].Length > 0)
|
||||
{
|
||||
var buffer = _delayBuffers[channel];
|
||||
int writeIdx = _delayWriteIndices[channel];
|
||||
float delayedSample = buffer[writeIdx];
|
||||
buffer[writeIdx] = sample;
|
||||
_delayWriteIndices[channel] = (writeIdx + 1) % _lookAheadSamples;
|
||||
return delayedSample * linearGain;
|
||||
}
|
||||
|
||||
return sample * linearGain;
|
||||
}
|
||||
|
||||
public void ProcessBlock(Span<float> buffer)
|
||||
{
|
||||
for (var i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
int channel = i % _channels;
|
||||
buffer[i] = ProcessSample(buffer[i], channel);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Array.Clear(_rmsState, 0, _rmsState.Length);
|
||||
Array.Clear(_gainState, 0, _gainState.Length);
|
||||
|
||||
foreach (var delayBuffer in _delayBuffers)
|
||||
{
|
||||
Array.Clear(delayBuffer, 0, delayBuffer.Length);
|
||||
}
|
||||
Array.Clear(_delayWriteIndices, 0, _delayWriteIndices.Length);
|
||||
}
|
||||
}
|
||||
+334
-335
@@ -5,413 +5,412 @@ using ADOFAIRenderer.Util.Audio.Filter;
|
||||
using MelonLoader;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ADOFAIRenderer.Util.Audio
|
||||
namespace ADOFAIRenderer.Util.Audio;
|
||||
|
||||
internal class AudioClipCache
|
||||
{
|
||||
internal class AudioClipCache
|
||||
private readonly Dictionary<string, CachedClipData> _cache = new();
|
||||
|
||||
public class CachedClipData
|
||||
{
|
||||
private readonly Dictionary<string, CachedClipData> _cache = new();
|
||||
public float[] Samples = null!; // Pre-resampled to target channels and sample rate
|
||||
public double Duration;
|
||||
}
|
||||
|
||||
public class CachedClipData
|
||||
public CachedClipData? GetOrCache(string clipName, int targetChannels, int targetSampleRate, bool allowLoad = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(clipName)) return null;
|
||||
|
||||
lock (_cache)
|
||||
{
|
||||
public float[] Samples = null!; // Pre-resampled to target channels and sample rate
|
||||
public double Duration;
|
||||
}
|
||||
if (_cache.TryGetValue(clipName, out var cached))
|
||||
return cached;
|
||||
|
||||
public CachedClipData? GetOrCache(string clipName, int targetChannels, int targetSampleRate, bool allowLoad = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(clipName)) return null;
|
||||
if (!allowLoad) return null;
|
||||
|
||||
lock (_cache)
|
||||
if (!AudioManager.Instance) return null;
|
||||
|
||||
var clip = AudioManager.Instance.FindOrLoadAudioClip(clipName);
|
||||
if (!clip) return null;
|
||||
|
||||
try
|
||||
{
|
||||
if (_cache.TryGetValue(clipName, out var cached))
|
||||
return cached;
|
||||
var totalSamples = clip.samples;
|
||||
var channels = clip.channels;
|
||||
var rawSamples = new float[totalSamples * channels];
|
||||
clip.GetData(rawSamples, 0);
|
||||
|
||||
if (!allowLoad) return null;
|
||||
// Auto-resample during cache generation
|
||||
var resampled = AudioResampler.Resample(rawSamples, channels, clip.frequency, targetChannels, targetSampleRate);
|
||||
|
||||
if (!AudioManager.Instance) return null;
|
||||
|
||||
var clip = AudioManager.Instance.FindOrLoadAudioClip(clipName);
|
||||
if (!clip) return null;
|
||||
|
||||
try
|
||||
var data = new CachedClipData
|
||||
{
|
||||
var totalSamples = clip.samples;
|
||||
var channels = clip.channels;
|
||||
var rawSamples = new float[totalSamples * channels];
|
||||
clip.GetData(rawSamples, 0);
|
||||
Samples = resampled,
|
||||
Duration = (double)resampled.Length / (targetChannels * targetSampleRate)
|
||||
};
|
||||
|
||||
// Auto-resample during cache generation
|
||||
var resampled = AudioResampler.Resample(rawSamples, channels, clip.frequency, targetChannels, targetSampleRate);
|
||||
|
||||
var data = new CachedClipData
|
||||
{
|
||||
Samples = resampled,
|
||||
Duration = (double)resampled.Length / (targetChannels * targetSampleRate)
|
||||
};
|
||||
|
||||
_cache[clipName] = data;
|
||||
return data;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
MelonLogger.Error($"[AudioClipCache] Failed to cache and resample clip {clipName}: {e}");
|
||||
return null;
|
||||
}
|
||||
_cache[clipName] = data;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_cache)
|
||||
catch (Exception e)
|
||||
{
|
||||
_cache.Clear();
|
||||
MelonLogger.Error($"[AudioClipCache] Failed to cache and resample clip {clipName}: {e}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class OfflineAudioMixer
|
||||
public void Clear()
|
||||
{
|
||||
public class SoundEvent
|
||||
lock (_cache)
|
||||
{
|
||||
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;
|
||||
public float Pitch;
|
||||
_cache.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<SoundEvent> _activeEvents = new();
|
||||
private ConditionalWeakTable<AudioSource, SoundEvent> _sourceMappings = new();
|
||||
private readonly object _lock = new();
|
||||
internal class OfflineAudioMixer
|
||||
{
|
||||
public class SoundEvent
|
||||
{
|
||||
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;
|
||||
public float Pitch;
|
||||
}
|
||||
|
||||
// Target Audio Settings
|
||||
private readonly int _targetChannels;
|
||||
private readonly int _targetSampleRate;
|
||||
private const float ClipVolume = .5f;
|
||||
private readonly List<SoundEvent> _activeEvents = new();
|
||||
private ConditionalWeakTable<AudioSource, SoundEvent> _sourceMappings = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
// Target Audio Settings
|
||||
private readonly int _targetChannels;
|
||||
private readonly int _targetSampleRate;
|
||||
private const float ClipVolume = .5f;
|
||||
|
||||
// Audio Filter Settings
|
||||
private readonly LinkedList<IAudioFilter> _filters = new();
|
||||
// Audio Filter Settings
|
||||
private readonly LinkedList<IAudioFilter> _filters = new();
|
||||
|
||||
// BGM Cached Samples (Pre-resampled)
|
||||
private float[]? _bgmSamples;
|
||||
private double _bgmDuration;
|
||||
// BGM Cached Samples (Pre-resampled)
|
||||
private float[]? _bgmSamples;
|
||||
private double _bgmDuration;
|
||||
|
||||
// Clip Cache
|
||||
private readonly AudioClipCache _clipCache = new();
|
||||
private bool _isPrimed;
|
||||
// Clip Cache
|
||||
private readonly AudioClipCache _clipCache = new();
|
||||
private bool _isPrimed;
|
||||
|
||||
public OfflineAudioMixer(int targetChannels, int targetSampleRate)
|
||||
public OfflineAudioMixer(int targetChannels, int targetSampleRate)
|
||||
{
|
||||
_targetChannels = targetChannels;
|
||||
_targetSampleRate = targetSampleRate;
|
||||
|
||||
var rmsCompressor = new RMSCompressor
|
||||
{
|
||||
_targetChannels = targetChannels;
|
||||
_targetSampleRate = targetSampleRate;
|
||||
|
||||
var rmsCompressor = new RMSCompressor
|
||||
{
|
||||
ThresholdDb = -10, Ratio = 2,
|
||||
AttackTime = 0.1f, ReleaseTime = 0.1f,
|
||||
KneeDb = 10, RmsWindowTime = 0.02f
|
||||
};
|
||||
rmsCompressor.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(rmsCompressor);
|
||||
ThresholdDb = -10, Ratio = 2,
|
||||
AttackTime = 0.1f, ReleaseTime = 0.1f,
|
||||
KneeDb = 10, RmsWindowTime = 0.02f
|
||||
};
|
||||
rmsCompressor.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(rmsCompressor);
|
||||
|
||||
var limiter = new RMSCompressor
|
||||
{
|
||||
ThresholdDb = 0, Ratio = float.PositiveInfinity,
|
||||
AttackTime = 0f, ReleaseTime = 0.05f,
|
||||
KneeDb = 0, RmsWindowTime = 0.02f,
|
||||
LookAheadTime = 0.01f
|
||||
};
|
||||
limiter.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(limiter);
|
||||
var limiter = new RMSCompressor
|
||||
{
|
||||
ThresholdDb = 0, Ratio = float.PositiveInfinity,
|
||||
AttackTime = 0f, ReleaseTime = 0.05f,
|
||||
KneeDb = 0, RmsWindowTime = 0.02f,
|
||||
LookAheadTime = 0.01f
|
||||
};
|
||||
limiter.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(limiter);
|
||||
|
||||
var hardClip = new HardClip();
|
||||
hardClip.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(hardClip);
|
||||
var hardClip = new HardClip();
|
||||
hardClip.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(hardClip);
|
||||
|
||||
var truePeakSafetyGain = new Gain { Level = -1f };
|
||||
truePeakSafetyGain.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(truePeakSafetyGain);
|
||||
}
|
||||
var truePeakSafetyGain = new Gain { Level = -1f };
|
||||
truePeakSafetyGain.Initialize(_targetSampleRate, _targetChannels);
|
||||
_filters.AddLast(truePeakSafetyGain);
|
||||
}
|
||||
|
||||
public void InitializeBGM(AudioClip? clip)
|
||||
public void InitializeBGM(AudioClip? clip)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
lock (_lock)
|
||||
if (!clip)
|
||||
{
|
||||
if (!clip)
|
||||
{
|
||||
_bgmSamples = null;
|
||||
_bgmDuration = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int originalSamples = clip.samples;
|
||||
int originalChannels = clip.channels;
|
||||
float[] rawSamples = new float[originalSamples * originalChannels];
|
||||
clip.GetData(rawSamples, 0);
|
||||
|
||||
// Pre-resample the BGM to the target format
|
||||
_bgmSamples = AudioResampler.Resample(rawSamples, originalChannels, clip.frequency, _targetChannels, _targetSampleRate);
|
||||
_bgmDuration = (double)_bgmSamples.Length / (_targetChannels * _targetSampleRate);
|
||||
|
||||
Melon<RenderMod>.Logger.Msg($"[OfflineAudioMixer] Initialized resampled BGM: {clip.name}, Samples: {_bgmSamples.Length / _targetChannels}, TargetFreq: {_targetSampleRate}Hz, TargetChannels: {_targetChannels}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error($"[OfflineAudioMixer] Failed to initialize and resample BGM: {e}");
|
||||
_bgmSamples = null;
|
||||
_bgmDuration = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CacheClip(string clipName)
|
||||
{
|
||||
_clipCache.GetOrCache(clipName, _targetChannels, _targetSampleRate, allowLoad: true);
|
||||
}
|
||||
|
||||
public void RegisterSoundEvent(AudioSource? source, string clipName, double startTime, double endTime, float volume, float pitch)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var ev = new SoundEvent
|
||||
{
|
||||
ClipName = clipName,
|
||||
StartTime = startTime,
|
||||
EndTime = endTime,
|
||||
Volume = volume,
|
||||
Pitch = pitch
|
||||
};
|
||||
_activeEvents.Add(ev);
|
||||
|
||||
if (!source) return;
|
||||
_sourceMappings.Remove(source);
|
||||
_sourceMappings.Add(source, ev);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterSoundEvent(string clipName, double startTime, double endTime, float volume, float pitch)
|
||||
{
|
||||
RegisterSoundEvent(null, clipName, startTime, endTime, volume, pitch);
|
||||
}
|
||||
|
||||
public bool TryGetSoundEvent(AudioSource? source, out SoundEvent ev)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
ev = null!;
|
||||
return false;
|
||||
}
|
||||
lock (_lock)
|
||||
{
|
||||
return _sourceMappings.TryGetValue(source, out ev);
|
||||
}
|
||||
}
|
||||
|
||||
public bool RemoveSourceMapping(AudioSource source)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _sourceMappings.Remove(source);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_activeEvents.Clear();
|
||||
_sourceMappings = new ConditionalWeakTable<AudioSource, SoundEvent>();
|
||||
_bgmSamples = null;
|
||||
_bgmDuration = 0;
|
||||
_clipCache.Clear();
|
||||
_isPrimed = false;
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
filter.Reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void PrimeFilters(double totalLatency, double dspTimeBase)
|
||||
{
|
||||
int latencySamples = (int)Math.Round(totalLatency * _targetSampleRate);
|
||||
if (latencySamples <= 0) return;
|
||||
|
||||
// Rent temporary buffer to prime filters with initial audio
|
||||
float[] tempBuffer = System.Buffers.ArrayPool<float>.Shared.Rent(latencySamples * _targetChannels);
|
||||
try
|
||||
{
|
||||
Array.Clear(tempBuffer, 0, latencySamples * _targetChannels);
|
||||
int originalSamples = clip.samples;
|
||||
int originalChannels = clip.channels;
|
||||
float[] rawSamples = new float[originalSamples * originalChannels];
|
||||
clip.GetData(rawSamples, 0);
|
||||
|
||||
// Mix starting from -totalLatency to 0
|
||||
MixBGM(tempBuffer, latencySamples, -totalLatency, dspTimeBase, totalLatency);
|
||||
MixEffectSounds(tempBuffer, latencySamples, -totalLatency, totalLatency);
|
||||
// Pre-resample the BGM to the target format
|
||||
_bgmSamples = AudioResampler.Resample(rawSamples, originalChannels, clip.frequency, _targetChannels, _targetSampleRate);
|
||||
_bgmDuration = (double)_bgmSamples.Length / (_targetChannels * _targetSampleRate);
|
||||
|
||||
// Feed the buffer into filters to prime circular delay buffers
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
filter.ProcessBlock(tempBuffer.AsSpan(0, latencySamples * _targetChannels));
|
||||
}
|
||||
Melon<RenderMod>.Logger.Msg($"[OfflineAudioMixer] Initialized resampled BGM: {clip.name}, Samples: {_bgmSamples.Length / _targetChannels}, TargetFreq: {_targetSampleRate}Hz, TargetChannels: {_targetChannels}");
|
||||
}
|
||||
finally
|
||||
catch (Exception e)
|
||||
{
|
||||
System.Buffers.ArrayPool<float>.Shared.Return(tempBuffer);
|
||||
Melon<RenderMod>.Logger.Error($"[OfflineAudioMixer] Failed to initialize and resample BGM: {e}");
|
||||
_bgmSamples = null;
|
||||
_bgmDuration = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void GenerateAudioForFrame(float[] mixedBuffer, int samplesNeeded, double frameStartTime, double dspTimeBase)
|
||||
public void CacheClip(string clipName)
|
||||
{
|
||||
_clipCache.GetOrCache(clipName, _targetChannels, _targetSampleRate, allowLoad: true);
|
||||
}
|
||||
|
||||
public void RegisterSoundEvent(AudioSource? source, string clipName, double startTime, double endTime, float volume, float pitch)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// mixedBuffer size must be at least samplesNeeded * _targetChannels
|
||||
Array.Clear(mixedBuffer, 0, samplesNeeded * _targetChannels);
|
||||
|
||||
lock (_lock)
|
||||
var ev = new SoundEvent
|
||||
{
|
||||
double totalLatency = 0;
|
||||
foreach (var filter in _filters)
|
||||
ClipName = clipName,
|
||||
StartTime = startTime,
|
||||
EndTime = endTime,
|
||||
Volume = volume,
|
||||
Pitch = pitch
|
||||
};
|
||||
_activeEvents.Add(ev);
|
||||
|
||||
if (!source) return;
|
||||
_sourceMappings.Remove(source);
|
||||
_sourceMappings.Add(source, ev);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterSoundEvent(string clipName, double startTime, double endTime, float volume, float pitch)
|
||||
{
|
||||
RegisterSoundEvent(null, clipName, startTime, endTime, volume, pitch);
|
||||
}
|
||||
|
||||
public bool TryGetSoundEvent(AudioSource? source, out SoundEvent ev)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
ev = null!;
|
||||
return false;
|
||||
}
|
||||
lock (_lock)
|
||||
{
|
||||
return _sourceMappings.TryGetValue(source, out ev);
|
||||
}
|
||||
}
|
||||
|
||||
public bool RemoveSourceMapping(AudioSource source)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _sourceMappings.Remove(source);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_activeEvents.Clear();
|
||||
_sourceMappings = new ConditionalWeakTable<AudioSource, SoundEvent>();
|
||||
_bgmSamples = null;
|
||||
_bgmDuration = 0;
|
||||
_clipCache.Clear();
|
||||
_isPrimed = false;
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
filter.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PrimeFilters(double totalLatency, double dspTimeBase)
|
||||
{
|
||||
int latencySamples = (int)Math.Round(totalLatency * _targetSampleRate);
|
||||
if (latencySamples <= 0) return;
|
||||
|
||||
// Rent temporary buffer to prime filters with initial audio
|
||||
float[] tempBuffer = System.Buffers.ArrayPool<float>.Shared.Rent(latencySamples * _targetChannels);
|
||||
try
|
||||
{
|
||||
Array.Clear(tempBuffer, 0, latencySamples * _targetChannels);
|
||||
|
||||
// Mix starting from -totalLatency to 0
|
||||
MixBGM(tempBuffer, latencySamples, -totalLatency, dspTimeBase, totalLatency);
|
||||
MixEffectSounds(tempBuffer, latencySamples, -totalLatency, totalLatency);
|
||||
|
||||
// Feed the buffer into filters to prime circular delay buffers
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
filter.ProcessBlock(tempBuffer.AsSpan(0, latencySamples * _targetChannels));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
System.Buffers.ArrayPool<float>.Shared.Return(tempBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
public void GenerateAudioForFrame(float[] mixedBuffer, int samplesNeeded, double frameStartTime, double dspTimeBase)
|
||||
{
|
||||
// mixedBuffer size must be at least samplesNeeded * _targetChannels
|
||||
Array.Clear(mixedBuffer, 0, samplesNeeded * _targetChannels);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
double totalLatency = 0;
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
totalLatency += filter.Latency;
|
||||
}
|
||||
|
||||
// Prime filters on first frame execution if latency exists
|
||||
if (!_isPrimed && totalLatency > 0)
|
||||
{
|
||||
PrimeFilters(totalLatency, dspTimeBase);
|
||||
_isPrimed = true;
|
||||
}
|
||||
|
||||
// 1. Mix BGM
|
||||
MixBGM(mixedBuffer, samplesNeeded, frameStartTime, dspTimeBase, totalLatency);
|
||||
|
||||
// 2. Mix Hitsounds/Effect Sounds
|
||||
MixEffectSounds(mixedBuffer, samplesNeeded, frameStartTime, totalLatency);
|
||||
|
||||
foreach (var filter in _filters)
|
||||
filter.ProcessBlock(mixedBuffer.AsSpan(0, samplesNeeded * _targetChannels));
|
||||
|
||||
for (var i = 0; i < samplesNeeded * _targetChannels; i++)
|
||||
{
|
||||
if (Math.Abs(mixedBuffer[i]) > 1) Melon<RenderMod>.Logger.Msg("Warning Why Audio Sample Is Larger Than 1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void MixBGM(float[] mixedBuffer, int samplesNeeded, double frameStartTime, double dspTimeBase, double totalLatency)
|
||||
{
|
||||
if (_bgmSamples == null || _bgmSamples.Length == 0) return;
|
||||
|
||||
var conductor = scrConductor.instance;
|
||||
if (!conductor) return;
|
||||
|
||||
double pitch = conductor.song.pitch;
|
||||
var dspTimeSong = conductor.dspTimeSong;
|
||||
var separateCountdownOffset = conductor.separateCountdownTime
|
||||
? conductor.crotchetAtStart / pitch * conductor.adjustedCountdownTicks
|
||||
: 0.0;
|
||||
|
||||
// BGM Playback start time relative to dspTimeBase
|
||||
var time1 = dspTimeSong + separateCountdownOffset;
|
||||
var relativeTime1 = time1 - dspTimeBase;
|
||||
var adjustedRelativeTime1 = relativeTime1 - totalLatency;
|
||||
|
||||
var totalSamples = _bgmSamples.Length / _targetChannels;
|
||||
|
||||
for (int s = 0; s < samplesNeeded; s++)
|
||||
{
|
||||
double sampleTime = frameStartTime + (double)s / _targetSampleRate;
|
||||
double clipTime = (sampleTime - adjustedRelativeTime1) * pitch;
|
||||
|
||||
if (clipTime >= 0 && clipTime < _bgmDuration)
|
||||
{
|
||||
double sampleIdxDouble = clipTime * _targetSampleRate;
|
||||
long idxLow = (long)Math.Floor(sampleIdxDouble);
|
||||
long idxHigh = idxLow + 1;
|
||||
float t = (float)(sampleIdxDouble - idxLow);
|
||||
|
||||
if (idxHigh >= totalSamples) idxHigh = totalSamples - 1;
|
||||
if (idxLow >= totalSamples) idxLow = totalSamples - 1;
|
||||
|
||||
for (var c = 0; c < _targetChannels; c++)
|
||||
{
|
||||
totalLatency += filter.Latency;
|
||||
}
|
||||
|
||||
// Prime filters on first frame execution if latency exists
|
||||
if (!_isPrimed && totalLatency > 0)
|
||||
{
|
||||
PrimeFilters(totalLatency, dspTimeBase);
|
||||
_isPrimed = true;
|
||||
}
|
||||
|
||||
// 1. Mix BGM
|
||||
MixBGM(mixedBuffer, samplesNeeded, frameStartTime, dspTimeBase, totalLatency);
|
||||
|
||||
// 2. Mix Hitsounds/Effect Sounds
|
||||
MixEffectSounds(mixedBuffer, samplesNeeded, frameStartTime, totalLatency);
|
||||
|
||||
foreach (var filter in _filters)
|
||||
filter.ProcessBlock(mixedBuffer.AsSpan(0, samplesNeeded * _targetChannels));
|
||||
|
||||
for (var i = 0; i < samplesNeeded * _targetChannels; i++)
|
||||
{
|
||||
if (Math.Abs(mixedBuffer[i]) > 1) Melon<RenderMod>.Logger.Msg("Warning Why Audio Sample Is Larger Than 1");
|
||||
float valLow = _bgmSamples[idxLow * _targetChannels + c];
|
||||
float valHigh = _bgmSamples[idxHigh * _targetChannels + c];
|
||||
mixedBuffer[s * _targetChannels + c] += (valLow + t * (valHigh - valLow)) * (scnGame.instance.levelData.volume * 0.01f)
|
||||
* ClipVolume;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void MixBGM(float[] mixedBuffer, int samplesNeeded, double frameStartTime, double dspTimeBase, double totalLatency)
|
||||
private void MixEffectSounds(float[] mixedBuffer, int samplesNeeded, double frameStartTime, double totalLatency)
|
||||
{
|
||||
for (var i = _activeEvents.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_bgmSamples == null || _bgmSamples.Length == 0) return;
|
||||
|
||||
var conductor = scrConductor.instance;
|
||||
if (!conductor) return;
|
||||
|
||||
double pitch = conductor.song.pitch;
|
||||
var dspTimeSong = conductor.dspTimeSong;
|
||||
var separateCountdownOffset = conductor.separateCountdownTime
|
||||
? conductor.crotchetAtStart / pitch * conductor.adjustedCountdownTicks
|
||||
: 0.0;
|
||||
|
||||
// BGM Playback start time relative to dspTimeBase
|
||||
var time1 = dspTimeSong + separateCountdownOffset;
|
||||
var relativeTime1 = time1 - dspTimeBase;
|
||||
var adjustedRelativeTime1 = relativeTime1 - totalLatency;
|
||||
|
||||
var totalSamples = _bgmSamples.Length / _targetChannels;
|
||||
|
||||
for (int s = 0; s < samplesNeeded; s++)
|
||||
var ev = _activeEvents[i];
|
||||
var clipData = _clipCache.GetOrCache(ev.ClipName, _targetChannels, _targetSampleRate, allowLoad: false);
|
||||
if (clipData == null)
|
||||
{
|
||||
double sampleTime = frameStartTime + (double)s / _targetSampleRate;
|
||||
double clipTime = (sampleTime - adjustedRelativeTime1) * pitch;
|
||||
|
||||
if (clipTime >= 0 && clipTime < _bgmDuration)
|
||||
{
|
||||
double sampleIdxDouble = clipTime * _targetSampleRate;
|
||||
long idxLow = (long)Math.Floor(sampleIdxDouble);
|
||||
long idxHigh = idxLow + 1;
|
||||
float t = (float)(sampleIdxDouble - idxLow);
|
||||
|
||||
if (idxHigh >= totalSamples) idxHigh = totalSamples - 1;
|
||||
if (idxLow >= totalSamples) idxLow = totalSamples - 1;
|
||||
|
||||
for (var c = 0; c < _targetChannels; c++)
|
||||
{
|
||||
float valLow = _bgmSamples[idxLow * _targetChannels + c];
|
||||
float valHigh = _bgmSamples[idxHigh * _targetChannels + c];
|
||||
mixedBuffer[s * _targetChannels + c] += (valLow + t * (valHigh - valLow)) * (scnGame.instance.levelData.volume * 0.01f)
|
||||
* ClipVolume;
|
||||
}
|
||||
}
|
||||
_activeEvents.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
private void MixEffectSounds(float[] mixedBuffer, int samplesNeeded, double frameStartTime, double totalLatency)
|
||||
{
|
||||
for (var i = _activeEvents.Count - 1; i >= 0; i--)
|
||||
var isSoundFinished = false;
|
||||
var totalSamples = clipData.Samples.Length / _targetChannels;
|
||||
double adjustedStartTime = ev.StartTime - totalLatency;
|
||||
double adjustedEndTime = ev.EndTime >= 0 ? ev.EndTime - totalLatency : -1.0;
|
||||
|
||||
for (var s = 0; s < samplesNeeded; s++)
|
||||
{
|
||||
var ev = _activeEvents[i];
|
||||
var clipData = _clipCache.GetOrCache(ev.ClipName, _targetChannels, _targetSampleRate, allowLoad: false);
|
||||
if (clipData == null)
|
||||
var sampleTime = frameStartTime + (double)s / _targetSampleRate;
|
||||
|
||||
// If it is a looping sound and has reached or exceeded its end time, stop playing.
|
||||
if (adjustedEndTime >= 0 && sampleTime >= adjustedEndTime)
|
||||
{
|
||||
_activeEvents.RemoveAt(i);
|
||||
continue;
|
||||
isSoundFinished = true;
|
||||
break;
|
||||
}
|
||||
|
||||
var isSoundFinished = false;
|
||||
var totalSamples = clipData.Samples.Length / _targetChannels;
|
||||
double adjustedStartTime = ev.StartTime - totalLatency;
|
||||
double adjustedEndTime = ev.EndTime >= 0 ? ev.EndTime - totalLatency : -1.0;
|
||||
var elapsed = sampleTime - adjustedStartTime;
|
||||
if (elapsed < 0) continue; // Sound hasn't started yet
|
||||
|
||||
for (var s = 0; s < samplesNeeded; s++)
|
||||
var soundTime = elapsed * ev.Pitch;
|
||||
|
||||
if (soundTime >= clipData.Duration)
|
||||
{
|
||||
var sampleTime = frameStartTime + (double)s / _targetSampleRate;
|
||||
|
||||
// If it is a looping sound and has reached or exceeded its end time, stop playing.
|
||||
if (adjustedEndTime >= 0 && sampleTime >= adjustedEndTime)
|
||||
if (ev.EndTime < 0) // One-shot
|
||||
{
|
||||
isSoundFinished = true;
|
||||
break;
|
||||
}
|
||||
|
||||
var elapsed = sampleTime - adjustedStartTime;
|
||||
if (elapsed < 0) continue; // Sound hasn't started yet
|
||||
|
||||
var soundTime = elapsed * ev.Pitch;
|
||||
|
||||
if (soundTime >= clipData.Duration)
|
||||
{
|
||||
if (ev.EndTime < 0) // One-shot
|
||||
{
|
||||
isSoundFinished = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Looping
|
||||
soundTime %= clipData.Duration;
|
||||
}
|
||||
|
||||
// Linear interpolation for pitch shifting in the mixer loop
|
||||
double sampleIdxDouble = soundTime * _targetSampleRate;
|
||||
long idxLow = (long)Math.Floor(sampleIdxDouble);
|
||||
long idxHigh = idxLow + 1;
|
||||
float t = (float)(sampleIdxDouble - idxLow);
|
||||
|
||||
if (idxHigh >= totalSamples) idxHigh = totalSamples - 1;
|
||||
if (idxLow >= totalSamples) idxLow = totalSamples - 1;
|
||||
|
||||
for (var c = 0; c < _targetChannels; c++)
|
||||
{
|
||||
float valLow = clipData.Samples[idxLow * _targetChannels + c];
|
||||
float valHigh = clipData.Samples[idxHigh * _targetChannels + c];
|
||||
mixedBuffer[s * _targetChannels + c] += (valLow + t * (valHigh - valLow)) * ev.Volume
|
||||
* ClipVolume * 2 / 3;
|
||||
}
|
||||
// Looping
|
||||
soundTime %= clipData.Duration;
|
||||
}
|
||||
|
||||
// Clean up finished sound events (both one-shot and looping)
|
||||
if (isSoundFinished)
|
||||
// Linear interpolation for pitch shifting in the mixer loop
|
||||
double sampleIdxDouble = soundTime * _targetSampleRate;
|
||||
long idxLow = (long)Math.Floor(sampleIdxDouble);
|
||||
long idxHigh = idxLow + 1;
|
||||
float t = (float)(sampleIdxDouble - idxLow);
|
||||
|
||||
if (idxHigh >= totalSamples) idxHigh = totalSamples - 1;
|
||||
if (idxLow >= totalSamples) idxLow = totalSamples - 1;
|
||||
|
||||
for (var c = 0; c < _targetChannels; c++)
|
||||
{
|
||||
_activeEvents.RemoveAt(i);
|
||||
float valLow = clipData.Samples[idxLow * _targetChannels + c];
|
||||
float valHigh = clipData.Samples[idxHigh * _targetChannels + c];
|
||||
mixedBuffer[s * _targetChannels + c] += (valLow + t * (valHigh - valLow)) * ev.Volume
|
||||
* ClipVolume * 2 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up finished sound events (both one-shot and looping)
|
||||
if (isSoundFinished)
|
||||
{
|
||||
_activeEvents.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user