From 8e7f8cb837b81c851afcc62a1f1db7c24630d7ca Mon Sep 17 00:00:00 2001 From: Will Bennion Date: Tue, 19 May 2020 12:12:30 +0100 Subject: [PATCH 01/15] Added audio device proxy class. Helps with easily swapping devices on-the-fly. --- .../Project-Aurora/Utils/AudioDeviceProxy.cs | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 Project-Aurora/Project-Aurora/Utils/AudioDeviceProxy.cs diff --git a/Project-Aurora/Project-Aurora/Utils/AudioDeviceProxy.cs b/Project-Aurora/Project-Aurora/Utils/AudioDeviceProxy.cs new file mode 100644 index 000000000..abf1f39fa --- /dev/null +++ b/Project-Aurora/Project-Aurora/Utils/AudioDeviceProxy.cs @@ -0,0 +1,110 @@ +using NAudio.CoreAudioApi; +using NAudio.Wave; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Aurora.Utils { + + /// + /// Utility class to make it easier to manage dealing with audio devices and input. + /// Will handle the creation of devices if required. If another AudioDevice is using that device, they will share the same reference. + /// Can be hot-swapped to a different device, moving all events to the newly selected device. + /// + public sealed class AudioDeviceProxy : IDisposable { + + private static readonly MMDeviceEnumerator deviceEnumerator = new MMDeviceEnumerator(); + + // Stores event handlers added to the proxy, so they can easily be added and removed from the MMDevice when it changes without + // needing to rely on the consumer manually removing and re-adding the events. + private EventHandler waveInDataAvailable; + + // ID of currently selected device. + private string deviceId; + + /// Creates a new reference to the default audio device with the given flow direction. + public AudioDeviceProxy(DataFlow flow) : this("", flow) { } + + /// Creates a new reference to the audio device with the given ID with the given flow direction. + public AudioDeviceProxy(string deviceId, DataFlow flow) { + Flow = flow; + DeviceId = deviceId; + } + + /// Indicates recorded data is available on the selected device. + /// This event is automatically reassigned to the new device when it is swapped. + public event EventHandler WaveInDataAvailable { + add { + waveInDataAvailable += value; // Update stored event listeners + if (WaveIn != null) WaveIn.DataAvailable += value; // If the device is valid, pass the event handler on + } + remove { + waveInDataAvailable -= value; // Update stored event listeners + if (WaveIn != null) WaveIn.DataAvailable -= value; // If the device is valid, pass the event handler on + } + } + + public MMDevice Device { get; private set; } + public WasapiCapture WaveIn { get; private set; } + + /// Gets the currently assigned direction of this device. + public DataFlow Flow { get; } + + /// Gets or sets the ID of the selected device. + public string DeviceId { + get => deviceId; + set { + value ??= ""; // Ensure not-null + if (deviceId == value) return; + deviceId = value; + UpdateDevice(); + } + } + + /// Gets a new MMDevice and wave in based on the current and + private void UpdateDevice() { + // Release the current device (if any), removing any events as required + if (WaveIn != null) + WaveIn.DataAvailable -= waveInDataAvailable; + DisposeCurrentDevice(); + + // Get a new device with this ID and flow direction + var mmDevice = string.IsNullOrEmpty(DeviceId) + ? deviceEnumerator.GetDefaultAudioEndpoint(Flow, Role.Multimedia) // Get default if no ID is provided + : deviceEnumerator.EnumerateAudioEndPoints(Flow, DeviceState.Active).FirstOrDefault(d => d.ID == DeviceId); // Otherwise, get the one with this ID + if (mmDevice == null) return; + + // Get a WaveIn from the device and start it, adding any events as requied + WaveIn = mmDevice.DataFlow == DataFlow.Render ? new WasapiLoopbackCapture(mmDevice) : new WasapiCapture(mmDevice); + WaveIn.DataAvailable += waveInDataAvailable; + WaveIn.StartRecording(); + } + + /// Disposes and clears the current and . + private void DisposeCurrentDevice() { + Device?.Dispose(); + Device = null; + + WaveIn?.StopRecording(); + WaveIn?.Dispose(); + WaveIn = null; + } + + #region Device Enumeration + public static IEnumerable> PlaybackDevices { get; } = deviceEnumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active).ToDictionary(d => d.ID, d => d.DeviceFriendlyName); + public static IEnumerable> RecordingDevices { get; } = deviceEnumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active).ToDictionary(d => d.ID, d => d.DeviceFriendlyName); + #endregion + + #region IDisposable Implementation + private bool disposedValue = false; + public void Dispose() => Dispose(true); + void Dispose(bool disposing) { + if (!disposedValue) { + if (disposing) + DisposeCurrentDevice(); + disposedValue = true; + } + } + #endregion + } +} From 5f7b780cbc62efa17569f889fecbf1234ce3a379 Mon Sep 17 00:00:00 2001 From: Will Bennion Date: Tue, 19 May 2020 13:00:31 +0100 Subject: [PATCH 02/15] Added default device entry to the device lists. --- .../Project-Aurora/Utils/AudioDeviceProxy.cs | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/Project-Aurora/Project-Aurora/Utils/AudioDeviceProxy.cs b/Project-Aurora/Project-Aurora/Utils/AudioDeviceProxy.cs index abf1f39fa..97045731f 100644 --- a/Project-Aurora/Project-Aurora/Utils/AudioDeviceProxy.cs +++ b/Project-Aurora/Project-Aurora/Utils/AudioDeviceProxy.cs @@ -2,6 +2,7 @@ using NAudio.Wave; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; namespace Aurora.Utils { @@ -13,6 +14,8 @@ namespace Aurora.Utils { /// public sealed class AudioDeviceProxy : IDisposable { + private const string DEFAULT_DEVICE_ID = ""; // special ID to indicate the default device + private static readonly MMDeviceEnumerator deviceEnumerator = new MMDeviceEnumerator(); // Stores event handlers added to the proxy, so they can easily be added and removed from the MMDevice when it changes without @@ -22,6 +25,12 @@ public sealed class AudioDeviceProxy : IDisposable { // ID of currently selected device. private string deviceId; + static AudioDeviceProxy() { + // Tried using a static class to update the device lists when they changed, but it caused an AccessViolation. Will try look into this again in future + //deviceEnumerator.RegisterEndpointNotificationCallback(new DeviceChangedHandler()); + RefreshDeviceLists(); + } + /// Creates a new reference to the default audio device with the given flow direction. public AudioDeviceProxy(DataFlow flow) : this("", flow) { } @@ -54,7 +63,7 @@ public event EventHandler WaveInDataAvailable { public string DeviceId { get => deviceId; set { - value ??= ""; // Ensure not-null + value ??= DEFAULT_DEVICE_ID; // Ensure not-null (if null, assume default device) if (deviceId == value) return; deviceId = value; UpdateDevice(); @@ -69,13 +78,13 @@ private void UpdateDevice() { DisposeCurrentDevice(); // Get a new device with this ID and flow direction - var mmDevice = string.IsNullOrEmpty(DeviceId) + var mmDevice = deviceId == DEFAULT_DEVICE_ID ? deviceEnumerator.GetDefaultAudioEndpoint(Flow, Role.Multimedia) // Get default if no ID is provided : deviceEnumerator.EnumerateAudioEndPoints(Flow, DeviceState.Active).FirstOrDefault(d => d.ID == DeviceId); // Otherwise, get the one with this ID if (mmDevice == null) return; // Get a WaveIn from the device and start it, adding any events as requied - WaveIn = mmDevice.DataFlow == DataFlow.Render ? new WasapiLoopbackCapture(mmDevice) : new WasapiCapture(mmDevice); + WaveIn = Flow == DataFlow.Render ? new WasapiLoopbackCapture(mmDevice) : new WasapiCapture(mmDevice); WaveIn.DataAvailable += waveInDataAvailable; WaveIn.StartRecording(); } @@ -91,8 +100,23 @@ private void DisposeCurrentDevice() { } #region Device Enumeration - public static IEnumerable> PlaybackDevices { get; } = deviceEnumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active).ToDictionary(d => d.ID, d => d.DeviceFriendlyName); - public static IEnumerable> RecordingDevices { get; } = deviceEnumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active).ToDictionary(d => d.ID, d => d.DeviceFriendlyName); + public static ObservableCollection> PlaybackDevices { get; } = new ObservableCollection>(); + public static ObservableCollection> RecordingDevices { get; } = new ObservableCollection>(); + + // Updates the target list with the devices of the given dataflow type. + private static void RefreshDeviceList(ObservableCollection> target, DataFlow flow) { + // Note: clear the target then repopulate it to make it easier for data binding. If we re-created this, we could not use {x:Static}. + target.Clear(); + target.Add(new KeyValuePair(DEFAULT_DEVICE_ID, "Default")); // Add default device to to the top of the list + foreach (var device in deviceEnumerator.EnumerateAudioEndPoints(flow, DeviceState.Active).OrderBy(d => d.DeviceFriendlyName)) + target.Add(new KeyValuePair(device.ID, device.DeviceFriendlyName)); + } + + // Refreshes both playback and recording devices lists. + private static void RefreshDeviceLists() { + RefreshDeviceList(PlaybackDevices, DataFlow.Render); + RefreshDeviceList(RecordingDevices, DataFlow.Capture); + } #endregion #region IDisposable Implementation From 16766e698fe76f1f8131d471462ecca3a6d3f015 Mon Sep 17 00:00:00 2001 From: Will Bennion Date: Tue, 19 May 2020 13:00:58 +0100 Subject: [PATCH 03/15] fixup! Added audio device proxy class. Helps with easily swapping devices on-the-fly. --- Project-Aurora/Project-Aurora/Project-Aurora.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Project-Aurora/Project-Aurora/Project-Aurora.csproj b/Project-Aurora/Project-Aurora/Project-Aurora.csproj index 157886780..e85a9cbc7 100644 --- a/Project-Aurora/Project-Aurora/Project-Aurora.csproj +++ b/Project-Aurora/Project-Aurora/Project-Aurora.csproj @@ -691,6 +691,7 @@ Window_ProcessSelection.xaml + From 56aac6ad5b7a998ae463bf61d8634da02dcbb112 Mon Sep 17 00:00:00 2001 From: Will Bennion Date: Tue, 19 May 2020 13:55:58 +0100 Subject: [PATCH 04/15] Added options for choosing the audio device for equalizer layer and for the local pc inf game state. Changing the gamestate device requires a restart. --- .../Profiles/LocalPCInformation.cs | 67 ++++++------------ .../Project-Aurora/Settings/Configuration.cs | 3 + .../Settings/Control_Settings.xaml | 41 ++++++----- .../Layers/Control_EqualizerLayer.xaml | 11 +-- .../Settings/Layers/EqualizerLayerHandler.cs | 68 ++++--------------- .../Project-Aurora/Utils/AudioDeviceProxy.cs | 7 +- 6 files changed, 74 insertions(+), 123 deletions(-) diff --git a/Project-Aurora/Project-Aurora/Profiles/LocalPCInformation.cs b/Project-Aurora/Project-Aurora/Profiles/LocalPCInformation.cs index 18115148e..54130be99 100644 --- a/Project-Aurora/Project-Aurora/Profiles/LocalPCInformation.cs +++ b/Project-Aurora/Project-Aurora/Profiles/LocalPCInformation.cs @@ -35,61 +35,39 @@ public class LocalPCInformation : Node { #endregion #region Audio Properties - private static readonly MMDeviceEnumerator mmDeviceEnumerator = new MMDeviceEnumerator(); - private static readonly NAudio.Wave.WaveInEvent waveInEvent = new NAudio.Wave.WaveInEvent(); - - /// - /// Gets the default endpoint for output (playback) devices e.g. speakers, headphones, etc. - /// This will return null if there are no playback devices available. - /// - private MMDevice DefaultAudioOutDevice { - get { - try { return mmDeviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Console); } - catch { return null; } - } - } - - /// - /// Gets the default endpoint for input (recording) devices e.g. microphones. - /// This will return null if there are no recording devices available. - /// - private MMDevice DefaultAudioInDevice { - get { - try { return mmDeviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Capture, Role.Console); } - catch { return null; } - } - } + private static AudioDeviceProxy captureProxy; + private static readonly AudioDeviceProxy renderProxy; /// /// Current system volume (as set from the speaker icon) /// // Note: Manually checks if muted to return 0 since this is not taken into account with the MasterVolumeLevelScalar. - public float SystemVolume => SystemVolumeIsMuted ? 0 : DefaultAudioOutDevice?.AudioEndpointVolume.MasterVolumeLevelScalar * 100 ?? 0; + public float SystemVolume => SystemVolumeIsMuted ? 0 : renderProxy?.Device?.AudioEndpointVolume.MasterVolumeLevelScalar * 100 ?? 0; /// /// Gets whether the system volume is muted. /// - public bool SystemVolumeIsMuted => DefaultAudioOutDevice?.AudioEndpointVolume.Mute ?? true; + public bool SystemVolumeIsMuted => renderProxy?.Device?.AudioEndpointVolume.Mute ?? true; /// /// The volume level that is being recorded by the default microphone even when muted. /// - public float MicrophoneLevel => DefaultAudioInDevice?.AudioMeterInformation.MasterPeakValue * 100 ?? 0; + public float MicrophoneLevel => captureProxy?.Device?.AudioMeterInformation.MasterPeakValue * 100 ?? 0; /// /// The volume level that is being emitted by the default speaker even when muted. /// - public float SpeakerLevel => DefaultAudioOutDevice?.AudioMeterInformation.MasterPeakValue * 100 ?? 0; + public float SpeakerLevel => renderProxy?.Device?.AudioMeterInformation.MasterPeakValue * 100 ?? 0; /// /// The volume level that is being recorded by the default microphone if not muted. /// - public float MicLevelIfNotMuted => MicrophoneIsMuted ? 0 : DefaultAudioInDevice?.AudioMeterInformation.MasterPeakValue * 100 ?? 0; + public float MicLevelIfNotMuted => MicrophoneIsMuted ? 0 : captureProxy?.Device?.AudioMeterInformation.MasterPeakValue * 100 ?? 0; /// /// Gets whether the default microphone is muted. /// - public bool MicrophoneIsMuted => DefaultAudioInDevice?.AudioEndpointVolume.Mute ?? true; + public bool MicrophoneIsMuted => captureProxy?.Device?.AudioEndpointVolume.Mute ?? true; #endregion #region Device Properties @@ -149,22 +127,19 @@ private MMDevice DefaultAudioInDevice { public bool IsDesktopLocked => DesktopUtils.IsDesktopLocked; static LocalPCInformation() { - void StartStopRecording() { - // We must start recording to be able to capture audio in, but only do this if the user has the option set. Allowing them - // to turn it off will give them piece of mind we're not spying on them and will stop the Windows 10 mic icon appearing. - try { - if (Global.Configuration.EnableAudioCapture) - waveInEvent.StartRecording(); - else - waveInEvent.StopRecording(); - } catch { } - } - - StartStopRecording(); - Global.Configuration.PropertyChanged += (sender, e) => { - if (e.PropertyName == "EnableAudioCapture") - StartStopRecording(); - }; + // Do not create a capture device if audio capture is disabled. Otherwise it will create a mic icon in win 10 and people will think we're spies. + if (Global.Configuration.EnableAudioCapture) + captureProxy = new AudioDeviceProxy(Global.Configuration.GSIAudioCaptureDevice, DataFlow.Capture); + renderProxy = new AudioDeviceProxy(Global.Configuration.GSIAudioRenderDevice, DataFlow.Render); + + /* Note that I tried setting up a PropertyChanged listener on the Global.Configuration instance to watch for when the audio device is + * changed/disabled. However, since the MMDevice needs to be re-created, this resulted in a COM exception when trying to cast to a + * different type, which according to NAudio's GitHub is due to it being created on a different thread (https://github.com/naudio/NAudio/issues/214) + * + * I tried using different syncronization contexts to recreate the devices on, however I think because they were created in the static + * constructor (where the SyncronizationContext.Current is null), we cannot retrieve that context or something. + * + * Idk maybe a smarter man than I can figure it out. */ } } diff --git a/Project-Aurora/Project-Aurora/Settings/Configuration.cs b/Project-Aurora/Project-Aurora/Settings/Configuration.cs index 35fa66a06..9ed26a8a6 100755 --- a/Project-Aurora/Project-Aurora/Settings/Configuration.cs +++ b/Project-Aurora/Project-Aurora/Settings/Configuration.cs @@ -515,6 +515,9 @@ public class Configuration : INotifyPropertyChanged public List ProfileOrder { get; set; } = new List(); + public string GSIAudioRenderDevice { get; set; } = AudioDeviceProxy.DEFAULT_DEVICE_ID; + public string GSIAudioCaptureDevice { get; set; } = AudioDeviceProxy.DEFAULT_DEVICE_ID; + public Configuration() { //First Time Installs diff --git a/Project-Aurora/Project-Aurora/Settings/Control_Settings.xaml b/Project-Aurora/Project-Aurora/Settings/Control_Settings.xaml index 0b5ff0e73..04581450b 100755 --- a/Project-Aurora/Project-Aurora/Settings/Control_Settings.xaml +++ b/Project-Aurora/Project-Aurora/Settings/Control_Settings.xaml @@ -8,7 +8,7 @@ xmlns:EnumDeviceKeys="clr-namespace:Aurora.Devices" xmlns:EnumPercentEffectType="clr-namespace:Aurora.Settings" xmlns:EnumInteractiveEffects="clr-namespace:Aurora.Profiles.Desktop" - xmlns:EnumValueConverters="clr-namespace:Aurora.Utils" + xmlns:u="clr-namespace:Aurora.Utils" xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit" xmlns:Controls="clr-namespace:Aurora.Controls" xmlns:params="http://schemas.codeplex.com/elysium/params" x:Class="Aurora.Settings.Control_Settings" mc:Ignorable="d" @@ -20,12 +20,12 @@ - + - + @@ -35,7 +35,7 @@ - + @@ -45,7 +45,7 @@ - + @@ -55,7 +55,7 @@ - + @@ -65,7 +65,7 @@ - + @@ -75,7 +75,7 @@ - + @@ -85,7 +85,7 @@ - + @@ -95,7 +95,7 @@ - + @@ -105,7 +105,7 @@ - + @@ -115,7 +115,7 @@ - + @@ -132,10 +132,10 @@ - - -