diff --git a/Services/Audio/AudioGainProvider.cs b/Services/Audio/AudioGainProvider.cs index 5eb1b51..c230c56 100644 --- a/Services/Audio/AudioGainProvider.cs +++ b/Services/Audio/AudioGainProvider.cs @@ -28,6 +28,8 @@ public sealed class AudioGainProvider /// Mic gain for the live mix (0 = muted). public double MicGain() => _micMuted() ? 0 : _micVolume(); - /// Loopback (desktop/game) gain for the live mix (0 = muted). - public double LoopbackGain() => _gameMuted() ? 0 : _gameVolume(); + /// Loopback (desktop/game) gain for the live mix (0 = muted, 1 = unity). + /// Unity because the slider now drives system volume via PushSystemVolume, + /// so the WASAPI loopback signal is already scaled by the user's volume choice. + public double LoopbackGain() => _gameMuted() ? 0 : 1.0; } diff --git a/Services/Audio/AudioMixer.cs b/Services/Audio/AudioMixer.cs index f45b666..e87211c 100644 --- a/Services/Audio/AudioMixer.cs +++ b/Services/Audio/AudioMixer.cs @@ -10,8 +10,8 @@ namespace ytLive.Services.Audio; /// through the voice chain (bass → treble → noise gate → compressor), THEN /// level-metered and queued; loopback samples are resampled to 48 kHz stereo /// and queued. While live (), a 10 ms loop drains both -/// queues, applies the honest gains (mic = MicVolume, loopback = GameAudioVolume -/// × auto-duck when the mic is hot), mixes them to stereo float, runs the +/// queues, applies the honest gains (mic = MicVolume, loopback = unity +/// × auto-duck when the mic is hot — the slider drives system volume via mixes them to stereo float, runs the /// master limiter (a -1 dBFS ceiling so the sum never clips the encoder), and /// writes the chunk to the encoder's audio pipe. /// diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 3d87a6a..35569b3 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -652,7 +652,7 @@ public class MainViewModel : ViewModelBase /// Called whenever the game volume/mute change and on TRAX load. private void UpdateTraxLocalGain() { - _musicPlayer.LocalGain = GameMuted ? 0f : (float)(GameAudioVolume * 0.2); + _musicPlayer.LocalGain = GameMuted ? 0f : 1f; } private static void PushSystemVolume(double volume) @@ -684,7 +684,7 @@ public class MainViewModel : ViewModelBase /// Displayed game meter level: 0 while muted; the volume position /// while the slider is dragged (or briefly after an unmute flash); otherwise /// the realtime live level scaled by volume. - private double GameMeterLevel => GameMuted ? 0 : _gameVolumeAdjusting || _gameVolumeFlash ? GameAudioVolume : Math.Min(1, AudioLevelMeter.ToDisplay((float)GameAudioLevel) * GameAudioVolume); + private double GameMeterLevel => GameMuted ? 0 : _gameVolumeAdjusting || _gameVolumeFlash ? GameAudioVolume : Math.Min(1, AudioLevelMeter.ToDisplay((float)GameAudioLevel)); public double GameMeterFillWidth => GameMeterLevel * 288; diff --git a/ai.md b/ai.md index 6736bf7..0c7f04f 100644 --- a/ai.md +++ b/ai.md @@ -447,10 +447,11 @@ devices, no timers). (mp3/wav/m4a) → `VolumeWaveProvider16` at the hardcoded **0.20** bed (no slider) → `WaveOutEvent` on the default device, **looping on any clean natural end** (`PlaybackStopped` with `e.Exception == null` → rewind + replay; the earlier `Position >= Length` check was unreliable for `MediaFoundationReader` - and let tracks play once then stop). The desktop/game bar scales it locally via `LocalGain` (the VM's - `UpdateTraxLocalGain` runs on every `GameAudioVolume`/`GameMuted` change and on TRAX load), so the - creator **hears** the game volume slider and mute work on the music in the headphones — matching the - stream, where it rides the loopback channel ducked with the game. No third mixer input. The footer + and let tracks play once then stop). The desktop/game bar's mute toggles `LocalGain` (0 or 1; + `UpdateTraxLocalGain` runs on every `GameMuted` change and on TRAX load), so muting kills TRAX + in the headphones — matching the stream, where it rides the loopback channel. The volume slider + drives system volume (`PushSystemVolume`), which scales the entire loopback signal including + TRAX — one knob controls everything. No third mixer input. The footer **TRAX** button lives **inside the game audio bar** (in the preview overlay), **left of the "Desktop Audio" label** (the creator's pick — it rides the desktop channel, so the music control sits with the desktop audio controls): status dot (**red** no track / **yellow** loaded stopped / @@ -469,8 +470,9 @@ devices, no timers). silently falling back to the default endpoint.** - **Game audio bar** (desktop/game, **always visible** — TASK 4's `IGameAudioDetector` show/hide gating was **removed 2026-08-15**: the bar used to appear only while a full-screen game with sound was up, which kept hiding the creator's desktop meter; the detector stack is gone, desktop audio is just automatic WASAPI loopback): overlaid **at the bottom of the preview window** (bottom-center, dark translucent chip, a mirror of the mic bar: meter + mute + volume slider). It's monitoring UI **in the preview - grid** — the meter is display-only, but its **volume slider + mute are stream-honest** (via - `AudioGainProvider.LoopbackGain`) **and drive TRAX locally** (`MusicPlayer.LocalGain`). Relabelled + grid** — the meter is display-only, but its **volume slider drives system volume** (`PushSystemVolume` + scales the entire WASAPI endpoint, so both the live headphones and the loopback capture reflect the + slider) **and mute kills TRAX** (`MusicPlayer.LocalGain` = 0). Relabelled **"Desktop Audio"** (TASK 8) since TRAX rides the same channel. - **`WaveToFloat`** (pure, shared): WASAPI mix formats → interleaved float — IEEE float 32-bit direct, PCM 16-bit normalized to -1..1, `WaveFormatExtensible` with the IEEE-float subformat GUID