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).
69 lines
3.2 KiB
C#
69 lines
3.2 KiB
C#
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));
|
||
}
|
||
}
|