Skip to content

Commit

Permalink
Cavern Filter Studio configuration file implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
VoidXH committed Jul 20, 2024
1 parent 385d18d commit d832313
Show file tree
Hide file tree
Showing 25 changed files with 654 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
using Cavern.Channels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Serialization;

using Cavern.Channels;
using Cavern.Filters;
using Cavern.Filters.Utilities;
using Cavern.QuickEQ.Equalization;

namespace Cavern.Format.ConfigurationFile {
/// <summary>
Expand All @@ -13,6 +22,11 @@ public sealed class CavernFilterStudioConfigurationFile : ConfigurationFile {
/// </summary>
public CavernFilterStudioConfigurationFile(ConfigurationFile other) : base(other) { }

/// <summary>
/// Import a Cavern Filter Studio configuration file from a <paramref name="path"/>.
/// </summary>
public CavernFilterStudioConfigurationFile(string path) : base(ParseSplitPoints(path)) { }

/// <summary>
/// Create an empty file for a standard layout.
/// </summary>
Expand All @@ -25,9 +39,159 @@ public CavernFilterStudioConfigurationFile(string name, int channelCount) :
public CavernFilterStudioConfigurationFile(string name, params ReferenceChannel[] channels) : base(name, channels) =>
FinishEmpty(channels);

/// <summary>
/// Import a Cavern Filter Studio configuration file from a <paramref name="path"/>.
/// </summary>
static List<(string, FilterGraphNode[])> ParseSplitPoints(string path) {
using XmlReader reader = XmlReader.Create(path);
int index = -1;
List<FilterGraphNode> nodes = new List<FilterGraphNode>();
List<(string name, FilterGraphNode[] roots)> splitPoints = new List<(string name, FilterGraphNode[] roots)>();
while (reader.Read()) {
if (reader.NodeType != XmlNodeType.Element || reader.Name == nameof(CavernFilterStudioConfigurationFile)) {
continue;
}

if (reader.Name == nameof(FilterGraphNode)) {
string parentsSource = null;
while (reader.MoveToNextAttribute()) {
switch (reader.Name) {
case indexAttribute:
index = int.Parse(reader.Value);
break;
case parentsAttribute:
parentsSource = reader.Value;
break;
default:
throw new NotImplementedException();
}
}

FilterGraphNode node = new FilterGraphNode(null);
if (!string.IsNullOrEmpty(parentsSource)) {
string[] parents = parentsSource.Split(',');
for (int i = 0; i < parents.Length; i++) {
node.AddParent(nodes[int.Parse(parents[i])]);
}
}
nodes.Add(node);
} else if (reader.Name == splitPointElement) {
string name = null,
rootsSource = null;
while (reader.MoveToNextAttribute()) {
switch (reader.Name) {
case nameAttribute:
name = reader.Value;
break;
case rootsAttribute:
rootsSource = reader.Value;
break;
default:
throw new NotImplementedException();
}
}
splitPoints.Add((name, rootsSource.Split(',').Select(x => nodes[int.Parse(x)]).ToArray()));
} else {
nodes[index].Filter = ParseFilter(reader);
}
}
return splitPoints;
}

/// <summary>
/// Parse a filter from a <see cref="CavernFilterStudioConfigurationFile"/>, including the ones not in the base Cavern library.
/// </summary>
static Filter ParseFilter(XmlReader reader) {
switch (reader.Name) {
case nameof(GraphicEQ):
GraphicEQ graphicEQ = new GraphicEQ(new Equalizer(), Listener.DefaultSampleRate);
graphicEQ.ReadXml(reader);
return graphicEQ;
case nameof(InputChannel):
InputChannel inputChannel = new InputChannel(ReferenceChannel.Unknown);
inputChannel.ReadXml(reader);
return inputChannel;
case nameof(OutputChannel):
OutputChannel outputChannel = new OutputChannel(ReferenceChannel.Unknown);
outputChannel.ReadXml(reader);
return outputChannel;
default:
return Filter.FromXml(reader);
};
}

/// <summary>
/// Throw a <see cref="NotCavernFilterStudioFilterException"/> if a filter couldn't be exported.
/// </summary>
static void ValidateForExport((FilterGraphNode node, int _)[] exportOrder) {
for (int i = 0; i < exportOrder.Length; i++) {
Filter filter = exportOrder[i].node.Filter;
if (filter != null && !(filter is IXmlSerializable)) {
throw new NotCavernFilterStudioFilterException(filter);
}
}
}

/// <inheritdoc/>
public override void Export(string path) {
throw new System.NotImplementedException();
(FilterGraphNode node, int channel)[] exportOrder = GetExportOrder();
ValidateForExport(exportOrder);

XmlWriterSettings settings = new XmlWriterSettings() {
Indent = true
};
using XmlWriter writer = XmlWriter.Create(path, settings);
writer.WriteStartElement(nameof(CavernFilterStudioConfigurationFile));
for (int i = 0; i < exportOrder.Length; i++) {
writer.WriteStartElement(nameof(FilterGraphNode));
writer.WriteAttributeString(indexAttribute, i.ToString());
string parents = string.Join(',', GetExportedParentIndices(exportOrder, i));
if (parents.Length != 0) {
writer.WriteAttributeString(parentsAttribute, parents);
}
Filter filter = exportOrder[i].node.Filter;
((IXmlSerializable)filter).WriteXml(writer);
writer.WriteEndElement();
}
for (int i = 0, c = SplitPoints.Count; i < c; i++) {
writer.WriteStartElement(splitPointElement);
writer.WriteAttributeString(nameAttribute, SplitPoints[i].name);
writer.WriteAttributeString(rootsAttribute, string.Join(',', SplitPoints[i].roots.Select(x => {
for (int j = 0; j < exportOrder.Length; j++) {
if (exportOrder[j].node == x) {
return j;
}
}
throw new IndexOutOfRangeException();
})));
writer.WriteEndElement();
}
writer.WriteEndElement();
}

/// <summary>
/// XML attribute of <see cref="FilterGraphNode"/> indices.
/// </summary>
const string indexAttribute = "Index";

/// <summary>
/// XML attribute of indices of a <see cref="FilterGraphNode"/>'s parents.
/// </summary>
const string parentsAttribute = "Parents";

/// <summary>
/// XML element representing one of the <see cref="ConfigurationFile.SplitPoints"/>.
/// </summary>
const string splitPointElement = "SplitPoint";

/// <summary>
/// Name of one of the <see cref="ConfigurationFile.SplitPoints"/>.
/// </summary>
const string nameAttribute = "Name";

/// <summary>
/// Indices of root elements in a split point.
/// </summary>
const string rootsAttribute = "Roots";
}
}
30 changes: 24 additions & 6 deletions Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ namespace Cavern.Format.ConfigurationFile {
/// </summary>
public abstract class ConfigurationFile : IExportable {
/// <summary>
/// Root nodes of each channel, start attaching their filters as a children chain.
/// Root nodes of each channel, start attaching their filters as a children chain. These nodes must contain
/// <see cref="InputChannel"/> filters.
/// </summary>
public (string name, FilterGraphNode root)[] InputChannels { get; }

Expand Down Expand Up @@ -43,6 +44,16 @@ protected ConfigurationFile(ConfigurationFile other) {
splitPoints = other.SplitPoints.Select(x => (x.name, x.roots.Select(x => mapping[x]).ToArray())).ToList();
}

/// <summary>
/// Construct a configuration file from a complete filter graph, including splitting to <paramref name="splitPoints"/>.
/// </summary>
/// <remarks>It's mandatory to have the corresponding output channels to close the split point. Refer to the constructors of
/// <see cref="CavernFilterStudioConfigurationFile"/> for how to add closing <see cref="OutputChannel"/>s.</remarks>
protected ConfigurationFile(List<(string name, FilterGraphNode[] roots)> splitPoints) {
InputChannels = splitPoints[0].roots.Select(x => (((InputChannel)x.Filter).Channel.GetShortName(), x)).ToArray();
this.splitPoints = splitPoints;
}

/// <summary>
/// Construct a configuration file from a complete filter graph, with references to its <paramref name="inputChannels"/>.
/// </summary>
Expand Down Expand Up @@ -313,18 +324,25 @@ protected void FinishLazySetup(int fftCacheSize) {
}

/// <summary>
/// Get the channels of the <see cref="FilterGraphNode"/> at a given <paramref name="index"/> in an <paramref name="exportOrder"/>
/// created with <see cref="GetExportOrder"/>.
/// Get the <see cref="FilterGraphNode"/> indices in the <paramref name="exportOrder"/> of the parents of the node at the
/// given <paramref name="index"/>.
/// </summary>
protected int[] GetExportedParents((FilterGraphNode node, int channel)[] exportOrder, int index) =>
protected IEnumerable<int> GetExportedParentIndices((FilterGraphNode node, int channel)[] exportOrder, int index) =>
exportOrder[index].node.Parents.Select(x => {
for (int i = 0; i < exportOrder.Length; i++) {
if (exportOrder[i].node == x) {
return exportOrder[i].channel;
return i;
}
}
throw new KeyNotFoundException();
}).ToArray();
});

/// <summary>
/// Get the channels of the <see cref="FilterGraphNode"/> at a given <paramref name="index"/> in an <paramref name="exportOrder"/>
/// created with <see cref="GetExportOrder"/>.
/// </summary>
protected int[] GetExportedParents((FilterGraphNode node, int channel)[] exportOrder, int index) =>
GetExportedParentIndices(exportOrder, index).Select(x => exportOrder[x].channel).ToArray();

/// <summary>
/// Remove as many merge nodes (null filters) as possible.
Expand Down
14 changes: 13 additions & 1 deletion Cavern.QuickEQ.Format/ConfigurationFile/_Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Cavern.Format.ConfigurationFile {
/// </summary>
public abstract class UnsupportedFilterForExportException : Exception {
/// <summary>
/// The filter not supported by Equalizer APO.
/// The filter not supported by the <see cref="ConfigurationFile"/>.
/// </summary>
public Filter Filter { get; }

Expand Down Expand Up @@ -42,6 +42,18 @@ public class NotEqualizerAPOFilterException : UnsupportedFilterForExportExceptio
public NotEqualizerAPOFilterException(Filter filter) : base(message + filter, filter) { }
}

/// <summary>
/// Thrown when an unsupported filter would be exported for Cavern Filter Studio.
/// </summary>
public class NotCavernFilterStudioFilterException : UnsupportedFilterForExportException {
const string message = "Cavern Filter Studio's format does not support the following filter: ";

/// <summary>
/// Thrown when an unsupported filter would be exported for Cavern Filter Studio.
/// </summary>
public NotCavernFilterStudioFilterException(Filter filter) : base(message + filter, filter) { }
}

/// <summary>
/// Thrown when a channel was used before it was created.
/// </summary>
Expand Down
23 changes: 22 additions & 1 deletion Cavern.QuickEQ.Format/Filters/EndpointFilter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using Cavern.Channels;
using Cavern.Format.ConfigurationFile;
using Cavern.Utilities;

using System;
using System.Xml;

namespace Cavern.Filters {
/// <summary>
Expand All @@ -17,7 +21,7 @@ public abstract class EndpointFilter : BypassFilter {
/// <summary>
/// The channel for which this filter marks the beginning of the filter pipeline.
/// </summary>
public ReferenceChannel Channel { get; }
public ReferenceChannel Channel { get; protected set; }

/// <summary>
/// Marks an endpoint on a parsed <see cref="ConfigurationFile"/> graph.
Expand All @@ -39,6 +43,23 @@ private protected EndpointFilter(string channel, string kind) : base($"{ParseNam
ChannelName = ParseName(channel);
}

/// <inheritdoc/>
public override void ReadXml(XmlReader reader) {
while (reader.MoveToNextAttribute()) {
switch (reader.Name) {
case nameof(Name):
Name = reader.Value;
break;
case nameof(Channel):
Channel = (ReferenceChannel)Enum.Parse(typeof(ReferenceChannel), reader.Value);
break;
case nameof(ChannelName):
ChannelName = reader.Value;
break;
}
}
}

/// <summary>
/// If the <paramref name="channel"/> name is a shorthand for a channel, like an Equalizer APO label, try to get the full channel.
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions Cavern.QuickEQ.Format/Filters/InputChannel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Globalization;
using System.Xml;

using Cavern.Channels;
using Cavern.Format.ConfigurationFile;
Expand Down Expand Up @@ -26,6 +27,15 @@ protected internal InputChannel(string channel) : base(channel, kind) { }
/// <inheritdoc/>
public override object Clone() => Channel != ReferenceChannel.Unknown ? new InputChannel(Channel) : new InputChannel(ChannelName);

/// <inheritdoc/>
public override void WriteXml(XmlWriter writer) {
writer.WriteStartElement(nameof(InputChannel));
writer.WriteAttributeString(nameof(Name), Name);
writer.WriteAttributeString(nameof(Channel), Channel.ToString());
writer.WriteAttributeString(nameof(ChannelName), ChannelName);
writer.WriteEndElement();
}

/// <inheritdoc/>
public string ToString(CultureInfo culture) {
string name = Channel != ReferenceChannel.Unknown ? Channel.GetShortName() : ChannelName;
Expand Down
10 changes: 10 additions & 0 deletions Cavern.QuickEQ.Format/Filters/OutputChannel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Globalization;
using System.Xml;

using Cavern.Channels;
using Cavern.Format.ConfigurationFile;
Expand Down Expand Up @@ -26,6 +27,15 @@ protected internal OutputChannel(string channel) : base(channel, kind) { }
/// <inheritdoc/>
public override object Clone() => Channel != ReferenceChannel.Unknown ? new OutputChannel(Channel) : new OutputChannel(ChannelName);

/// <inheritdoc/>
public override void WriteXml(XmlWriter writer) {
writer.WriteStartElement(nameof(OutputChannel));
writer.WriteAttributeString(nameof(Name), Name);
writer.WriteAttributeString(nameof(Channel), Channel.ToString());
writer.WriteAttributeString(nameof(ChannelName), ChannelName);
writer.WriteEndElement();
}

/// <inheritdoc/>
public string ToString(CultureInfo culture) {
string name = Channel != ReferenceChannel.Unknown ? Channel.GetShortName() : ChannelName;
Expand Down
Loading

0 comments on commit d832313

Please sign in to comment.