Skip to content

Commit

Permalink
Configuration file export skeleton
Browse files Browse the repository at this point in the history
  • Loading branch information
VoidXH committed Jun 24, 2024
1 parent 5dd974f commit ba70f64
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 40 deletions.
18 changes: 18 additions & 0 deletions Cavern.Format/Common/IExportable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Cavern.Format.Common {
/// <summary>
/// Interface for a format that can be exported to a file.
/// </summary>
public interface IExportable {
/// <summary>
/// Extension of the main file created with <see cref="Export(string)"/>. If multiple files are created, this is the extension of
/// the root file. This should be displayed on export dialogs.
/// </summary>
public string FileExtension { get; }

/// <summary>
/// Export this object to a target file.
/// </summary>
/// <param name="path">Location of the target file</param>
public void Export(string path);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
using Cavern.Channels;
using Cavern.Filters.Utilities;
using Cavern.Filters;

namespace Cavern.Format.ConfigurationFile {
/// <summary>
/// Cavern Filter Studio's own export format for full grouped filter pipelines.
/// </summary>
public class CavernFilterStudioConfigurationFile : ConfigurationFile {
public sealed class CavernFilterStudioConfigurationFile : ConfigurationFile {
/// <inheritdoc/>
public override string FileExtension => ".cfs";

/// <summary>
/// Convert an<paramref name="other"/> configuration file to Cavern's format.
/// </summary>
public CavernFilterStudioConfigurationFile(ConfigurationFile other) : base(other) { }

/// <summary>
/// Create an empty file for a standard layout.
/// </summary>
Expand All @@ -16,11 +22,12 @@ public CavernFilterStudioConfigurationFile(string name, int channelCount) :
/// <summary>
/// Create an empty file for a custom layout.
/// </summary>
public CavernFilterStudioConfigurationFile(string name, params ReferenceChannel[] channels) :
base(name, channels) {
for (int i = 0; i < channels.Length; i++) { // Output markers
InputChannels[i].root.AddChild(new FilterGraphNode(new OutputChannel(channels[i])));
}
public CavernFilterStudioConfigurationFile(string name, params ReferenceChannel[] channels) : base(name, channels) =>
FinishEmpty(channels);

/// <inheritdoc/>
public override void Export(string path) {
throw new System.NotImplementedException();
}
}
}
160 changes: 137 additions & 23 deletions Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Cavern.Format.ConfigurationFile {
/// <summary>
/// Full parsed setup of a freely configurable system-wide equalizer or audio processor software.
/// </summary>
public abstract class ConfigurationFile {
public abstract class ConfigurationFile : IExportable {
/// <summary>
/// Root nodes of each channel, start attaching their filters as a children chain.
/// </summary>
Expand All @@ -24,9 +24,24 @@ public abstract class ConfigurationFile {
/// </summary>
public IReadOnlyList<(string name, FilterGraphNode[] roots)> SplitPoints { get; }

/// <inheritdoc/>
public abstract string FileExtension { get; }

/// <summary>
/// Copy constructor from any <paramref name="other"/> configuration file.
/// </summary>
protected ConfigurationFile(ConfigurationFile other) {
// TODO: deep copy
InputChannels = other.InputChannels.FastClone();
SplitPoints = other.SplitPoints.ToList();
}

/// <summary>
/// Create an empty configuration file with the passed input channels.
/// </summary>
/// <remarks>It's mandatory to have the corresponding output channels to close the split point. It's not done here as there might
/// be an initial configuration. Refer to the code of <see cref="CavernFilterStudioConfigurationFile(string, ReferenceChannel[])"/>
/// for how to add closing <see cref="OutputChannel"/>s.</remarks>
protected ConfigurationFile(string name, ReferenceChannel[] inputs) {
InputChannels = new (string name, FilterGraphNode root)[inputs.Length];
for (int i = 0; i < inputs.Length; i++) {
Expand All @@ -41,6 +56,9 @@ protected ConfigurationFile(string name, ReferenceChannel[] inputs) {
/// <summary>
/// Create an empty configuration file with the passed input channel names/labels.
/// </summary>
/// <remarks>It's mandatory to have the corresponding output channels to close the split point. It's not done here as there might
/// be an initial configuration. Refer to the code of <see cref="EqualizerAPOConfigurationFile(string, int)"/> for how to implement
/// addition and finishing up with closing <see cref="OutputChannel"/>s.</remarks>
protected ConfigurationFile(string name, string[] inputs) {
InputChannels = new (string name, FilterGraphNode root)[inputs.Length];
for (int i = 0; i < inputs.Length; i++) {
Expand All @@ -52,6 +70,9 @@ protected ConfigurationFile(string name, string[] inputs) {
};
}

/// <inheritdoc/>
public abstract void Export(string path);

/// <summary>
/// Add a new split with a custom <paramref name="name"/> at a specific <paramref name="index"/> of <see cref="SplitPoints"/>.
/// </summary>
Expand Down Expand Up @@ -105,6 +126,37 @@ public void ClearSplitPoint(int index) {
/// among the <see cref="SplitPoints"/></exception>
public void ClearSplitPoint(string name) => ClearSplitPoint(GetSplitPointIndexByName(name));

/// <summary>
/// Remove any splits, leave only one continuous graph of filters.
/// </summary>
public void MergeSplitPoints() {
int c = SplitPoints.Count;
if (c <= 1) {
return;
}

for (int i = 1; i < c; i++) {
FilterGraphNode[] roots = SplitPoints[i].roots;
for (int j = 0; j < roots.Length; j++) {
roots[j].Parents[0].DetachFromGraph(); // Output of the previous split
roots[j].DetachFromGraph(); // Input of the current split
}
}
((List<(string, FilterGraphNode[])>)SplitPoints).RemoveRange(1, SplitPoints.Count - 1);
}

/// <summary>
/// Get the index of a given <paramref name="channel"/> in the configuration. This is the input and output it's wired to.
/// </summary>
public int GetChannelIndex(ReferenceChannel channel) {
for (int i = 0; i < InputChannels.Length; i++) {
if (((InputChannel)InputChannels[i].root.Filter).Channel == channel) {
return i;
}
}
throw new ArgumentOutOfRangeException(nameof(channel));
}

/// <summary>
/// Get the node for a split point's (referenced with an <paramref name="index"/>) given <paramref name="channel"/>.
/// </summary>
Expand Down Expand Up @@ -176,40 +228,102 @@ protected void CreateNewSplitPoint(string name) {
}

/// <summary>
/// Remove as many merge nodes (null filters) as possible.
/// Add the neccessary <see cref="OutputChannel"/> entries for an empty configuration file.
/// </summary>
protected void Optimize() {
for (int i = 0; i < InputChannels.Length; i++) {
IReadOnlyList<FilterGraphNode> children = InputChannels[i].root.Children;
for (int j = 0, c = children.Count; j < c;) {
if (!Optimize(children[j])) {
j++;
protected void FinishEmpty(ReferenceChannel[] channels) {
for (int i = 0; i < channels.Length; i++) {
InputChannels[i].root.AddChild(new FilterGraphNode(new OutputChannel(channels[i])));
}
}

/// <summary>
/// Get the nodes in topological order in which they can be exported to a linear description of the graph. This means that for
/// each node that does a merge of two inputs (has multiple parents) will contain the full graph of its parents. The returned
/// array has two values for each node: the node itself, and the channel index. This index can be negative: that means a virtual
/// channel.
/// </summary>
protected (FilterGraphNode node, int channel)[] GetExportOrder() {
List<FilterGraphNode> orderedNodes = new List<FilterGraphNode>();
HashSet<FilterGraphNode> visitedNodes = new HashSet<FilterGraphNode>();

void VisitNode(FilterGraphNode node) {
if (visitedNodes.Contains(node)) {
return;
}
visitedNodes.Add(node);
foreach (FilterGraphNode child in node.Children) {
VisitNode(child);
}
orderedNodes.Add(node);
}

for (int i = InputChannels.Length - 1; i >= 0; i--) { // The reverse will have the channels in order
VisitNode(InputChannels[i].root);
}

(FilterGraphNode node, int channel)[] result = new (FilterGraphNode, int)[orderedNodes.Count];
Dictionary<int, FilterGraphNode> lastNodes = new Dictionary<int, FilterGraphNode>(); // For channel indices
for (int i = 0, c = result.Length - 1; i <= c; i++) {
FilterGraphNode source = orderedNodes[c - i];
int channelIndex = 0;
if (source.Children.Count == 0 && source.Filter is OutputChannel output) { // Actual exit node, not terminated virtual ch
channelIndex = GetChannelIndex(output.Channel);
} else if (source.Parents.Count == 0) { // Entry node
channelIndex = GetChannelIndex(((InputChannel)source.Filter).Channel);
} else if (source.Parents.Count == 1 && source.Parents[0].Children.Count == 1) { // Direct path
FilterGraphNode parent = source.Parents[0];
foreach (KeyValuePair<int, FilterGraphNode> node in lastNodes) {
if (node.Value == parent) {
channelIndex = node.Key;
}
}
} else { // Either merge node or exit from a split: new virtual channel
channelIndex = lastNodes.Keys.Min() - 1;
}
lastNodes[channelIndex] = source;
result[i] = (source, channelIndex);
}

// Channel slot optimization: two non-parallel virtual channels should only occupy one virtual channel, but at different times
// TODO: easy, find ranges, and it's the standard scheduling problem from uni - make use of the channel's path if it's unused
return result;
}

/// <summary>
/// Recursive part of <see cref="Optimize()"/>.
/// Remove as many merge nodes (null filters) as possible.
/// </summary>
/// <returns>Optimization was done and the children of the passed <paramref name="node"/> was modified.
/// This means the currently processed element was removed and new were added, so the loop counter shouldn't increase
/// in the iteration where this function was called from.</returns>
bool Optimize(FilterGraphNode node) {
bool optimized = false;
if (node.Filter == null) {
node.DetachFromGraph();
optimized = true;
protected void Optimize() {
/// <summary>
/// Recursive part of this function.
/// </summary>
/// <returns>Optimization was done and the children of the passed <paramref name="node"/> was modified.
/// This means the currently processed element was removed and new were added, so the loop counter shouldn't increase
/// in the iteration where this function was called from.</returns>
static bool Optimize(FilterGraphNode node) {
bool optimized = false;
if (node.Filter == null) {
node.DetachFromGraph();
optimized = true;
}

IReadOnlyList<FilterGraphNode> children = node.Children;
for (int i = 0, c = children.Count; i < c; i++) {
if (Optimize(children[i])) {
optimized = true;
i--;
}
}
return optimized;
}

IReadOnlyList<FilterGraphNode> children = node.Children;
for (int i = 0, c = children.Count; i < c; i++) {
if (Optimize(children[i])) {
optimized = true;
i--;
for (int i = 0; i < InputChannels.Length; i++) {
IReadOnlyList<FilterGraphNode> children = InputChannels[i].root.Children;
for (int j = 0, c = children.Count; j < c;) {
if (!Optimize(children[j])) {
j++;
}
}
}
return optimized;
}

/// <summary>
Expand Down
28 changes: 28 additions & 0 deletions Cavern.QuickEQ.Format/ConfigurationFile/ConvolutionBoxFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Cavern.Channels;
using Cavern.Filters.Utilities;

namespace Cavern.Format.ConfigurationFile {
/// <summary>
/// A file format only supporting matrix mixing and convolution filters.
/// </summary>
public sealed class ConvolutionBoxFormat : ConfigurationFile {
/// <inheritdoc/>
public override string FileExtension => ".cbf";

/// <summary>
/// Convert an<paramref name="other"/> configuration file to Convolution Box Format.
/// </summary>
public ConvolutionBoxFormat(ConfigurationFile other) : base(other) { }

/// <summary>
/// Create an empty file for a custom layout.
/// </summary>
public ConvolutionBoxFormat(string name, ReferenceChannel[] inputs) : base(name, inputs) => FinishEmpty(inputs);

/// <inheritdoc/>
public override void Export(string path) {
(FilterGraphNode node, int channel)[] exportOrder = GetExportOrder();
// TODO: file format definition to repo
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@ namespace Cavern.Format.ConfigurationFile {
/// <summary>
/// Parsed single Equalizer APO configuration file.
/// </summary>
public class EqualizerAPOConfigurationFile : ConfigurationFile {
public sealed class EqualizerAPOConfigurationFile : ConfigurationFile {
/// <inheritdoc/>
public override string FileExtension => ".txt";

/// <summary>
/// Convert an<paramref name="other"/> configuration file to Equalizer APO's format.
/// </summary>
public EqualizerAPOConfigurationFile(ConfigurationFile other) : base(other) { }

/// <summary>
/// Parse a single Equalizer APO configuration file.
/// </summary>
/// <param name="path">Filesystem location of the configuration file</param>
/// <param name="sampleRate">The sample rate to use when</param>
/// <param name="sampleRate">The sample rate to use for the internally created filters</param>
public EqualizerAPOConfigurationFile(string path, int sampleRate) : base(Path.GetFileNameWithoutExtension(path), channelLabels) {
Dictionary<string, FilterGraphNode> lastNodes = InputChannels.ToDictionary(x => x.name, x => x.root);
List<string> activeChannels = channelLabels.ToList();
Expand All @@ -31,6 +39,11 @@ public EqualizerAPOConfigurationFile(string path, int sampleRate) : base(Path.Ge
Optimize();
}

/// <inheritdoc/>
public override void Export(string path) {
throw new NotImplementedException();
}

/// <summary>
/// Read a configuration file and append it to the previously parsed configuration.
/// </summary>
Expand Down
11 changes: 4 additions & 7 deletions Cavern.QuickEQ.Format/FilterSet/BaseClasses/FilterSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@

using Cavern.Channels;
using Cavern.Filters;
using Cavern.Format.Common;

namespace Cavern.Format.FilterSet {
/// <summary>
/// A filter set containing equalization info for each channel of a system.
/// </summary>
public abstract class FilterSet {
public abstract class FilterSet : IExportable {
/// <summary>
/// Basic information needed for a channel.
/// </summary>
Expand Down Expand Up @@ -53,19 +54,15 @@ public abstract class ChannelData {
/// </summary>
public CultureInfo Culture { get; protected set; } = CultureInfo.InvariantCulture;

/// <summary>
/// Extension of the root file or the single-file export. This should be displayed on export dialogs.
/// </summary>
/// <inheritdoc/>
public virtual string FileExtension => "txt";

/// <summary>
/// A filter set containing equalization info for each channel of a system on a given sample rate.
/// </summary>
protected FilterSet(int sampleRate) => SampleRate = sampleRate;

/// <summary>
/// Export the filter set to a target file.
/// </summary>
/// <inheritdoc/>
public abstract void Export(string path);

/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions CavernSamples/FilterStudio/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
<MenuItem Header="{StaticResource MFile}" Style="{StaticResource RootMenuItem}">
<MenuItem Header="{StaticResource OpNew}" Click="NewConfiguration"/>
<MenuItem Header="{StaticResource OpCfg}" Click="LoadConfiguration"/>
<MenuItem Header="{StaticResource OpSaA}" Click="ExportConfiguration"/>
<Separator/>
<MenuItem Header="{StaticResource OpChs}" Click="SelectChannels"/>
</MenuItem>
<MenuItem Header="{StaticResource MFilt}" Style="{StaticResource RootMenuItem}">
Expand Down
Loading

0 comments on commit ba70f64

Please sign in to comment.