Files
LlamaCasty/Helpers/MeterScale.cs
T
gramps e92ce4bb63 fix(dock): the master ladder moves when no session is running
Reported: no activity in the master meter.

Cause: `MasterLevel` measures the buffer the ENCODER receives, and that buffer only exists while the
mix is being pulled — streaming or recording. Idle, nothing fills it, so the master read zero while
the bays below it moved. A master that is dead between sessions is a gap in the design.

Now: while a session runs the master shows the measured mix (unchanged); idle it shows an estimate
computed from the same measured sources and the same gains the mix applies
(`MeterScale.MonitorFraction`, pure + tested). Two ladders can therefore never tell contradictory
stories about whether audio exists.

Guard: MeterScaleTests pins the estimate — silence reads zero, a quiet source at unity is visible,
two sources add, and gain is applied in the LINEAR domain like the mix (scaling the display fraction
would be a different, wrong curve).
2026-10-30 14:01:29 -07:00

69 lines
3.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
namespace ytLive.Helpers;
/// <summary>Which colour band a level falls in (DESIGN.md meter thresholds).</summary>
public enum MeterBand
{
Nominal,
Caution,
Peak,
}
/// <summary>The mathematics of the dock's segment meter, kept out of the control so it can be
/// tested: a segment ladder is only honest if the band it paints is the level it measured.
///
/// <para>Bands follow DESIGN.md: nominal to −12 dB, caution to −3 dB, peak above. The display
/// fraction spreads −60…0 dBFS across 0…1 — the same scale <c>AudioLevelMeter.ToDisplay</c> uses,
/// so the ladder and the old bars cannot disagree.</para></summary>
public static class MeterScale
{
public const float FloorDb = -60f;
public const float CautionDb = -12f;
public const float PeakDb = -3f;
/// <summary>dBFS → 0…1 across the whole ladder.</summary>
public static float ToFraction(float dbfs)
=> Math.Clamp(1f + Math.Clamp(dbfs, FloorDb, 0f) / 60f, 0f, 1f);
/// <summary>The band boundaries are INCLUSIVE at the bottom edge (a meter reading exactly
/// −12.0 dB is cautious, exactly −3.0 is peaking), matching the ranges as DESIGN.md states
/// them: "−12 … −3" and "−3 … 0+".</summary>
public static MeterBand BandFor(float dbfs)
=> dbfs >= PeakDb ? MeterBand.Peak
: dbfs >= CautionDb ? MeterBand.Caution
: MeterBand.Nominal;
/// <summary>The master ladder when no session is running.
///
/// <para>`AudioMixer.MasterLevel` measures the buffer the encoder receives, and that buffer only
/// exists while the mix is being pulled (streaming or recording). Idle, the master would read
/// zero while the source ladders move — so the monitor level is computed from the same measured
/// sources and the same gains the mix applies. It is an estimate of the mix, not a substitute
/// for it: once a session starts, the real measured value takes over.</para></summary>
public static float MonitorFraction(float micLevel, double micGain, float gameLevel, double gameGain)
{
var linear = Math.Max(0f, (float)(micLevel * micGain) + (float)(gameLevel * gameGain));
if (linear <= 0.001f) return 0f;
return ToFraction(20f * MathF.Log10(linear));
}
/// <summary>Ballistics for a level: reach a new peak quickly, fall back gently. Raw per-block
/// peaks sampled by a UI timer look like a stutter; this is what makes the mic feel fluid, and
/// TRAX gets the same curve so the two ladders move alike.</summary>
public static float NextLevel(float current, float incoming, float attack = 0.6f, float decay = 0.12f)
{
var alpha = incoming > current ? attack : decay;
return current + (incoming - current) * alpha;
}
/// <summary>Peak hold: jumps up instantly to a new maximum and falls back at
/// <paramref name="decayPerSecond"/> — a moment-to-moment bar hides clipping, which is the one
/// audio fault that cannot be fixed afterwards.</summary>
public static float NextPeak(float held, float incoming, float seconds, float decayPerSecond = 0.35f)
{
if (incoming >= held) return incoming;
return Math.Max(0f, held - decayPerSecond * Math.Max(0f, seconds));
}
}