diff --git a/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs b/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs
index 1360d76..6014041 100644
--- a/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs
+++ b/Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs
@@ -183,10 +183,18 @@ public void MergeSplitPoints() {
///
/// Get the index of a given in the configuration. This is the input and output it's wired to.
///
- public int GetChannelIndex(ReferenceChannel channel) {
- for (int i = 0; i < InputChannels.Length; i++) {
- if (((InputChannel)InputChannels[i].root.Filter).Channel == channel) {
- return i;
+ public int GetChannelIndex(EndpointFilter channel) {
+ if (channel.Channel != ReferenceChannel.Unknown) { // Faster if the reference is available
+ for (int i = 0; i < InputChannels.Length; i++) {
+ if (((InputChannel)InputChannels[i].root.Filter).Channel == channel.Channel) {
+ return i;
+ }
+ }
+ } else {
+ for (int i = 0; i < InputChannels.Length; i++) {
+ if (((InputChannel)InputChannels[i].root.Filter).ChannelName == channel.ChannelName) {
+ return i;
+ }
}
}
throw new ArgumentOutOfRangeException(nameof(channel));
@@ -279,9 +287,9 @@ protected void FinishEmpty(ReferenceChannel[] channels) {
FilterGraphNode source = orderedNodes[i];
int channelIndex;
if (source.Children.Count == 0 && source.Filter is OutputChannel output) { // Actual exit node, not terminated virtual ch
- channelIndex = GetChannelIndex(output.Channel);
+ channelIndex = GetChannelIndex(output);
} else if (source.Parents.Count == 0) { // Entry node
- channelIndex = GetChannelIndex(((InputChannel)source.Filter).Channel);
+ channelIndex = GetChannelIndex((InputChannel)source.Filter);
} else {
channelIndex = --lowestChannel;
}
diff --git a/Cavern.QuickEQ.Format/ConfigurationFile/ConvolutionBoxFormatConfigurationFile.cs b/Cavern.QuickEQ.Format/ConfigurationFile/ConvolutionBoxFormatConfigurationFile.cs
index fa0d49a..6c9d7ac 100644
--- a/Cavern.QuickEQ.Format/ConfigurationFile/ConvolutionBoxFormatConfigurationFile.cs
+++ b/Cavern.QuickEQ.Format/ConfigurationFile/ConvolutionBoxFormatConfigurationFile.cs
@@ -73,8 +73,8 @@ public ConvolutionBoxFormatConfigurationFile(string path) : base(Path.GetFileNam
if (stream.ReadInt32() != syncWord) {
throw new SyncException();
}
- stream.Position = 8; // Sample rate is read in the constructor
- int entries = stream.ReadInt32();
+ int sampleRate = stream.ReadInt32(),
+ entries = stream.ReadInt32();
List<(int index, FilterGraphNode root)> inputChannels = new List<(int, FilterGraphNode)>();
Dictionary lastNodes = new Dictionary();
FilterGraphNode GetChannel(int index) { // Get an actual channel's last node
@@ -113,7 +113,7 @@ FilterGraphNode GetChannel(int index) { // Get an actual channel's last node
} else if (entry is ConvolutionEntry convolution) {
FilterGraphNode last = lastNodes.ContainsKey(convolution.Channel) ?
lastNodes[convolution.Channel] : GetChannel(convolution.Channel);
- FastConvolver filter = new FastConvolver(convolution.Filter);
+ FastConvolver filter = new FastConvolver(convolution.Filter, sampleRate, 0);
if (last.Filter == null) {
last.Filter = filter;
} else {
diff --git a/Cavern.QuickEQ.Format/ConfigurationFile/EqualizerAPOConfigurationFile.cs b/Cavern.QuickEQ.Format/ConfigurationFile/EqualizerAPOConfigurationFile.cs
index fb47d4d..b07d3a1 100644
--- a/Cavern.QuickEQ.Format/ConfigurationFile/EqualizerAPOConfigurationFile.cs
+++ b/Cavern.QuickEQ.Format/ConfigurationFile/EqualizerAPOConfigurationFile.cs
@@ -40,13 +40,88 @@ public EqualizerAPOConfigurationFile(string path, int sampleRate) : base(Path.Ge
Optimize();
}
+ ///
+ /// Add a filter to the currently active channels.
+ ///
+ static void AddFilter(Dictionary lastNodes, List channels, Filter filter) {
+ List addedTo = new List();
+ bool clone = false; // Filters have to be individually editable on different paths = make copies after the first was set
+ for (int i = 0, c = channels.Count; i < c; i++) {
+ FilterGraphNode oldLastNode = lastNodes[channels[i]];
+ if (addedTo.Contains(oldLastNode)) {
+ lastNodes[channels[i]] = oldLastNode.Children[^1]; // The channel pipelines were merged with a Copy filter
+ } else {
+ lastNodes[channels[i]] = oldLastNode.AddChild(clone ? (Filter)filter.Clone() : filter);
+ clone = true;
+ addedTo.Add(oldLastNode);
+ }
+ }
+ }
+
+ ///
+ /// Parse a Copy filter from the last of the configuration file. Mixing will be handled by edges and
+ /// filters where needed.
+ ///
+ static void AddCopyFilter(Dictionary lastNodes, string[] split) {
+ Dictionary oldLastNodes = lastNodes.ToDictionary(x => x.Key, x => x.Value);
+ for (int i = 1; i < split.Length; i++) {
+ string[] copy = split[i].Split('=', '+');
+ FilterGraphNode target = new FilterGraphNode(null);
+ for (int j = 1; j < copy.Length; j++) {
+ string channel;
+ int mul = copy[j].IndexOf('*');
+ if (mul != -1) {
+ channel = copy[j][(mul + 1)..];
+ double copyGain = double.Parse(copy[j][..mul].Replace(',', '.'), CultureInfo.InvariantCulture),
+ gainDb = QMath.GainToDb(Math.Abs(copyGain));
+ Gain gainFilter = new Gain(gainDb) {
+ Invert = gainDb >= 0
+ };
+ FilterGraphNode gainNode = new FilterGraphNode(gainFilter);
+ gainNode.AddParent(oldLastNodes[channel]);
+ target.AddParent(gainNode);
+ } else {
+ channel = copy[j];
+ target.AddParent(oldLastNodes[channel]);
+ }
+ }
+ if (!lastNodes.ContainsKey(copy[0])) {
+ target.Filter = new BypassFilter(copy[0]);
+ }
+ lastNodes[copy[0]] = target;
+ }
+ }
+
+ ///
+ /// Parse a Channel filter and make the next parsed filters only affect those channels.
+ ///
+ static void SelectChannels(Dictionary lastNodes, List activeChannels, string[] split) {
+ activeChannels.Clear();
+ if (split.Length == 2 && split[1].ToLowerInvariant() == "all") {
+ activeChannels.AddRange(channelLabels);
+ return;
+ }
+
+ for (int i = 1; i < split.Length; i++) {
+ if (lastNodes.ContainsKey(split[i])) {
+ activeChannels.Add(split[i]);
+ } else {
+ throw new InvalidChannelException(split[i]);
+ }
+ }
+ }
+
///
public override void Export(string path) {
string GetChannelLabel(int channel) { // Convert index to label
if (channel < 0) {
return "V" + -channel;
} else {
- return InputChannels[channel].name;
+ if (channelLabels.Contains(InputChannels[channel].name) || channel > 7) {
+ return InputChannels[channel].name;
+ } else {
+ return channelLabels[channel];
+ }
}
}
@@ -60,8 +135,12 @@ string GetChannelLabel(int channel) { // Convert index to label
}
}
+ string convolutionRoot = Path.GetFileNameWithoutExtension(path);
+ string ConvolutionFileName(int index) => $"{convolutionRoot}_{index}.wav";
+
(FilterGraphNode node, int channel)[] exportOrder = GetExportOrder();
int lastChannel = int.MaxValue;
+ List convolutions = new List();
for (int i = 0; i < exportOrder.Length; i++) {
int channel = exportOrder[i].channel;
int[] parents = GetExportedParents(exportOrder, i);
@@ -80,7 +159,10 @@ string GetChannelLabel(int channel) { // Convert index to label
if (baseFilter == null || baseFilter is BypassFilter) {
continue;
}
- if (baseFilter is IEqualizerAPOFilter filter) {
+ if (baseFilter is IConvolution convolution) {
+ result.Add(convolutionFilter + ConvolutionFileName(convolutions.Count));
+ convolutions.Add(convolution);
+ } else if (baseFilter is IEqualizerAPOFilter filter) {
filter.ExportToEqualizerAPO(result);
} else {
throw new NotEqualizerAPOFilterException(baseFilter);
@@ -91,7 +173,13 @@ string GetChannelLabel(int channel) { // Convert index to label
if (last != -1 && result[last].StartsWith(channelFilter)) {
result.RemoveAt(last); // A selector of a bypass might remain
}
+
+ string folder = Path.GetDirectoryName(path);
File.WriteAllLines(path, result);
+ for (int i = 0; i < convolutions.Count; i++) {
+ string convolutionFile = Path.Combine(folder, ConvolutionFileName(i));
+ RIFFWaveWriter.Write(convolutionFile, convolutions[i].Impulse, 1, convolutions[i].SampleRate, BitDepth.Float32);
+ }
}
///
@@ -112,19 +200,7 @@ void AddConfigFile(string path, Dictionary lastNodes, L
AddConfigFile(included, lastNodes, activeChannels, sampleRate);
break;
case "channel":
- activeChannels.Clear();
- if (split.Length == 2 && split[1].ToLowerInvariant() == "all") {
- activeChannels.AddRange(channelLabels);
- continue;
- }
-
- for (int i = 1; i < split.Length; i++) {
- if (lastNodes.ContainsKey(split[i])) {
- activeChannels.Add(split[i]);
- } else {
- throw new InvalidChannelException(split[i]);
- }
- }
+ SelectChannels(lastNodes, activeChannels, split);
break;
// Basic filters
case "preamp":
@@ -135,33 +211,7 @@ void AddConfigFile(string path, Dictionary lastNodes, L
AddFilter(lastNodes, activeChannels, Delay.FromEqualizerAPO(split, sampleRate));
break;
case "copy":
- Dictionary oldLastNodes = lastNodes.ToDictionary(x => x.Key, x => x.Value);
- for (int i = 1; i < split.Length; i++) {
- string[] copy = split[i].Split(new[] { '=', '+' });
- FilterGraphNode target = new FilterGraphNode(null);
- for (int j = 1; j < copy.Length; j++) {
- string channel;
- int mul = copy[j].IndexOf('*');
- if (mul != -1) {
- channel = copy[j][(mul + 1)..];
- double copyGain = double.Parse(copy[j][..mul].Replace(',', '.'), CultureInfo.InvariantCulture),
- gainDb = QMath.GainToDb(Math.Abs(copyGain));
- Gain gainFilter = new Gain(gainDb) {
- Invert = gainDb >= 0
- };
- FilterGraphNode gainNode = new FilterGraphNode(gainFilter);
- gainNode.AddParent(oldLastNodes[channel]);
- target.AddParent(gainNode);
- } else {
- channel = copy[j];
- target.AddParent(oldLastNodes[channel]);
- }
- }
- if (!lastNodes.ContainsKey(copy[0])) {
- target.Filter = new BypassFilter(copy[0]);
- }
- lastNodes[copy[0]] = target;
- }
+ AddCopyFilter(lastNodes, split);
break;
// Parametric filters
case "filter":
@@ -179,24 +229,6 @@ void AddConfigFile(string path, Dictionary lastNodes, L
}
}
- ///
- /// Add a filter to the currently active channels.
- ///
- void AddFilter(Dictionary lastNodes, List channels, Filter filter) {
- List addedTo = new List();
- bool clone = false; // Filters have to be individually editable on different paths = make copies after the first was set
- for (int i = 0, c = channels.Count; i < c; i++) {
- FilterGraphNode oldLastNode = lastNodes[channels[i]];
- if (addedTo.Contains(oldLastNode)) {
- lastNodes[channels[i]] = oldLastNode.Children[^1]; // The channel pipelines were merged with a Copy filter
- } else {
- lastNodes[channels[i]] = oldLastNode.AddChild(clone ? (Filter)filter.Clone() : filter);
- clone = true;
- addedTo.Add(oldLastNode);
- }
- }
- }
-
///
/// Mark the current point of the configuration as the beginning of the next section of filters or next pipeline step.
///
@@ -221,5 +253,10 @@ void CreateSplit(string name, Dictionary lastNodes) {
/// Prefix for channel selection lines in an Equalizer APO configuration file.
///
const string channelFilter = "Channel: ";
+
+ ///
+ /// Prefix for convolution file selection lines in an Equalizer APO configuration file.
+ ///
+ const string convolutionFilter = "Convolution: ";
}
}
\ No newline at end of file
diff --git a/Cavern.QuickEQ/Equalization/EQGenerator.cs b/Cavern.QuickEQ/Equalization/EQGenerator.cs
index 27ddec2..3a60300 100644
--- a/Cavern.QuickEQ/Equalization/EQGenerator.cs
+++ b/Cavern.QuickEQ/Equalization/EQGenerator.cs
@@ -329,11 +329,11 @@ public static float[] GetConvolution(this Equalizer eq, int sampleRate, int leng
length <<= 1;
Complex[] filter = new Complex[length];
if (initial == null) {
- for (int i = 0; i < length; ++i) {
+ for (int i = 0; i < length; i++) {
filter[i].Real = gain; // FFT of DiracDelta(x)
}
} else {
- for (int i = 0; i < length; ++i) {
+ for (int i = 0; i < length; i++) {
filter[i].Real = initial[i].Magnitude * gain;
}
}
@@ -342,6 +342,13 @@ public static float[] GetConvolution(this Equalizer eq, int sampleRate, int leng
Measurements.MinimumPhaseSpectrum(filter, cache);
filter.InPlaceIFFT(cache);
}
+
+ // Hann windowing for increased precision
+ float mul = 2 * MathF.PI / length;
+ length >>= 1;
+ for (int i = 0; i < length; i++) {
+ filter[i].Real *= .5f * (1 + MathF.Cos(i * mul));
+ }
return Measurements.GetRealPartHalf(filter);
}
diff --git a/Cavern/Filters/Convolver.cs b/Cavern/Filters/Convolver.cs
index a2b26d0..c6c1b14 100644
--- a/Cavern/Filters/Convolver.cs
+++ b/Cavern/Filters/Convolver.cs
@@ -108,7 +108,7 @@ public override void Process(float[] samples) {
}
///
- public override object Clone() => new Convolver((float[])impulse.Clone(), delay);
+ public override object Clone() => new Convolver((float[])impulse.Clone(), SampleRate, delay);
///
public override string ToString() => "Convolution";
diff --git a/Cavern/Filters/FastConvolver.cs b/Cavern/Filters/FastConvolver.cs
index ecc6d06..3252c43 100644
--- a/Cavern/Filters/FastConvolver.cs
+++ b/Cavern/Filters/FastConvolver.cs
@@ -149,7 +149,7 @@ public override void Process(float[] samples, int channel, int channels) {
}
///
- public override object Clone() => new FastConvolver(Impulse, delay);
+ public override object Clone() => new FastConvolver(Impulse, SampleRate, delay);
///
/// Free up the native resources if they were used.