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)