fix(trax): the fader now actually changes the level — and the meter follows it

Reported: pegging the TRAX fader changes nothing audible, the ladder never reaches red, and the
ladder does not scale with the fader.

Cause (two defects in the same setters):
- UserGain wrote the OUTPUT DEVICE's volume. NAudio's WaveOut ignores that, so the fader was inert.
- LocalGain (the desktop-mute follow) recomputed the sample-chain volume WITHOUT the user gain, so
  every duck/mute silently discarded whatever the creator had set.

Fix: one `EffectiveVolume(user, local)` = MusicVolume × user × local, applied to the sample chain by
both setters. The meter sits AFTER that stage, so the ladder now scales with the fader and can reach
the peak band on a loud track — exactly what pegging should look like. Max user gain is +6 dB, so
"pegged" means the file at unity, not a magic boost.

Guard: MusicLevelTests pins the arithmetic (default half, pegged double, mute multiplies to zero),
that the user gain survives the mute follow, and that nothing may drive the output device's volume
again — the no-op that made this invisible.
This commit is contained in:
2026-10-30 13:32:24 -07:00
parent 2607ecf210
commit 8d23502220
2 changed files with 58 additions and 5 deletions
+23 -5
View File
@@ -25,6 +25,17 @@ public sealed class MusicPlayer : ITraxPlayer
/// ~−15..−20 LUFS — beside the leveled voice (≈ −17 LUFS), and well under it when ducked.</para></summary>
public const float MusicVolume = 0.5f;
/// <summary>+6 dB of headroom on the bed, matching the dock's fader range.</summary>
public const float MaxUserGain = 2f;
/// <summary>The effective multiplier for a (user, local) pair, computed in ONE place.
///
/// <para>The fader used to set the OUTPUT DEVICE's volume, which NAudio's WaveOut ignores, while
/// the mute-follow rewrote the sample-chain volume without the user gain — so pegging the fader
/// changed nothing and then lost the setting at the next duck.</para></summary>
public static float EffectiveVolume(float userGain, float localGain)
=> MusicVolume * Math.Clamp(userGain, 0f, MaxUserGain) * Math.Clamp(localGain, 0f, 1f);
private MediaFoundationReader? _reader;
private VolumeWaveProvider16? _volume;
private WaveOutEvent? _output;
@@ -56,8 +67,8 @@ public sealed class MusicPlayer : ITraxPlayer
get => _userGain;
set
{
_userGain = Math.Clamp(value, 0f, 2f);
if (_output != null) _output.Volume = MusicVolume * _userGain * LocalGain;
_userGain = Math.Clamp(value, 0f, MaxUserGain);
ApplyVolume();
}
}
@@ -67,11 +78,18 @@ public sealed class MusicPlayer : ITraxPlayer
set
{
_localGain = Math.Clamp(value, 0f, 1f);
if (_volume != null)
_volume.Volume = MusicVolume * _localGain;
ApplyVolume();
}
}
/// <summary>Pushes the current gains onto the sample chain. The meter sits AFTER this stage, so
/// the ladder follows the fader — that is the point of both controls being on the same bay.</summary>
private void ApplyVolume()
{
if (_volume != null)
_volume.Volume = EffectiveVolume(_userGain, _localGain);
}
/// <summary>Loads a track and prepares playback (does not start it).</summary>
public void Load(string path)
{
@@ -81,7 +99,7 @@ public sealed class MusicPlayer : ITraxPlayer
Stop();
TrackPath = path;
_reader = new MediaFoundationReader(path);
_volume = new VolumeWaveProvider16(_reader) { Volume = MusicVolume * _userGain * _localGain };
_volume = new VolumeWaveProvider16(_reader) { Volume = EffectiveVolume(_userGain, _localGain) };
_metering = new MeteringSampleProvider(_volume.ToSampleProvider());
_metering.StreamVolume += (_, e) =>
Level = Helpers.MeterScale.NextLevel(Level,