namespace TravelEar.Core; /// /// The Mixer Stage's reverb approximation (ADR-0002: the game's mixer reverb cannot be read, so it /// is approximated and calibrated by ear against a second-client recording). A Schroeder / /// Freeverb-style mono reverb: eight parallel damped feedback combs summed into four series /// allpasses. The comb feedback follows the requested decay time (RT60) at each comb's delay, so /// one knob () sets the tail. Returns wet only; the caller mixes. /// Allocation-free after construction; one instance per thread. /// public sealed class Reverb { // Freeverb tunings at 44.1 kHz, scaled to the sample rate. private static readonly int[] CombTunings = { 1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617 }; private static readonly int[] AllpassTunings = { 556, 441, 341, 225 }; private const float ReferenceRate = 44_100f; private const float AllpassFeedback = 0.5f; private const float InputGain = 0.015f; private const float Damping = 0.2f; private readonly float[][] _comb; private readonly int[] _combIndex; private readonly float[] _combFeedback; private readonly float[] _combFilterStore; private readonly float[][] _allpass; private readonly int[] _allpassIndex; public int SampleRate { get; } public float DecaySeconds { get; private set; } public Reverb(int sampleRate, float decaySeconds) { SampleRate = sampleRate; var scale = sampleRate / ReferenceRate; _comb = new float[CombTunings.Length][]; _combIndex = new int[CombTunings.Length]; _combFeedback = new float[CombTunings.Length]; _combFilterStore = new float[CombTunings.Length]; for (var i = 0; i < CombTunings.Length; i++) _comb[i] = new float[Math.Max(1, (int)(CombTunings[i] * scale))]; _allpass = new float[AllpassTunings.Length][]; _allpassIndex = new int[AllpassTunings.Length]; for (var i = 0; i < AllpassTunings.Length; i++) _allpass[i] = new float[Math.Max(1, (int)(AllpassTunings[i] * scale))]; SetDecay(decaySeconds); } /// Sets the RT60 in seconds: comb feedback 10^(-3 * delay / (rt60 * rate)), capped below 1. public void SetDecay(float seconds) { DecaySeconds = MathF.Max(0.05f, seconds); for (var i = 0; i < _comb.Length; i++) { var fb = MathF.Pow(10f, -3f * _comb[i].Length / (DecaySeconds * SampleRate)); _combFeedback[i] = MathF.Min(fb, 0.98f); } } public void Reset() { foreach (var c in _comb) Array.Clear(c); foreach (var a in _allpass) Array.Clear(a); Array.Clear(_combIndex); Array.Clear(_allpassIndex); Array.Clear(_combFilterStore); } // [impl->REQ-MIXER-RESYNTH] /// One sample in, the wet sample out. public float Process(float input) { var x = input * InputGain; var sum = 0f; for (var i = 0; i < _comb.Length; i++) { var buf = _comb[i]; var idx = _combIndex[i]; var output = buf[idx]; var store = _combFilterStore[i] = output * (1f - Damping) + _combFilterStore[i] * Damping; buf[idx] = x + store * _combFeedback[i]; if (++idx >= buf.Length) idx = 0; _combIndex[i] = idx; sum += output; } var y = sum; for (var i = 0; i < _allpass.Length; i++) { var buf = _allpass[i]; var idx = _allpassIndex[i]; var bufOut = buf[idx]; var output = -y + bufOut; buf[idx] = y + bufOut * AllpassFeedback; if (++idx >= buf.Length) idx = 0; _allpassIndex[i] = idx; y = output; } return y; } }