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:
@@ -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>
|
/// ~−15..−20 LUFS — beside the leveled voice (≈ −17 LUFS), and well under it when ducked.</para></summary>
|
||||||
public const float MusicVolume = 0.5f;
|
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 MediaFoundationReader? _reader;
|
||||||
private VolumeWaveProvider16? _volume;
|
private VolumeWaveProvider16? _volume;
|
||||||
private WaveOutEvent? _output;
|
private WaveOutEvent? _output;
|
||||||
@@ -56,8 +67,8 @@ public sealed class MusicPlayer : ITraxPlayer
|
|||||||
get => _userGain;
|
get => _userGain;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_userGain = Math.Clamp(value, 0f, 2f);
|
_userGain = Math.Clamp(value, 0f, MaxUserGain);
|
||||||
if (_output != null) _output.Volume = MusicVolume * _userGain * LocalGain;
|
ApplyVolume();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,11 +78,18 @@ public sealed class MusicPlayer : ITraxPlayer
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
_localGain = Math.Clamp(value, 0f, 1f);
|
_localGain = Math.Clamp(value, 0f, 1f);
|
||||||
if (_volume != null)
|
ApplyVolume();
|
||||||
_volume.Volume = MusicVolume * _localGain;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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>
|
/// <summary>Loads a track and prepares playback (does not start it).</summary>
|
||||||
public void Load(string path)
|
public void Load(string path)
|
||||||
{
|
{
|
||||||
@@ -81,7 +99,7 @@ public sealed class MusicPlayer : ITraxPlayer
|
|||||||
Stop();
|
Stop();
|
||||||
TrackPath = path;
|
TrackPath = path;
|
||||||
_reader = new MediaFoundationReader(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 = new MeteringSampleProvider(_volume.ToSampleProvider());
|
||||||
_metering.StreamVolume += (_, e) =>
|
_metering.StreamVolume += (_, e) =>
|
||||||
Level = Helpers.MeterScale.NextLevel(Level,
|
Level = Helpers.MeterScale.NextLevel(Level,
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using Xunit;
|
||||||
|
using ytLive.Services.Audio;
|
||||||
|
|
||||||
|
namespace ytLive.Tests;
|
||||||
|
|
||||||
|
/// <summary>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.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user