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.