diff --git a/SoundBoard/GlobalSettings.cs b/SoundBoard/GlobalSettings.cs
index 717b903..5715950 100644
--- a/SoundBoard/GlobalSettings.cs
+++ b/SoundBoard/GlobalSettings.cs
@@ -97,6 +97,43 @@ public static class GlobalSettings
#endregion
+ #region Passthrough output device
+
+ ///
+ /// Add an output device to the current list
+ ///
+ public static void AddPassthroughOutputDeviceGuid(Guid guid) => PassthroughOutputDeviceGuids.Add(guid);
+
+ ///
+ /// Remove an output device from the current list
+ ///
+ public static void RemovePassthroughOutputDeviceGuid(Guid guid) => PassthroughOutputDeviceGuids.Remove(guid);
+
+ ///
+ /// Removes all current output devices
+ ///
+ public static void RemoveAllPassthroughOutputDeviceGuids() => PassthroughOutputDeviceGuids.Clear();
+
+ ///
+ /// Get the current list of output devices
+ ///
+ public static List GetPassthroughOutputDeviceGuids() => (PassthroughOutputDeviceGuids.Any() ? PassthroughOutputDeviceGuids : Enumerable.Empty()).ToList();
+
+ ///
+ /// The name of the OutputDeviceGuid setting name.
+ ///
+ public static string PassthroughOutputDeviceGuidSettingName = "PassthroughOutputDeviceGuid";
+
+ ///
+ /// Defines the ID(s) of the audio output device(s) to use when playing sounds
+ ///
+ ///
+ /// HashSet to prevent duplicate GUIDs.
+ ///
+ private static HashSet PassthroughOutputDeviceGuids { get; } = new HashSet();
+
+ #endregion
+
///
/// The latency to use when chaining input to outputs
///
diff --git a/SoundBoard/MainWindow.xaml.cs b/SoundBoard/MainWindow.xaml.cs
index c7213e9..9fefdb9 100644
--- a/SoundBoard/MainWindow.xaml.cs
+++ b/SoundBoard/MainWindow.xaml.cs
@@ -139,7 +139,7 @@ public MainWindow()
DownloadIdentifier = "portable"
};
- HandleInputOutputChange();
+ HandleAudioPassthroughChange();
Tabs.SelectionChanged += (_, __) =>
{
@@ -373,6 +373,17 @@ private void LoadSettings(string configFilePath)
});
}
+ if (globalSettings.Attributes?[GlobalSettings.PassthroughOutputDeviceGuidSettingName] is XmlAttribute passthroughOutputDeviceGuidAttribute)
+ {
+ passthroughOutputDeviceGuidAttribute.Value.Split(',').ToList().ForEach(guid =>
+ {
+ if (Guid.TryParse(guid, out var passthroughOutputDeviceGuid))
+ {
+ GlobalSettings.AddPassthroughOutputDeviceGuid(passthroughOutputDeviceGuid);
+ }
+ });
+ }
+
if (globalSettings.Attributes?[nameof(GlobalSettings.AudioPassthroughLatency)] is XmlAttribute audioPassthroughLatencyAttribute
&& int.TryParse(audioPassthroughLatencyAttribute.Value, out int audioPassthroughLatency))
{
@@ -844,6 +855,7 @@ private void SaveSettings(string configFilePath)
textWriter.WriteStartElement(nameof(GlobalSettings)); //
textWriter.WriteAttributeString(GlobalSettings.OutputDeviceGuidSettingName, string.Join(@",", GlobalSettings.GetOutputDeviceGuids()));
textWriter.WriteAttributeString(GlobalSettings.InputDeviceGuidSettingName, string.Join(@",", GlobalSettings.GetInputDeviceGuids()));
+ textWriter.WriteAttributeString(GlobalSettings.PassthroughOutputDeviceGuidSettingName, string.Join(@",", GlobalSettings.GetPassthroughOutputDeviceGuids()));
textWriter.WriteAttributeString(nameof(GlobalSettings.AudioPassthroughLatency), GlobalSettings.AudioPassthroughLatency.ToString());
textWriter.WriteAttributeString(nameof(GlobalSettings.NewPageDefaultRows), GlobalSettings.NewPageDefaultRows.ToString());
textWriter.WriteAttributeString(nameof(GlobalSettings.NewPageDefaultColumns), GlobalSettings.NewPageDefaultColumns.ToString());
@@ -1217,10 +1229,10 @@ private void overflow_Click(object sender, RoutedEventArgs e)
_newPageDefaultMenu.Click += NewPageDefault_Click;
_newPageDefaultMenu.SetSeparator(true);
- _inputDeviceMenu = new MenuItem { Header = Properties.Resources.InputDevice };
- _inputDeviceMenu.SubmenuOpened += InputDeviceMenuOpened;
+ _audioPassthroughMenu = new MenuItem { Header = Properties.Resources.AudioPassthrough };
+ _audioPassthroughMenu.SubmenuOpened += AudioPassthroughMenuOpened;
- _outputDeviceMenu = new MenuItem {Header = Properties.Resources.OutputDevice};
+ _outputDeviceMenu = new MenuItem {Header = Properties.Resources.SoundOutputDevice};
_outputDeviceMenu.SubmenuOpened += OutputDeviceMenuOpened;
// Add a placeholder menu item so that "Output device" will have a submenu
@@ -1228,16 +1240,16 @@ private void overflow_Click(object sender, RoutedEventArgs e)
MenuItem placeholder = new MenuItem();
_outputDeviceMenu.Items.Add(placeholder);
- // Add a placeholder menu item so that "Input device" will have a submenu
+ // Add a placeholder menu item so that "Audio Passthrough" will have a submenu
// even before we have evaluated the audio devices to add to the menu
placeholder = new MenuItem();
- _inputDeviceMenu.Items.Add(placeholder);
+ _audioPassthroughMenu.Items.Add(placeholder);
overflowMenu.Items.Add(importConfig);
overflowMenu.Items.Add(exportConfig);
overflowMenu.Items.Add(clearConfig);
overflowMenu.Items.Add(_newPageDefaultMenu);
- overflowMenu.Items.Add(_inputDeviceMenu);
+ overflowMenu.Items.Add(_audioPassthroughMenu);
overflowMenu.Items.Add(_outputDeviceMenu);
overflowMenu.AddSeparators();
@@ -1423,17 +1435,19 @@ private async void NewPageDefault_Click(object sender, RoutedEventArgs e)
}
}
- private void InputDeviceMenuOpened(object sender, RoutedEventArgs e)
+ private void AudioPassthroughMenuOpened(object sender, RoutedEventArgs e)
{
- if (sender is MenuItem inputDeviceMenu)
+ if (sender is MenuItem audioPassthroughMenu)
{
// Clear the current items, whether they are the placeholder
// or the previously evaluated audio devices.
// We're marking them for removal instead of removing them immediately so that the
// menu doesn't resize and decide to close because our mouse is no longer over it.
// Instead we'll add all the new items, then remove the old ones at the very end.
- // Use Control istead of MenuItem to capture the Separator.
- List itemsToRemove = inputDeviceMenu.Items.OfType().ToList();
+ // Use Control instead of MenuItem to capture the Separator.
+ List itemsToRemove = audioPassthroughMenu.Items.OfType().ToList();
+
+ #region Output
// Create a menu item for each output device
using (MMDeviceEnumerator deviceEnumerator = new MMDeviceEnumerator())
@@ -1441,37 +1455,75 @@ private void InputDeviceMenuOpened(object sender, RoutedEventArgs e)
// Note: We're going in reverse order to preserve the separator and "Close" item at the bottom
// Now add the rest
- foreach (MMDevice device in deviceEnumerator.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active).Reverse())
+ foreach (MMDevice device in deviceEnumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active).Reverse())
{
MenuItem menuItem = new MenuItem
{
Header = string.Format(Properties.Resources.SingleSpecifier, device.FriendlyName),
- Icon = GlobalSettings.GetInputDeviceGuids().Contains(device.GetGuid()) ? ImageHelper.GetImage(ImageHelper.CheckIconPath) : null,
+ Icon = GlobalSettings.GetPassthroughOutputDeviceGuids().Contains(device.GetGuid()) ? ImageHelper.GetImage(ImageHelper.CheckIconPath) : null,
StaysOpenOnClick = true
};
- menuItem.PreviewMouseUp += (_, args) => HandleInputDeviceSelection(device.GetGuid());
- inputDeviceMenu.Items.Insert(0, menuItem);
+ menuItem.PreviewMouseUp += (_, args) => HandlePassthroughOutputDeviceSelection(device.GetGuid(), args.ChangedButton);
+ audioPassthroughMenu.Items.Insert(0, menuItem);
}
- MenuItem closeDeviceMenuMenuItem = new MenuItem
+ // First, add the default device
+ var defaultDevice = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
+ var defaultDeviceMenuItem = new MenuItem
{
- Header = Properties.Resources.Close
+ Header = string.Format(Properties.Resources.DefaultDevice, defaultDevice.FriendlyName),
+ Icon = GlobalSettings.GetPassthroughOutputDeviceGuids().Contains(Guid.Empty) ? ImageHelper.GetImage(ImageHelper.CheckIconPath) : null,
+ StaysOpenOnClick = true
};
- inputDeviceMenu.Items.Add(new Separator());
- inputDeviceMenu.Items.Add(closeDeviceMenuMenuItem);
+ defaultDeviceMenuItem.PreviewMouseUp += (_, args) => HandlePassthroughOutputDeviceSelection(Guid.Empty, args.ChangedButton);
+ audioPassthroughMenu.Items.Insert(0, defaultDeviceMenuItem);
}
- // Finally, remove the items marked for removal
- foreach (Control control in itemsToRemove)
+ // Add an item for output heading
+ audioPassthroughMenu.Items.Insert(0, new MenuItem { Header = Properties.Resources.OutputDevice, IsEnabled = false });
+
+ audioPassthroughMenu.Items.Insert(0, new Separator());
+
+ #endregion
+
+ #region Input
+
+ // Create a menu item for each output device
+ using (MMDeviceEnumerator deviceEnumerator = new MMDeviceEnumerator())
{
- inputDeviceMenu.Items.Remove(control);
+ // Note: We're going in reverse order to preserve the separator and "Close" item at the bottom
+
+ // Now add the rest
+ foreach (MMDevice device in deviceEnumerator.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active).Reverse())
+ {
+ MenuItem menuItem = new MenuItem
+ {
+ Header = string.Format(Properties.Resources.SingleSpecifier, device.FriendlyName),
+ Icon = GlobalSettings.GetInputDeviceGuids().Contains(device.GetGuid()) ? ImageHelper.GetImage(ImageHelper.CheckIconPath) : null,
+ StaysOpenOnClick = true
+ };
+ menuItem.PreviewMouseUp += (_, args) => HandlePassthroughInputDeviceSelection(device.GetGuid());
+ audioPassthroughMenu.Items.Insert(0, menuItem);
+ }
}
- // We only have close and separator, which looks funny, so remove the separator
- if (inputDeviceMenu.Items.Count == 2
- && inputDeviceMenu.Items.OfType().FirstOrDefault() is Separator separator)
+ // Add an item for input heading
+ audioPassthroughMenu.Items.Insert(0, new MenuItem { Header = Properties.Resources.InputDevice, IsEnabled = false });
+
+ #endregion
+
+ // Add close
+ MenuItem closeDeviceMenuMenuItem = new MenuItem
{
- inputDeviceMenu.Items.Remove(separator);
+ Header = Properties.Resources.Close
+ };
+ audioPassthroughMenu.Items.Add(new Separator());
+ audioPassthroughMenu.Items.Add(closeDeviceMenuMenuItem);
+
+ // Finally, remove the items marked for removal
+ foreach (Control control in itemsToRemove)
+ {
+ audioPassthroughMenu.Items.Remove(control);
}
}
}
@@ -1549,7 +1601,7 @@ private void OutputDeviceMenuOpened(object sender, RoutedEventArgs e)
// This behavior is different from the output devices in that we can only select one.
// However, all of the pieces are in place to allow multiple selection if needed.
- private void HandleInputDeviceSelection(Guid deviceGuid)
+ private void HandlePassthroughInputDeviceSelection(Guid deviceGuid)
{
if (GlobalSettings.GetInputDeviceGuids().Contains(deviceGuid))
{
@@ -1564,12 +1616,58 @@ private void HandleInputDeviceSelection(Guid deviceGuid)
}
// Refresh the menu
- InputDeviceMenuOpened(_inputDeviceMenu, new RoutedEventArgs());
+ AudioPassthroughMenuOpened(_audioPassthroughMenu, new RoutedEventArgs());
- HandleInputOutputChange();
+ HandleAudioPassthroughChange();
}
- private void HandleInputOutputChange()
+ // This behavior is different from both in put selection (which only allows one) and output selection (which requires at least one)
+ private void HandlePassthroughOutputDeviceSelection(Guid guid, MouseButton mouseButton)
+ {
+ if (mouseButton == MouseButton.Right)
+ {
+ // This is a toggle. Do not clear the list, and add or removing depending on existence.
+ if (GlobalSettings.GetPassthroughOutputDeviceGuids().Contains(guid))
+ {
+ if (GlobalSettings.GetPassthroughOutputDeviceGuids().All(g => g == guid))
+ {
+ // This is the only device in the list, so we can't really toggle it. Do nothing.
+ }
+ else
+ {
+ // This is in the list, and it's being toggled off, so remove it.
+ GlobalSettings.RemovePassthroughOutputDeviceGuid(guid);
+ }
+ }
+ else
+ {
+ // This is not the list, and it's being togged on, so add it.
+ GlobalSettings.AddPassthroughOutputDeviceGuid(guid);
+ }
+ }
+ else
+ {
+ // A single click will toggle this one
+ if (GlobalSettings.GetPassthroughOutputDeviceGuids().Any() && GlobalSettings.GetPassthroughOutputDeviceGuids().All(g => g == guid))
+ {
+ // Toggle it off
+ GlobalSettings.RemovePassthroughOutputDeviceGuid(guid);
+ }
+ else
+ {
+ // Toggle it on and remove others
+ GlobalSettings.RemoveAllPassthroughOutputDeviceGuids();
+ GlobalSettings.AddPassthroughOutputDeviceGuid(guid);
+ }
+ }
+
+ // Refresh the menu
+ AudioPassthroughMenuOpened(_audioPassthroughMenu, new RoutedEventArgs());
+
+ HandleAudioPassthroughChange();
+ }
+
+ private void HandleAudioPassthroughChange()
{
// Clear any existing chaining
CleanUpAudioPassthrough();
@@ -1578,14 +1676,14 @@ private void HandleInputOutputChange()
{
Guid inputDeviceId = GlobalSettings.GetInputDeviceGuids().First();
- foreach (var outputDeviceId in GlobalSettings.GetOutputDeviceGuids())
+ foreach (var outputDeviceId in GlobalSettings.GetPassthroughOutputDeviceGuids())
{
try
{
// Create the input
MMDevice inputDevice = Utilities.GetDevice(inputDeviceId, DataFlow.Capture);
WasapiCapture inputCapture = new WasapiCapture(inputDevice);
- inputCapture.RecordingStopped += HandleRecordingStopped;
+ inputCapture.RecordingStopped += HandlePassthroughRecordingStopped;
_inputCaptures.Add(inputCapture);
// Create the buffer
@@ -1602,6 +1700,7 @@ private void HandleInputOutputChange()
// Create the outputs
WasapiOut output = new WasapiOut(Utilities.GetDevice(outputDeviceId, DataFlow.Render), AudioClientShareMode.Shared, true, GlobalSettings.AudioPassthroughLatency);
+ output.PlaybackStopped += HandlePassthroughPlaybackStopped;
_outputCaptures.Add(output);
output.Init(bufferedWaveProvider);
@@ -1611,7 +1710,8 @@ private void HandleInputOutputChange()
}
catch (Exception ex)
{
- HandleRecordingStopped(this, new StoppedEventArgs());
+ HandlePassthroughRecordingStopped(this, new StoppedEventArgs());
+ HandlePassthroughPlaybackStopped(this, new StoppedEventArgs());
Dispatcher.Invoke(async () =>
{
@@ -1663,20 +1763,31 @@ private void HandleInputOutputChange()
}
}
- private void HandleRecordingStopped(object sender, StoppedEventArgs args)
+ private void HandlePassthroughRecordingStopped(object sender, StoppedEventArgs args)
{
- // Handle the device being disabled/disconnected/etc.
+ // Handle the input device being disabled/disconnected/etc.
GlobalSettings.RemoveAllInputDeviceGuids();
CleanUpAudioPassthrough();
}
+ private void HandlePassthroughPlaybackStopped(object sender, StoppedEventArgs args)
+ {
+ // This fires even for normal stoppages, so we only want to clean things up if there was an exception
+ if (args.Exception != null)
+ {
+ // Handle the output device being disabled/disconnected/etc.
+ GlobalSettings.RemoveAllPassthroughOutputDeviceGuids();
+ CleanUpAudioPassthrough();
+ }
+ }
+
private void CleanUpAudioPassthrough()
{
try
{
_inputCaptures.ForEach(ic =>
{
- ic.RecordingStopped -= HandleRecordingStopped;
+ ic.RecordingStopped -= HandlePassthroughRecordingStopped;
ic.StopRecording();
ic.Dispose();
});
@@ -1745,8 +1856,6 @@ private void HandleOutputDeviceSelection(Guid deviceGuid, MouseButton mouseButto
// Refresh the menu
OutputDeviceMenuOpened(_outputDeviceMenu, new RoutedEventArgs());
-
- HandleInputOutputChange();
}
private void CloseSnackbarButton_Click(object sender, RoutedEventArgs e)
@@ -1911,7 +2020,7 @@ internal void OnAnySoundRenamed()
private readonly Dictionary _tabContextMenus = new Dictionary();
private readonly WpfUpdateChecker _updateChecker;
private MenuItem _newPageDefaultMenu;
- private MenuItem _inputDeviceMenu;
+ private MenuItem _audioPassthroughMenu;
private MenuItem _outputDeviceMenu;
#endregion
diff --git a/SoundBoard/Properties/Resources.Designer.cs b/SoundBoard/Properties/Resources.Designer.cs
index 2711d1a..ea8b7ba 100644
--- a/SoundBoard/Properties/Resources.Designer.cs
+++ b/SoundBoard/Properties/Resources.Designer.cs
@@ -87,6 +87,15 @@ internal class Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Audio passthrough.
+ ///
+ internal static string AudioPassthrough {
+ get {
+ return ResourceManager.GetString("AudioPassthrough", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to There was an error opening the input ({0}) or output ({1}) audio device for audio passthrough..
///
@@ -684,6 +693,15 @@ internal class Resources {
}
}
+ ///
+ /// Looks up a localized string similar to Sound output device.
+ ///
+ internal static string SoundOutputDevice {
+ get {
+ return ResourceManager.GetString("SoundOutputDevice", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to This sound is set to loop.
///
diff --git a/SoundBoard/Properties/Resources.resx b/SoundBoard/Properties/Resources.resx
index 07151fb..de744a3 100644
--- a/SoundBoard/Properties/Resources.resx
+++ b/SoundBoard/Properties/Resources.resx
@@ -271,6 +271,12 @@
Input device
+
+ Audio passthrough
+
+
+ Sound output device
+
Output device