namespace TravelEar.Core; /// /// Port of the game's VoiceCompressor (AudioSystem; docs/reference/big-walk-voice-dsp.md /// section 2): peak follower with instant attack and exponential release, envelope smoothing, /// soft-knee 2:1 gain reduction above the threshold. Scalar DSP with eight floats of state; the /// only external input is the threshold the game moves with its voice-volume slider. /// public sealed class VoiceCompressor { public const float MaxThreshold = 0.6f; public const float KneeFactor = 1.4f; public const float Ratio = 2f; public const float AttackSeconds = 0.005f; public const float ReleaseSeconds = 0.15f; public const float DenormalFloor = 1e-12f; private float _peak; private float _envelope; private float _threshold = MaxThreshold; private float _kneeWidth = MaxThreshold * (KneeFactor - 1f); private int _sampleRate; private float _attackCoefficient; private float _releaseCoefficient; /// Gain applied to the last sample (1 = no reduction). public float Reduction { get; private set; } = 1f; public float Threshold => _threshold; public void Reset() { _peak = 0f; _envelope = 0f; Reduction = 1f; } /// Called once per block, as the game does: re-reads the threshold and refreshes the coefficients on a rate change. public void Prepare(int sampleRate, float threshold) { _threshold = threshold; _kneeWidth = threshold * (KneeFactor - 1f); if (sampleRate > 0 && sampleRate != _sampleRate) { _sampleRate = sampleRate; _attackCoefficient = 1f - MathF.Exp(-1f / (sampleRate * AttackSeconds)); _releaseCoefficient = 1f - MathF.Exp(-1f / (sampleRate * ReleaseSeconds)); } } public float Process(float sample) { var magnitude = MathF.Abs(sample); if (magnitude > _peak) { _peak = magnitude; } else { _peak += (magnitude - _peak) * _releaseCoefficient; if (_peak < DenormalFloor) _peak = 0f; } _envelope += (_peak - _envelope) * _attackCoefficient; if (_envelope < DenormalFloor) _envelope = 0f; float gain; if (_envelope <= _threshold) { gain = 1f; } else { var t = MathF.Min((_envelope - _threshold) / _kneeWidth, 1f); gain = MathF.Pow(_threshold / _envelope, t * (1f - 1f / Ratio)); } Reduction = gain; return sample * gain; } }