namespace ytLive.Services.Audio; /// /// The always-on voice processing chain for the mic (TASK 9 audio milestone). /// Pure, per-sample, unit-tested: bass boost → treble lift → noise gate → /// compressor. Applied BEFORE the meter and the stream mix so what the creator /// hears on the meter is what viewers hear — and so background hum is gated /// out before it reaches the encoder. /// public sealed class VoiceFilterChain { private readonly LowShelfFilter _bass; private readonly HighShelfFilter _treble; private readonly NoiseGate _gate; private readonly Compressor _compressor; /// The (post-resample) mic rate — the RBJ shelf /// coefficients are rate-dependent, so the chain is built once at 48 kHz. public VoiceFilterChain(int sampleRate) { _bass = new LowShelfFilter(sampleRate, 120f, 4f); _treble = new HighShelfFilter(sampleRate, 8000f, 3f); _gate = new NoiseGate(); _compressor = new Compressor(); } public float Process(float sample) { var bass = _bass.Process(sample); var treble = _treble.Process(bass); var gated = _gate.Process(treble); return _compressor.Process(gated); } public void Reset() { _bass.Reset(); _treble.Reset(); _gate.Reset(); _compressor.Reset(); } } /// /// RBJ audio EQ cookbook low-shelf biquad (bass boost). Transposed direct /// form II — stable, cheap, standard. /// public sealed class LowShelfFilter { private readonly float _b0, _b1, _b2, _a1, _a2; private float _z1, _z2; public LowShelfFilter(int sampleRate, float cutoffHz, float gainDb) { var a = MathF.Pow(10f, gainDb / 40f); var omega = 2f * MathF.PI * cutoffHz / sampleRate; var sin = MathF.Sin(omega); var cos = MathF.Cos(omega); var alpha = sin / 2f * MathF.Sqrt(2f); var twoSqrtA = 2f * MathF.Sqrt(a); var b0 = a * ((a + 1f) - (a - 1f) * cos + twoSqrtA * alpha); var b1 = 2f * a * ((a - 1f) - (a + 1f) * cos); var b2 = a * ((a + 1f) - (a - 1f) * cos - twoSqrtA * alpha); var a0 = (a + 1f) + (a - 1f) * cos + twoSqrtA * alpha; var a1 = -2f * ((a - 1f) + (a + 1f) * cos); var a2 = (a + 1f) + (a - 1f) * cos - twoSqrtA * alpha; _b0 = b0 / a0; _b1 = b1 / a0; _b2 = b2 / a0; _a1 = a1 / a0; _a2 = a2 / a0; } public float Process(float x) { var y = _b0 * x + _z1; _z1 = _b1 * x - _a1 * y + _z2; _z2 = _b2 * x - _a2 * y; return y; } public void Reset() { _z1 = 0; _z2 = 0; } } /// RBJ audio EQ cookbook high-shelf biquad (treble lift). public sealed class HighShelfFilter { private readonly float _b0, _b1, _b2, _a1, _a2; private float _z1, _z2; public HighShelfFilter(int sampleRate, float cutoffHz, float gainDb) { var a = MathF.Pow(10f, gainDb / 40f); var omega = 2f * MathF.PI * cutoffHz / sampleRate; var sin = MathF.Sin(omega); var cos = MathF.Cos(omega); var alpha = sin / 2f * MathF.Sqrt(2f); var twoSqrtA = 2f * MathF.Sqrt(a); var b0 = a * ((a + 1f) + (a - 1f) * cos + twoSqrtA * alpha); var b1 = -2f * a * ((a - 1f) + (a + 1f) * cos); var b2 = a * ((a + 1f) + (a - 1f) * cos - twoSqrtA * alpha); var a0 = (a + 1f) - (a - 1f) * cos + twoSqrtA * alpha; var a1 = 2f * ((a - 1f) - (a + 1f) * cos); var a2 = (a + 1f) - (a - 1f) * cos - twoSqrtA * alpha; _b0 = b0 / a0; _b1 = b1 / a0; _b2 = b2 / a0; _a1 = a1 / a0; _a2 = a2 / a0; } public float Process(float x) { var y = _b0 * x + _z1; _z1 = _b1 * x - _a1 * y + _z2; _z2 = _b2 * x - _a2 * y; return y; } public void Reset() { _z1 = 0; _z2 = 0; } } /// /// A simple noise gate with open/release hysteresis (pure C#, no external DSP /// dependency). The envelope follows the input peak fast and decays slowly; /// the gate opens above and closes below half of it, so /// quiet background hum stays silent and borderline signals don't chatter. /// public sealed class NoiseGate { private const float HysteresisRatio = 0.5f; private const float Attack = 0.5f; private const float Release = 0.0005f; private float _envelope; private bool _open; public NoiseGate(float threshold = DefaultThreshold) { Threshold = threshold; } public const float DefaultThreshold = 0.005f; public float Threshold { get; } public bool IsOpen => _open; public float Process(float sample) { var abs = MathF.Abs(sample); _envelope = abs > _envelope ? _envelope + (abs - _envelope) * Attack : _envelope * (1f - Release); if (!_open && _envelope > Threshold) _open = true; else if (_open && _envelope < Threshold * HysteresisRatio) _open = false; return _open ? sample : 0f; } public void Reset() { _envelope = 0; _open = false; } } /// /// A static soft-limit compressor: samples at or under the threshold pass /// through; louder samples fold down at Ratio:1 so hot mic peaks can't /// slam the stream. State-free. /// public sealed class Compressor { public const float Threshold = 0.5f; public const float Ratio = 4f; public float Process(float sample) { var abs = MathF.Abs(sample); if (abs <= Threshold) return sample; return MathF.CopySign(Threshold + (abs - Threshold) / Ratio, sample); } public void Reset() { } }