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; } = new TextBlock(); int quantaCount; #pragma warning disable CS8618 // Cannot convert null literal to non-nullable reference type. 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); } /// /// Creates a grid populated with UI controls containing information about this item, i.e. for rendering /// /// 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 == 0) { if (latencyLabel is null) return; string result = (Graph.LatencyInSamples / (double)Graph.SamplesPerQuantum * SamplesPerMS).ToString("0.#") + " ms"; uiDispatcher.Invoke(() => { try { // Try to set the label, otherwise it may have already been destroyed latencyLabel.Text = result; } catch (NullReferenceException) { } }, DispatcherPriority.Background); result = null; } /// ---------------------- /// || About this: || /// ---------------------- /// Running AudioGraph in WPF causes what seems to be the captured audio data to /// never go out of scope (this does not happen in UWP, as shown in the near-identical code from the /// UWP project), so the memory allocations for the project keep increasing. /// Stopping the AudioGraph causes these allocations to finalise, so frees up the space. /// we can call ResetAllNodes() periodically (i.e.: every 10s (1000 * 10 ms)) to reset the /// nodes /// if (quantaCount == 1000) { //SourceDevice.Reset(); Graph.ResetAllNodes(); // GC.Collect(); // We can let the GC handle itself } quantaCount++; quantaCount %= 1001; } private void VolumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { TargetDevice.OutgoingGain = e.NewValue; SourceDevice.OutgoingGain = e.NewValue; } #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. private void DestroyButton_Click(object sender, RoutedEventArgs e) { Graph.QuantumProcessed -= Graph_QuantumProcessed; Graph.ResetAllNodes(); latencyLabel = null; 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; Graph.QuantumProcessed -= Graph_QuantumProcessed; } } }