diff --git a/Controls/AudioDock.xaml b/Controls/AudioDock.xaml index e36c80a..049adb2 100644 --- a/Controls/AudioDock.xaml +++ b/Controls/AudioDock.xaml @@ -114,10 +114,60 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Controls/AudioDock.xaml.cs b/Controls/AudioDock.xaml.cs index 2b12e86..1fcd815 100644 --- a/Controls/AudioDock.xaml.cs +++ b/Controls/AudioDock.xaml.cs @@ -39,6 +39,21 @@ public partial class AudioDock : UserControl vm.OpenMicPickerCommand.Execute(null); } + private void TraxPlayPause_Click(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.MainViewModel vm) vm.TraxLeftClick(); + } + + private void TraxLabel_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + if (DataContext is ViewModels.MainViewModel vm) vm.TraxLeftClick(); + } + + private void TraxLabel_MouseRightButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + if (DataContext is ViewModels.MainViewModel vm) vm.ChooseTrack(); + } + private void OnLoaded(object sender, RoutedEventArgs e) { _window = Window.GetWindow(this); diff --git a/Services/Audio/ITraxPlayer.cs b/Services/Audio/ITraxPlayer.cs index 7df17a8..5031a2a 100644 --- a/Services/Audio/ITraxPlayer.cs +++ b/Services/Audio/ITraxPlayer.cs @@ -8,6 +8,10 @@ public interface ITraxPlayer : IDisposable string? TrackName { get; } bool IsPlaying { get; } float LocalGain { get; set; } + + /// The creator's TRAX level (1 = the bed's fixed design level). Separate from + /// , which follows the desktop mute; the dock's fader moves this. + float UserGain { get; set; } void Load(string path); void Play(); void Pause(); diff --git a/Services/Audio/MusicPlayer.cs b/Services/Audio/MusicPlayer.cs index 1be0cef..0a55e12 100644 --- a/Services/Audio/MusicPlayer.cs +++ b/Services/Audio/MusicPlayer.cs @@ -41,6 +41,19 @@ public sealed class MusicPlayer : ITraxPlayer /// Local gain multiplier from the desktop/game audio bar (0 = mute, /// 1 = full bed). Applied live on top of the fixed . + private float _userGain = 1f; + + /// Creator-set level, multiplied onto the fixed design volume. + public float UserGain + { + get => _userGain; + set + { + _userGain = Math.Clamp(value, 0f, 2f); + if (_output != null) _output.Volume = MusicVolume * _userGain * LocalGain; + } + } + public float LocalGain { get => _localGain; @@ -61,7 +74,7 @@ public sealed class MusicPlayer : ITraxPlayer Stop(); TrackPath = path; _reader = new MediaFoundationReader(path); - _volume = new VolumeWaveProvider16(_reader) { Volume = MusicVolume * _localGain }; + _volume = new VolumeWaveProvider16(_reader) { Volume = MusicVolume * _userGain * _localGain }; _output = new WaveOutEvent(); _output.PlaybackStopped += OnPlaybackStopped; _output.Init(_volume); diff --git a/ViewModels/MainViewModel.Trax.cs b/ViewModels/MainViewModel.Trax.cs index e21f544..dbc147b 100644 --- a/ViewModels/MainViewModel.Trax.cs +++ b/ViewModels/MainViewModel.Trax.cs @@ -63,6 +63,28 @@ public partial class MainViewModel ? BarOnBrush : TraxError != null ? BarOffBrush : TraxIdleBrush; + /// The dock's TRAX fader, in dB. Unity means the bed's design level, not the raw file. + public double TraxVolumeDb + { + get => Helpers.GainDb.ToDb(_musicPlayer.UserGain); + set + { + var gain = Helpers.GainDb.FromDb(value); + if (Math.Abs(gain - _musicPlayer.UserGain) < 1e-6) return; + _musicPlayer.UserGain = (float)gain; + OnPropertyChanged(nameof(TraxVolumeDb)); + OnPropertyChanged(nameof(TraxVolumeDbLabel)); + } + } + + public string TraxVolumeDbLabel => Helpers.GainDb.Label(TraxVolumeDb) + " dB"; + + /// Track name, or an honest "No track". + public string TraxTrackLabel => _musicPlayer.TrackName ?? "No track"; + + /// Same ducker signal as the game bay — the bed ducks with it. + public bool IsTraxDucking => IsGameDucking; + public string TraxToolTip => _musicPlayer.TrackName != null ? $"TRAX: {_musicPlayer.TrackName} — left-click {(IsMusicPlaying ? "pauses" : "plays")}, right-click chooses a track" : "No track — left-click or right-click to choose background music"; @@ -81,6 +103,9 @@ public partial class MainViewModel /// Right-click on TRAX: always open the track picker (never the OS viewer). public void TraxRightClick() => PickTraxTrack(); + /// Open the track picker (the dock's right-click on the TRAX name). + internal void ChooseTrack() => PickTraxTrack(); + private void PickTraxTrack() { var dialog = new OpenFileDialog diff --git a/ytLive.Tests/TraxBayTests.cs b/ytLive.Tests/TraxBayTests.cs new file mode 100644 index 0000000..d44b2e4 --- /dev/null +++ b/ytLive.Tests/TraxBayTests.cs @@ -0,0 +1,41 @@ +using Xunit; +using ytLive.Helpers; +using ytLive.Services.Audio; +using ytLive.ViewModels; + +namespace ytLive.Tests; + +/// The TRAX bay shows the track, plays/pauses it, and carries a level — but no meter: +/// nothing measures the bed, and a decorative ladder would be a lie. Its fader moves a gain the +/// player actually applies, separate from the desktop-mute follow. +/// +public class TraxBayTests +{ + [Fact] + public void The_trax_bay_binds_the_track_level_and_play_state_without_inventing_a_meter() + { + var dock = SourceAssert.Text("Controls", "AudioDock.xaml"); + Assert.Contains("Text=\"{Binding TraxTrackLabel}\"", dock); + Assert.Contains("Value=\"{Binding TraxVolumeDb, Mode=TwoWay}\"", dock); + Assert.Contains("Text=\"{Binding TraxVolumeDbLabel}\"", dock); + Assert.Contains("Binding=\"{Binding IsMusicPlaying}\" Value=\"True\"", dock); + Assert.Contains("Click=\"TraxPlayPause_Click\"", dock); + + // No ladder in the TRAX bay: nothing measures the bed, so there is nothing honest to show. + var traxBay = dock[dock.IndexOf("Text=\"TRAX\"", System.StringComparison.Ordinal) + ..dock.IndexOf("Text=\"LIP-SYNC\"", System.StringComparison.Ordinal)]; + Assert.Contains("TraxTrackLabel", traxBay); + Assert.DoesNotContain("SegmentMeter", traxBay); + + foreach (var name in new[] { "TraxVolumeDb", "TraxVolumeDbLabel", "TraxTrackLabel", "IsTraxDucking" }) + Assert.NotNull(typeof(MainViewModel).GetProperty(name)); + + // The player gained a real user level, separate from the mute-follow LocalGain. + Assert.NotNull(typeof(ITraxPlayer).GetProperty("UserGain")); + Assert.NotNull(typeof(MusicPlayer).GetProperty("UserGain")); + + // Unity means the bed's design level, and the fader's scale is the same one as the others. + Assert.Equal(0, GainDb.ToDb(1f), 6); + Assert.Contains("∞", GainDb.Label(GainDb.FloorDb)); // true silence, not "-60.0" + } +} diff --git a/ytLive.Tests/TraxToggleTests.cs b/ytLive.Tests/TraxToggleTests.cs index d770006..64c10e6 100644 --- a/ytLive.Tests/TraxToggleTests.cs +++ b/ytLive.Tests/TraxToggleTests.cs @@ -21,6 +21,7 @@ public sealed class TraxToggleTests public string? TrackName { get; private set; } public bool IsPlaying { get; private set; } public float LocalGain { get; set; } = 1f; + public float UserGain { get; set; } = 1f; public void Load(string path) => TrackName = Path.GetFileNameWithoutExtension(path); public void Play() => IsPlaying = Starts; public void Pause() => IsPlaying = false;