diff --git a/ViewModels/MainViewModel.Audio.cs b/ViewModels/MainViewModel.Audio.cs
index ba347a4..412c427 100644
--- a/ViewModels/MainViewModel.Audio.cs
+++ b/ViewModels/MainViewModel.Audio.cs
@@ -13,6 +13,49 @@ namespace ytLive.ViewModels;
public partial class MainViewModel
{
+ private readonly DispatcherTimer _meterTimer;
+ private int _traxSilentTicks;
+ private bool _traxSilenceLogged;
+
+ /// The dock's heartbeat: every meter readout is refreshed here, 20 times a second,
+ /// from whatever the audio threads last measured.
+ private void TickMeters()
+ {
+ OnPropertyChanged(nameof(MicMeterFraction));
+ OnPropertyChanged(nameof(MeterFillWidth));
+ OnPropertyChanged(nameof(MeterBrush));
+ OnPropertyChanged(nameof(GameMeterFraction));
+ OnPropertyChanged(nameof(GameMeterFillWidth));
+ OnPropertyChanged(nameof(GameMeterBrush));
+ OnPropertyChanged(nameof(TraxMeterFraction));
+ RaiseMasterReadouts();
+ UpdateDuckState();
+ CheckTraxSilence();
+ }
+
+ /// One honest diagnostic: "playing" with a flat player meter means the samples never
+ /// reached the device (or the metering was not attached). Logged once per playback, so the next
+ /// run says which side is broken instead of another round of guessing.
+ private void CheckTraxSilence()
+ {
+ if (!IsMusicPlaying)
+ {
+ _traxSilentTicks = 0;
+ _traxSilenceLogged = false;
+ return;
+ }
+ if (_musicPlayer.Level > 0f)
+ {
+ _traxSilentTicks = 0;
+ _traxSilenceLogged = false;
+ return;
+ }
+ if (++_traxSilentTicks < 40 || _traxSilenceLogged) return; // 2s at 20 Hz
+ _traxSilenceLogged = true;
+ AppLog.Write("TRAX: playing but the player meter reads 0 — the output device is silent or " +
+ "the metering stream is not attached (the dock's ladder has nothing to show)");
+ }
+
private readonly DispatcherTimer _volumeFlashTimer;
private readonly DispatcherTimer _gameVolumeFlashTimer;
private double _audioLevel;
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
index 86b3303..3ed8928 100644
--- a/ViewModels/MainViewModel.cs
+++ b/ViewModels/MainViewModel.cs
@@ -178,6 +178,13 @@ public partial class MainViewModel : ViewModelBase
_gameVolumeFlashTimer.Tick += (_, _) => EndGameVolumeFlash();
_saveDebounce = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1500) };
+
+ // Dock meters beat on their own 20 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.Tick += (_, _) => TickMeters();
+ _meterTimer.Start();
_saveDebounce.Tick += (_, _) => SaveLayoutNow();
_brandFlash = new BrandFlashPresenter();