Skip to content

Commit

Permalink
Complete Equalizer APO configuration file export
Browse files Browse the repository at this point in the history
  • Loading branch information
VoidXH committed Jul 20, 2024
1 parent b3ef03e commit 819aa26
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 73 deletions.
20 changes: 14 additions & 6 deletions Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,18 @@ public void MergeSplitPoints() {
/// <summary>
/// Get the index of a given <paramref name="channel"/> in the configuration. This is the input and output it's wired to.
/// </summary>
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));
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, FilterGraphNode> lastNodes = new Dictionary<int, FilterGraphNode>();
FilterGraphNode GetChannel(int index) { // Get an actual channel's last node
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,88 @@ public EqualizerAPOConfigurationFile(string path, int sampleRate) : base(Path.Ge
Optimize();
}

/// <summary>
/// Add a filter to the currently active channels.
/// </summary>
static void AddFilter(Dictionary<string, FilterGraphNode> lastNodes, List<string> channels, Filter filter) {
List<FilterGraphNode> addedTo = new List<FilterGraphNode>();
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);
}
}
}

/// <summary>
/// Parse a Copy filter from the last <paramref name="split"/> of the configuration file. Mixing will be handled by edges and
/// <see cref="Gain"/> filters where needed.
/// </summary>
static void AddCopyFilter(Dictionary<string, FilterGraphNode> lastNodes, string[] split) {
Dictionary<string, FilterGraphNode> 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;
}
}

/// <summary>
/// Parse a Channel filter and make the next parsed filters only affect those channels.
/// </summary>
static void SelectChannels(Dictionary<string, FilterGraphNode> lastNodes, List<string> 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]);
}
}
}

/// <inheritdoc/>
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];
}
}
}

Expand All @@ -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<IConvolution> convolutions = new List<IConvolution>();
for (int i = 0; i < exportOrder.Length; i++) {
int channel = exportOrder[i].channel;
int[] parents = GetExportedParents(exportOrder, i);
Expand All @@ -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);
Expand All @@ -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);
}
}

/// <summary>
Expand All @@ -112,19 +200,7 @@ void AddConfigFile(string path, Dictionary<string, FilterGraphNode> 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":
Expand All @@ -135,33 +211,7 @@ void AddConfigFile(string path, Dictionary<string, FilterGraphNode> lastNodes, L
AddFilter(lastNodes, activeChannels, Delay.FromEqualizerAPO(split, sampleRate));
break;
case "copy":
Dictionary<string, FilterGraphNode> 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":
Expand All @@ -179,24 +229,6 @@ void AddConfigFile(string path, Dictionary<string, FilterGraphNode> lastNodes, L
}
}

/// <summary>
/// Add a filter to the currently active channels.
/// </summary>
void AddFilter(Dictionary<string, FilterGraphNode> lastNodes, List<string> channels, Filter filter) {
List<FilterGraphNode> addedTo = new List<FilterGraphNode>();
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);
}
}
}

/// <summary>
/// Mark the current point of the configuration as the beginning of the next section of filters or next pipeline step.
/// </summary>
Expand All @@ -221,5 +253,10 @@ void CreateSplit(string name, Dictionary<string, FilterGraphNode> lastNodes) {
/// Prefix for channel selection lines in an Equalizer APO configuration file.
/// </summary>
const string channelFilter = "Channel: ";

/// <summary>
/// Prefix for convolution file selection lines in an Equalizer APO configuration file.
/// </summary>
const string convolutionFilter = "Convolution: ";
}
}
11 changes: 9 additions & 2 deletions Cavern.QuickEQ/Equalization/EQGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand All @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion Cavern/Filters/Convolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public override void Process(float[] samples) {
}

/// <inheritdoc/>
public override object Clone() => new Convolver((float[])impulse.Clone(), delay);
public override object Clone() => new Convolver((float[])impulse.Clone(), SampleRate, delay);

/// <inheritdoc/>
public override string ToString() => "Convolution";
Expand Down
2 changes: 1 addition & 1 deletion Cavern/Filters/FastConvolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ public override void Process(float[] samples, int channel, int channels) {
}

/// <inheritdoc/>
public override object Clone() => new FastConvolver(Impulse, delay);
public override object Clone() => new FastConvolver(Impulse, SampleRate, delay);

/// <summary>
/// Free up the native resources if they were used.
Expand Down

0 comments on commit 819aa26

Please sign in to comment.