diff --git a/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs b/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs index b3998e5c..4c4cf331 100644 --- a/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs +++ b/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs @@ -52,23 +52,41 @@ protected ConfigurationFile(string name, string[] inputs) { } /// - /// Removes one of the by and clears all the filters it contains. + /// Add a new split with a custom at a specific of . /// - /// The was not found - /// among the - /// The last split point can't be removed. To bypass this restriction, - /// you could add an empty split point and remove the previous last one. - public void ClearSplitPoint(string name) { - int pos = GetSplitPointIndexByName(name); - if (pos == SplitPoints.Count - 1) { // Last split can be cleared and replaced with new outputs. - FilterGraphNode[] roots = SplitPoints[pos].roots; + public void AddSplitPoint(int index, string name) { + if (index != SplitPoints.Count) { + List<(string name, FilterGraphNode[] roots)> splits = (List<(string, FilterGraphNode[])>)SplitPoints; + FilterGraphNode[] start = (FilterGraphNode[])splits[index].roots.Clone(); + for (int i = 0; i < start.Length; i++) { + ReferenceChannel channel = ((InputChannel)start[i].Filter).Channel; + start[i] = start[i].AddAfterParents(new OutputChannel(channel)).AddAfterParents(new InputChannel(channel)); + } + splits.Insert(index, (name, start)); + } else { + CreateNewSplitPoint(name); + FilterGraphNode[] end = SplitPoints[^1].roots; + for (int i = 0; i < end.Length; i++) { + ReferenceChannel channel = ((InputChannel)end[i].Filter).Channel; + end[i].AddChild(new OutputChannel(channel)); + } + return; + } + } + + /// + /// Clears all filters in one of the by . + /// + public void ClearSplitPoint(int index) { + if (index == SplitPoints.Count - 1) { // Last split can be cleared and replaced with new outputs + FilterGraphNode[] roots = SplitPoints[index].roots; for (int i = 0; i < roots.Length; i++) { roots[i].DetachChildren(); roots[i].AddChild(new OutputChannel(((InputChannel)roots[i].Filter).Channel)); } } else { // General case: clear the children and use the next split to fetch the outputs - FilterGraphNode[] roots = SplitPoints[pos].roots, - next = SplitPoints[pos + 1].roots; + FilterGraphNode[] roots = SplitPoints[index].roots, + next = SplitPoints[index + 1].roots; for (int i = 0; i < roots.Length; i++) { ReferenceChannel channel = ((InputChannel)roots[i].Filter).Channel; FilterGraphNode equivalent = next.First(x => ((InputChannel)x.Filter).Channel == channel); @@ -81,26 +99,30 @@ public void ClearSplitPoint(string name) { } /// - /// Removes one of the by and clears all the filters it contains. + /// Clears all filters in one of the by . /// /// The was not found /// among the + public void ClearSplitPoint(string name) => ClearSplitPoint(GetSplitPointIndexByName(name)); + + /// + /// Removes one of the by and clears all the filters it contains. + /// /// The last split point can't be removed. To bypass this restriction, /// you could add an empty split point and remove the previous last one. - public void RemoveSplitPoint(string name) { + public void RemoveSplitPoint(int index) { if (SplitPoints.Count == 1) { throw new IndexOutOfRangeException(); } - int pos = GetSplitPointIndexByName(name); - if (pos == SplitPoints.Count - 1) { // Last split can be just removed - FilterGraphNode[] roots = SplitPoints[pos].roots; + if (index == SplitPoints.Count - 1) { // Last split can be just removed + FilterGraphNode[] roots = SplitPoints[index].roots; for (int i = 0; i < roots.Length; i++) { roots[i].DetachFromGraph(false); } } else { // General case: transfer children from the next set of roots, then swap roots - FilterGraphNode[] roots = SplitPoints[pos].roots, - next = SplitPoints[pos + 1].roots; + FilterGraphNode[] roots = SplitPoints[index].roots, + next = SplitPoints[index + 1].roots; for (int i = 0; i < roots.Length; i++) { ReferenceChannel channel = ((InputChannel)roots[i].Filter).Channel; FilterGraphNode equivalent = next.First(x => ((InputChannel)x.Filter).Channel == channel); @@ -110,11 +132,20 @@ public void RemoveSplitPoint(string name) { equivalent.DetachChildren(); roots[i].AddChildren(oldChildren); } - ((List<(string, FilterGraphNode[])>)SplitPoints)[pos + 1] = (SplitPoints[pos + 1].name, roots); + ((List<(string, FilterGraphNode[])>)SplitPoints)[index + 1] = (SplitPoints[index + 1].name, roots); } - ((List<(string, FilterGraphNode[])>)SplitPoints).RemoveAt(pos); + ((List<(string, FilterGraphNode[])>)SplitPoints).RemoveAt(index); } + /// + /// Removes one of the by and clears all the filters it contains. + /// + /// The was not found + /// among the + /// The last split point can't be removed. To bypass this restriction, + /// you could add an empty split point and remove the previous last one. + public void RemoveSplitPoint(string name) => RemoveSplitPoint(GetSplitPointIndexByName(name)); + /// /// Adds an entry to the with the current state of the configuration, creating new /// s after each existing . @@ -123,7 +154,8 @@ public void RemoveSplitPoint(string name) { /// 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(); + FilterGraphNodeUtils.MapGraph(InputChannels.Select(x => x.root)) + .Where(x => x.Filter is OutputChannel && x.Children.Count == 0).ToArray(); for (int i = 0; i < nodes.Length; i++) { nodes[i] = nodes[i].AddChild(new InputChannel(((OutputChannel)nodes[i].Filter).Channel)); } diff --git a/Cavern.QuickEQ/Equalization/Equalizer.Transform.cs b/Cavern.QuickEQ/Equalization/Equalizer.Transform.cs index 0c95db07..7d5023be 100644 --- a/Cavern.QuickEQ/Equalization/Equalizer.Transform.cs +++ b/Cavern.QuickEQ/Equalization/Equalizer.Transform.cs @@ -39,6 +39,7 @@ public void AddSlope(double slope, double startFreq, double endFreq) { /// /// Set this equalizer so if the is linear, this will be the difference from it. + /// This is calculated by LHS (this instance) - RHS () for each value. /// /// Matching frequencies have to be guaranteed before calling this function with /// . diff --git a/Cavern/Filters/Utilities/FilterGraphNode.cs b/Cavern/Filters/Utilities/FilterGraphNode.cs index 88b47090..c21e602d 100644 --- a/Cavern/Filters/Utilities/FilterGraphNode.cs +++ b/Cavern/Filters/Utilities/FilterGraphNode.cs @@ -43,7 +43,7 @@ public class FilterGraphNode { /// Place a between this and the . /// public void AddAfterParents(FilterGraphNode newParent) { - newParent.parents.AddRange(children); + newParent.parents.AddRange(parents); for (int i = 0, c = parents.Count; i < c; i++) { parents[i].children.Clear(); parents[i].children.Add(newParent); diff --git a/CavernSamples/FilterStudio/Consts/Language.cs b/CavernSamples/FilterStudio/Consts/Language.cs index 135f85e0..b409ca23 100644 --- a/CavernSamples/FilterStudio/Consts/Language.cs +++ b/CavernSamples/FilterStudio/Consts/Language.cs @@ -2,6 +2,8 @@ using System.Globalization; using System.Windows; +using FilterStudio.Windows; + namespace FilterStudio.Consts { /// /// Handle fetching of language strings and translations. @@ -13,9 +15,10 @@ static class Language { public static ResourceDictionary GetMainWindowStrings() => mainWindowCache ??= GetFor("MainWindowStrings"); /// - /// Get the translations of dialogs. + /// Get the 's translation. /// - public static ResourceDictionary GetDialogStrings() => dialogCache ??= GetFor("DialogStrings"); + public static ResourceDictionary GetConvolutionLengthDialogStrings() => + convolutionLengthDialogCache ??= GetFor("ConvolutionLengthDialogStrings"); /// /// Get the translation of a resource file in the user's language, or in English if a translation couldn't be found. @@ -40,8 +43,8 @@ static ResourceDictionary GetFor(string resource) { static ResourceDictionary mainWindowCache; /// - /// The loaded translation of the dialogs for reuse. + /// The loaded translation of the for reuse. /// - static ResourceDictionary dialogCache; + static ResourceDictionary convolutionLengthDialogCache; } } \ No newline at end of file diff --git a/CavernSamples/FilterStudio/Graphs/ManipulatableGraph.cs b/CavernSamples/FilterStudio/Graphs/ManipulatableGraph.cs index d6f45f00..66e43029 100644 --- a/CavernSamples/FilterStudio/Graphs/ManipulatableGraph.cs +++ b/CavernSamples/FilterStudio/Graphs/ManipulatableGraph.cs @@ -93,6 +93,11 @@ public void SelectNode(string uid) { return; } + foreach (Node other in Graph.Nodes) { + if (other.Attr.LineWidth != 1) { + other.Attr.LineWidth = 1; + } + } node.Attr.LineWidth = 2; Dispatcher.BeginInvoke(() => { // Call after the graph was redrawn OnLeftClick?.Invoke(node); diff --git a/CavernSamples/FilterStudio/Graphs/PipelineEditor.cs b/CavernSamples/FilterStudio/Graphs/PipelineEditor.cs index 71c21789..a516b50b 100644 --- a/CavernSamples/FilterStudio/Graphs/PipelineEditor.cs +++ b/CavernSamples/FilterStudio/Graphs/PipelineEditor.cs @@ -40,7 +40,7 @@ public ConfigurationFile Source { set { source = value; RecreateGraph(); - SelectNode(source.SplitPoints[0].roots.GetHashCode().ToString()); + SelectNode("0"); OnSplitChanged?.Invoke(source.SplitPoints[0].roots); } } @@ -62,16 +62,16 @@ void RecreateGraph() { graph.Attr.BackgroundColor = background; graph.Attr.LayerDirection = LayerDirection.LR; - string lastUid = "a"; + string lastUid = inNodeUid; 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(); + string newUid = i.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.AddNode(new StyledNode(outNodeUid, (string)language["NOutp"])); + new StyledEdge(graph, lastUid, outNodeUid); Graph = graph; } @@ -83,10 +83,20 @@ void LeftClick(object element) { return; } - if (int.TryParse(node.Id, out int rootCode)) { - (string _, FilterGraphNode[] roots) = source.SplitPoints.FirstOrDefault(x => x.roots.GetHashCode() == rootCode); + if (int.TryParse(node.Id, out int root)) { + (string _, FilterGraphNode[] roots) = source.SplitPoints[root]; OnSplitChanged?.Invoke(roots); } } + + /// + /// UID of the node that represents the filter set input. + /// + internal const string inNodeUid = "in"; + + /// + /// UID of the node that represents the filter set output. + /// + internal const string outNodeUid = "out"; } } \ No newline at end of file diff --git a/CavernSamples/FilterStudio/MainWindow.Graph.cs b/CavernSamples/FilterStudio/MainWindow.Graph.cs index 7ad503ad..ec733f5a 100644 --- a/CavernSamples/FilterStudio/MainWindow.Graph.cs +++ b/CavernSamples/FilterStudio/MainWindow.Graph.cs @@ -61,9 +61,14 @@ void SetDirection(LayerDirection direction) { /// Converts all filters to convolutions and merges them downwards if they only have a single child. /// void ConvertToConvolution(object _, RoutedEventArgs e) { + if (pipeline.Source == null) { + Error((string)language["NoCon"]); + return; + } + ConvolutionLengthDialog length = new(); if (length.ShowDialog().Value) { - FilterGraphNodeUtils.ConvertToConvolution(rootNodes, length.Size); + FilterGraphNodeUtils.ConvertToConvolution(pipeline.Source.SplitPoints[0].roots, length.Size); ReloadGraph(); } } diff --git a/CavernSamples/FilterStudio/MainWindow.Pipeline.cs b/CavernSamples/FilterStudio/MainWindow.Pipeline.cs index c419b387..cccdb9c3 100644 --- a/CavernSamples/FilterStudio/MainWindow.Pipeline.cs +++ b/CavernSamples/FilterStudio/MainWindow.Pipeline.cs @@ -10,6 +10,30 @@ namespace FilterStudio { // Handlers of the pipeline graph control partial class MainWindow { + /// + /// Add a new empty pipeline step after the selected node. + /// + void AddStep(object sender, RoutedEventArgs e) { + StyledNode node = pipeline.GetSelectedNode(sender); + if (node == null) { + Error((string)language["NPNod"]); + return; + } + + if (!int.TryParse(node.Id, out int uid)) { + if (node.Id == PipelineEditor.inNodeUid) { + Error((string)language["NPNod"]); + return; + } else { + uid = pipeline.Source.SplitPoints.Count; + } + } + + pipeline.Source.AddSplitPoint(uid, (string)language["NSNew"]); + pipeline.Source = pipeline.Source; // Force a reload of the pipeline graph + pipeline.SelectNode(uid.ToString()); // Force a reload of the filter graph on the new step + } + /// /// Clear the currently selected pipeline step (remove all its filters). /// @@ -20,9 +44,9 @@ void ClearStep(object sender, RoutedEventArgs e) { return; } - try { - pipeline.Source.ClearSplitPoint(node.LabelText); - } catch (ArgumentOutOfRangeException) { + if (int.TryParse(node.Id, out int uid)) { + pipeline.Source.ClearSplitPoint(uid); + } else { Error((string)language["NPiSi"]); return; } @@ -57,17 +81,16 @@ void DeleteStep(object sender, RoutedEventArgs e) { /// Handle right-clicking on a pipeline . /// void PipelineRightClick(object element) { - if (element is not Node && element is not Edge) { + if (element is not Node) { return; } List<(string, Action)> menuItems = [ + ((string)language["OpAdP"], (_, e) => AddStep(element, e)), + (null, null), // Separator for deletion + ((string)language["CoCle"], (_, e) => ClearStep(element, e)), + ((string)language["CoDel"], (_, e) => DeleteStep(element, e)) ]; - if (element is Node) { - menuItems.Add((null, null)); // Separator for deletion - menuItems.Add(((string)language["CoCle"], (_, e) => ClearStep(element, e))); - menuItems.Add(((string)language["CoDel"], (_, e) => DeleteStep(element, e))); - } QuickContextMenu.Show(menuItems); } } diff --git a/CavernSamples/FilterStudio/MainWindow.xaml b/CavernSamples/FilterStudio/MainWindow.xaml index 192d7cfa..22310384 100644 --- a/CavernSamples/FilterStudio/MainWindow.xaml +++ b/CavernSamples/FilterStudio/MainWindow.xaml @@ -33,6 +33,7 @@ +