diff --git a/Helpers/MeterScale.cs b/Helpers/MeterScale.cs index a352e99..3901a83 100644 --- a/Helpers/MeterScale.cs +++ b/Helpers/MeterScale.cs @@ -34,6 +34,20 @@ public static class MeterScale : dbfs >= CautionDb ? MeterBand.Caution : MeterBand.Nominal; + /// The master ladder when no session is running. + /// + /// `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. + 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)); + } + /// 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. diff --git a/ViewModels/MainViewModel.Audio.cs b/ViewModels/MainViewModel.Audio.cs index 412c427..abd2aa5 100644 --- a/ViewModels/MainViewModel.Audio.cs +++ b/ViewModels/MainViewModel.Audio.cs @@ -262,8 +262,14 @@ public partial class MainViewModel } } - /// Master ladder input: the measured mix, 0…1 on the same display scale. - public double MasterMeterFraction => AudioLevelMeter.ToDisplay(_audioMixer.MasterLevel); + /// Master ladder input: the MEASURED mix while a session runs (that is the buffer the + /// encoder receives), and an estimate from the measured sources when idle — otherwise the master + /// sits at zero between sessions while every bay below it moves. + public double MasterMeterFraction => + IsLive || IsRecording + ? AudioLevelMeter.ToDisplay(_audioMixer.MasterLevel) + : Helpers.MeterScale.MonitorFraction((float)AudioLevel, MicVolume, + (float)GameAudioLevel, GameAudioVolume); public string MasterLevelLabel => Helpers.GainDb.Label(Helpers.GainDb.ToDb(_audioMixer.MasterLevel)) + " dB"; diff --git a/ytLive.Tests/MeterScaleTests.cs b/ytLive.Tests/MeterScaleTests.cs index b1b587b..06180d6 100644 --- a/ytLive.Tests/MeterScaleTests.cs +++ b/ytLive.Tests/MeterScaleTests.cs @@ -33,6 +33,14 @@ public class MeterScaleTests Assert.True(MeterScale.NextLevel(0.9f, 0.1f) > 0.8f); // decay is gentle Assert.True(MeterScale.NextLevel(0.1f, 0.9f) > 0.5f); // rise is fast + // The idle master estimate: sources × their gains, on the same display scale. With nothing + // playing it is zero; a quiet source at unity is visible; two sources add. + Assert.Equal(0f, MeterScale.MonitorFraction(0f, 1, 0f, 1)); + Assert.True(MeterScale.MonitorFraction(0.05f, 1, 0f, 1) > 0.3f); + Assert.True(MeterScale.MonitorFraction(0.05f, 1, 0.05f, 1) > MeterScale.MonitorFraction(0.05f, 1, 0f, 1)); + // Gain is applied in the LINEAR domain, like the mix does — not to the display fraction. + Assert.True(MeterScale.MonitorFraction(0.05f, 0.5, 0f, 1) < MeterScale.MonitorFraction(0.05f, 1, 0f, 1)); + // Peak hold rises instantly, then decays at the stated rate. Assert.Equal(0.8f, MeterScale.NextPeak(0.2f, 0.8f, 0.05f)); Assert.Equal(0.7f, MeterScale.NextPeak(0.8f, 0.1f, 0.1f / 0.35f), 3); // 0.1 fall at 0.35/s