diff --git a/Cavern.QuickEQ.Format/ConfigurationFile/CavernFilterStudioConfigurationFile.cs b/Cavern.QuickEQ.Format/ConfigurationFile/CavernFilterStudioConfigurationFile.cs
index 18dc9d0..3a80ab4 100644
--- a/Cavern.QuickEQ.Format/ConfigurationFile/CavernFilterStudioConfigurationFile.cs
+++ b/Cavern.QuickEQ.Format/ConfigurationFile/CavernFilterStudioConfigurationFile.cs
@@ -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 {
///
@@ -13,6 +22,11 @@ public sealed class CavernFilterStudioConfigurationFile : ConfigurationFile {
///
public CavernFilterStudioConfigurationFile(ConfigurationFile other) : base(other) { }
+ ///
+ /// Import a Cavern Filter Studio configuration file from a .
+ ///
+ public CavernFilterStudioConfigurationFile(string path) : base(ParseSplitPoints(path)) { }
+
///
/// Create an empty file for a standard layout.
///
@@ -25,9 +39,159 @@ public CavernFilterStudioConfigurationFile(string name, int channelCount) :
public CavernFilterStudioConfigurationFile(string name, params ReferenceChannel[] channels) : base(name, channels) =>
FinishEmpty(channels);
+ ///
+ /// Import a Cavern Filter Studio configuration file from a .
+ ///
+ static List<(string, FilterGraphNode[])> ParseSplitPoints(string path) {
+ using XmlReader reader = XmlReader.Create(path);
+ int index = -1;
+ List nodes = new List();
+ 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;
+ }
+
+ ///
+ /// Parse a filter from a , including the ones not in the base Cavern library.
+ ///
+ 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);
+ };
+ }
+
+ ///
+ /// Throw a if a filter couldn't be exported.
+ ///
+ 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);
+ }
+ }
+ }
+
///
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();
}
+
+ ///
+ /// XML attribute of indices.
+ ///
+ const string indexAttribute = "Index";
+
+ ///
+ /// XML attribute of indices of a 's parents.
+ ///
+ const string parentsAttribute = "Parents";
+
+ ///
+ /// XML element representing one of the .
+ ///
+ const string splitPointElement = "SplitPoint";
+
+ ///
+ /// Name of one of the .
+ ///
+ const string nameAttribute = "Name";
+
+ ///
+ /// Indices of root elements in a split point.
+ ///
+ const string rootsAttribute = "Roots";
}
}
\ No newline at end of file
diff --git a/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs b/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs
index e7017ed..105c996 100644
--- a/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs
+++ b/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs
@@ -15,7 +15,8 @@ namespace Cavern.Format.ConfigurationFile {
///
public abstract class ConfigurationFile : IExportable {
///
- /// 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
+ /// filters.
///
public (string name, FilterGraphNode root)[] InputChannels { get; }
@@ -43,6 +44,16 @@ protected ConfigurationFile(ConfigurationFile other) {
splitPoints = other.SplitPoints.Select(x => (x.name, x.roots.Select(x => mapping[x]).ToArray())).ToList();
}
+ ///
+ /// Construct a configuration file from a complete filter graph, including splitting to .
+ ///
+ /// It's mandatory to have the corresponding output channels to close the split point. Refer to the constructors of
+ /// for how to add closing s.
+ protected ConfigurationFile(List<(string name, FilterGraphNode[] roots)> splitPoints) {
+ InputChannels = splitPoints[0].roots.Select(x => (((InputChannel)x.Filter).Channel.GetShortName(), x)).ToArray();
+ this.splitPoints = splitPoints;
+ }
+
///
/// Construct a configuration file from a complete filter graph, with references to its .
///
@@ -313,18 +324,25 @@ protected void FinishLazySetup(int fftCacheSize) {
}
///
- /// Get the channels of the at a given in an
- /// created with .
+ /// Get the indices in the of the parents of the node at the
+ /// given .
///
- protected int[] GetExportedParents((FilterGraphNode node, int channel)[] exportOrder, int index) =>
+ protected IEnumerable 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();
+ });
+
+ ///
+ /// Get the channels of the at a given in an
+ /// created with .
+ ///
+ protected int[] GetExportedParents((FilterGraphNode node, int channel)[] exportOrder, int index) =>
+ GetExportedParentIndices(exportOrder, index).Select(x => exportOrder[x].channel).ToArray();
///
/// Remove as many merge nodes (null filters) as possible.
diff --git a/Cavern.QuickEQ.Format/ConfigurationFile/_Exceptions.cs b/Cavern.QuickEQ.Format/ConfigurationFile/_Exceptions.cs
index 9a39937..9ab1873 100644
--- a/Cavern.QuickEQ.Format/ConfigurationFile/_Exceptions.cs
+++ b/Cavern.QuickEQ.Format/ConfigurationFile/_Exceptions.cs
@@ -8,7 +8,7 @@ namespace Cavern.Format.ConfigurationFile {
///
public abstract class UnsupportedFilterForExportException : Exception {
///
- /// The filter not supported by Equalizer APO.
+ /// The filter not supported by the .
///
public Filter Filter { get; }
@@ -42,6 +42,18 @@ public class NotEqualizerAPOFilterException : UnsupportedFilterForExportExceptio
public NotEqualizerAPOFilterException(Filter filter) : base(message + filter, filter) { }
}
+ ///
+ /// Thrown when an unsupported filter would be exported for Cavern Filter Studio.
+ ///
+ public class NotCavernFilterStudioFilterException : UnsupportedFilterForExportException {
+ const string message = "Cavern Filter Studio's format does not support the following filter: ";
+
+ ///
+ /// Thrown when an unsupported filter would be exported for Cavern Filter Studio.
+ ///
+ public NotCavernFilterStudioFilterException(Filter filter) : base(message + filter, filter) { }
+ }
+
///
/// Thrown when a channel was used before it was created.
///
diff --git a/Cavern.QuickEQ.Format/Filters/EndpointFilter.cs b/Cavern.QuickEQ.Format/Filters/EndpointFilter.cs
index 0769ae6..88f0407 100644
--- a/Cavern.QuickEQ.Format/Filters/EndpointFilter.cs
+++ b/Cavern.QuickEQ.Format/Filters/EndpointFilter.cs
@@ -1,5 +1,9 @@
using Cavern.Channels;
using Cavern.Format.ConfigurationFile;
+using Cavern.Utilities;
+
+using System;
+using System.Xml;
namespace Cavern.Filters {
///
@@ -17,7 +21,7 @@ public abstract class EndpointFilter : BypassFilter {
///
/// The channel for which this filter marks the beginning of the filter pipeline.
///
- public ReferenceChannel Channel { get; }
+ public ReferenceChannel Channel { get; protected set; }
///
/// Marks an endpoint on a parsed graph.
@@ -39,6 +43,23 @@ private protected EndpointFilter(string channel, string kind) : base($"{ParseNam
ChannelName = ParseName(channel);
}
+ ///
+ 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;
+ }
+ }
+ }
+
///
/// If the name is a shorthand for a channel, like an Equalizer APO label, try to get the full channel.
///
diff --git a/Cavern.QuickEQ.Format/Filters/InputChannel.cs b/Cavern.QuickEQ.Format/Filters/InputChannel.cs
index c01d0f8..d66824b 100644
--- a/Cavern.QuickEQ.Format/Filters/InputChannel.cs
+++ b/Cavern.QuickEQ.Format/Filters/InputChannel.cs
@@ -1,4 +1,5 @@
using System.Globalization;
+using System.Xml;
using Cavern.Channels;
using Cavern.Format.ConfigurationFile;
@@ -26,6 +27,15 @@ protected internal InputChannel(string channel) : base(channel, kind) { }
///
public override object Clone() => Channel != ReferenceChannel.Unknown ? new InputChannel(Channel) : new InputChannel(ChannelName);
+ ///
+ 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();
+ }
+
///
public string ToString(CultureInfo culture) {
string name = Channel != ReferenceChannel.Unknown ? Channel.GetShortName() : ChannelName;
diff --git a/Cavern.QuickEQ.Format/Filters/OutputChannel.cs b/Cavern.QuickEQ.Format/Filters/OutputChannel.cs
index 7d4ffdb..56e75c4 100644
--- a/Cavern.QuickEQ.Format/Filters/OutputChannel.cs
+++ b/Cavern.QuickEQ.Format/Filters/OutputChannel.cs
@@ -1,4 +1,5 @@
using System.Globalization;
+using System.Xml;
using Cavern.Channels;
using Cavern.Format.ConfigurationFile;
@@ -26,6 +27,15 @@ protected internal OutputChannel(string channel) : base(channel, kind) { }
///
public override object Clone() => Channel != ReferenceChannel.Unknown ? new OutputChannel(Channel) : new OutputChannel(ChannelName);
+ ///
+ 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();
+ }
+
///
public string ToString(CultureInfo culture) {
string name = Channel != ReferenceChannel.Unknown ? Channel.GetShortName() : ChannelName;
diff --git a/Cavern.QuickEQ/Filters/GraphicEQ.cs b/Cavern.QuickEQ/Filters/GraphicEQ.cs
index 5e3b9ea..7431103 100644
--- a/Cavern.QuickEQ/Filters/GraphicEQ.cs
+++ b/Cavern.QuickEQ/Filters/GraphicEQ.cs
@@ -1,7 +1,9 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
+using System.Xml;
using Cavern.Filters.Interfaces;
using Cavern.QuickEQ.Equalization;
@@ -42,8 +44,8 @@ public Equalizer Equalizer {
///
[IgnoreDataMember]
public new int Delay {
- get => base.Delay;
- set => base.Delay = value;
+ get => 0;
+ set => throw new InvalidOperationException();
}
///
@@ -82,7 +84,7 @@ public GraphicEQ(Equalizer equalizer, int sampleRate, FFTCache cache) :
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GraphicEQ FromEqualizerAPO(string line, int sampleRate) =>
- FromEqualizerAPO(line.Split(' ', System.StringSplitOptions.RemoveEmptyEntries), sampleRate);
+ FromEqualizerAPO(line.Split(' ', StringSplitOptions.RemoveEmptyEntries), sampleRate);
///
/// Parse a Graphic EQ line of Equalizer APO which was split at spaces to a Cavern filter.
@@ -94,6 +96,28 @@ public static GraphicEQ FromEqualizerAPO(string[] splitLine, int sampleRate) =>
///
public override object Clone() => new GraphicEQ((Equalizer)equalizer.Clone(), SampleRate);
+ ///
+ public override void ReadXml(XmlReader reader) {
+ while (reader.MoveToNextAttribute()) {
+ switch (reader.Name) {
+ case nameof(SampleRate):
+ SampleRate = int.Parse(reader.Value);
+ break;
+ case nameof(Equalizer):
+ Equalizer = EQGenerator.FromEqualizerAPO(reader.Value);
+ break;
+ }
+ }
+ }
+
+ ///
+ public override void WriteXml(XmlWriter writer) {
+ writer.WriteStartElement(nameof(GraphicEQ));
+ writer.WriteAttributeString(nameof(SampleRate), SampleRate.ToString());
+ writer.WriteAttributeString(nameof(Equalizer), equalizer.ExportToEqualizerAPO());
+ writer.WriteEndElement();
+ }
+
///
public void ExportToEqualizerAPO(List wipConfig) => wipConfig.Add(equalizer.ExportToEqualizerAPO());
diff --git a/Cavern.WPF/ConvolutionEditor.xaml.cs b/Cavern.WPF/ConvolutionEditor.xaml.cs
index 7de8bb6..a71abe6 100644
--- a/Cavern.WPF/ConvolutionEditor.xaml.cs
+++ b/Cavern.WPF/ConvolutionEditor.xaml.cs
@@ -36,6 +36,9 @@ public float[] Impulse {
}
float[] impulse;
+ ///
+ public int Delay { get; set; }
+
///
/// The initial value of as received in the constructor. When the editing is cancelled,
/// or no new convolution samples are loaded, will return its original reference.
diff --git a/Cavern/Filters/BiquadFilter.cs b/Cavern/Filters/BiquadFilter.cs
index eebcec5..0aaa02e 100644
--- a/Cavern/Filters/BiquadFilter.cs
+++ b/Cavern/Filters/BiquadFilter.cs
@@ -4,6 +4,9 @@
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
+using System.Xml;
+using System.Xml.Schema;
+using System.Xml.Serialization;
using Cavern.Filters.Interfaces;
using Cavern.Filters.Utilities;
@@ -13,14 +16,14 @@ namespace Cavern.Filters {
///
/// Simple first-order biquad filter.
///
- public abstract class BiquadFilter : Filter, IEqualizerAPOFilter, ISampleRateDependentFilter, ILocalizableToString {
+ public abstract class BiquadFilter : Filter, IEqualizerAPOFilter, ILocalizableToString, ISampleRateDependentFilter, IXmlSerializable {
///
[IgnoreDataMember]
public int SampleRate {
get => sampleRate;
set {
sampleRate = value;
- Reset(value, q, gain);
+ Reset(centerFreq, q, gain);
}
}
int sampleRate;
@@ -251,6 +254,40 @@ protected void SetupPass(float cosW0, float alpha, float divisor, float b1Pre) {
b0 = MathF.Pow(10, (float)gain * .025f) * b2;
}
+ ///
+ public XmlSchema GetSchema() => null;
+
+ ///
+ public void ReadXml(XmlReader reader) {
+ while (reader.MoveToNextAttribute()) {
+ switch (reader.Name) {
+ case nameof(SampleRate):
+ sampleRate = int.Parse(reader.Value);
+ break;
+ case nameof(CenterFreq):
+ centerFreq = QMath.ParseDouble(reader.Value);
+ break;
+ case nameof(Q):
+ q = QMath.ParseDouble(reader.Value);
+ break;
+ case nameof(Gain):
+ gain = QMath.ParseDouble(reader.Value);
+ break;
+ }
+ }
+ Reset(centerFreq, q, gain);
+ }
+
+ ///
+ public void WriteXml(XmlWriter writer) {
+ writer.WriteStartElement(FilterType.ToString());
+ writer.WriteAttributeString(nameof(SampleRate), sampleRate.ToString());
+ writer.WriteAttributeString(nameof(CenterFreq), centerFreq.ToString(CultureInfo.InvariantCulture));
+ writer.WriteAttributeString(nameof(Q), q.ToString(CultureInfo.InvariantCulture));
+ writer.WriteAttributeString(nameof(Gain), gain.ToString(CultureInfo.InvariantCulture));
+ writer.WriteEndElement();
+ }
+
///
/// Display the filter's parameters when converting to string.
///
diff --git a/Cavern/Filters/BypassFilter.cs b/Cavern/Filters/BypassFilter.cs
index 69bb5ea..b6b05f3 100644
--- a/Cavern/Filters/BypassFilter.cs
+++ b/Cavern/Filters/BypassFilter.cs
@@ -1,8 +1,14 @@
-namespace Cavern.Filters {
+using System.Xml;
+using System.Xml.Schema;
+using System.Xml.Serialization;
+
+using Cavern.Utilities;
+
+namespace Cavern.Filters {
///
/// A filter that doesn't do anything. Used to display empty filter nodes with custom names, like the beginning of virtual channels.
///
- public class BypassFilter : Filter {
+ public class BypassFilter : Filter, IXmlSerializable {
///
/// Name of this filter node.
///
@@ -24,6 +30,25 @@ public override void Process(float[] samples, int channel, int channels) {
// Bypass
}
+ ///
+ public XmlSchema GetSchema() => null;
+
+ ///
+ public virtual void ReadXml(XmlReader reader) {
+ while (reader.MoveToNextAttribute()) {
+ if (reader.Name == nameof(Name)) {
+ Name = reader.Value;
+ }
+ }
+ }
+
+ ///
+ public virtual void WriteXml(XmlWriter writer) {
+ writer.WriteStartElement(nameof(BypassFilter));
+ writer.WriteAttributeString(nameof(Name), Name);
+ writer.WriteEndElement();
+ }
+
///
public override object Clone() => new BypassFilter(Name);
diff --git a/Cavern/Filters/Comb.cs b/Cavern/Filters/Comb.cs
index 5d680b2..85c9b9a 100644
--- a/Cavern/Filters/Comb.cs
+++ b/Cavern/Filters/Comb.cs
@@ -2,6 +2,9 @@
using System.ComponentModel;
using System.Globalization;
using System.Runtime.Serialization;
+using System.Xml;
+using System.Xml.Schema;
+using System.Xml.Serialization;
using Cavern.Filters.Interfaces;
using Cavern.Utilities;
@@ -11,7 +14,7 @@ namespace Cavern.Filters {
/// Normalized feedforward comb filter.
///
/// The feedback comb filter is called .
- public class Comb : Filter, ISampleRateDependentFilter, ILocalizableToString {
+ public class Comb : Filter, ILocalizableToString, ISampleRateDependentFilter, IXmlSerializable {
///
[IgnoreDataMember]
public int SampleRate {
@@ -112,6 +115,35 @@ public override void Process(float[] samples) {
///
public override object Clone() => new Comb(sampleRate, K, Alpha);
+ ///
+ public XmlSchema GetSchema() => null;
+
+ ///
+ public void ReadXml(XmlReader reader) {
+ while (reader.MoveToNextAttribute()) {
+ switch (reader.Name) {
+ case nameof(SampleRate):
+ sampleRate = int.Parse(reader.Value);
+ break;
+ case nameof(K):
+ K = int.Parse(reader.Value);
+ break;
+ case nameof(Alpha):
+ Alpha = QMath.ParseDouble(reader.Value);
+ break;
+ }
+ }
+ }
+
+ ///
+ public void WriteXml(XmlWriter writer) {
+ writer.WriteStartElement(nameof(Comb));
+ writer.WriteAttributeString(nameof(SampleRate), sampleRate.ToString());
+ writer.WriteAttributeString(nameof(K), K.ToString());
+ writer.WriteAttributeString(nameof(Alpha), Alpha.ToString(CultureInfo.InvariantCulture));
+ writer.WriteEndElement();
+ }
+
///
public override string ToString() =>
$"Comb: {QMath.ToStringLimitDecimals(Alpha, 3)}x, {QMath.ToStringLimitDecimals(DelayMs, 3)} ms";
diff --git a/Cavern/Filters/Convolver.cs b/Cavern/Filters/Convolver.cs
index c6c1b14..bc4a8a0 100644
--- a/Cavern/Filters/Convolver.cs
+++ b/Cavern/Filters/Convolver.cs
@@ -2,6 +2,9 @@
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
+using System.Xml;
+using System.Xml.Schema;
+using System.Xml.Serialization;
using Cavern.Filters.Interfaces;
using Cavern.Utilities;
@@ -12,7 +15,7 @@ namespace Cavern.Filters {
///
/// This filter is performing convolution by definition, which is faster if the window size is very small.
/// For most cases, is preferred.
- public class Convolver : Filter, IConvolution, ILocalizableToString {
+ public class Convolver : Filter, IConvolution, ILocalizableToString, IXmlSerializable {
///
[IgnoreDataMember]
public int SampleRate { get; set; }
@@ -110,6 +113,15 @@ public override void Process(float[] samples) {
///
public override object Clone() => new Convolver((float[])impulse.Clone(), SampleRate, delay);
+ ///
+ public XmlSchema GetSchema() => null;
+
+ ///
+ public virtual void ReadXml(XmlReader reader) => this.ReadCommonXml(reader);
+
+ ///
+ public virtual void WriteXml(XmlWriter writer) => this.WriteCommonXml(writer, nameof(Convolver));
+
///
public override string ToString() => "Convolution";
diff --git a/Cavern/Filters/Delay.cs b/Cavern/Filters/Delay.cs
index e01fb97..634c839 100644
--- a/Cavern/Filters/Delay.cs
+++ b/Cavern/Filters/Delay.cs
@@ -4,6 +4,9 @@
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
+using System.Xml;
+using System.Xml.Schema;
+using System.Xml.Serialization;
using Cavern.Filters.Interfaces;
using Cavern.Utilities;
@@ -12,7 +15,7 @@ namespace Cavern.Filters {
///
/// Delays the audio.
///
- public class Delay : Filter, IEqualizerAPOFilter, ISampleRateDependentFilter, ILocalizableToString {
+ public class Delay : Filter, IEqualizerAPOFilter, ISampleRateDependentFilter, ILocalizableToString, IXmlSerializable {
///
/// If the filter was set up with a time delay, this is the sample rate used to calculate the delay in samples.
///
@@ -89,7 +92,7 @@ public Delay(int samples) {
/// Create a delay for a given length in seconds.
///
public Delay(double time, int sampleRate) {
- this.SampleRate = sampleRate;
+ SampleRate = sampleRate;
delayMs = time;
RecreateCaches((int)(time * sampleRate * .001 + .5));
}
@@ -149,6 +152,31 @@ public override void Process(float[] samples) {
///
public override object Clone() => double.IsNaN(delayMs) ? new Delay(DelaySamples) : new Delay(DelayMs, SampleRate);
+ ///
+ public XmlSchema GetSchema() => null;
+
+ ///
+ public void ReadXml(XmlReader reader) {
+ while (reader.MoveToNextAttribute()) {
+ switch (reader.Name) {
+ case nameof(SampleRate):
+ SampleRate = int.Parse(reader.Value);
+ break;
+ case nameof(DelaySamples):
+ DelaySamples = int.Parse(reader.Value);
+ break;
+ }
+ }
+ }
+
+ ///
+ public void WriteXml(XmlWriter writer) {
+ writer.WriteStartElement(nameof(Delay));
+ writer.WriteAttributeString(nameof(SampleRate), SampleRate.ToString());
+ writer.WriteAttributeString(nameof(DelaySamples), DelaySamples.ToString());
+ writer.WriteEndElement();
+ }
+
///
public override string ToString() {
if (double.IsNaN(delayMs)) {
diff --git a/Cavern/Filters/Echo.cs b/Cavern/Filters/Echo.cs
index bc1526a..23955b7 100644
--- a/Cavern/Filters/Echo.cs
+++ b/Cavern/Filters/Echo.cs
@@ -2,6 +2,9 @@
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
+using System.Xml.Schema;
+using System.Xml;
+using System.Xml.Serialization;
using Cavern.Filters.Interfaces;
using Cavern.Utilities;
@@ -10,7 +13,7 @@ namespace Cavern.Filters {
///
/// Simple echo/reverberation filter with delay.
///
- public class Echo : Filter, ISampleRateDependentFilter, ILocalizableToString {
+ public class Echo : Filter, ILocalizableToString, IXmlSerializable, ISampleRateDependentFilter {
///
[IgnoreDataMember]
public int SampleRate {
@@ -130,6 +133,35 @@ public override void Process(float[] samples, int channel, int channels) {
///
public override object Clone() => new Echo(sampleRate, Strength, delay);
+ ///
+ public XmlSchema GetSchema() => null;
+
+ ///
+ public void ReadXml(XmlReader reader) {
+ while (reader.MoveToNextAttribute()) {
+ switch (reader.Name) {
+ case nameof(SampleRate):
+ SampleRate = int.Parse(reader.Value);
+ break;
+ case nameof(Strength):
+ Strength = QMath.ParseDouble(reader.Value);
+ break;
+ case nameof(DelaySamples):
+ DelaySamples = int.Parse(reader.Value);
+ break;
+ }
+ }
+ }
+
+ ///
+ public void WriteXml(XmlWriter writer) {
+ writer.WriteStartElement(nameof(Echo));
+ writer.WriteAttributeString(nameof(SampleRate), SampleRate.ToString());
+ writer.WriteAttributeString(nameof(Strength), Strength.ToString(CultureInfo.InvariantCulture));
+ writer.WriteAttributeString(nameof(DelaySamples), DelaySamples.ToString());
+ writer.WriteEndElement();
+ }
+
///
public override string ToString() =>
$"Echo: {QMath.ToStringLimitDecimals(Strength, 3)}x, {QMath.ToStringLimitDecimals(DelayTime, 3)} s";
diff --git a/Cavern/Filters/FastConvolver.cs b/Cavern/Filters/FastConvolver.cs
index bc34054..03c18e6 100644
--- a/Cavern/Filters/FastConvolver.cs
+++ b/Cavern/Filters/FastConvolver.cs
@@ -2,6 +2,9 @@
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
+using System.Xml;
+using System.Xml.Schema;
+using System.Xml.Serialization;
using Cavern.Filters.Interfaces;
using Cavern.Utilities;
@@ -13,7 +16,7 @@ namespace Cavern.Filters {
/// This filter is using the overlap and add method using FFTs, with non-thread-safe caches.
/// For a thread-safe fast convolver, use . This filter is also
/// only for a single channel, use for multichannel signals.
- public partial class FastConvolver : Filter, IConvolution, IDisposable, ILocalizableToString {
+ public partial class FastConvolver : Filter, IConvolution, IDisposable, ILocalizableToString, IXmlSerializable {
///
[IgnoreDataMember]
public int SampleRate { get; set; }
@@ -46,13 +49,13 @@ public float[] Impulse {
///
public int Length => filter.Length;
- ///
- /// Added filter delay to the impulse, in samples.
- ///
+ ///
public int Delay {
get => delay;
set {
- future = new float[Length + value];
+ if (future == null || future.Length != Length + value) {
+ future = new float[Length + value];
+ }
delay = value;
}
}
@@ -162,6 +165,15 @@ public void Dispose() {
}
}
+ ///
+ public XmlSchema GetSchema() => null;
+
+ ///
+ public virtual void ReadXml(XmlReader reader) => this.ReadCommonXml(reader);
+
+ ///
+ public virtual void WriteXml(XmlWriter writer) => this.WriteCommonXml(writer, nameof(FastConvolver));
+
///
public override string ToString() => "Convolution";
diff --git a/Cavern/Filters/Filter.cs b/Cavern/Filters/Filter.cs
index c85aee2..54ceee8 100644
--- a/Cavern/Filters/Filter.cs
+++ b/Cavern/Filters/Filter.cs
@@ -1,4 +1,5 @@
using System;
+using System.Xml;
using Cavern.Utilities;
@@ -14,6 +15,56 @@ public abstract class Filter : ICloneable {
///
public virtual bool LinearTimeInvariant => true;
+ ///
+ /// Using an XML that is currently at an element start, return the described filter in the XML file.
+ ///
+ /// The filter is unknown or the reading of the filter from XML is not implemented
+ public static Filter FromXml(XmlReader reader) {
+ Type filterType = Type.GetType("Cavern.Filters." + reader.Name);
+ if (filterType != null && typeof(BiquadFilter).IsAssignableFrom(filterType)) {
+ BiquadFilter filter = (BiquadFilter)Activator.CreateInstance(filterType, Listener.DefaultSampleRate, 100);
+ filter.ReadXml(reader);
+ return filter;
+ }
+
+ switch (reader.Name) {
+ case nameof(BypassFilter):
+ BypassFilter bypass = new BypassFilter(string.Empty);
+ bypass.ReadXml(reader);
+ return bypass;
+ case nameof(Comb):
+ Comb comb = new Comb(Listener.DefaultSampleRate, 0, 0);
+ comb.ReadXml(reader);
+ return comb;
+ case nameof(Convolver):
+ Convolver convolver = new Convolver(Array.Empty(), Listener.DefaultSampleRate);
+ convolver.ReadXml(reader);
+ return convolver;
+ case nameof(Delay):
+ Delay delay = new Delay(0);
+ delay.ReadXml(reader);
+ return delay;
+ case nameof(Echo):
+ Echo echo = new Echo(Listener.DefaultSampleRate);
+ echo.ReadXml(reader);
+ return echo;
+ case nameof(FastConvolver):
+ FastConvolver fastConvolver = new FastConvolver(new float[2]);
+ fastConvolver.ReadXml(reader);
+ return fastConvolver;
+ case nameof(Gain):
+ Gain gain = new Gain(0);
+ gain.ReadXml(reader);
+ return gain;
+ case nameof(SpikeConvolver):
+ SpikeConvolver spikeConvolver = new SpikeConvolver(new float[0], 0);
+ spikeConvolver.ReadXml(reader);
+ return spikeConvolver;
+ default:
+ throw new XmlException();
+ }
+ }
+
///
/// Apply this filter on an array of samples. One filter should be applied to only one continuous stream of samples.
///
diff --git a/Cavern/Filters/Gain.cs b/Cavern/Filters/Gain.cs
index 6f968c9..6dcf372 100644
--- a/Cavern/Filters/Gain.cs
+++ b/Cavern/Filters/Gain.cs
@@ -2,6 +2,9 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
+using System.Xml.Schema;
+using System.Xml;
+using System.Xml.Serialization;
using Cavern.Filters.Interfaces;
using Cavern.Utilities;
@@ -10,7 +13,7 @@ namespace Cavern.Filters {
///
/// Signal level multiplier filter.
///
- public class Gain : Filter, IEqualizerAPOFilter, ILocalizableToString {
+ public class Gain : Filter, IEqualizerAPOFilter, ILocalizableToString, IXmlSerializable {
///
/// Filter gain in decibels.
///
@@ -58,6 +61,31 @@ public override void Process(float[] samples, int channel, int channels) {
Invert = Invert
};
+ ///
+ public XmlSchema GetSchema() => null;
+
+ ///
+ public void ReadXml(XmlReader reader) {
+ while (reader.MoveToNextAttribute()) {
+ switch (reader.Name) {
+ case nameof(GainValue):
+ GainValue = QMath.ParseDouble(reader.Value);
+ break;
+ case nameof(Invert):
+ Invert = bool.Parse(reader.Value);
+ break;
+ }
+ }
+ }
+
+ ///
+ public void WriteXml(XmlWriter writer) {
+ writer.WriteStartElement(nameof(Gain));
+ writer.WriteAttributeString(nameof(GainValue), GainValue.ToString(CultureInfo.InvariantCulture));
+ writer.WriteAttributeString(nameof(Invert), Invert.ToString());
+ writer.WriteEndElement();
+ }
+
///
public override string ToString() => $"Gain: {QMath.ToStringLimitDecimals(GainValue, 2)} dB";
diff --git a/Cavern/Filters/Interfaces/IConvolution.cs b/Cavern/Filters/Interfaces/IConvolution.cs
index 9253a49..b392cc4 100644
--- a/Cavern/Filters/Interfaces/IConvolution.cs
+++ b/Cavern/Filters/Interfaces/IConvolution.cs
@@ -7,5 +7,10 @@ public interface IConvolution : ISampleRateDependentFilter {
/// Impulse response to convolve with.
///
float[] Impulse { get; set; }
+
+ ///
+ /// Added delay to the filter's , in samples.
+ ///
+ int Delay { get; set; }
}
}
\ No newline at end of file
diff --git a/Cavern/Filters/Interfaces/IConvolutionExtensions.cs b/Cavern/Filters/Interfaces/IConvolutionExtensions.cs
new file mode 100644
index 0000000..6b8b2ca
--- /dev/null
+++ b/Cavern/Filters/Interfaces/IConvolutionExtensions.cs
@@ -0,0 +1,40 @@
+using System.Xml;
+
+using Cavern.Utilities;
+
+namespace Cavern.Filters.Interfaces {
+ ///
+ /// Common functions of s.
+ ///
+ public static class IConvolutionExtensions {
+ ///
+ /// Read an from an XML, if only the common properties are required.
+ ///
+ public static void ReadCommonXml(this IConvolution conv, XmlReader reader) {
+ while (reader.MoveToNextAttribute()) {
+ switch (reader.Name) {
+ case nameof(conv.SampleRate):
+ conv.SampleRate = int.Parse(reader.Value);
+ break;
+ case nameof(conv.Delay):
+ conv.Delay = int.Parse(reader.Value);
+ break;
+ case nameof(conv.Impulse):
+ conv.Impulse = EncodingUtils.Base64ToFloatArray(reader.Value);
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Write an to an XML, if only the common properties are required.
+ ///
+ public static void WriteCommonXml(this IConvolution conv, XmlWriter writer, string name) {
+ writer.WriteStartElement(name);
+ writer.WriteAttributeString(nameof(conv.SampleRate), conv.SampleRate.ToString());
+ writer.WriteAttributeString(nameof(conv.Delay), conv.Delay.ToString());
+ writer.WriteAttributeString(nameof(conv.Impulse), EncodingUtils.ToBase64(conv.Impulse));
+ writer.WriteEndElement();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cavern/Filters/SpikeConvolver.cs b/Cavern/Filters/SpikeConvolver.cs
index e3c0024..2dfad60 100644
--- a/Cavern/Filters/SpikeConvolver.cs
+++ b/Cavern/Filters/SpikeConvolver.cs
@@ -1,4 +1,7 @@
using System.Runtime.CompilerServices;
+using System.Xml;
+
+using Cavern.Filters.Interfaces;
namespace Cavern.Filters {
///
@@ -57,5 +60,8 @@ public override void Process(float[] samples) {
///
public override object Clone() => new SpikeConvolver((float[])impulse.Clone(), delay);
+
+ ///
+ public override void WriteXml(XmlWriter writer) => this.WriteCommonXml(writer, nameof(SpikeConvolver));
}
}
\ No newline at end of file
diff --git a/Cavern/Utilities/EncodingUtils.cs b/Cavern/Utilities/EncodingUtils.cs
new file mode 100644
index 0000000..38b5fc3
--- /dev/null
+++ b/Cavern/Utilities/EncodingUtils.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace Cavern.Utilities {
+ ///
+ /// Functions for common encoding and decoding tasks.
+ ///
+ public static class EncodingUtils {
+ ///
+ /// Encode a float array to a base 64 string.
+ ///
+ public static string ToBase64(float[] source) {
+ if (source == null || source.Length == 0) {
+ return string.Empty;
+ }
+
+ byte[] result = new byte[source.Length * sizeof(float)];
+ Buffer.BlockCopy(source, 0, result, 0, result.Length);
+ return Convert.ToBase64String(result);
+ }
+
+ ///
+ /// Decode a base 64-encoded float array.
+ ///
+ public static float[] Base64ToFloatArray(string source) {
+ if (string.IsNullOrEmpty(source)) {
+ return new float[0];
+ }
+
+ byte[] from = Convert.FromBase64String(source);
+ float[] result = new float[from.Length / sizeof(float)];
+ Buffer.BlockCopy(from, 0, result, 0, from.Length);
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cavern/Utilities/QMath.cs b/Cavern/Utilities/QMath.cs
index a6b19ef..d3dcf5b 100644
--- a/Cavern/Utilities/QMath.cs
+++ b/Cavern/Utilities/QMath.cs
@@ -247,16 +247,26 @@ public static int Log2Ceil(int val) {
return log;
}
+ ///
+ /// Parse a double value regardless of the system's culture.
+ ///
+ public static double ParseDouble(string s) {
+ char separator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator[0];
+ if (s.IndexOf(separator) >= 0) {
+ return Convert.ToDouble(s);
+ }
+ return Convert.ToDouble(s.Replace(',', '.'), CultureInfo.InvariantCulture);
+ }
+
///
/// Parse a float value regardless of the system's culture.
///
public static float ParseFloat(string s) {
char separator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator[0];
- int idx = s.IndexOf(separator);
- if (idx >= 0) {
+ if (s.IndexOf(separator) >= 0) {
return Convert.ToSingle(s);
}
- return Convert.ToSingle(s.Replace(separator == '.' ? ',' : '.', separator));
+ return Convert.ToSingle(s.Replace(',', '.'), CultureInfo.InvariantCulture);
}
///
diff --git a/CavernSamples/FilterStudio/MainWindow.xaml.cs b/CavernSamples/FilterStudio/MainWindow.xaml.cs
index 214090e..4aa5191 100644
--- a/CavernSamples/FilterStudio/MainWindow.xaml.cs
+++ b/CavernSamples/FilterStudio/MainWindow.xaml.cs
@@ -131,12 +131,15 @@ void LoadConfiguration(object _, RoutedEventArgs e) {
ConfigurationFileType type = ConfigurationFileType.EqualizerAPO;
using (FileStream file = File.OpenRead(dialog.FileName)) {
int magicNumber = file.ReadInt32();
- if (magicNumber == ConvolutionBoxFormatConfigurationFile.syncWord) {
+ if (magicNumber == 0x3CBFBBEF) {
+ type = ConfigurationFileType.CavernFilterStudio;
+ } else if (magicNumber == ConvolutionBoxFormatConfigurationFile.syncWord) {
type = ConfigurationFileType.ConvolutionBoxFormat;
}
}
ConfigurationFile import = type switch {
+ ConfigurationFileType.CavernFilterStudio => new CavernFilterStudioConfigurationFile(dialog.FileName),
ConfigurationFileType.ConvolutionBoxFormat => new ConvolutionBoxFormatConfigurationFile(dialog.FileName),
_ => new EqualizerAPOConfigurationFile(dialog.FileName, SampleRate)
};
diff --git a/CavernSamples/FilterStudio/Resources/MainWindowStrings.hu-HU.xaml b/CavernSamples/FilterStudio/Resources/MainWindowStrings.hu-HU.xaml
index b78c347..2bf7bc1 100644
--- a/CavernSamples/FilterStudio/Resources/MainWindowStrings.hu-HU.xaml
+++ b/CavernSamples/FilterStudio/Resources/MainWindowStrings.hu-HU.xaml
@@ -54,8 +54,8 @@
Jobb klikkelj egy nyílra, hogy új szűrőt adj hozzá két szűrő közé.
Húzz egy szűrőt lenyomott egérgombbal a másikba, hogy összekösd őket.
- Minden támogatott formátum|*.cbf;*.txt|Convolution Box Format|*.cbf|Equalizer APO
- konfigurációk|*.txt
+ Minden támogatott formátum|*.cbf;*.cfs;*.txt|Cavern Filter Studio|*.cfs|Convolution Box Format|*.cbf|Equalizer
+ APO konfigurációk|*.txt
Siker
Sikeres export.
Hiba
diff --git a/CavernSamples/FilterStudio/Resources/MainWindowStrings.xaml b/CavernSamples/FilterStudio/Resources/MainWindowStrings.xaml
index 859bf27..fdcf2f8 100644
--- a/CavernSamples/FilterStudio/Resources/MainWindowStrings.xaml
+++ b/CavernSamples/FilterStudio/Resources/MainWindowStrings.xaml
@@ -54,7 +54,7 @@
Right click an arrow to add a filter between two filters.
Drag and drop from one filter to another to connect them.
- All supported formats|*.cbf;*.txt|Convolution Box Format|*.cbf|Equalizer APO
+ All supported formats|*.cbf;*.cfs;*.txt|Cavern Filter Studio|*.cfs|Convolution Box Format|*.cbf|Equalizer APO
configurations|*.txt
Success
Export successful.