feat(dock): TRAX gets its own ladder — measured at the player's output

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).
This commit is contained in:
2026-10-30 13:22:13 -07:00
parent 8aa483f933
commit a54149bd7c
7 changed files with 32 additions and 5 deletions
+11 -1
View File
@@ -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;
/// <summary>Measured peak of what the player is actually sending to the device.</summary>
public float Level { get; private set; }
private MeteringSampleProvider? _metering;
/// <summary>Local gain multiplier from the desktop/game audio bar (0 = mute,
/// 1 = full bed). Applied live on top of the fixed <see cref="MusicVolume"/>.</summary>
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()