From 2607ecf210151ccd09ca394cbb69400266e3dac9 Mon Sep 17 00:00:00 2001 From: gramps Date: Fri, 30 Oct 2026 13:28:32 -0700 Subject: [PATCH] fix(dock): TRAX ladder gets the mic's ballistics (and a 33 Hz beat) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported: the TRAX bar moves, but its sampling looks far slower than the mic meter. Cause: the TRAX number was a raw instantaneous peak per metering block — a stutter sampled by the UI timer, with no smoothing. The mic looks fluid because it is smoothed at audio-callback rate. Fix: the player now smooths its measured level with the SAME curve the mic uses (MeterScale.NextLevel: fast attack, gentle decay), and the dock's meter clock runs at ~30 Hz instead of 20. Two ladders that move alike, because they are the same ballistics. Guard: MeterScaleTests pins the attack/decay behaviour alongside peak-hold. --- Helpers/MeterScale.cs | 9 +++++++++ Services/Audio/MusicPlayer.cs | 3 ++- ViewModels/MainViewModel.cs | 4 ++-- ytLive.Tests/MeterScaleTests.cs | 6 ++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Helpers/MeterScale.cs b/Helpers/MeterScale.cs index 7a2991f..a352e99 100644 --- a/Helpers/MeterScale.cs +++ b/Helpers/MeterScale.cs @@ -34,6 +34,15 @@ public static class MeterScale : dbfs >= CautionDb ? MeterBand.Caution : MeterBand.Nominal; + /// 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. + 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; + } + /// Peak hold: jumps up instantly to a new maximum and falls back at /// — a moment-to-moment bar hides clipping, which is the one /// audio fault that cannot be fixed afterwards. diff --git a/Services/Audio/MusicPlayer.cs b/Services/Audio/MusicPlayer.cs index 6c70e4d..70acb53 100644 --- a/Services/Audio/MusicPlayer.cs +++ b/Services/Audio/MusicPlayer.cs @@ -84,7 +84,8 @@ public sealed class MusicPlayer : ITraxPlayer _volume = new VolumeWaveProvider16(_reader) { Volume = MusicVolume * _userGain * _localGain }; _metering = new MeteringSampleProvider(_volume.ToSampleProvider()); _metering.StreamVolume += (_, e) => - Level = e.MaxSampleValues.Length == 0 ? 0f : e.MaxSampleValues.Max(); + Level = Helpers.MeterScale.NextLevel(Level, + e.MaxSampleValues.Length == 0 ? 0f : e.MaxSampleValues.Max()); _output = new WaveOutEvent(); _output.PlaybackStopped += OnPlaybackStopped; _output.Init(_metering); diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 3ed8928..6bc4ac5 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -179,10 +179,10 @@ public partial class MainViewModel : ViewModelBase _saveDebounce = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1500) }; - // Dock meters beat on their own 20 Hz clock instead of riding the mic's level callback. + // Dock meters beat on their own ~30 Hz clock instead of riding the mic's level callback. // Coupling them to capture was a bug: with a silent or absent mic the ladder never // refreshed, so a playing TRAX showed nothing (creator, 2026-10-30). - _meterTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50) }; + _meterTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(33) }; _meterTimer.Tick += (_, _) => TickMeters(); _meterTimer.Start(); _saveDebounce.Tick += (_, _) => SaveLayoutNow(); diff --git a/ytLive.Tests/MeterScaleTests.cs b/ytLive.Tests/MeterScaleTests.cs index e852d25..b1b587b 100644 --- a/ytLive.Tests/MeterScaleTests.cs +++ b/ytLive.Tests/MeterScaleTests.cs @@ -27,6 +27,12 @@ public class MeterScaleTests Assert.Equal(1f, MeterScale.ToFraction(+12f)); Assert.Equal(0f, MeterScale.ToFraction(-120f)); + // Level ballistics: a jump is followed quickly, a drop falls gently — the same curve that + // makes the mic meter look fluid. Raw peaks at a 30 Hz UI tick stutter without it. + Assert.Equal(0.6f, MeterScale.NextLevel(0f, 1f), 3); // attack + 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 + // 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