Added WPF (desktop) application
This commit is contained in:
parent
7f65b4a7a6
commit
09a2398967
@ -13,5 +13,16 @@ namespace Audio_Router_WPF
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class App : Application
|
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>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,16 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net5.0-windows</TargetFramework>
|
<TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
|
||||||
<RootNamespace>Audio_Router_WPF</RootNamespace>
|
<RootNamespace>Audio_Router_WPF</RootNamespace>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<UseWPF>true</UseWPF>
|
<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>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
177
Audio Router WPF/AudioGraphConnection.cs
Normal file
177
Audio Router WPF/AudioGraphConnection.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,27 @@
|
|||||||
xmlns:local="clr-namespace:Audio_Router_WPF"
|
xmlns:local="clr-namespace:Audio_Router_WPF"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="MainWindow" Height="450" Width="800">
|
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>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
@ -11,7 +11,14 @@ using System.Windows.Input;
|
|||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using System.Windows.Navigation;
|
using System.Windows.Navigation;
|
||||||
|
using Windows.UI.Core;
|
||||||
using System.Windows.Shapes;
|
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
|
namespace Audio_Router_WPF
|
||||||
{
|
{
|
||||||
@ -20,9 +27,152 @@ namespace Audio_Router_WPF
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
|
//AudioGraph graph;
|
||||||
|
// The list of audio devices
|
||||||
|
DeviceInformationCollection sourceDevices;
|
||||||
|
DeviceInformationCollection targetDevices;
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ VisualStudioVersion = 17.0.31710.8
|
|||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
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}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Audio Router UWP", "Audio Router UWP\Audio Router\Audio Router UWP.csproj", "{92D76120-5578-484A-BB93-24D105B48043}"
|
||||||
EndProject
|
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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Loading…
x
Reference in New Issue
Block a user