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()
{
}
}