diff --git a/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs b/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs index bf7383a..d6e5d0d 100644 --- a/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs +++ b/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs @@ -88,7 +88,15 @@ protected ConfigurationFile(string name, string[] inputs) { }; } - /// + /// + /// Export this configuration to a target file. The general formula for most formats is: + /// + /// Get the filters in exportable order with . This guarantees that all filters will be + /// handled in an order where all their parents were already exported. + /// For each entry, the parent channel indices can be queried with . Handling parent + /// connections shall be before exporting said filter, because the filter is between the parents and children. + /// + /// public abstract void Export(string path); /// @@ -283,6 +291,20 @@ protected void FinishEmpty(ReferenceChannel[] channels) { return result; } + /// + /// Get the channels of the at a given in an + /// created with . + /// + protected int[] GetExportedParents((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; + } + } + throw new KeyNotFoundException(); + }).ToArray(); + /// /// Remove as many merge nodes (null filters) as possible. /// diff --git a/Cavern.QuickEQ.Format/ConfigurationFile/ConvolutionBoxFormatConfigurationFile.cs b/Cavern.QuickEQ.Format/ConfigurationFile/ConvolutionBoxFormatConfigurationFile.cs index 93d63c8..484d2f4 100644 --- a/Cavern.QuickEQ.Format/ConfigurationFile/ConvolutionBoxFormatConfigurationFile.cs +++ b/Cavern.QuickEQ.Format/ConfigurationFile/ConvolutionBoxFormatConfigurationFile.cs @@ -137,23 +137,17 @@ public override void Export(string path) { ValidateForExport(); (FilterGraphNode node, int channel)[] exportOrder = GetExportOrder(); List entries = new List(); - - int GetIndex(FilterGraphNode node) { // Get filter index by node - for (int i = 0; i < exportOrder.Length; i++) { - if (exportOrder[i].node == node) { - return exportOrder[i].channel; - } - } - throw new KeyNotFoundException(); - } - for (int i = 0; i < exportOrder.Length; i++) { int channel = exportOrder[i].channel; // Keeping only incoming nodes is a full solution - optimizing for that few bytes of space would be possible if you're bored - int[] parents = exportOrder[i].node.Parents.Select(x => GetIndex(x)).ToArray(); - MatrixEntry mixer = new MatrixEntry(); - mixer.Expand(parents, channel); - entries.Add(mixer); + int[] parents = GetExportedParents(exportOrder, i); + if (parents.Length == 0 || (parents.Length == 1 && parents[0] == channel)) { + // If there are no parents or the single parent is from the same channel, don't mix + } else { + MatrixEntry mixer = new MatrixEntry(); + mixer.Expand(parents, channel); + entries.Add(mixer); + } Filter filter = exportOrder[i].node.Filter; if (filter is FastConvolver fastConvolver) { diff --git a/Cavern.QuickEQ.Format/ConfigurationFile/EqualizerAPOConfigurationFile.cs b/Cavern.QuickEQ.Format/ConfigurationFile/EqualizerAPOConfigurationFile.cs index d21c076..fb47d4d 100644 --- a/Cavern.QuickEQ.Format/ConfigurationFile/EqualizerAPOConfigurationFile.cs +++ b/Cavern.QuickEQ.Format/ConfigurationFile/EqualizerAPOConfigurationFile.cs @@ -6,6 +6,7 @@ using Cavern.Channels; using Cavern.Filters; +using Cavern.Filters.Interfaces; using Cavern.Filters.Utilities; using Cavern.Format.Common; using Cavern.Utilities; @@ -41,7 +42,56 @@ public EqualizerAPOConfigurationFile(string path, int sampleRate) : base(Path.Ge /// public override void Export(string path) { - throw new NotImplementedException(); + string GetChannelLabel(int channel) { // Convert index to label + if (channel < 0) { + return "V" + -channel; + } else { + return InputChannels[channel].name; + } + } + + List result = new List(); + void AppendSelector(string newLine) { // Add this step, and overwrite the previous line if it selected an unfiltered channel + int last = result.Count - 1; + if (last != -1 && result[last].StartsWith(channelFilter)) { + result[last] = newLine; // No filter comes after this selector, overwrite it + } else { + result.Add(newLine); + } + } + + (FilterGraphNode node, int channel)[] exportOrder = GetExportOrder(); + int lastChannel = int.MaxValue; + for (int i = 0; i < exportOrder.Length; i++) { + int channel = exportOrder[i].channel; + int[] parents = GetExportedParents(exportOrder, i); + if (parents.Length == 0 || (parents.Length == 1 && parents[0] == channel)) { + // If there are no parents or the single parent is from the same channel, don't mix + } else { + AppendSelector($"Copy: {GetChannelLabel(channel)}={string.Join('+', parents.Select(GetChannelLabel))}"); + } + + if (channel != lastChannel) { // When the channel has changed, select it + AppendSelector(channelFilter + GetChannelLabel(channel)); + lastChannel = channel; + } + + Filter baseFilter = exportOrder[i].node.Filter; + if (baseFilter == null || baseFilter is BypassFilter) { + continue; + } + if (baseFilter is IEqualizerAPOFilter filter) { + filter.ExportToEqualizerAPO(result); + } else { + throw new NotEqualizerAPOFilterException(baseFilter); + } + } + + int last = result.Count - 1; + if (last != -1 && result[last].StartsWith(channelFilter)) { + result.RemoveAt(last); // A selector of a bypass might remain + } + File.WriteAllLines(path, result); } /// @@ -166,5 +216,10 @@ void CreateSplit(string name, Dictionary lastNodes) { /// Default initial channels in Equalizer APO. /// static readonly string[] channelLabels = { "L", "R", "C", "SUB", "RL", "RR", "SL", "SR" }; + + /// + /// Prefix for channel selection lines in an Equalizer APO configuration file. + /// + const string channelFilter = "Channel: "; } } \ No newline at end of file diff --git a/Cavern.QuickEQ.Format/ConfigurationFile/_Exceptions.cs b/Cavern.QuickEQ.Format/ConfigurationFile/_Exceptions.cs new file mode 100644 index 0000000..e64d456 --- /dev/null +++ b/Cavern.QuickEQ.Format/ConfigurationFile/_Exceptions.cs @@ -0,0 +1,17 @@ +using System; + +using Cavern.Filters; + +namespace Cavern.Format.ConfigurationFile { + /// + /// Thrown when an unsupported filter would be exported for Equalizer APO. + /// + public class NotEqualizerAPOFilterException : Exception { + const string message = "Equalizer APO does not support the following filter: "; + + /// + /// Thrown when an unsupported filter would be exported for Equalizer APO. + /// + public NotEqualizerAPOFilterException(Filter filter) : base(message + filter) { } + } +} \ No newline at end of file diff --git a/Cavern.QuickEQ/Filters/GraphicEQ.cs b/Cavern.QuickEQ/Filters/GraphicEQ.cs index 2844797..756e231 100644 --- a/Cavern.QuickEQ/Filters/GraphicEQ.cs +++ b/Cavern.QuickEQ/Filters/GraphicEQ.cs @@ -1,7 +1,9 @@ -using System.Globalization; +using System.Collections.Generic; +using System.Globalization; using System.Runtime.CompilerServices; using System.Runtime.Serialization; +using Cavern.Filters.Interfaces; using Cavern.QuickEQ.Equalization; namespace Cavern.Filters { @@ -10,7 +12,7 @@ namespace Cavern.Filters { /// /// This filter is part of the Cavern.QuickEQ library and is not available in the Cavern library's Filters namespace, /// because it requires QuickEQ library functions. - public class GraphicEQ : FastConvolver { + public class GraphicEQ : FastConvolver, IEqualizerAPOFilter { /// /// Copy of the equalizer curve for further alignment. /// @@ -84,6 +86,9 @@ public static GraphicEQ FromEqualizerAPO(string[] splitLine, int sampleRate) => /// public override object Clone() => new GraphicEQ((Equalizer)equalizer.Clone(), sampleRate); + /// + public void ExportToEqualizerAPO(List wipConfig) => wipConfig.Add(equalizer.ExportToEqualizerAPO()); + /// public override string ToString() { double roundedPeak = (int)(Equalizer.PeakGain * 100 + .5) * .01; diff --git a/Cavern/Filters/Gain.cs b/Cavern/Filters/Gain.cs index 19380a6..4a19240 100644 --- a/Cavern/Filters/Gain.cs +++ b/Cavern/Filters/Gain.cs @@ -63,7 +63,7 @@ public override void Process(float[] samples, int channel, int channels) { /// public void ExportToEqualizerAPO(List wipConfig) => - wipConfig.Add($"Gain: {GainValue.ToString(CultureInfo.InvariantCulture)} dB"); + wipConfig.Add($"Preamp: {GainValue.ToString(CultureInfo.InvariantCulture)} dB"); /// public string ToString(CultureInfo culture) => culture.Name switch { diff --git a/Cavern/Filters/Utilities/FilterGraphNodeUtils.Mapping.cs b/Cavern/Filters/Utilities/FilterGraphNodeUtils.Mapping.cs index 51d5cd4..ca8a3ad 100644 --- a/Cavern/Filters/Utilities/FilterGraphNodeUtils.Mapping.cs +++ b/Cavern/Filters/Utilities/FilterGraphNodeUtils.Mapping.cs @@ -49,6 +49,30 @@ public static HashSet MapGraph(this IEnumerable /// Node - channel mapping to optimize, virtual channels take negative indices public static void OptimizeChannelUse(this (FilterGraphNode node, int channel)[] mapping) { + // Trivial: if a member's only parent is before it, don't assign it to a new virtual channel + int lowestChannel = 0; + for (int i = 0; i < mapping.Length; i++) { + if (mapping[i].channel >= 0) { + continue; // Don't touch already assigned physical channels + } + + FilterGraphNode node = mapping[i].node; + if (node.Parents.Count == 1) { + FilterGraphNode parent = node.Parents[0]; + if (parent.Children.Count == 1 && parent.Children[0] == node) { + for (int j = i - 1; j >= 0; j--) { + if (mapping[j].node == parent) { + mapping[i].channel = mapping[j].channel; + break; + } + } + } + } + if (mapping[i].channel < lowestChannel) { // Erases gaps in virtual channel indices + mapping[i].channel = --lowestChannel; + } + } + int virtualChannels = -mapping.Min(x => x.channel); // Partition channels to "time" intervals (mapping indices)