TASK 24 post-pause polish batch (creator's 8 review issues, 2026-08-15): (1) desktop/game audio volume slider + (2) mute were no-ops — the AudioMixer's gain Func seams defaulted to unity because the VM never passed them; new AudioGainProvider (Services/Audio/) wires MicMuted/MicVolume/GameMuted/GameAudioVolume into the mixer at construction, read live each mix tick, so sliders and mutes are stream-honest; the game bar also scales TRAX locally via new MusicPlayer.LocalGain (UpdateTraxLocalGain on every game volume/mute change + on load) so the creator HEARS the controls work on the music in the headphones. (3) TRAX played once then stopped — OnPlaybackStopped only looped on Position>=Length (unreliable for MediaFoundationReader); now any clean stop rewinds + replays, errors/explicit stops surface via PlaybackEnded; unused using dropped. (4+6) TRAX moved right of Socials in the footer's left cluster (centered under scenes/sources); the mic cluster is now MIC + meter + mute + volume. (5) mic source persists — LayoutStore gains a Settings key/value table (SaveMicSourceName/LoadMicSourceName); the VM saves on pick and restores before the mixer's first Start, so a restart reconnects the same vetted device (green) or reports it missing (yellow). (7) Backdrop's missing trashcan shifted the edit/eye icons right — new HiddenBoolToVisibilityConverter keeps the trash column reserved (Hidden, not Collapsed) so every source row's icons stay in fixed columns. (8) a 1px hairline with top/bottom padding separates scenes from sources in the left panel. Docs in the same commit: ai.md (gains honest + drive TRAX locally, TRAX loops, mic persistence, footer layout), TASKS.md TASK 24, HANDOFF.md shipped state, ViewModels/index.md. ONE integration test: AudioPipelineTests.Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback (real mixer + provider gains through the pipe harness: scaled loopback audible, silence after mute). Build 0 warnings, 197 tests passing
This commit is contained in:
@@ -58,6 +58,7 @@ public class MainViewModel : ViewModelBase
|
||||
private readonly MusicPlayer _musicPlayer = new();
|
||||
private Music? _music;
|
||||
private bool _musicPlaying;
|
||||
private AudioGainProvider? _gainProvider;
|
||||
private StreamStatus _streamStatus = StreamStatus.Offline;
|
||||
private StreamHealth _currentHealth = new();
|
||||
private string _streamTitle = string.Empty;
|
||||
@@ -450,6 +451,7 @@ public class MainViewModel : ViewModelBase
|
||||
if (dialog.ShowDialog() == true && dialog.PickedDevice != null)
|
||||
{
|
||||
MicSourceName = dialog.PickedDevice.DisplayName;
|
||||
_layoutStore.SaveMicSourceName(MicSourceName); // persist across restarts
|
||||
_audioMixer.RestartMic(); // swap the live device immediately
|
||||
}
|
||||
}
|
||||
@@ -560,6 +562,7 @@ public class MainViewModel : ViewModelBase
|
||||
try
|
||||
{
|
||||
_musicPlayer.Load(_music.TrackPath);
|
||||
UpdateTraxLocalGain();
|
||||
_music.IsEnabled = true;
|
||||
if (play)
|
||||
_musicPlayer.Play();
|
||||
@@ -591,6 +594,15 @@ public class MainViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(TraxToolTip));
|
||||
}
|
||||
|
||||
/// <summary>Mirrors the desktop/game audio bar's volume and mute onto the
|
||||
/// music so the creator hears the controls work in the headphones (the
|
||||
/// track rides the loopback channel, so it must duck the same way locally).
|
||||
/// Called whenever the game volume/mute change and on TRAX load.</summary>
|
||||
private void UpdateTraxLocalGain()
|
||||
{
|
||||
_musicPlayer.LocalGain = GameMuted ? 0f : (float)GameAudioVolume;
|
||||
}
|
||||
|
||||
/// <summary>Live desktop/game input level (0..1), fed by the mixer's loopback
|
||||
/// capture. Read by the game meter, scaled by GameAudioVolume.</summary>
|
||||
public double GameAudioLevel
|
||||
@@ -680,6 +692,7 @@ public class MainViewModel : ViewModelBase
|
||||
_gameVolumeBeforeMute = null;
|
||||
OnPropertyChanged(nameof(GameMeterFillWidth));
|
||||
OnPropertyChanged(nameof(GameMeterBrush));
|
||||
UpdateTraxLocalGain();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1148,6 +1161,11 @@ public class MainViewModel : ViewModelBase
|
||||
_layoutStore = new LayoutStore(LayoutPathOverride ?? DefaultLayoutPath);
|
||||
_activeLayoutPath = _layoutStore.ActivePath;
|
||||
|
||||
// Restore the last picked mic so the mixer's first capture attaches to
|
||||
// the same, already-vetted device (green dot) or reports it missing
|
||||
// (yellow) instead of silently falling back to the default.
|
||||
MicSourceName = _layoutStore.LoadMicSourceName();
|
||||
|
||||
_cameraEnumerator = new MediaCaptureCameraEnumerator();
|
||||
_cameraManager = new CameraManager(
|
||||
_cameraEnumerator,
|
||||
@@ -1158,10 +1176,21 @@ public class MainViewModel : ViewModelBase
|
||||
|
||||
_microphoneEnumerator = new WinRtMicrophoneEnumerator();
|
||||
|
||||
// The mixer consumes these Func seams live each mix tick, so the volume
|
||||
// sliders and mute buttons are stream-honest (they also update the
|
||||
// meters' display). Before this wiring the gains defaulted to unity and
|
||||
// the controls were decorative.
|
||||
_gainProvider = new AudioGainProvider(
|
||||
() => MicMuted,
|
||||
() => MicVolume,
|
||||
() => GameMuted,
|
||||
() => GameAudioVolume);
|
||||
_audioMixer = new AudioMixer(
|
||||
new WasapiMicAudioSource(() => MicSourceName),
|
||||
new WasapiLoopbackAudioSource(),
|
||||
message => AppLog.Write(message));
|
||||
message => AppLog.Write(message),
|
||||
micGain: _gainProvider.MicGain,
|
||||
loopbackGain: _gainProvider.LoopbackGain);
|
||||
_audioMixer.MicLevelChanged += OnMicLevelChanged;
|
||||
_audioMixer.LoopbackLevelChanged += OnLoopbackLevelChanged;
|
||||
_audioMixer.MicConnected += OnMicConnected;
|
||||
|
||||
Reference in New Issue
Block a user