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; } }