Skip to content

Commit

Permalink
Equalizer APO configuration file export
Browse files Browse the repository at this point in the history
  • Loading branch information
VoidXH committed Jul 15, 2024
1 parent 57209bf commit f1a60d3
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 19 deletions.
24 changes: 23 additions & 1 deletion Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,15 @@ protected ConfigurationFile(string name, string[] inputs) {
};
}

/// <inheritdoc/>
/// <summary>
/// Export this configuration to a target file. The general formula for most formats is:
/// <list type="bullet">
/// <item>Get the filters in exportable order with <see cref="GetExportOrder"/>. This guarantees that all filters will be
/// handled in an order where all their parents were already exported.</item>
/// <item>For each entry, the parent channel indices can be queried with <see cref="GetExportedParents"/>. Handling parent
/// connections shall be before exporting said filter, because the filter is between the parents and children.</item>
/// </list>
/// </summary>
public abstract void Export(string path);

/// <summary>
Expand Down Expand Up @@ -283,6 +291,20 @@ protected void FinishEmpty(ReferenceChannel[] channels) {
return result;
}

/// <summary>
/// Get the channels of the <see cref="FilterGraphNode"/> at a given <paramref name="index"/> in an <paramref name="exportOrder"/>
/// created with <see cref="GetExportOrder"/>.
/// </summary>
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();

/// <summary>
/// Remove as many merge nodes (null filters) as possible.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,23 +137,17 @@ public override void Export(string path) {
ValidateForExport();
(FilterGraphNode node, int channel)[] exportOrder = GetExportOrder();
List<CBFEntry> entries = new List<CBFEntry>();

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,7 +42,56 @@ public EqualizerAPOConfigurationFile(string path, int sampleRate) : base(Path.Ge

/// <inheritdoc/>
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<string> result = new List<string>();
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);
}

/// <summary>
Expand Down Expand Up @@ -166,5 +216,10 @@ void CreateSplit(string name, Dictionary<string, FilterGraphNode> lastNodes) {
/// Default initial channels in Equalizer APO.
/// </summary>
static readonly string[] channelLabels = { "L", "R", "C", "SUB", "RL", "RR", "SL", "SR" };

/// <summary>
/// Prefix for channel selection lines in an Equalizer APO configuration file.
/// </summary>
const string channelFilter = "Channel: ";
}
}
17 changes: 17 additions & 0 deletions Cavern.QuickEQ.Format/ConfigurationFile/_Exceptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;

using Cavern.Filters;

namespace Cavern.Format.ConfigurationFile {
/// <summary>
/// Thrown when an unsupported filter would be exported for Equalizer APO.
/// </summary>
public class NotEqualizerAPOFilterException : Exception {
const string message = "Equalizer APO does not support the following filter: ";

/// <summary>
/// Thrown when an unsupported filter would be exported for Equalizer APO.
/// </summary>
public NotEqualizerAPOFilterException(Filter filter) : base(message + filter) { }
}
}
9 changes: 7 additions & 2 deletions Cavern.QuickEQ/Filters/GraphicEQ.cs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -10,7 +12,7 @@ namespace Cavern.Filters {
/// </summary>
/// <remarks>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.</remarks>
public class GraphicEQ : FastConvolver {
public class GraphicEQ : FastConvolver, IEqualizerAPOFilter {
/// <summary>
/// Copy of the equalizer curve for further alignment.
/// </summary>
Expand Down Expand Up @@ -84,6 +86,9 @@ public static GraphicEQ FromEqualizerAPO(string[] splitLine, int sampleRate) =>
/// <inheritdoc/>
public override object Clone() => new GraphicEQ((Equalizer)equalizer.Clone(), sampleRate);

/// <inheritdoc/>
public void ExportToEqualizerAPO(List<string> wipConfig) => wipConfig.Add(equalizer.ExportToEqualizerAPO());

/// <inheritdoc/>
public override string ToString() {
double roundedPeak = (int)(Equalizer.PeakGain * 100 + .5) * .01;
Expand Down
2 changes: 1 addition & 1 deletion Cavern/Filters/Gain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public override void Process(float[] samples, int channel, int channels) {

/// <inheritdoc/>
public void ExportToEqualizerAPO(List<string> wipConfig) =>
wipConfig.Add($"Gain: {GainValue.ToString(CultureInfo.InvariantCulture)} dB");
wipConfig.Add($"Preamp: {GainValue.ToString(CultureInfo.InvariantCulture)} dB");

/// <inheritdoc/>
public string ToString(CultureInfo culture) => culture.Name switch {
Expand Down
24 changes: 24 additions & 0 deletions Cavern/Filters/Utilities/FilterGraphNodeUtils.Mapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,30 @@ public static HashSet<FilterGraphNode> MapGraph(this IEnumerable<FilterGraphNode
/// </summary>
/// <param name="mapping">Node - channel mapping to optimize, virtual channels take negative indices</param>
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)
Expand Down

0 comments on commit f1a60d3

Please sign in to comment.