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.