diff --git a/Services/Audio/MusicPlayer.cs b/Services/Audio/MusicPlayer.cs
index 70acb53..afe209b 100644
--- a/Services/Audio/MusicPlayer.cs
+++ b/Services/Audio/MusicPlayer.cs
@@ -25,6 +25,17 @@ public sealed class MusicPlayer : ITraxPlayer
/// ~−15..−20 LUFS — beside the leveled voice (≈ −17 LUFS), and well under it when ducked.
public const float MusicVolume = 0.5f;
+ /// +6 dB of headroom on the bed, matching the dock's fader range.
+ public const float MaxUserGain = 2f;
+
+ /// The effective multiplier for a (user, local) pair, computed in ONE place.
+ ///
+ /// 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.
+ 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();
}
}
+ /// 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.
+ private void ApplyVolume()
+ {
+ if (_volume != null)
+ _volume.Volume = EffectiveVolume(_userGain, _localGain);
+ }
+
/// Loads a track and prepares playback (does not start it).
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,
diff --git a/ytLive.Tests/MusicLevelTests.cs b/ytLive.Tests/MusicLevelTests.cs
new file mode 100644
index 0000000..8f393c6
--- /dev/null
+++ b/ytLive.Tests/MusicLevelTests.cs
@@ -0,0 +1,35 @@
+using Xunit;
+using ytLive.Services.Audio;
+
+namespace ytLive.Tests;
+
+/// The TRAX fader must actually change the level the meter measures — and it did not:
+/// `UserGain` wrote to the output device's volume (a no-op for NAudio's WaveOut), while the
+/// mute-follow rewrote the sample-chain volume WITHOUT the user gain, so pegging the fader changed
+/// nothing and the next desktop mute/duck silently discarded the setting.
+///
+public class MusicLevelTests
+{
+ [Fact]
+ public void The_trax_fader_reaches_the_chain_and_survives_the_mute_follow()
+ {
+ // Default: the bed sits at the design volume; pegging doubles it (+6 dB).
+ Assert.Equal(0.5f, MusicPlayer.EffectiveVolume(1f, 1f), 3);
+ Assert.Equal(1.0f, MusicPlayer.EffectiveVolume(MusicPlayer.MaxUserGain, 1f), 3);
+
+ // The mute-follow multiplies, it does not replace: the user gain survives.
+ Assert.Equal(1.0f, MusicPlayer.EffectiveVolume(MusicPlayer.MaxUserGain, 1f), 3);
+ Assert.Equal(0.5f, MusicPlayer.EffectiveVolume(1f, 1f), 3);
+
+ // Muted (or a desktop mute) is silence whatever the fader says.
+ Assert.Equal(0f, MusicPlayer.EffectiveVolume(MusicPlayer.MaxUserGain, 0f));
+
+ // And the floor is a real zero, not a residue.
+ Assert.Equal(0f, MusicPlayer.EffectiveVolume(0f, 1f));
+
+ // The old defect cannot come back: nothing may drive the output device's volume.
+ var source = SourceAssert.Text("Services", "Audio", "MusicPlayer.cs");
+ Assert.DoesNotContain("_output.Volume", source);
+ Assert.Contains("EffectiveVolume(_userGain, _localGain)", source);
+ }
+}