diff --git a/Cavern.QuickEQ.Format/ConfigurationFile/CavernFilterStudioConfigurationFile.cs b/Cavern.QuickEQ.Format/ConfigurationFile/CavernFilterStudioConfigurationFile.cs index aefd61ad..fdb0f6d8 100644 --- a/Cavern.QuickEQ.Format/ConfigurationFile/CavernFilterStudioConfigurationFile.cs +++ b/Cavern.QuickEQ.Format/ConfigurationFile/CavernFilterStudioConfigurationFile.cs @@ -8,9 +8,10 @@ namespace Cavern.Format.ConfigurationFile { /// public class CavernFilterStudioConfigurationFile : ConfigurationFile { /// - /// Cavern Filter Studio's own export format for full grouped filter pipelines. + /// Create an empty file for a standard layout. /// - public CavernFilterStudioConfigurationFile(int channelCount) : base(ChannelPrototype.GetStandardMatrix(channelCount)) { + public CavernFilterStudioConfigurationFile(string name, int channelCount) : + base(name, ChannelPrototype.GetStandardMatrix(channelCount)) { for (int i = 0; i < channelCount; i++) { // Output markers InputChannels[i].root.AddChild(new FilterGraphNode(new OutputChannel(InputChannels[i].name))); } diff --git a/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs b/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs index 480f23f4..4f89d515 100644 --- a/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs +++ b/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; +using System.Linq; using Cavern.Channels; using Cavern.Filters; using Cavern.Filters.Utilities; +using Cavern.Utilities; namespace Cavern.Format.ConfigurationFile { /// @@ -12,28 +14,55 @@ public abstract class ConfigurationFile { /// /// Root nodes of each channel, start attaching their filters as a children chain. /// - /// The root node has a null filter, it's only used to mark in a single instance if the channel is - /// processed on two separate pipelines from the root. public (string name, FilterGraphNode root)[] InputChannels { get; } + /// + /// Named points where the configuration file can be separated to new sections. Split points only consist of input nodes after the + /// previous split point's output nodes. + /// + public IReadOnlyList<(string name, FilterGraphNode[] roots)> SplitPoints { get; } + /// /// Create an empty configuration file with the passed input channels. /// - protected ConfigurationFile(ReferenceChannel[] inputs) { + protected ConfigurationFile(string name, ReferenceChannel[] inputs) { InputChannels = new (string name, FilterGraphNode root)[inputs.Length]; for (int i = 0; i < inputs.Length; i++) { InputChannels[i] = (inputs[i].GetShortName(), new FilterGraphNode(new InputChannel(inputs[i]))); } + + SplitPoints = new List<(string, FilterGraphNode[])> { + (name, InputChannels.GetItem2s()) + }; } /// /// Create an empty configuration file with the passed input channel names/labels. /// - protected ConfigurationFile(string[] inputs) { + protected ConfigurationFile(string name, string[] inputs) { InputChannels = new (string name, FilterGraphNode root)[inputs.Length]; for (int i = 0; i < inputs.Length; i++) { InputChannels[i] = (inputs[i], new FilterGraphNode(new InputChannel(inputs[i]))); } + + SplitPoints = new List<(string, FilterGraphNode[])> { + (name, InputChannels.GetItem2s()) + }; + } + + /// + /// Adds an entry to the with the current state of the configuration, creating new + /// s after each existing . + /// + /// If you keep track of your currently handled output nodes, set them to their children, + /// because new input nodes are created in this function. + protected void CreateNewSplitPoint(string name) { + FilterGraphNode[] nodes = + FilterGraphNodeUtils.MapGraph(InputChannels.Select(x => x.root)).Where(x => x.Filter is OutputChannel).ToArray(); + for (int i = 0; i < nodes.Length; i++) { + nodes[i] = nodes[i].AddChild(new InputChannel(((OutputChannel)nodes[i].Filter).Channel)); + } + ((List<(string, FilterGraphNode[])>)SplitPoints).Add((name, nodes)); } /// diff --git a/Cavern.QuickEQ.Format/ConfigurationFile/EqualizerAPOConfigurationFile.cs b/Cavern.QuickEQ.Format/ConfigurationFile/EqualizerAPOConfigurationFile.cs index e82de23a..04da0053 100644 --- a/Cavern.QuickEQ.Format/ConfigurationFile/EqualizerAPOConfigurationFile.cs +++ b/Cavern.QuickEQ.Format/ConfigurationFile/EqualizerAPOConfigurationFile.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; +using Cavern.Channels; using Cavern.Filters; using Cavern.Filters.Utilities; using Cavern.Format.Common; @@ -19,7 +20,7 @@ public class EqualizerAPOConfigurationFile : ConfigurationFile { /// /// Filesystem location of the configuration file /// The sample rate to use when - public EqualizerAPOConfigurationFile(string path, int sampleRate) : base(channelLabels) { + public EqualizerAPOConfigurationFile(string path, int sampleRate) : base(Path.GetFileName(path), channelLabels) { Dictionary lastNodes = InputChannels.ToDictionary(x => x.name, x => x.root); List activeChannels = channelLabels.ToList(); AddConfigFile(path, lastNodes, activeChannels, sampleRate); @@ -43,6 +44,7 @@ void AddConfigFile(string path, Dictionary lastNodes, L switch (split[0].ToLower(CultureInfo.InvariantCulture)) { case "include": string included = Path.Combine(Path.GetDirectoryName(path), string.Join(' ', split, 1, split.Length - 1)); + CreateSplit(Path.GetFileName(included), lastNodes); AddConfigFile(included, lastNodes, activeChannels, sampleRate); break; case "channel": @@ -115,6 +117,21 @@ void AddFilter(Dictionary lastNodes, List chann } } + /// + /// Mark the current point of the configuration as the beginning of the next section of filters or next pipeline step. + /// + void CreateSplit(string name, Dictionary lastNodes) { + KeyValuePair[] outputs = + lastNodes.Where(x => ReferenceChannelExtensions.FromStandardName(x.Key) != ReferenceChannel.Unknown).ToArray(); + for (int i = 0; i < outputs.Length; i++) { + lastNodes[outputs[i].Key] = lastNodes[outputs[i].Key].AddChild(new OutputChannel(outputs[i].Key)); + } + CreateNewSplitPoint(name); + for (int i = 0; i < outputs.Length; i++) { + lastNodes[outputs[i].Key] = lastNodes[outputs[i].Key].Children[0]; + } + } + /// /// Default initial channels in Equalizer APO. /// diff --git a/Cavern/Filters/Utilities/FilterGraphNode.cs b/Cavern/Filters/Utilities/FilterGraphNode.cs index e5661a39..04a2d5cb 100644 --- a/Cavern/Filters/Utilities/FilterGraphNode.cs +++ b/Cavern/Filters/Utilities/FilterGraphNode.cs @@ -36,6 +36,28 @@ public class FilterGraphNode { /// The wrapped filter public FilterGraphNode(Filter filter) => Filter = filter; + /// + /// Place a between this and the . + /// + public void AddAfterParents(FilterGraphNode newParent) { + newParent.parents.AddRange(children); + for (int i = 0, c = parents.Count; i < c; i++) { + parents[i].children.Clear(); + parents[i].children.Add(newParent); + } + parents.Clear(); + AddParent(newParent); + } + + /// + /// Place a between this and the , then return the new node containing that filter. + /// + public FilterGraphNode AddAfterParents(Filter filter) { + FilterGraphNode node = new FilterGraphNode(filter); + AddAfterParents(node); + return node; + } + /// /// Place a between this and the . /// @@ -50,7 +72,7 @@ public void AddBeforeChildren(FilterGraphNode newChild) { } /// - /// Place a between this and the and return the new node containing that filter. + /// Place a between this and the , then return the new node containing that filter. /// public FilterGraphNode AddBeforeChildren(Filter filter) { FilterGraphNode node = new FilterGraphNode(filter); diff --git a/Cavern/Filters/Utilities/FilterGraphNodeUtils.cs b/Cavern/Filters/Utilities/FilterGraphNodeUtils.cs new file mode 100644 index 00000000..bc75ec4d --- /dev/null +++ b/Cavern/Filters/Utilities/FilterGraphNodeUtils.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Cavern.Filters.Utilities { + /// + /// Special functions for handling s. + /// + public static class FilterGraphNodeUtils { + /// + /// Get all nodes in a filter graph knowing the root nodes. + /// + public static HashSet MapGraph(IEnumerable rootNodes) { + HashSet visited = new HashSet(); + Queue queue = new Queue(rootNodes); + while (queue.Count > 0) { + FilterGraphNode currentNode = queue.Dequeue(); + if (visited.Contains(currentNode)) { + continue; + } + + visited.Add(currentNode); + foreach (FilterGraphNode child in currentNode.Children) { + queue.Enqueue(child); + } + } + + return visited; + } + } +} \ No newline at end of file diff --git a/Cavern/Utilities/TupleUtils.cs b/Cavern/Utilities/TupleUtils.cs new file mode 100644 index 00000000..edb98acc --- /dev/null +++ b/Cavern/Utilities/TupleUtils.cs @@ -0,0 +1,17 @@ +namespace Cavern.Utilities { + /// + /// Advanced functions for handling tuples. + /// + public static class TupleUtils { + /// + /// From an array of tuples, get only the second "column". + /// + public static T2[] GetItem2s(this (T1, T2)[] items) { + T2[] result = new T2[items.Length]; + for (int i = 0; i < items.Length; i++) { + result[i] = items[i].Item2; + } + return result; + } + } +} \ No newline at end of file diff --git a/CavernSamples/CavernSamples.sln b/CavernSamples/CavernSamples.sln index b98e7a50..325ec046 100644 --- a/CavernSamples/CavernSamples.sln +++ b/CavernSamples/CavernSamples.sln @@ -45,7 +45,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cavern.WPF", "Cavern.WPF\Ca EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microprojects", "Microprojects", "{679D71F9-B8C0-4D52-B3C8-8DE338E3888C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FilterStudio", "FilterStudio\FilterStudio.csproj", "{5F0DDE27-A6F0-4A6D-B7D6-BB7E3AC95471}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FilterStudio", "FilterStudio\FilterStudio.csproj", "{5F0DDE27-A6F0-4A6D-B7D6-BB7E3AC95471}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/CavernSamples/FilterStudio/Graphs/ManipulatableGraph.cs b/CavernSamples/FilterStudio/Graphs/ManipulatableGraph.cs index 40b94b58..1885f2ea 100644 --- a/CavernSamples/FilterStudio/Graphs/ManipulatableGraph.cs +++ b/CavernSamples/FilterStudio/Graphs/ManipulatableGraph.cs @@ -63,9 +63,13 @@ public ManipulatableGraph() { /// public void SelectNode(string uid) { Node node = viewer.Graph.FindNode(uid); + if (node == null) { + return; + } + node.Attr.LineWidth = 2; Dispatcher.BeginInvoke(() => { // Call after the graph was redrawn - OnLeftClick(node); + OnLeftClick?.Invoke(node); }); } diff --git a/CavernSamples/FilterStudio/Graphs/Parsing.cs b/CavernSamples/FilterStudio/Graphs/Parsing.cs index 440afcae..8bb478da 100644 --- a/CavernSamples/FilterStudio/Graphs/Parsing.cs +++ b/CavernSamples/FilterStudio/Graphs/Parsing.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Windows.Media; +using Cavern.Filters; using Cavern.Filters.Utilities; using Cavern.Format.ConfigurationFile; @@ -21,18 +22,17 @@ public static class Parsing { /// Convert a 's filter graph to an MSAGL . /// /// Filter graph to convert, from - /// Graph background color - public static Graph ParseConfigurationFile((string name, FilterGraphNode root)[] rootNodes, Color background) { + public static Graph ParseConfigurationFile(FilterGraphNode[] rootNodes) { Graph result = new(); - result.Attr.BackgroundColor = background; - for (int i = 0; i < rootNodes.Length; i++) { - result.AddNode(new StyledNode(rootNodes[i].name, rootNodes[i].root.ToString()) { - Filter = rootNodes[i].root + string uid = rootNodes[i].GetHashCode().ToString(); + result.AddNode(new StyledNode(uid, rootNodes[i].ToString()) { + Filter = rootNodes[i] }); - IReadOnlyList children = rootNodes[i].root.Children; + + IReadOnlyList children = rootNodes[i].Children; for (int j = 0, c = children.Count; j < c; j++) { - AddToGraph(rootNodes[i].name, children[j], result); + AddToGraph(uid, children[j], result); } } return result; @@ -60,6 +60,10 @@ static void AddToGraph(string parent, FilterGraphNode source, Graph target) { } new StyledEdge(target, parent, uid); + + if (source.Filter is OutputChannel) { + return; // Filters after output channels are part of different splits + } for (int i = 0, c = source.Children.Count; i < c; i++) { AddToGraph(uid, source.Children[i], target); } diff --git a/CavernSamples/FilterStudio/Graphs/PipelineEditor.cs b/CavernSamples/FilterStudio/Graphs/PipelineEditor.cs new file mode 100644 index 00000000..71c21789 --- /dev/null +++ b/CavernSamples/FilterStudio/Graphs/PipelineEditor.cs @@ -0,0 +1,92 @@ +using Microsoft.Msagl.Drawing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; + +using Cavern.Filters.Utilities; +using Cavern.Format.ConfigurationFile; + +using Color = Microsoft.Msagl.Drawing.Color; + +namespace FilterStudio.Graphs { + /// + /// The layout on which the steps of the filter pipeline can be selected. Each step has all input and output channels, + /// they're just parts cut off the whole filter pipeline for better presentation. Think of them as groups on the full filter graph. + /// The main feature this makes possible is having preset pipeline steps that can be added later with different configurations, + /// such as crossovers. + /// + public class PipelineEditor : ManipulatableGraph { + /// + /// Pass the root nodes of the user's selected split. + /// + public event Action OnSplitChanged; + + /// + /// Overrides the background color of the graph. + /// + public Color background; + + /// + /// Source of language strings. + /// + public ResourceDictionary language; + + /// + /// The of which its split points will be presented. + /// + public ConfigurationFile Source { + get => source; + set { + source = value; + RecreateGraph(); + SelectNode(source.SplitPoints[0].roots.GetHashCode().ToString()); + OnSplitChanged?.Invoke(source.SplitPoints[0].roots); + } + } + ConfigurationFile source; + + /// + /// The layout on which the steps of the filter pipeline can be selected. + /// + public PipelineEditor() { + OnLeftClick += LeftClick; + } + + /// + /// When the has changed, display its split points. + /// + void RecreateGraph() { + IReadOnlyList<(string name, FilterGraphNode[] roots)> splits = source.SplitPoints; + Graph graph = new Graph(); + graph.Attr.BackgroundColor = background; + graph.Attr.LayerDirection = LayerDirection.LR; + + string lastUid = "a"; + graph.AddNode(new StyledNode(lastUid, (string)language["NInpu"])); + for (int i = 0, c = splits.Count; i < c; i++) { + string newUid = splits[i].roots.GetHashCode().ToString(); + graph.AddNode(new StyledNode(newUid, splits[i].name)); + new StyledEdge(graph, lastUid, newUid); + lastUid = newUid; + } + graph.AddNode(new StyledNode("b", (string)language["NOutp"])); + new StyledEdge(graph, lastUid, "b"); + Graph = graph; + } + + /// + /// Open the split the user selects. + /// + void LeftClick(object element) { + if (element is not StyledNode node) { + return; + } + + if (int.TryParse(node.Id, out int rootCode)) { + (string _, FilterGraphNode[] roots) = source.SplitPoints.FirstOrDefault(x => x.roots.GetHashCode() == rootCode); + OnSplitChanged?.Invoke(roots); + } + } + } +} \ No newline at end of file diff --git a/CavernSamples/FilterStudio/MainWindow.Graph.cs b/CavernSamples/FilterStudio/MainWindow.Graph.cs index 21146287..582ff30c 100644 --- a/CavernSamples/FilterStudio/MainWindow.Graph.cs +++ b/CavernSamples/FilterStudio/MainWindow.Graph.cs @@ -1,8 +1,7 @@ using Microsoft.Msagl.Drawing; +using System; using System.Collections.Generic; using System.Windows; -using System.Windows.Media; -using System; using VoidX.WPF; @@ -11,6 +10,44 @@ namespace FilterStudio { // Handlers of the filter graph control partial class MainWindow { + /// + /// The direction where the graph tree is layed out. + /// + LayerDirection graphDirection; + + /// + /// Change the direction where the graph tree is layed out to top to bottom. + /// + void SetDirectionTB(object _, RoutedEventArgs e) => SetDirection(LayerDirection.TB); + + /// + /// Change the direction where the graph tree is layed out to left to right. + /// + void SetDirectionLR(object _, RoutedEventArgs e) => SetDirection(LayerDirection.LR); + + /// + /// Change the direction where the graph tree is layed out to bottom to top. + /// + void SetDirectionBT(object _, RoutedEventArgs e) => SetDirection(LayerDirection.BT); + + /// + /// Change the direction where the graph tree is layed out to right to left. + /// + void SetDirectionRL(object _, RoutedEventArgs e) => SetDirection(LayerDirection.RL); + + /// + /// Change the direction where the graph tree is layed out. + /// + void SetDirection(LayerDirection direction) { + graphDirection = direction; + ReloadGraph(); + } + + /// + /// When the user lost the graph because it was moved outside the screen, this function redisplays it in the center of the frame. + /// + void Recenter(object _, RoutedEventArgs e) => ReloadGraph(); + /// /// When selecting a node, open it for modification. /// @@ -48,7 +85,10 @@ void GraphRightClick(object element) { /// void ReloadGraph() { if (rootNodes != null) { - graph.Graph = Parsing.ParseConfigurationFile(rootNodes, Parsing.ParseBackground((SolidColorBrush)Background)); + Graph newGraph = Parsing.ParseConfigurationFile(rootNodes); + newGraph.Attr.BackgroundColor = pipeline.background; + newGraph.Attr.LayerDirection = graphDirection; + graph.Graph = newGraph; } } } diff --git a/CavernSamples/FilterStudio/MainWindow.xaml b/CavernSamples/FilterStudio/MainWindow.xaml index 7e6a90dc..1005e473 100644 --- a/CavernSamples/FilterStudio/MainWindow.xaml +++ b/CavernSamples/FilterStudio/MainWindow.xaml @@ -28,10 +28,17 @@ + + + + + + + + - @@ -43,7 +50,9 @@ - + + + diff --git a/CavernSamples/FilterStudio/MainWindow.xaml.cs b/CavernSamples/FilterStudio/MainWindow.xaml.cs index 7ac6ef5a..95dd7996 100644 --- a/CavernSamples/FilterStudio/MainWindow.xaml.cs +++ b/CavernSamples/FilterStudio/MainWindow.xaml.cs @@ -1,11 +1,14 @@ -using Microsoft.Win32; +using Microsoft.Msagl.Drawing; +using Microsoft.Win32; using System; using System.Windows; +using System.Windows.Media; using Cavern; using Cavern.Filters; using Cavern.Filters.Utilities; using Cavern.Format.ConfigurationFile; +using Cavern.Utilities; using FilterStudio.Graphs; using FilterStudio.Resources; @@ -23,7 +26,7 @@ public partial class MainWindow : Window { /// /// Each channel's full filter graph. /// - (string name, FilterGraphNode root)[] rootNodes; + FilterGraphNode[] rootNodes; /// /// Any setting has changed in the application and it should be saved. @@ -35,6 +38,9 @@ public partial class MainWindow : Window { /// public MainWindow() { InitializeComponent(); + pipeline.OnSplitChanged += SplitChanged; + pipeline.background = Parsing.ParseBackground((SolidColorBrush)Background); + pipeline.language = language; graph.OnLeftClick += GraphLeftClick; graph.OnRightClick += GraphRightClick; @@ -63,7 +69,7 @@ protected override void OnClosed(EventArgs e) { /// Create a new empty configuration. /// void NewConfiguration(object _, RoutedEventArgs e) { - rootNodes = new CavernFilterStudioConfigurationFile(8).InputChannels; + rootNodes = new CavernFilterStudioConfigurationFile((string)language["NSNew"], 8).InputChannels.GetItem2s(); ReloadGraph(); } @@ -77,16 +83,10 @@ void LoadConfiguration(object _, RoutedEventArgs e) { if (dialog.ShowDialog().Value) { ConfigurationFile file = new EqualizerAPOConfigurationFile(dialog.FileName, Listener.DefaultSampleRate); - rootNodes = file.InputChannels; - ReloadGraph(); + pipeline.Source = file; } } - /// - /// When the user lost the graph because it was moved outside the screen, this function redisplays it in the center of the frame. - /// - void Recenter(object _, RoutedEventArgs e) => ReloadGraph(); - /// /// Delete the currently selected node. /// @@ -118,6 +118,14 @@ void SetInstructions(object _, RoutedEventArgs e) { /// void About(object _, RoutedEventArgs e) => MessageBox.Show(Listener.Info, (string)language["HAbou"]); + /// + /// A different split of the edited file is selected. + /// + void SplitChanged(FilterGraphNode[] splitRoots) { + rootNodes = splitRoots; + ReloadGraph(); + } + /// /// Update the name of a filter when any property of it was modified. /// diff --git a/CavernSamples/FilterStudio/Resources/MainWindowStrings.hu-HU.xaml b/CavernSamples/FilterStudio/Resources/MainWindowStrings.hu-HU.xaml index 5b93dd30..f07fb23a 100644 --- a/CavernSamples/FilterStudio/Resources/MainWindowStrings.hu-HU.xaml +++ b/CavernSamples/FilterStudio/Resources/MainWindowStrings.hu-HU.xaml @@ -5,17 +5,22 @@ _Új konfiguráció _Konfigurációs fájl megnyitása - _Szűrő hozzáadása + _Szűrők _Párhuzamos hozzáadás (Shift nyomva tartásával azonos) _Címke _Erősítés _Késleltetés E_gyszerű parametrikus szűrő... + Kiválasztott szűrő _törlése + _Törlés _Gráf - Középre _mozgatás - _Kiválasztott szűrő törlése - _Törlés + _Irány + _Fentről le + _Balról jobbra + _Lentről fel + _Jobbról balra + _Középre mozgatás Sú_gó _Segítség mutatása @@ -31,6 +36,10 @@ Kimenetek után nem adható hozzá szűrő. Egy csatorna bemenete nem törölhető. Egy csatorna kimenete nem törölhető. + + Új + Bemenet + Kimenet Új címke Névjegy diff --git a/CavernSamples/FilterStudio/Resources/MainWindowStrings.xaml b/CavernSamples/FilterStudio/Resources/MainWindowStrings.xaml index 23ab3bf6..c93c9cb4 100644 --- a/CavernSamples/FilterStudio/Resources/MainWindowStrings.xaml +++ b/CavernSamples/FilterStudio/Resources/MainWindowStrings.xaml @@ -5,17 +5,22 @@ _New configuration _Open configuration file - _Add filter + F_ilters _Add in parallel (same as holding Shift) _Label _Gain _Delay _Basic parametric filter... + _Delete selected filter + _Delete _Graph + _Direction + _Top to bottom + _Left to right + _Bottom to top + _Right to left _Recenter - _Delete selected filter - _Delete _Help _Show instructions @@ -31,6 +36,10 @@ Filters can't be added after an output. The input of a channel can't be deleted. The output of a channel can't be deleted. + + New + Input + Output New label About