Added WPF (desktop) application

This commit is contained in:
Brychan Dempsey 2021-09-23 19:21:08 +12:00
parent 7f65b4a7a6
commit 09a2398967
6 changed files with 368 additions and 4 deletions

View File

@ -13,5 +13,16 @@ namespace Audio_Router_WPF
/// </summary>
public partial class App : Application
{
public static App appRef;
// The list of audio graphs
public List<AudioGraphConnection> audioGraphConnections;
public static Windows.System.Display.DisplayRequest displayRequest = new Windows.System.Display.DisplayRequest();
public App()
{
appRef = this;
audioGraphConnections = new List<AudioGraphConnection>();
}
}
}

View File

@ -2,10 +2,16 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0-windows</TargetFramework>
<TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
<RootNamespace>Audio_Router_WPF</RootNamespace>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
<FileVersion>1.2.3.2</FileVersion>
<AssemblyVersion>1.2.3.2</AssemblyVersion>
<NeutralLanguage>en-NZ</NeutralLanguage>
<RepositoryUrl>https://git.software.kauripeak.co.nz/BrychanD/Audio-Router</RepositoryUrl>
<Copyright>(C) 2021 Brychan Dempsey</Copyright>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Threading;
using System.Threading.Tasks;
using Windows.Media.Audio;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
namespace Audio_Router_WPF
{
public class AudioGraphConnection
{
string SourceNames { get; }
string TargetNames { get; }
int SamplesPerMS { get; }
public AudioGraph Graph { get; private set; }
AudioDeviceOutputNode TargetDevice { get; set; }
AudioDeviceInputNode SourceDevice { get; set; }
Dispatcher uiDispatcher { get; set; }
TextBlock latencyLabel { get; set; }
int quantaCount = 0;
public AudioGraphConnection(AudioGraph graph, AudioDeviceOutputNode targetDevice, AudioDeviceInputNode sourceDevice, Dispatcher uiDispatcher)
{
Graph = graph;
TargetDevice = targetDevice;
SourceDevice = sourceDevice;
this.uiDispatcher = uiDispatcher;
SourceNames = sourceDevice.Device.Name;
TargetNames = targetDevice.Device.Name;
SamplesPerMS = (int)(Graph.SamplesPerQuantum / (double)Graph.EncodingProperties.SampleRate * 1000);
}
/// <summary>
/// Creates a grid populated with UI controls containing information about this item, i.e. for rendering
/// </summary>
/// <returns></returns>
public Grid GetAsDisplayItem(out Button deletionButton)
{
// Create the display elements
Grid result = new Grid();
Button destroyButton = new Button()
{
Content = "Stop",
HorizontalAlignment = HorizontalAlignment.Right
};
TextBlock label = new TextBlock()
{
HorizontalAlignment = HorizontalAlignment.Left
};
latencyLabel = new TextBlock()
{
Padding = new Thickness(0, 0, 5, 0)
};
Slider volumeSlider = new Slider
{
Maximum = 5.0,
Minimum = 0.0,
Value = 1.0,
SmallChange = 0.1,
MinWidth = 250,
Margin = new Thickness(5, 0, 5, 0)
};
StackPanel rightSide = new StackPanel
{
Orientation = Orientation.Horizontal
};
// Set the base grid to span the full screen width available
result.HorizontalAlignment = HorizontalAlignment.Stretch;
result.Margin = new Thickness(1, 1, 1, 5);
// Add Column definitions
result.ColumnDefinitions.Add(new ColumnDefinition()); // '*' size; fits the maximum space it can, evenly
result.ColumnDefinitions.Add(new ColumnDefinition() // 'auto'; automatically resizes to the total size of the children elements
{
Width = GridLength.Auto
});
Run sourceDev = new Run()
{
Text = SourceNames
};
Run arrow = new Run
{
Text = "  ",
FontFamily = new FontFamily("Segoe MDL2 Assets")
};
Run targetDev = new Run()
{
Text = TargetNames
};
// Add the text to the label
label.Inlines.Add(sourceDev);
label.Inlines.Add(arrow);
label.Inlines.Add(targetDev);
// Register the event
destroyButton.Click += DestroyButton_Click;
result.Children.Add(label);
volumeSlider.ValueChanged += VolumeSlider_ValueChanged;
Graph.QuantumProcessed += Graph_QuantumProcessed;
rightSide.Children.Add(latencyLabel);
rightSide.Children.Add(volumeSlider);
rightSide.Children.Add(destroyButton);
rightSide.HorizontalAlignment = HorizontalAlignment.Right;
result.Children.Add(rightSide);
Grid.SetColumn(rightSide, 1);
deletionButton = destroyButton;
return result;
}
private void Graph_QuantumProcessed(AudioGraph sender, object args)
{
// updates aren't urgent, only process after some time (this is 1s, with the Windows default of 10 ms / quanta
if (quantaCount++ == 100)
{
string result = (Graph.LatencyInSamples / (double)Graph.SamplesPerQuantum * SamplesPerMS).ToString("0.#") + " ms";
if (latencyLabel is null) return;
uiDispatcher.Invoke(() =>
{
try
{
latencyLabel.Text = result;
}
catch
{
}
}, DispatcherPriority.Background);
quantaCount = 0;
}
}
private void VolumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
TargetDevice.OutgoingGain = e.NewValue;
SourceDevice.OutgoingGain = e.NewValue;
}
private void DestroyButton_Click(object sender, RoutedEventArgs e)
{
latencyLabel = null;
Graph.QuantumProcessed -= Graph_QuantumProcessed;
Graph.Stop();
TargetDevice.Dispose();
SourceDevice.Dispose();
Graph.Dispose();
Graph = null;
TargetDevice = null;
SourceDevice = null;
uiDispatcher = null;
_ = App.appRef.audioGraphConnections.Remove(this);
GC.Collect();
}
public void OnHide()
{
latencyLabel = null;
}
}
}

View File

@ -6,7 +6,27 @@
xmlns:local="clr-namespace:Audio_Router_WPF"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.Background>
<SolidColorBrush Color="#CC000000" />
</Grid.Background>
<StackPanel x:Name="SourceFlyout" Margin="20,30,20,20" Grid.RowSpan="1" VerticalAlignment="Stretch">
<TextBlock Text="Input Audio Device:" TextWrapping="Wrap" Margin="0,10,0,0" Foreground="White" HorizontalAlignment="Center"/>
<ComboBox x:Name="InputAudioComboBox" MinWidth="500" HorizontalAlignment="Center"/>
<TextBlock Text="Output Audio Device:" TextWrapping="Wrap" Margin="0,10,0,0" Foreground="White" HorizontalAlignment="Center"/>
<ComboBox x:Name="OutputAudioComboBox" MinWidth="500" HorizontalAlignment="Center"/>
<CheckBox x:Name="LowLatencyCheckbox" Content="Low Latency Mode (Reduces audio quality)" HorizontalAlignment="Center" Margin="0,10,0,0"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,20,0,5">
<Button x:Name="AddAudioGraphButton" Content="Add Audio Graph" HorizontalContentAlignment="Center" Background="#54FFFFFF" Click="AddAudioGraphButton_Click"/>
</StackPanel>
</StackPanel>
<ScrollViewer Grid.Row="1" Margin="10,5,10,5" Padding="0,0,10,0">
<StackPanel x:Name="CreatedGraphs"/>
</ScrollViewer>
</Grid>
</Window>

View File

@ -11,7 +11,14 @@ using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using Windows.UI.Core;
using System.Windows.Shapes;
using Windows.Devices.Enumeration;
using Windows.Media.Audio;
using System.Windows.Threading;
using Windows.Media.Devices;
using Windows.Media.Render;
using Windows.Media.Capture;
namespace Audio_Router_WPF
{
@ -20,9 +27,152 @@ namespace Audio_Router_WPF
/// </summary>
public partial class MainWindow : Window
{
//AudioGraph graph;
// The list of audio devices
DeviceInformationCollection sourceDevices;
DeviceInformationCollection targetDevices;
public MainWindow()
{
{
InitializeComponent();
_ = Task.Run(() => FindAudioSources());
}
private async void AddAudioGraphButton_Click(object sender, RoutedEventArgs e)
{
await CreateAudioGraph();
}
private async Task FindAudioSources()
{
// First, clear the lists
Dispatcher.Invoke(() =>
{
InputAudioComboBox.Items.Clear();
OutputAudioComboBox.Items.Clear();
}, DispatcherPriority.Normal);
// Perform this on our spawned thread, as the operation is asynchronous-synchronous await
sourceDevices = await DeviceInformation.FindAllAsync(MediaDevice.GetAudioCaptureSelector());
targetDevices = await DeviceInformation.FindAllAsync(MediaDevice.GetAudioRenderSelector());
DeviceInformation defaultOutput = await DeviceInformation.CreateFromIdAsync(MediaDevice.GetDefaultAudioRenderId(AudioDeviceRole.Default));
DeviceInformation defaultInput = await DeviceInformation.CreateFromIdAsync(MediaDevice.GetDefaultAudioCaptureId(AudioDeviceRole.Default));
Dispatcher.Invoke(() =>
{
// Set the Input device
for (int i = 0; i < sourceDevices.Count; i++)
{
if (sourceDevices[i].Id == defaultInput.Id)
{
_ = InputAudioComboBox.Items.Add(sourceDevices[i].Name + " (Default Device)");
InputAudioComboBox.SelectedIndex = i;
}
else
{
_ = InputAudioComboBox.Items.Add(sourceDevices[i].Name);
}
}
for (int i = 0; i < targetDevices.Count; i++)
{
if (targetDevices[i].Id == defaultOutput.Id)
{
_ = OutputAudioComboBox.Items.Add(targetDevices[i].Name + " (Default Device)");
OutputAudioComboBox.SelectedIndex = i;
}
else
{
_ = OutputAudioComboBox.Items.Add(targetDevices[i].Name);
}
}
});
}
private async Task CreateAudioGraph()
{
App.displayRequest.RequestActive();
DeviceInformation outputDevice = targetDevices[OutputAudioComboBox.SelectedIndex];
DeviceInformation inputDevice = sourceDevices[InputAudioComboBox.SelectedIndex];
AudioGraphSettings settings = LowLatencyCheckbox.IsChecked == true
? new AudioGraphSettings(AudioRenderCategory.Media)
{
QuantumSizeSelectionMode = QuantumSizeSelectionMode.ClosestToDesired,
DesiredSamplesPerQuantum = 10,
PrimaryRenderDevice = outputDevice,
DesiredRenderDeviceAudioProcessing = Windows.Media.AudioProcessing.Raw,
EncodingProperties = new Windows.Media.MediaProperties.AudioEncodingProperties()
{
Subtype = "Float",
SampleRate = 128000, // Manually set to 128 000 samples/second so that the delay is significantly reduced
ChannelCount = 2,
BitsPerSample = 32,
Bitrate = 3072000
}
}
: new AudioGraphSettings(AudioRenderCategory.Media)
{
PrimaryRenderDevice = outputDevice
};
CreateAudioGraphResult result = await AudioGraph.CreateAsync(settings);
AudioGraph ag = result.Graph;
if (result.Status != AudioGraphCreationStatus.Success)
{
return;
}
CreateAudioDeviceInputNodeResult deviceInputNodeResult = await ag.CreateDeviceInputNodeAsync(MediaCategory.Other, ag.EncodingProperties, inputDevice);
if (deviceInputNodeResult.Status != AudioDeviceNodeCreationStatus.Success)
{
// Fail-safe, switch to using the default encoding properties
settings = new AudioGraphSettings(AudioRenderCategory.Media)
{
PrimaryRenderDevice = outputDevice
};
result = await AudioGraph.CreateAsync(settings);
ag = result.Graph;
if (result.Status != AudioGraphCreationStatus.Success)
{
return;
}
deviceInputNodeResult = await ag.CreateDeviceInputNodeAsync(MediaCategory.Other, ag.EncodingProperties, inputDevice);
if (deviceInputNodeResult.Status != AudioDeviceNodeCreationStatus.Success)
{
return;
}
}
// Create the device output node connection
CreateAudioDeviceOutputNodeResult deviceOutputNodeResult = await ag.CreateDeviceOutputNodeAsync();
if (deviceOutputNodeResult.Status != AudioDeviceNodeCreationStatus.Success)
{
return;
}
deviceInputNodeResult.DeviceInputNode.AddOutgoingConnection(deviceOutputNodeResult.DeviceOutputNode);
// appMediaControls.PlaybackStatus = MediaPlaybackStatus.Playing;
ag.Start();
AudioGraphConnection graphConnection = new AudioGraphConnection(ag, deviceOutputNodeResult.DeviceOutputNode, deviceInputNodeResult.DeviceInputNode, Dispatcher);
App.appRef.audioGraphConnections.Add(graphConnection);
// Hold a reference to the created button; when clicked we must also ensure we remove this visual element from the grid display
Grid renderGrid = graphConnection.GetAsDisplayItem(out Button visualButton);
CreatedGraphs.Children.Add(renderGrid);
// Inline decl to add a second listener to the click event - will remove the grid from the display
void handler(object sender, RoutedEventArgs e)
{
visualButton.Click -= handler;
DestroyVisualElement(ref renderGrid);
}
visualButton.Click += handler;
}
private void DestroyVisualElement(ref Grid element)
{
CreatedGraphs.Children.Remove(element);
element = null;
}
}
}

View File

@ -5,7 +5,7 @@ VisualStudioVersion = 17.0.31710.8
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Audio Router UWP", "Audio Router UWP\Audio Router\Audio Router UWP.csproj", "{92D76120-5578-484A-BB93-24D105B48043}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Audio Router WPF", "Audio Router WPF\Audio Router WPF.csproj", "{E0BEDD27-7800-4434-BA2D-7D71964F557F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Audio Router WPF", "Audio Router WPF\Audio Router WPF.csproj", "{E0BEDD27-7800-4434-BA2D-7D71964F557F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution