namespace ytLive.Services.Audio;
///
/// Computes the smoothed mic level (0..1) from captured samples. Pure —
/// unit-tested; the only feeds it and forwards the
/// result. RMS-based so it tracks perceived loudness, smoothed so the meter
/// does not flicker.
///
public sealed class AudioLevelMeter
{
private const float Smoothing = 0.2f;
private const float DecaySmoothing = 0.5f;
private float _level;
private float _left;
private float _right;
public float Level => _level;
/// Per-channel levels, smoothed with the same attack/decay as .
/// A mono sample reports the same value on both sides (a mono source is centred), so the two
/// never disagree about whether audio exists.
///
/// Why they exist: the dock shows ONE ladder per source, but a creator who is deaf on
/// one side needs to know when content is panned away from it. The imbalance flag is computed
/// from these, not from a second ladder.
public float Left => _left;
public float Right => _right;
/// How far apart the channels are, in dB. 0 only when BOTH are silent.
///
/// A side that carries nothing is compared at the −60 dBFS floor, so a fully one-sided
/// signal reads as a large imbalance (≈54 dB) rather than "nothing to compare" — one-sided is
/// the maximum imbalance, not the absence of one. That distinction is the whole point for a
/// creator who cannot hear the missing side.
public float ImbalanceDb
{
get
{
const float floor = 0.001f; // −60 dBFS
if (_left < floor && _right < floor) return 0f;
var l = MathF.Max(_left, floor);
var r = MathF.Max(_right, floor);
return MathF.Abs(20f * MathF.Log10(l / r));
}
}
///
/// Maps a linear level (0..1) onto the meter's display scale: -60 dBFS..0 dBFS
/// spread linearly across 0..1 — an HONEST dBFS scale. Linear RMS of real speech is
/// ~0.01..0.1 (-40..-20 dBFS), which leaves a flat (linear) meter looking dead; the
/// log scale makes typical levels occupy the bar. Inputs at or below -60 dBFS
/// (0.001 linear) read as zero — the meter never idles on background noise.
/// 2026-10-09: the +10 dB "input amplification" that used to sit here is gone.
/// It made a −30 dBFS voice look dialed in, and a creator's upload came out at
/// −28.5 LUFS. The speech leveler now puts voice at −20 dBFS, which lands at 2/3 of
/// the bar (the green/yellow edge); loud peaks reach the red.
///
public static float ToDisplay(float linear)
{
if (linear <= 0.001f)
return 0f;
var db = 20f * MathF.Log10(linear);
return Math.Clamp(1f + db / 60f, 0f, 1f);
}
public float Push(AudioSample sample)
{
if (sample.Samples.Length == 0)
return _level;
double sumSquares = 0;
var count = 0;
foreach (var value in sample.Samples)
{
sumSquares += value * value;
count++;
}
var rms = (float)Math.Sqrt(sumSquares / count);
var alpha = rms > _level ? Smoothing : DecaySmoothing;
_level = _level * (1 - alpha) + rms * alpha;
MeasureChannels(sample);
return _level;
}
private void MeasureChannels(AudioSample sample)
{
double leftSquares = 0, rightSquares = 0;
int frames;
if (sample.Channels <= 1)
{
// Mono is centred: one number, both sides.
for (var i = 0; i < sample.Samples.Length; i++) leftSquares += sample.Samples[i] * sample.Samples[i];
frames = Math.Max(1, sample.Samples.Length);
rightSquares = leftSquares;
}
else
{
frames = Math.Max(1, sample.Samples.Length / sample.Channels);
for (var frame = 0; frame < frames; frame++)
{
var l = sample.Samples[frame * sample.Channels];
var r = sample.Samples[frame * sample.Channels + 1];
leftSquares += l * l;
rightSquares += r * r;
}
}
var left = (float)Math.Sqrt(leftSquares / frames);
var right = (float)Math.Sqrt(rightSquares / frames);
_left = Smooth(_left, left);
_right = Smooth(_right, right);
static float Smooth(float current, float incoming)
{
var a = incoming > current ? Smoothing : DecaySmoothing;
return current * (1 - a) + incoming * a;
}
}
public void Reset()
{
_level = 0;
_left = 0;
_right = 0;
}
}