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); + } +}