From a54149bd7c23f4e0259606bcf74cfc8c69e33346 Mon Sep 17 00:00:00 2001 From: gramps Date: Fri, 30 Oct 2026 13:22:13 -0700 Subject: [PATCH] =?UTF-8?q?feat(dock):=20TRAX=20gets=20its=20own=20ladder?= =?UTF-8?q?=20=E2=80=94=20measured=20at=20the=20player's=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creator: "why is there no meter for when trax is playing?" Routing answer: TRAX rides the desktop loopback, so it already moved the GAME ladder — there was no separate TRAX signal in the mix. But that ladder cannot answer the question a deaf creator actually has: "is the music producing sound at all?" The play glyph cannot either — an open file with a dead decoder still reports Playing. So the player now measures its own output: \`MusicPlayer\` wraps the volume stage in an NAudio \`MeteringSampleProvider\` and exposes \`Level\`; \`ITraxPlayer.Level\` keeps the fake honest. The VM maps it through the same display scale (\`TraxMeterFraction\`), zeroed when not playing, and the TRAX bay carries a 24-segment ladder above its fader. This reverses my earlier "no ladder, nothing honest to show" — the honest thing arrived with a real measurement. TraxBayTests flipped accordingly (it now asserts the ladder AND its source). --- Controls/AudioDock.xaml | 5 ++++- Services/Audio/ITraxPlayer.cs | 4 ++++ Services/Audio/MusicPlayer.cs | 12 +++++++++++- ViewModels/MainViewModel.Audio.cs | 1 + ViewModels/MainViewModel.Trax.cs | 5 +++++ ytLive.Tests/TraxBayTests.cs | 9 ++++++--- ytLive.Tests/TraxToggleTests.cs | 1 + 7 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Controls/AudioDock.xaml b/Controls/AudioDock.xaml index efabd9d..7943ddb 100644 --- a/Controls/AudioDock.xaml +++ b/Controls/AudioDock.xaml @@ -159,7 +159,10 @@ - + + + ? PlaybackEnded; string? TrackName { get; } bool IsPlaying { get; } + + /// The player's OWN measured output level (0…1). A play glyph can lie — an open file + /// with a dead decoder still says "playing" — so the dock's TRAX ladder shows sound, not state. + float Level { get; } float LocalGain { get; set; } /// The creator's TRAX level (1 = the bed's fixed design level). Separate from diff --git a/Services/Audio/MusicPlayer.cs b/Services/Audio/MusicPlayer.cs index 0a55e12..6c70e4d 100644 --- a/Services/Audio/MusicPlayer.cs +++ b/Services/Audio/MusicPlayer.cs @@ -1,5 +1,7 @@ using System.IO; +using System.Linq; using NAudio.Wave; +using NAudio.Wave.SampleProviders; namespace ytLive.Services.Audio; @@ -39,6 +41,11 @@ public sealed class MusicPlayer : ITraxPlayer public bool IsPlaying => _output?.PlaybackState == PlaybackState.Playing; + /// Measured peak of what the player is actually sending to the device. + public float Level { get; private set; } + + private MeteringSampleProvider? _metering; + /// Local gain multiplier from the desktop/game audio bar (0 = mute, /// 1 = full bed). Applied live on top of the fixed . private float _userGain = 1f; @@ -75,9 +82,12 @@ public sealed class MusicPlayer : ITraxPlayer TrackPath = path; _reader = new MediaFoundationReader(path); _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(); _output = new WaveOutEvent(); _output.PlaybackStopped += OnPlaybackStopped; - _output.Init(_volume); + _output.Init(_metering); } public void Play() diff --git a/ViewModels/MainViewModel.Audio.cs b/ViewModels/MainViewModel.Audio.cs index 8763a37..ba347a4 100644 --- a/ViewModels/MainViewModel.Audio.cs +++ b/ViewModels/MainViewModel.Audio.cs @@ -56,6 +56,7 @@ public partial class MainViewModel OnPropertyChanged(nameof(MicMeterFraction)); UpdateDuckState(); RaiseMasterReadouts(); + OnPropertyChanged(nameof(TraxMeterFraction)); OnPropertyChanged(nameof(MicMeterFraction)); OnPropertyChanged(nameof(MicVolumeDb)); OnPropertyChanged(nameof(MicVolumeDbLabel)); diff --git a/ViewModels/MainViewModel.Trax.cs b/ViewModels/MainViewModel.Trax.cs index dbc147b..cf24c13 100644 --- a/ViewModels/MainViewModel.Trax.cs +++ b/ViewModels/MainViewModel.Trax.cs @@ -79,6 +79,11 @@ public partial class MainViewModel public string TraxVolumeDbLabel => Helpers.GainDb.Label(TraxVolumeDb) + " dB"; + /// The TRAX ladder: the player's own measured output, so "is music actually making + /// sound" is answerable without hearing it. + public double TraxMeterFraction => + IsMusicPlaying ? Services.Audio.AudioLevelMeter.ToDisplay(_musicPlayer.Level) : 0; + /// Track name, or an honest "No track". public string TraxTrackLabel => _musicPlayer.TrackName ?? "No track"; diff --git a/ytLive.Tests/TraxBayTests.cs b/ytLive.Tests/TraxBayTests.cs index d44b2e4..517098e 100644 --- a/ytLive.Tests/TraxBayTests.cs +++ b/ytLive.Tests/TraxBayTests.cs @@ -21,17 +21,20 @@ public class TraxBayTests Assert.Contains("Binding=\"{Binding IsMusicPlaying}\" Value=\"True\"", dock); Assert.Contains("Click=\"TraxPlayPause_Click\"", dock); - // No ladder in the TRAX bay: nothing measures the bed, so there is nothing honest to show. + // The bay DOES carry a ladder now — the player measures its own output, so "is music making + // sound" is answerable without hearing it (a play glyph cannot answer that). var traxBay = dock[dock.IndexOf("Text=\"TRAX\"", System.StringComparison.Ordinal) ..dock.IndexOf("Text=\"LIP-SYNC\"", System.StringComparison.Ordinal)]; Assert.Contains("TraxTrackLabel", traxBay); - Assert.DoesNotContain("SegmentMeter", traxBay); + Assert.Contains("SegmentMeter", traxBay); + Assert.Contains("TraxMeterFraction", traxBay); - foreach (var name in new[] { "TraxVolumeDb", "TraxVolumeDbLabel", "TraxTrackLabel", "IsTraxDucking" }) + foreach (var name in new[] { "TraxVolumeDb", "TraxVolumeDbLabel", "TraxTrackLabel", "IsTraxDucking", "TraxMeterFraction" }) Assert.NotNull(typeof(MainViewModel).GetProperty(name)); // The player gained a real user level, separate from the mute-follow LocalGain. Assert.NotNull(typeof(ITraxPlayer).GetProperty("UserGain")); + Assert.NotNull(typeof(ITraxPlayer).GetProperty("Level")); Assert.NotNull(typeof(MusicPlayer).GetProperty("UserGain")); // Unity means the bed's design level, and the fader's scale is the same one as the others. diff --git a/ytLive.Tests/TraxToggleTests.cs b/ytLive.Tests/TraxToggleTests.cs index 64c10e6..00d3709 100644 --- a/ytLive.Tests/TraxToggleTests.cs +++ b/ytLive.Tests/TraxToggleTests.cs @@ -22,6 +22,7 @@ public sealed class TraxToggleTests public bool IsPlaying { get; private set; } public float LocalGain { get; set; } = 1f; public float UserGain { get; set; } = 1f; + public float Level { get; set; } public void Load(string path) => TrackName = Path.GetFileNameWithoutExtension(path); public void Play() => IsPlaying = Starts; public void Pause() => IsPlaying = false;