using System; namespace FluidSim.Core { public class OutdoorExhaustReverb { // ---- Geometry ---- private const float GroundReflDelay = 0.008f; // 8 ms (≈1.3 m) private const float WallRefl1Delay = 0.045f; // ≈15 m private const float WallRefl2Delay = 0.080f; // ≈27 m private DelayLine groundRefl; private DelayLine wallRefl1; private DelayLine wallRefl2; // ---- FDN for late diffuse tail ---- private const int FDN_CHANNELS = 8; // dense, realistic private DelayLine[] fdnDelays; private float[] fdnState; private OrthonormalMixer mixer; // energy‑preserving mixing private LowPassFilter[] channelFilters; // per‑channel air absorption public float DryMix { get; set; } = 1.0f; public float EarlyMix { get; set; } = 0.5f; public float TailMix { get; set; } = 0.9f; public float Feedback { get; set; } = 0.75f; // safe range 0.7‑0.9 public float DampingFreq { get; set; } = 6000f; // Hz, above which air absorbs strongly public float MatrixCoeff { get; set; } = 0.5f; // (kept for compatibility, not used) public OutdoorExhaustReverb(int sampleRate) { // Early reflection lines groundRefl = new DelayLine((int)(sampleRate * GroundReflDelay)); wallRefl1 = new DelayLine((int)(sampleRate * WallRefl1Delay)); wallRefl2 = new DelayLine((int)(sampleRate * WallRefl2Delay)); // FDN delays: prime numbers for dense modal density (70‑150 ms) int[] baseLengths = { 3203, 4027, 5521, 7027, 8521, 10007, 11503, 13009 }; fdnDelays = new DelayLine[FDN_CHANNELS]; for (int i = 0; i < FDN_CHANNELS; i++) { int len = Math.Min(baseLengths[i], (int)(sampleRate * 0.25)); // max 250 ms fdnDelays[i] = new DelayLine(len); } fdnState = new float[FDN_CHANNELS]; mixer = new OrthonormalMixer(FDN_CHANNELS); // Air absorption: a gentle first‑order low‑pass per channel channelFilters = new LowPassFilter[FDN_CHANNELS]; float initialCutoff = DampingFreq; for (int i = 0; i < FDN_CHANNELS; i++) channelFilters[i] = new LowPassFilter(sampleRate, initialCutoff); } public float Process(float drySample) { // ---- Early reflections ---- float g = groundRefl.ReadWrite(drySample * 0.8f); float w1 = wallRefl1.ReadWrite(drySample * 0.5f); float w2 = wallRefl2.ReadWrite(drySample * 0.4f); float early = (g + w1 + w2) * EarlyMix; // ---- FDN diffuse tail ---- // Read the delayed outputs (which were stored last iteration) float[] delOut = new float[FDN_CHANNELS]; for (int i = 0; i < FDN_CHANNELS; i++) delOut[i] = fdnDelays[i].Read(); // Mix the delayed outputs with the orthonormal matrix -> scattered signals mixer.Process(delOut, fdnState); // result written into fdnState // Add fresh input to all channels for (int i = 0; i < FDN_CHANNELS; i++) fdnState[i] = drySample * 0.15f + Feedback * fdnState[i]; // Air absorption: per‑channel one‑pole low‑pass for (int i = 0; i < FDN_CHANNELS; i++) fdnState[i] = channelFilters[i].Process(fdnState[i]); // Write the new states into the delay lines for (int i = 0; i < FDN_CHANNELS; i++) fdnDelays[i].Write(fdnState[i]); // The tail output is the sum of the delayed outputs *before* the loop float tailSum = 0f; for (int i = 0; i < FDN_CHANNELS; i++) tailSum += delOut[i]; float tail = tailSum * TailMix; // Final mix return drySample * DryMix + early + tail; } // ---------- Helper classes (same as before but with separate Read/Write) ---------- private class DelayLine { private float[] buffer; private int writePos; public DelayLine(int length) { buffer = new float[Math.Max(length, 1)]; writePos = 0; } // Separated Read/Write to avoid ringing with immediate feedback public float Read() { return buffer[writePos]; } public void Write(float value) { buffer[writePos] = value; writePos = (writePos + 1) % buffer.Length; } // Old combined method (not used in FDN, only for early reflections) public float ReadWrite(float value) { float outVal = buffer[writePos]; buffer[writePos] = value; writePos = (writePos + 1) % buffer.Length; return outVal; } } private class LowPassFilter { private float b0, a1; private float y1; private float sampleRate; public LowPassFilter(int sampleRate, float cutoff) { this.sampleRate = sampleRate; SetCutoff(cutoff); } public void SetCutoff(float cutoff) { float w = 2 * (float)Math.PI * cutoff / sampleRate; float a0 = 1 + w; b0 = w / a0; a1 = (1 - w) / a0; // first‑order low‑pass } public float Process(float x) { float y = b0 * x - a1 * y1; y1 = y; return y; } } /// /// Computes a fast orthonormal mixing matrix (like Hadamard, but energy‑preserving). /// private class OrthonormalMixer { private int size; public OrthonormalMixer(int size) { this.size = size; } public void Process(float[] input, float[] output) { // Simple energy‑conserving “allpass” mixing: // Use a Householder reflection: y = (2/n) * sum(x) * ones - x float sum = 0; for (int i = 0; i < size; i++) sum += input[i]; float factor = 2.0f / size; for (int i = 0; i < size; i++) output[i] = factor * sum - input[i]; } } } }