Skip to content

Commit

Permalink
Configuration file splitting
Browse files Browse the repository at this point in the history
  • Loading branch information
VoidXH committed May 6, 2024
1 parent 12d27a2 commit 7698c32
Show file tree
Hide file tree
Showing 15 changed files with 330 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ namespace Cavern.Format.ConfigurationFile {
/// </summary>
public class CavernFilterStudioConfigurationFile : ConfigurationFile {
/// <summary>
/// Cavern Filter Studio's own export format for full grouped filter pipelines.
/// Create an empty file for a standard layout.
/// </summary>
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)));
}
Expand Down
37 changes: 33 additions & 4 deletions Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs
Original file line number Diff line number Diff line change
@@ -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 {
/// <summary>
Expand All @@ -12,28 +14,55 @@ public abstract class ConfigurationFile {
/// <summary>
/// Root nodes of each channel, start attaching their filters as a children chain.
/// </summary>
/// <remarks>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.</remarks>
public (string name, FilterGraphNode root)[] InputChannels { get; }

/// <summary>
/// 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.
/// </summary>
public IReadOnlyList<(string name, FilterGraphNode[] roots)> SplitPoints { get; }

/// <summary>
/// Create an empty configuration file with the passed input channels.
/// </summary>
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())
};
}

/// <summary>
/// Create an empty configuration file with the passed input channel names/labels.
/// </summary>
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())
};
}

/// <summary>
/// Adds an entry to the <see cref="SplitPoints"/> with the current state of the configuration, creating new
/// <see cref="InputChannel"/>s after each existing <see cref="OutputChannel"/>.
/// </summary>
/// <remarks>If you keep track of your currently handled output nodes, set them to their children,
/// because new input nodes are created in this function.</remarks>
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));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.IO;
using System.Linq;

using Cavern.Channels;
using Cavern.Filters;
using Cavern.Filters.Utilities;
using Cavern.Format.Common;
Expand All @@ -19,7 +20,7 @@ public class EqualizerAPOConfigurationFile : ConfigurationFile {
/// </summary>
/// <param name="path">Filesystem location of the configuration file</param>
/// <param name="sampleRate">The sample rate to use when</param>
public EqualizerAPOConfigurationFile(string path, int sampleRate) : base(channelLabels) {
public EqualizerAPOConfigurationFile(string path, int sampleRate) : base(Path.GetFileName(path), channelLabels) {
Dictionary<string, FilterGraphNode> lastNodes = InputChannels.ToDictionary(x => x.name, x => x.root);
List<string> activeChannels = channelLabels.ToList();
AddConfigFile(path, lastNodes, activeChannels, sampleRate);
Expand All @@ -43,6 +44,7 @@ void AddConfigFile(string path, Dictionary<string, FilterGraphNode> 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":
Expand Down Expand Up @@ -115,6 +117,21 @@ void AddFilter(Dictionary<string, FilterGraphNode> lastNodes, List<string> chann
}
}

/// <summary>
/// Mark the current point of the configuration as the beginning of the next section of filters or next pipeline step.
/// </summary>
void CreateSplit(string name, Dictionary<string, FilterGraphNode> lastNodes) {
KeyValuePair<string, FilterGraphNode>[] 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];
}
}

/// <summary>
/// Default initial channels in Equalizer APO.
/// </summary>
Expand Down
24 changes: 23 additions & 1 deletion Cavern/Filters/Utilities/FilterGraphNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,28 @@ public class FilterGraphNode {
/// <param name="filter">The wrapped filter</param>
public FilterGraphNode(Filter filter) => Filter = filter;

/// <summary>
/// Place a <see cref="FilterGraphNode"/> between this and the <see cref="parents"/>.
/// </summary>
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);
}

/// <summary>
/// Place a <paramref name="filter"/> between this and the <see cref="parents"/>, then return the new node containing that filter.
/// </summary>
public FilterGraphNode AddAfterParents(Filter filter) {
FilterGraphNode node = new FilterGraphNode(filter);
AddAfterParents(node);
return node;
}

/// <summary>
/// Place a <see cref="FilterGraphNode"/> between this and the <see cref="children"/>.
/// </summary>
Expand All @@ -50,7 +72,7 @@ public void AddBeforeChildren(FilterGraphNode newChild) {
}

/// <summary>
/// Place a <paramref name="filter"/> between this and the <see cref="children"/> and return the new node containing that filter.
/// Place a <paramref name="filter"/> between this and the <see cref="children"/>, then return the new node containing that filter.
/// </summary>
public FilterGraphNode AddBeforeChildren(Filter filter) {
FilterGraphNode node = new FilterGraphNode(filter);
Expand Down
29 changes: 29 additions & 0 deletions Cavern/Filters/Utilities/FilterGraphNodeUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Collections.Generic;

namespace Cavern.Filters.Utilities {
/// <summary>
/// Special functions for handling <see cref="FilterGraphNode"/>s.
/// </summary>
public static class FilterGraphNodeUtils {
/// <summary>
/// Get all nodes in a filter graph knowing the root nodes.
/// </summary>
public static HashSet<FilterGraphNode> MapGraph(IEnumerable<FilterGraphNode> rootNodes) {
HashSet<FilterGraphNode> visited = new HashSet<FilterGraphNode>();
Queue<FilterGraphNode> queue = new Queue<FilterGraphNode>(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;
}
}
}
17 changes: 17 additions & 0 deletions Cavern/Utilities/TupleUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Cavern.Utilities {
/// <summary>
/// Advanced functions for handling tuples.
/// </summary>
public static class TupleUtils {
/// <summary>
/// From an array of tuples, get only the second &quot;column&quot;.
/// </summary>
public static T2[] GetItem2s<T1, T2>(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;
}
}
}
2 changes: 1 addition & 1 deletion CavernSamples/CavernSamples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion CavernSamples/FilterStudio/Graphs/ManipulatableGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,13 @@ public ManipulatableGraph() {
/// </summary>
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);
});
}

Expand Down
20 changes: 12 additions & 8 deletions CavernSamples/FilterStudio/Graphs/Parsing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Windows.Media;

using Cavern.Filters;
using Cavern.Filters.Utilities;
using Cavern.Format.ConfigurationFile;

Expand All @@ -21,18 +22,17 @@ public static class Parsing {
/// Convert a <see cref="ConfigurationFile"/>'s filter graph to an MSAGL <see cref="Graph"/>.
/// </summary>
/// <param name="rootNodes">Filter graph to convert, from <see cref="ConfigurationFile.InputChannels"/></param>
/// <param name="background">Graph background color</param>
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<FilterGraphNode> children = rootNodes[i].root.Children;

IReadOnlyList<FilterGraphNode> 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;
Expand Down Expand Up @@ -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);
}
Expand Down
92 changes: 92 additions & 0 deletions CavernSamples/FilterStudio/Graphs/PipelineEditor.cs
Original file line number Diff line number Diff line change
@@ -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 {
/// <summary>
/// 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.
/// </summary>
public class PipelineEditor : ManipulatableGraph {
/// <summary>
/// Pass the root nodes of the user's selected split.
/// </summary>
public event Action<FilterGraphNode[]> OnSplitChanged;

/// <summary>
/// Overrides the background color of the graph.
/// </summary>
public Color background;

/// <summary>
/// Source of language strings.
/// </summary>
public ResourceDictionary language;

/// <summary>
/// The <see cref="ConfigurationFile"/> of which its split points will be presented.
/// </summary>
public ConfigurationFile Source {
get => source;
set {
source = value;
RecreateGraph();
SelectNode(source.SplitPoints[0].roots.GetHashCode().ToString());
OnSplitChanged?.Invoke(source.SplitPoints[0].roots);
}
}
ConfigurationFile source;

/// <summary>
/// The layout on which the steps of the filter pipeline can be selected.
/// </summary>
public PipelineEditor() {
OnLeftClick += LeftClick;
}

/// <summary>
/// When the <see cref="Source"/> has changed, display its split points.
/// </summary>
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;
}

/// <summary>
/// Open the split the user selects.
/// </summary>
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);
}
}
}
}
Loading

0 comments on commit 7698c32

Please sign in to comment.