Skip to content

Commit

Permalink
FIR crossover filter generation
Browse files Browse the repository at this point in the history
  • Loading branch information
VoidXH committed Jan 7, 2024
1 parent 23a5334 commit cf7fbb8
Show file tree
Hide file tree
Showing 21 changed files with 168 additions and 43 deletions.
4 changes: 1 addition & 3 deletions Cavern.QuickEQ/Crossover/BasicCrossover.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ public class BasicCrossover : Crossover {
/// <param name="subs">Channels to route bass to</param>
public BasicCrossover(float[] frequencies, bool[] subs) : base(frequencies, subs) { }

/// <summary>
/// Attach the crossover to an Equalizer APO configuration file in the making.
/// </summary>
/// <inheritdoc/>
public override void ExportToEqualizerAPO(List<string> wipConfig) {
(float frequency, string[] channelLabels)[] groups = GetCrossoverGroups();
string[] targets = GetSubLabels();
Expand Down
25 changes: 17 additions & 8 deletions Cavern.QuickEQ/Crossover/CavernCrossover.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,45 @@
using System.Collections.Generic;
using System.Globalization;

using Cavern.QuickEQ.Equalization;

namespace Cavern.QuickEQ.Crossover {
/// <summary>
/// A FIR brickwall crossover, first introduced in Cavern.
/// </summary>
internal class CavernCrossover : BasicCrossover {
public class CavernCrossover : BasicCrossover {
/// <summary>
/// Creates a FIR brickwall crossover, first introduced in Cavern.
/// </summary>
/// <param name="frequencies">Crossover frequencies for each channel, only values over 0 mean crossovered channels</param>
/// <param name="subs">Channels to route bass to</param>
public CavernCrossover(float[] frequencies, bool[] subs) : base(frequencies, subs) { }

/// <summary>
/// Add the filter's interpretation of highpass to the previously selected channel in a WIP configuration file.
/// </summary>
/// <inheritdoc/>
public override void AddHighpass(List<string> wipConfig, float frequency) {
float offsetFreq = frequency * .967741875f; // Removes crossover notch caused by FIR resolution
wipConfig.Add($"GraphicEQ: {(offsetFreq - 1).ToString(CultureInfo.InvariantCulture)} -48;" +
$" {offsetFreq.ToString(CultureInfo.InvariantCulture)} 0");
}

/// <summary>
/// Add the filter's interpretation of lowpass to the previously selected channel in a WIP configuration file.
/// </summary>
/// <remarks>Don't forget to call AddExtraOperations, this is generally the best place for it.</remarks>
/// <inheritdoc/>
public override float[] GetHighpass(int sampleRate, float frequency, int length) => new Equalizer(new List<Band> {
new Band(frequency * .967741875f, -48), // Removes crossover notch caused by FIR resolution
new Band(frequency, 0)
}, true).GetConvolution(sampleRate, length);

/// <inheritdoc/>
public override void AddLowpass(List<string> wipConfig, float frequency) {
float offsetFreq = frequency * 1.032258f; // Removes crossover notch caused by FIR resolution
wipConfig.Add($"GraphicEQ: {offsetFreq.ToString(CultureInfo.InvariantCulture)} 0;" +
$" {(offsetFreq + 1).ToString(CultureInfo.InvariantCulture)} -48");
AddExtraOperations(wipConfig);
}

/// <inheritdoc/>
public override float[] GetLowpass(int sampleRate, float frequency, int length) => new Equalizer(new List<Band> {
new Band(frequency, 0),
new Band(frequency * 1.032258f, -48) // Removes crossover notch caused by FIR resolution
}, true).GetConvolution(sampleRate, length);
}
}
36 changes: 33 additions & 3 deletions Cavern.QuickEQ/Crossover/Crossover.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;

using Cavern.Channels;
using Cavern.Filters;

namespace Cavern.QuickEQ.Crossover {
/// <summary>
Expand All @@ -24,7 +25,7 @@ public enum CrossoverType {
}

/// <summary>
/// A crossover to modify an Equalizer APO configuration file with.
/// A crossover to be exported as FIR filters or written into an Equalizer APO configuration file.
/// </summary>
public abstract class Crossover {
/// <summary>
Expand Down Expand Up @@ -67,13 +68,24 @@ public static Crossover Create(CrossoverType type, float[] frequencies, bool[] s
};
}

/// <summary>
/// Generate a 2nd order impulse response for a simple filter.
/// </summary>
static float[] Simulate(BiquadFilter filter, int length) {
float[] impulse = new float[length];
impulse[0] = 1;
filter.Process(impulse);
((BiquadFilter)filter.Clone()).Process(impulse);
return impulse;
}

/// <summary>
/// Attach the crossover to an Equalizer APO configuration file in the making.
/// </summary>
public abstract void ExportToEqualizerAPO(List<string> wipConfig);

/// <summary>
/// Add the filter's interpretation of highpass to the previously selected channel in a WIP configuration file.
/// Add the filter's interpretation of highpass to the previously selected channel in an Equalizer APO configuration file.
/// </summary>
public virtual void AddHighpass(List<string> wipConfig, float frequency) {
string hpf = $"Filter: ON HP Fc {frequency} Hz";
Expand All @@ -82,7 +94,16 @@ public virtual void AddHighpass(List<string> wipConfig, float frequency) {
}

/// <summary>
/// Add the filter's interpretation of lowpass to the previously selected channel in a WIP configuration file.
/// Get a FIR filter for the highpass part of the crossover.
/// </summary>
/// <param name="sampleRate">Filter sample rate</param>
/// <param name="frequency">Highpass cutoff point</param>
/// <param name="length">Filter length in samples</param>
public virtual float[] GetHighpass(int sampleRate, float frequency, int length) =>
Simulate(new Highpass(sampleRate, frequency), length);

/// <summary>
/// Add the filter's interpretation of lowpass to the previously selected channel in an Equalizer APO configuration file.
/// </summary>
/// <remarks>Don't forget to call <see cref="AddExtraOperations(List{string})"/>, this is generally the best place for it.</remarks>
public virtual void AddLowpass(List<string> wipConfig, float frequency) {
Expand All @@ -92,6 +113,15 @@ public virtual void AddLowpass(List<string> wipConfig, float frequency) {
AddExtraOperations(wipConfig);
}

/// <summary>
/// Get a FIR filter for the lowpass part of the crossover.
/// </summary>
/// <param name="sampleRate">Filter sample rate</param>
/// <param name="frequency">Lowpass cutoff point</param>
/// <param name="length">Filter length in samples</param>
public virtual float[] GetLowpass(int sampleRate, float frequency, int length) =>
Simulate(new Lowpass(sampleRate, frequency), length);

/// <summary>
/// Get the labels of channels to route bass to.
/// </summary>
Expand Down
38 changes: 27 additions & 11 deletions Cavern.QuickEQ/Crossover/SyntheticBiquadCrossover.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Generic;

using Cavern.Filters;
using Cavern.QuickEQ.Equalization;
using Cavern.QuickEQ.Utilities;

namespace Cavern.QuickEQ.Crossover {
Expand All @@ -16,25 +17,40 @@ public class SyntheticBiquadCrossover : BasicCrossover {
public SyntheticBiquadCrossover(float[] frequencies, bool[] subs) : base(frequencies, subs) { }

/// <summary>
/// Add the filter's interpretation of highpass to the previously selected channel in a WIP configuration file.
/// Get a <see cref="FilterAnalyzer"/> instance for a 2nd order <see cref="BiquadFilter"/>.
/// </summary>
public override void AddHighpass(List<string> wipConfig, float frequency) {
static FilterAnalyzer GetAnalyzer(BiquadFilter filter) {
ComplexFilter bw = new ComplexFilter();
bw.Filters.Add(new Highpass(48000, frequency));
bw.Filters.Add(new Highpass(48000, frequency));
wipConfig.Add(new FilterAnalyzer(bw, 48000).ToEqualizer(10, 480, 1 / 24.0).ExportToEqualizerAPO());
bw.Filters.Add(filter);
bw.Filters.Add((BiquadFilter)filter.Clone());
return new FilterAnalyzer(bw, filter.SampleRate);
}

/// <summary>
/// Add the filter's interpretation of lowpass to the previously selected channel in a WIP configuration file.
/// Get a FIR filter for a 2nd order biquad filter's response in minimum phase.
/// </summary>
/// <remarks>Don't forget to call AddExtraOperations, this is generally the best place for it.</remarks>
static float[] GetImpulse(BiquadFilter filter, int length) {
FilterAnalyzer analyzer = GetAnalyzer(filter);
analyzer.Resolution = length;
return analyzer.ToEqualizer(10, 20000, 1 / 12.0).GetConvolution(filter.SampleRate, length);
}

/// <inheritdoc/>
public override void AddHighpass(List<string> wipConfig, float frequency) =>
wipConfig.Add(GetAnalyzer(new Highpass(48000, frequency)).ToEqualizer(10, 480, 1 / 24.0).ExportToEqualizerAPO());

/// <inheritdoc/>
public override float[] GetHighpass(int sampleRate, float frequency, int length) =>
GetImpulse(new Highpass(sampleRate, frequency), length);

/// <inheritdoc/>
public override void AddLowpass(List<string> wipConfig, float frequency) {
ComplexFilter bw = new ComplexFilter();
bw.Filters.Add(new Lowpass(48000, frequency));
bw.Filters.Add(new Lowpass(48000, frequency));
wipConfig.Add(new FilterAnalyzer(bw, 48000).ToEqualizer(10, 480, 1 / 24.0).ExportToEqualizerAPO());
wipConfig.Add(GetAnalyzer(new Lowpass(48000, frequency)).ToEqualizer(10, 480, 1 / 24.0).ExportToEqualizerAPO());
AddExtraOperations(wipConfig);
}

/// <inheritdoc/>
public override float[] GetLowpass(int sampleRate, float frequency, int length) =>
GetImpulse(new Lowpass(sampleRate, frequency), length);
}
}
2 changes: 1 addition & 1 deletion Cavern.QuickEQ/Equalization/EQGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ public static float[] GetConvolution(this Equalizer eq, int sampleRate, int leng
}
}
eq.Apply(filter, sampleRate);
using (FFTCache cache = new FFTCache(length)) {
using (FFTCache cache = new ThreadSafeFFTCache(length)) {
Measurements.MinimumPhaseSpectrum(filter, cache);
filter.InPlaceIFFT(cache);
}
Expand Down
3 changes: 2 additions & 1 deletion Cavern.QuickEQ/Utilities/FilterAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,8 @@ public Equalizer ToEqualizer(double startFreq, double endFreq, double resolution
octaveRange = Math.Log(endFreq, 2) - Math.Log(startFreq, 2);
int windowSize = (int)(graph.Length / (octaveRange / resolution + 1));
for (int pos = graph.Length - 1; pos >= 0; pos -= windowSize) {
bands.Add(new Band(Math.Pow(10, startPow + powRange * pos), 20 * Math.Log10(graph[pos])));
double gain = graph[pos] != 0 ? 20 * Math.Log10(graph[pos]) : -150; // -150 dB is the lowest float value
bands.Add(new Band(Math.Pow(10, startPow + powRange * pos), gain));
}
bands.Reverse();
return new Equalizer(bands, true);
Expand Down
2 changes: 1 addition & 1 deletion Cavern/Filters/Allpass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public Allpass(int sampleRate, double centerFreq, double q, double gain) : base(
/// <summary>
/// Create a copy of this filter.
/// </summary>
public override object Clone() => new Allpass(sampleRate, centerFreq, q, gain);
public override object Clone() => new Allpass(SampleRate, centerFreq, q, gain);

/// <summary>
/// Create a copy of this filter with a changed sampleRate.
Expand Down
2 changes: 1 addition & 1 deletion Cavern/Filters/Bandpass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public Bandpass(int sampleRate, double centerFreq, double q, double gain) : base
/// <summary>
/// Create a copy of this filter.
/// </summary>
public override object Clone() => new Bandpass(sampleRate, centerFreq, q, gain);
public override object Clone() => new Bandpass(SampleRate, centerFreq, q, gain);

/// <summary>
/// Create a copy of this filter with a changed sampleRate.
Expand Down
14 changes: 7 additions & 7 deletions Cavern/Filters/BiquadFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ namespace Cavern.Filters {
/// Simple first-order biquad filter.
/// </summary>
public abstract class BiquadFilter : Filter, ICloneable {
/// <summary>
/// Sample rate of the filter.
/// </summary>
public int SampleRate { get; protected set; }

/// <summary>
/// Center frequency (-3 dB point) of the filter.
/// </summary>
Expand Down Expand Up @@ -74,11 +79,6 @@ public double Gain {
/// </summary>
protected double gain;

/// <summary>
/// Cached sample rate.
/// </summary>
protected int sampleRate;

/// <summary>
/// History sample.
/// </summary>
Expand Down Expand Up @@ -107,7 +107,7 @@ protected BiquadFilter(int sampleRate, double centerFreq, double q) : this(sampl
/// <param name="q">Q-factor of the filter</param>
/// <param name="gain">Gain of the filter in decibels</param>
protected BiquadFilter(int sampleRate, double centerFreq, double q, double gain) {
this.sampleRate = sampleRate;
SampleRate = sampleRate;
Reset(centerFreq, q, gain);
}

Expand Down Expand Up @@ -144,7 +144,7 @@ public void Reset(double centerFreq, double q, double gain) {
this.centerFreq = centerFreq;
this.q = q;
this.gain = gain;
float w0 = (float)(MathF.PI * 2 * centerFreq / sampleRate), cos = (float)Math.Cos(w0),
float w0 = (float)(MathF.PI * 2 * centerFreq / SampleRate), cos = (float)Math.Cos(w0),
alpha = (float)(Math.Sin(w0) / (q + q)), divisor = 1 / (1 + alpha);
Reset(cos, alpha, divisor);
}
Expand Down
1 change: 0 additions & 1 deletion Cavern/Filters/Crossover.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Runtime.CompilerServices;

namespace Cavern.Filters {
/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion Cavern/Filters/HighShelf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public HighShelf(int sampleRate, double centerFreq, double q, double gain) : bas
/// <summary>
/// Create a copy of this filter.
/// </summary>
public override object Clone() => new HighShelf(sampleRate, centerFreq, q, gain);
public override object Clone() => new HighShelf(SampleRate, centerFreq, q, gain);

/// <summary>
/// Create a copy of this filter with a changed sampleRate.
Expand Down
2 changes: 1 addition & 1 deletion Cavern/Filters/Highpass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public Highpass(int sampleRate, double centerFreq, double q, double gain) : base
/// <summary>
/// Create a copy of this filter.
/// </summary>
public override object Clone() => new Highpass(sampleRate, centerFreq, q, gain);
public override object Clone() => new Highpass(SampleRate, centerFreq, q, gain);

/// <summary>
/// Create a copy of this filter with a changed sampleRate.
Expand Down
2 changes: 1 addition & 1 deletion Cavern/Filters/LowShelf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public LowShelf(int sampleRate, double centerFreq, double q, double gain) : base
/// <summary>
/// Create a copy of this filter.
/// </summary>
public override object Clone() => new LowShelf(sampleRate, centerFreq, q, gain);
public override object Clone() => new LowShelf(SampleRate, centerFreq, q, gain);

/// <summary>
/// Create a copy of this filter with a changed sampleRate.
Expand Down
2 changes: 1 addition & 1 deletion Cavern/Filters/Lowpass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public Lowpass(int sampleRate, double centerFreq, double q, double gain) : base(
/// <summary>
/// Create a copy of this filter.
/// </summary>
public override object Clone() => new Lowpass(sampleRate, centerFreq, q, gain);
public override object Clone() => new Lowpass(SampleRate, centerFreq, q, gain);

/// <summary>
/// Create a copy of this filter with a changed sampleRate.
Expand Down
2 changes: 1 addition & 1 deletion Cavern/Filters/Notch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public Notch(int sampleRate, double centerFreq, double q, double gain) : base(sa
/// <summary>
/// Create a copy of this filter.
/// </summary>
public override object Clone() => new Notch(sampleRate, centerFreq, q, gain);
public override object Clone() => new Notch(SampleRate, centerFreq, q, gain);

/// <summary>
/// Create a copy of this filter with a changed sampleRate.
Expand Down
2 changes: 1 addition & 1 deletion Cavern/Filters/PeakingEQ.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public PeakingEQ(int sampleRate, double centerFreq, double q, double gain) : bas
/// <summary>
/// Create a copy of this filter.
/// </summary>
public override object Clone() => new PeakingEQ(sampleRate, centerFreq, q, gain);
public override object Clone() => new PeakingEQ(SampleRate, centerFreq, q, gain);

/// <summary>
/// Create a copy of this filter with a changed sampleRate.
Expand Down
15 changes: 15 additions & 0 deletions Tests/Test.Cavern.QuickEQ/Crossover/BasicCrossover_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Cavern.QuickEQ.Crossover;

namespace Test.Cavern.QuickEQ.Crossover {
/// <summary>
/// Tests the <see cref="BasicCrossover"/> class.
/// </summary>
[TestClass]
public class BasicCrossover_Tests {
/// <summary>
/// Tests if <see cref="BasicCrossover"/> generates correct impulse responses.
/// </summary>
[TestMethod, Timeout(1000)]
public void ImpulseResponse() => Utils.ImpulseResponse(new BasicCrossover(null, null), 0.49152157f, 0.50847834f);
}
}
15 changes: 15 additions & 0 deletions Tests/Test.Cavern.QuickEQ/Crossover/CavernCrossover_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Cavern.QuickEQ.Crossover;

namespace Test.Cavern.QuickEQ.Crossover {
/// <summary>
/// Tests the <see cref="CavernCrossover"/> class.
/// </summary>
[TestClass]
public class CavernCrossover_Tests {
/// <summary>
/// Tests if <see cref="CavernCrossover"/> generates correct impulse responses.
/// </summary>
[TestMethod, Timeout(1000)]
public void ImpulseResponse() => Utils.ImpulseResponse(new CavernCrossover(null, null), 0.4130373f, 0.979050338f);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Cavern.QuickEQ.Crossover;

namespace Test.Cavern.QuickEQ.Crossover {
/// <summary>
/// Tests the <see cref="SyntheticBiquadCrossover"/> class.
/// </summary>
[TestClass]
public class SyntheticBiquadCrossover_Tests {
/// <summary>
/// Tests if <see cref="SyntheticBiquadCrossover"/> generates correct impulse responses.
/// </summary>
[TestMethod, Timeout(1000)]
public void ImpulseResponse() => Utils.ImpulseResponse(new SyntheticBiquadCrossover(null, null), 0.47490564f, 0.5231629f);
}
}
Loading

0 comments on commit cf7fbb8

Please sign in to comment.