Added WPF (desktop) application
This commit is contained in:
parent
7f65b4a7a6
commit
09a2398967
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
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"
|
||||
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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user