Skip to content

Commit

Permalink
Crossover filter graph creator
Browse files Browse the repository at this point in the history
  • Loading branch information
VoidXH committed Jun 15, 2024
1 parent 1ed5542 commit 4e43a5b
Show file tree
Hide file tree
Showing 20 changed files with 252 additions and 35 deletions.
14 changes: 14 additions & 0 deletions Cavern.QuickEQ.Format/ConfigurationFile/ConfigurationFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Cavern.Channels;
using Cavern.Filters;
using Cavern.Filters.Utilities;
using Cavern.Format.Common;
using Cavern.Utilities;

namespace Cavern.Format.ConfigurationFile {
Expand Down Expand Up @@ -104,6 +105,19 @@ public void ClearSplitPoint(int index) {
/// among the <see cref="SplitPoints"/></exception>
public void ClearSplitPoint(string name) => ClearSplitPoint(GetSplitPointIndexByName(name));

/// <summary>
/// Get the node for a split point's (referenced with an <paramref name="index"/>) given <paramref name="channel"/>.
/// </summary>
public FilterGraphNode GetSplitPointRoot(int index, ReferenceChannel channel) {
FilterGraphNode[] roots = SplitPoints[index].roots;
for (int i = 0; i < roots.Length; i++) {
if (((InputChannel)roots[i].Filter).Channel == channel) {
return roots[i];
}
}
throw new InvalidChannelException(new[] { channel });
}

/// <summary>
/// Removes one of the <see cref="SplitPoints"/> by <paramref name="index"/> and clears all the filters it contains.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Collections.Generic;
using System.Linq;

using Cavern.Channels;
using Cavern.Filters;
using Cavern.Filters.Utilities;
using Cavern.QuickEQ.Crossover;
using Cavern.Utilities;

using Crossover = Cavern.QuickEQ.Crossover.Crossover;

namespace Cavern.Format.ConfigurationFile.Presets {
/// <summary>
/// An added crossover step to a <see cref="ConfigurationFile"/> filter graph.
/// </summary>
public class CrossoverFilterSet : FilterSetPreset {
/// <summary>
/// User-defined name of this crossover that will be given to the created split point.
/// </summary>
readonly string name;

/// <summary>
/// Crossover implementation algorithm.
/// </summary>
readonly CrossoverType type;

/// <summary>
/// Sample rate of the DSP this filter set is applied to.
/// </summary>
readonly int sampleRate;

/// <summary>
/// If the crossover <see cref="type"/> can only be implemented as a convolution, this will be its sample count.
/// </summary>
readonly int filterLength;

/// <summary>
/// The target channel to cross over to.
/// </summary>
readonly ReferenceChannel targetChannel;

/// <summary>
/// The channels to cross over to the <see cref="targetChannel"/>.
/// </summary>
readonly (ReferenceChannel channel, float frequency)[] sourceChannels;

/// <summary>
/// An added crossover step to a <see cref="ConfigurationFile"/> filter graph.
/// </summary>
/// <param name="name">User-defined name of this crossover that will be given to the created split point</param>
/// <param name="type">Crossover implementation algorithm</param>
/// <param name="sampleRate">Sample rate of the DSP this filter set is applied to</param>
/// <param name="filterLength">If the crossover <see cref="type"/> can only be implemented as a convolution,
/// this will be its sample count</param>
/// <param name="targetChannel">The target channel to cross over to</param>
/// <param name="sourceChannels">The channels to cross over to the <see cref="targetChannel"/></param>
public CrossoverFilterSet(string name, CrossoverType type, int sampleRate, int filterLength, ReferenceChannel targetChannel,
params (ReferenceChannel channel, float frequency)[] sourceChannels) {
this.name = name;
this.type = type;
this.sampleRate = sampleRate;
this.filterLength = filterLength;
this.targetChannel = targetChannel;
this.sourceChannels = sourceChannels;
}

/// <inheritdoc/>
public override void Add(ConfigurationFile file, int index) {
file.AddSplitPoint(index, name);
FilterGraphNode lowpassOut = file.GetSplitPointRoot(index, targetChannel).Children[0]; // OutputChannel filter for the target
float[] freqsPerChannel = sourceChannels.GetItem2s();
float[] freqs = freqsPerChannel.Distinct().ToArray();
Crossover generator = Crossover.Create(type, freqsPerChannel, new bool[sourceChannels.Length]);
Dictionary<float, FilterGraphNode> aggregators = new Dictionary<float, FilterGraphNode>();
for (int i = 0; i < freqs.Length; i++) {
Filter lowpass = generator.GetLowpassOptimized(sampleRate, freqs[i], filterLength);
FilterGraphNode node = new FilterGraphNode(lowpass);
node.AddChild(lowpassOut);
aggregators[freqs[i]] = node;
}

for (int i = 0; i < sourceChannels.Length; i++) {
Filter highpass = generator.GetHighpassOptimized(sampleRate, freqsPerChannel[i], filterLength);
FilterGraphNode root = file.GetSplitPointRoot(index, sourceChannels[i].channel);
root.AddBeforeChildren(highpass);
root.AddChild(aggregators[freqsPerChannel[i]]);
}
}
}
}
11 changes: 11 additions & 0 deletions Cavern.QuickEQ.Format/ConfigurationFile/Presets/FilterSetPreset.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Cavern.Format.ConfigurationFile.Presets {
/// <summary>
/// An added preconfigured step to a <see cref="ConfigurationFile"/> filter graph.
/// </summary>
public abstract class FilterSetPreset {
/// <summary>
/// Add this preset to a work in progress configuration <paramref name="file"/> at the given split point <paramref name="index"/>.
/// </summary>
public abstract void Add(ConfigurationFile file, int index);
}
}
32 changes: 4 additions & 28 deletions Cavern.QuickEQ/Crossover/BasicCrossover.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Cavern.Filters;

namespace Cavern.QuickEQ.Crossover {
/// <summary>
Expand All @@ -16,31 +14,9 @@ public class BasicCrossover : Crossover {
public BasicCrossover(float[] frequencies, bool[] subs) : base(frequencies, subs) { }

/// <inheritdoc/>
public override void ExportToEqualizerAPO(List<string> wipConfig) {
(float frequency, string[] channelLabels)[] groups = GetCrossoverGroups();
string[] targets = GetSubLabels();
string subGain = (MathF.Sqrt(1f / targets.Length) * minus10dB).ToString("0.000", CultureInfo.InvariantCulture);
public override Filter GetHighpassOptimized(int sampleRate, float frequency, int length) => new Highpass(sampleRate, frequency);

List<string> outputMix = new List<string>();
for (int i = 0; i < groups.Length; i++) {
if (subs[i] || groups[i].frequency <= 0) {
continue;
}

wipConfig.Add($"Copy: XO{i + 1}={string.Join('+', groups[i].channelLabels)}");
wipConfig.Add("Channel: " + string.Join(" ", groups[i].channelLabels));
AddHighpass(wipConfig, groups[i].frequency);
wipConfig.Add("Channel: XO" + (i + 1));
AddLowpass(wipConfig, groups[i].frequency);
outputMix.Add($"{subGain}*XO{i + 1}");
}

if (outputMix.Count > 0) {
string mix = string.Join('+', outputMix);
for (int i = 0; i < targets.Length; i++) {
wipConfig.Add($"Copy: {targets[i]}={targets[i]}+{mix}");
}
}
}
/// <inheritdoc/>
public override Filter GetLowpassOptimized(int sampleRate, float frequency, int length) => new Lowpass(sampleRate, frequency);
}
}
2 changes: 1 addition & 1 deletion Cavern.QuickEQ/Crossover/CavernCrossover.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Cavern.QuickEQ.Crossover {
/// <summary>
/// A FIR brickwall crossover, first introduced in Cavern.
/// </summary>
public class CavernCrossover : BasicCrossover {
public class CavernCrossover : Crossover {
/// <summary>
/// Creates a FIR brickwall crossover, first introduced in Cavern.
/// </summary>
Expand Down
50 changes: 48 additions & 2 deletions Cavern.QuickEQ/Crossover/Crossover.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

using Cavern.Channels;
Expand Down Expand Up @@ -80,8 +81,35 @@ static float[] Simulate(BiquadFilter filter, int length) {
return impulse;
}

/// <inheritdoc/>
public abstract void ExportToEqualizerAPO(List<string> wipConfig);
/// <summary>
/// Append the completed crossover to a work-in-progress Equalizer APO configuration file.
/// </summary>
public virtual void ExportToEqualizerAPO(List<string> wipConfig) {
(float frequency, string[] channelLabels)[] groups = GetCrossoverGroups();
string[] targets = GetSubLabels();
string subGain = (MathF.Sqrt(1f / targets.Length) * minus10dB).ToString("0.000", CultureInfo.InvariantCulture);

List<string> outputMix = new List<string>();
for (int i = 0; i < groups.Length; i++) {
if (subs[i] || groups[i].frequency <= 0) {
continue;
}

wipConfig.Add($"Copy: XO{i + 1}={string.Join('+', groups[i].channelLabels)}");
wipConfig.Add("Channel: " + string.Join(" ", groups[i].channelLabels));
AddHighpass(wipConfig, groups[i].frequency);
wipConfig.Add("Channel: XO" + (i + 1));
AddLowpass(wipConfig, groups[i].frequency);
outputMix.Add($"{subGain}*XO{i + 1}");
}

if (outputMix.Count > 0) {
string mix = string.Join('+', outputMix);
for (int i = 0; i < targets.Length; i++) {
wipConfig.Add($"Copy: {targets[i]}={targets[i]}+{mix}");
}
}
}

/// <summary>
/// Add the filter's interpretation of highpass to the previously selected channel in an Equalizer APO configuration file.
Expand All @@ -101,6 +129,15 @@ public virtual void AddHighpass(List<string> wipConfig, float frequency) {
public virtual float[] GetHighpass(int sampleRate, float frequency, int length) =>
Simulate(new Highpass(sampleRate, frequency), length);

/// <summary>
/// Get the most quickly processed version of this crossover's highpass.
/// </summary>
/// <param name="sampleRate">Filter sample rate</param>
/// <param name="frequency">Lowpass cutoff point</param>
/// <param name="length">Filter length in samples, if the filter can only be synthesized as a convolution</param>
public virtual Filter GetHighpassOptimized(int sampleRate, float frequency, int length) =>
new FastConvolver(GetHighpass(sampleRate, frequency, length));

/// <summary>
/// Add the filter's interpretation of lowpass to the previously selected channel in an Equalizer APO configuration file.
/// </summary>
Expand All @@ -121,6 +158,15 @@ public virtual void AddLowpass(List<string> wipConfig, float frequency) {
public virtual float[] GetLowpass(int sampleRate, float frequency, int length) =>
Simulate(new Lowpass(sampleRate, frequency), length);

/// <summary>
/// Get the most quickly processed version of this crossover's lowpass.
/// </summary>
/// <param name="sampleRate">Filter sample rate</param>
/// <param name="frequency">Lowpass cutoff point</param>
/// <param name="length">Filter length in samples, if the filter can only be synthesized as a convolution</param>
public virtual Filter GetLowpassOptimized(int sampleRate, float frequency, int length) =>
new FastConvolver(GetLowpass(sampleRate, frequency, length));

/// <summary>
/// Get the labels of channels to route bass to.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion Cavern.QuickEQ/Crossover/SyntheticBiquadCrossover.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Cavern.QuickEQ.Crossover {
/// <summary>
/// The generally used 2nd order highpass/lowpass, but without the phase distortions by applying the spectrum with FIR filters.
/// </summary>
public class SyntheticBiquadCrossover : BasicCrossover {
public class SyntheticBiquadCrossover : Crossover {
/// <summary>
/// Creates a phase distortion-less <see cref="BasicCrossover"/>.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions CavernSamples/Cavern.WPF/Cavern.WPF.csproj.user
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,11 @@
<Page Update="Resources\CommonStrings.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Resources\CrossoverStrings.hu-HU.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Resources\CrossoverStrings.xaml">
<SubType>Designer</SubType>
</Page>
</ItemGroup>
</Project>
25 changes: 25 additions & 0 deletions CavernSamples/Cavern.WPF/Consts/Language.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Globalization;
using System.Threading.Channels;
using System.Windows;

using Cavern.Channels;
using Cavern.QuickEQ.Crossover;

namespace Cavern.WPF.Consts {
/// <summary>
Expand All @@ -23,6 +25,11 @@ public static class Language {
/// </summary>
public static ResourceDictionary GetChannelSelectorStrings() => channelSelectorCache ??= GetFor("ChannelSelectorStrings");

/// <summary>
/// Get the translations related to crossover handling.
/// </summary>
public static ResourceDictionary GetCrossoverStrings() => crossoverCache ??= GetFor("CrossoverStrings");

/// <summary>
/// Return a channel's name in the user's language or fall back to its short name.
/// </summary>
Expand Down Expand Up @@ -55,6 +62,19 @@ public static string Translate(this ReferenceChannel channel) {
};
}

/// <summary>
/// Return a crossover type's name in the user's language or fall back to its enum name.
/// </summary>
public static string Translate(this CrossoverType type) {
ResourceDictionary dictionary = GetCrossoverStrings();
return type switch {
CrossoverType.Biquad => (string)dictionary["TyBiq"],
CrossoverType.Cavern => "Cavern",
CrossoverType.SyntheticBiquad => (string)dictionary["TySBi"],
_ => type.ToString()
};
}

/// <summary>
/// Get the translation of a resource file in the user's language, or in English if a translation couldn't be found.
/// </summary>
Expand Down Expand Up @@ -86,5 +106,10 @@ static ResourceDictionary GetFor(string resource) {
/// The loaded translation of the <see cref="ChannelSelector"/> for reuse.
/// </summary>
static ResourceDictionary channelSelectorCache;

/// <summary>
/// The loaded translation of crossover handling for reuse.
/// </summary>
static ResourceDictionary crossoverCache;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="TyBiq">Bikvadratikus</system:String>
<system:String x:Key="TySBi">Szintetikus bikvadratikus</system:String>
</ResourceDictionary>
6 changes: 6 additions & 0 deletions CavernSamples/Cavern.WPF/Resources/CrossoverStrings.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="TyBiq">Biquadratic</system:String>
<system:String x:Key="TySBi">Synthetic biquadratic</system:String>
</ResourceDictionary>
2 changes: 1 addition & 1 deletion CavernSamples/Cavern.WPF/Utils/ChannelOnUI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class ChannelOnUI(ReferenceChannel channel) {
/// <summary>
/// The displayed channel.
/// </summary>
public ReferenceChannel Channel = channel;
public ReferenceChannel Channel => channel;

/// <inheritdoc/>
public override string ToString() => Channel.Translate();
Expand Down
17 changes: 17 additions & 0 deletions CavernSamples/Cavern.WPF/Utils/CrossoverTypeOnUI.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Cavern.QuickEQ.Crossover;
using Cavern.WPF.Consts;

namespace Cavern.WPF.Utils {
/// <summary>
/// Used to display a crossover type's name on a UI in the user's language and contain which <see cref="CrossoverType"/> it is.
/// </summary>
public class CrossoverTypeOnUI(CrossoverType type) {
/// <summary>
/// The displayed channel.
/// </summary>
public CrossoverType Type => type;

/// <inheritdoc/>
public override string ToString() => Type.Translate();
}
}
Loading

0 comments on commit 4e43a5b

Please sign in to comment.