Files
FluidSim/Core/OutdoorExhaustReverb.cs
2026-05-05 16:10:06 +02:00

170 lines
6.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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; // energypreserving mixing
private LowPassFilter[] channelFilters; // perchannel 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.70.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 (70150 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 firstorder lowpass 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: perchannel onepole lowpass
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; // firstorder lowpass
}
public float Process(float x)
{
float y = b0 * x - a1 * y1;
y1 = y;
return y;
}
}
/// <summary>
/// Computes a fast orthonormal mixing matrix (like Hadamard, but energypreserving).
/// </summary>
private class OrthonormalMixer
{
private int size;
public OrthonormalMixer(int size) { this.size = size; }
public void Process(float[] input, float[] output)
{
// Simple energyconserving “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];
}
}
}
}