Skip to content

Commit

Permalink
Equalizer refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
VoidXH committed Jan 9, 2024
1 parent a65d467 commit 56e87e1
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 107 deletions.
140 changes: 140 additions & 0 deletions Cavern.QuickEQ/Equalization/Equalizer.Analyzers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Cavern.QuickEQ.Equalization {
partial class Equalizer {
/// <summary>
/// Get the average level between the <paramref name="minFreq"/> and <paramref name="maxFreq"/>,
/// calculated in voltage scale, returned in dB scale.
/// </summary>
public double GetAverageLevel(double minFreq, double maxFreq) {
int i = 0, c = bands.Count;
while (i < c && bands[i].Frequency < minFreq) {
i++;
}
double sum = 0;
int n = 0;
while (i < c && bands[i].Frequency <= maxFreq) {
sum += Math.Pow(10, bands[i++].Gain * .05);
n++;
}
return 20 * Math.Log10(sum / n);
}

/// <summary>
/// Get the average level between the rolloff points (-<paramref name="range"/> dB points)
/// of the curve drawn by this <see cref="Equalizer"/>. <paramref name="stableRangeStart"/> and <paramref name="stableRangeEnd"/> are
/// the limits of a frequency band that can't be overly distorted on the curve and shall work for regression line calculation.
/// </summary>
public double GetAverageLevel(double range, double stableRangeStart, double stableRangeEnd) {
(double minFreq, double maxFreq) = GetRolloffs(range, stableRangeStart, stableRangeEnd);
return GetAverageLevel(minFreq, maxFreq);
}

/// <summary>
/// Detects valleys as band index ranges in the spectrum by comparing this <see cref="Equalizer"/> with an oversmoothed version.
/// </summary>
/// <param name="depth">Minimum valley in decibels for a range to be considered a result</param>
/// <param name="oversmoothing">Compare the current <see cref="Equalizer"/>
/// to a version which is smoothed by this many octaves</param>
public List<(int startInclusive, int stopExclusive)> GetValleys(double depth, double oversmoothing) {
Equalizer smoothed = (Equalizer)Clone();
smoothed.Smooth(oversmoothing);
List<(int, int)> result = new List<(int, int)>();
IReadOnlyList<Band> dryBands = bands,
wetBands = smoothed.bands;
bool inValley = false;
int valleyStarted = 0;
double maxInValley = 0;
for (int i = 0, c = dryBands.Count; i < c; i++) {
double valley = wetBands[i].Gain - dryBands[i].Gain;
bool willBeInValley = valley > 0;
if (maxInValley < valley) {
maxInValley = valley;
}
if (inValley != willBeInValley) {
if (willBeInValley) {
maxInValley = valley;
valleyStarted = i;
} else if (inValley && maxInValley >= depth) {
result.Add((valleyStarted, i));
}
inValley = willBeInValley;
}
}
return result;
}

/// <summary>
/// Get the median level between the <paramref name="minFreq"/> and <paramref name="maxFreq"/>.
/// </summary>
public double GetMedianLevel(double minFreq, double maxFreq) {
int i = 0, c = bands.Count;
while (i < c && bands[i].Frequency < minFreq) {
i++;
}
int startBand = i;
while (i < c && bands[i].Frequency <= maxFreq) {
i++;
}

List<double> sortedBands = bands.GetRange(startBand, i - startBand).Select(x => x.Gain).ToList();
sortedBands.Sort();
return sortedBands[sortedBands.Count >> 1];
}

/// <summary>
/// Get a line in the form of slope * log10(x) + intercept that fits the curve drawn by this <see cref="Equalizer"/>.
/// The regression line will only be calculated on the bands between <paramref name="startFreq"/> and <paramref name="endFreq"/>.
/// </summary>
public (double slope, double intercept) GetRegressionLine(double startFreq, double endFreq) {
double freqSum = 0,
freqSqSum = 0,
gainSum = 0,
mac = 0,
n = 0;
for (int i = GetFirstBandSafe(startFreq), c = bands.Count; i < c && bands[i].Frequency <= endFreq; i++) {
double freq = Math.Log10(bands[i].Frequency);
freqSum += freq;
freqSqSum += freq * freq;
gainSum += bands[i].Gain;
mac += freq * bands[i].Gain;
n++;
}
double slope = (n * mac - freqSum * gainSum) / (n * freqSqSum - freqSum * freqSum);
return (slope, (gainSum - slope * freqSum) / n);
}

/// <summary>
/// Get the rolloff points of the EQ by fitting a line on it and finding the first and last points
/// with a given range in decibels below it. <paramref name="stableRangeStart"/> and <paramref name="stableRangeEnd"/> are
/// the limits of a frequency band that can't be overly distorted on the curve and shall work for regression line calculation.
/// </summary>
public (double minFreq, double maxFreq) GetRolloffs(double range, double stableRangeStart, double stableRangeEnd) {
(double m, double b) = GetRegressionLine(stableRangeStart, stableRangeEnd);
int first = 0, last = bands.Count - 1;
while (first < last && bands[first].Gain + range < m * Math.Log10(bands[first].Frequency) + b) {
first++;
}
while (last > first && bands[last].Gain + range < m * Math.Log10(bands[last].Frequency) + b) {
last--;
}
return (bands[first].Frequency, bands[last].Frequency);
}

/// <summary>
/// Get the average level between the <paramref name="firstBand"/> (inclusive) and <paramref name="lastBand"/> (exclusive),
/// calculated in voltage scale, returned in dB scale.
/// </summary>
double GetAverageLevel(int firstBand, int lastBand) {
double sum = 0;
int n = 0;
while (firstBand < lastBand) {
sum += Math.Pow(10, bands[firstBand++].Gain * .05);
n++;
}
return 20 * Math.Log10(sum / n);
}
}
}
12 changes: 10 additions & 2 deletions Cavern.QuickEQ/Equalization/Equalizer.Transform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void AddSlope(double slope, double startFreq, double endFreq) {
range = Math.Log(endFreq) - logStart;
slope *= 1.44269504089f; // Slope /= log(2)
double slopeMax = slope * range;
for (int i = GetFirstBand(startFreq), c = bands.Count; i < c; i++) {
for (int i = GetFirstBandSafe(startFreq), c = bands.Count; i < c; i++) {
if (bands[i].Frequency > endFreq) {
bands[i] = new Band(bands[i].Frequency, bands[i].Gain + slopeMax);
} else {
Expand Down Expand Up @@ -183,7 +183,7 @@ public void MonotonousIncrease(double startFreq, double endFreq) {
/// Set the average gain of the curve to 0 dB between frequency limits.
/// </summary>
public void Normalize(double startFreq, double endFreq) {
int first = GetFirstBand(startFreq),
int first = GetFirstBandSafe(startFreq),
last = bands.Count;
double total = 0;
for (int i = first; i < last; i++) {
Expand Down Expand Up @@ -272,6 +272,14 @@ int GetFirstBand(double freq) {
return -1;
}

/// <summary>
/// Get which band index is the first after a given <paramref name="freq"/>uency, or 0 if such a band was not found.
/// </summary>
int GetFirstBandSafe(double freq) {
int result = GetFirstBand(freq);
return result != -1 ? result : 0;
}

/// <summary>
/// Get the band index range corresponding to the selected frequency limits.
/// </summary>
Expand Down
105 changes: 0 additions & 105 deletions Cavern.QuickEQ/Equalization/Equalizer.Visualization.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;

using Cavern.QuickEQ.Utilities;
Expand Down Expand Up @@ -103,95 +101,6 @@ public string ExportToEqualizerAPO() {
return result.ToString();
}

/// <summary>
/// Get the average level between the <paramref name="minFreq"/> and <paramref name="maxFreq"/>,
/// calculated in voltage scale, returned in dB scale.
/// </summary>
public double GetAverageLevel(double minFreq, double maxFreq) {
int i = 0, c = bands.Count;
while (i < c && bands[i].Frequency < minFreq) {
i++;
}
double sum = 0;
int n = 0;
while (i < c && bands[i].Frequency <= maxFreq) {
sum += Math.Pow(10, bands[i++].Gain * .05);
n++;
}
return 20 * Math.Log10(sum / n);
}

/// <summary>
/// Get the average level between the rolloff points (-<paramref name="range"/> dB points)
/// of the curve drawn by this <see cref="Equalizer"/>. <paramref name="stableRangeStart"/> and <paramref name="stableRangeEnd"/> are
/// the limits of a frequency band that can't be overly distorted on the curve and shall work for regression line calculation.
/// </summary>
public double GetAverageLevel(double range, double stableRangeStart, double stableRangeEnd) {
(double minFreq, double maxFreq) = GetRolloffs(range, stableRangeStart, stableRangeEnd);
return GetAverageLevel(minFreq, maxFreq);
}

/// <summary>
/// Get the median level between the <paramref name="minFreq"/> and <paramref name="maxFreq"/>.
/// </summary>
public double GetMedianLevel(double minFreq, double maxFreq) {
int i = 0, c = bands.Count;
while (i < c && bands[i].Frequency < minFreq) {
i++;
}
int startBand = i;
while (i < c && bands[i].Frequency <= maxFreq) {
i++;
}

List<double> sortedBands = bands.GetRange(startBand, i - startBand).Select(x => x.Gain).ToList();
sortedBands.Sort();
return sortedBands[sortedBands.Count >> 1];
}

/// <summary>
/// Get a line in the form of slope * log10(x) + intercept that fits the curve drawn by this <see cref="Equalizer"/>.
/// The regression line will only be calculated on the bands between <paramref name="startFreq"/> and <paramref name="endFreq"/>.
/// </summary>
public (double slope, double intercept) GetRegressionLine(double startFreq, double endFreq) {
double freqSum = 0,
freqSqSum = 0,
gainSum = 0,
mac = 0,
n = 0;
int i, c = bands.Count;
for (i = 0; i < c && bands[i].Frequency < startFreq; i++) {
// Skip the bands below the start freq
}
for (; i < c && bands[i].Frequency <= endFreq; i++) {
double freq = Math.Log10(bands[i].Frequency);
freqSum += freq;
freqSqSum += freq * freq;
gainSum += bands[i].Gain;
mac += freq * bands[i].Gain;
n++;
}
double slope = (n * mac - freqSum * gainSum) / (n * freqSqSum - freqSum * freqSum);
return (slope, (gainSum - slope * freqSum) / n);
}

/// <summary>
/// Get the rolloff points of the EQ by fitting a line on it and finding the first and last points
/// with a given range in decibels below it. <paramref name="stableRangeStart"/> and <paramref name="stableRangeEnd"/> are
/// the limits of a frequency band that can't be overly distorted on the curve and shall work for regression line calculation.
/// </summary>
public (double minFreq, double maxFreq) GetRolloffs(double range, double stableRangeStart, double stableRangeEnd) {
(double m, double b) = GetRegressionLine(stableRangeStart, stableRangeEnd);
int first = 0, last = bands.Count - 1;
while (first < last && bands[first].Gain + range < m * Math.Log10(bands[first].Frequency) + b) {
first++;
}
while (last > first && bands[last].Gain + range < m * Math.Log10(bands[last].Frequency) + b) {
last--;
}
return (bands[first].Frequency, bands[last].Frequency);
}

/// <summary>
/// Shows the EQ curve in a logarithmically scaled frequency axis.
/// </summary>
Expand Down Expand Up @@ -250,20 +159,6 @@ public float[] VisualizeLinear(double startFreq, double endFreq, int length) {
return result;
}

/// <summary>
/// Get the average level between the <paramref name="firstBand"/> (inclusive) and <paramref name="lastBand"/> (exclusive),
/// calculated in voltage scale, returned in dB scale.
/// </summary>
double GetAverageLevel(int firstBand, int lastBand) {
double sum = 0;
int n = 0;
while (firstBand < lastBand) {
sum += Math.Pow(10, bands[firstBand++].Gain * .05);
n++;
}
return 20 * Math.Log10(sum / n);
}

/// <summary>
/// End of a Dirac calibration file. We don't need these features for FIR.
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions Tests/Test.Cavern.QuickEQ/Equalization/Equalizer_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ public void GetAverageLevel() {
Assert.AreEqual(8.760460922297078, equalizer.GetAverageLevel(100, 500), Consts.delta);
}

/// <summary>
/// Tests if <see cref="Equalizer.GetValleys(double, double)"/> works as intended.
/// </summary>
[TestMethod, Timeout(1000)]
public void GetValleys() {
Equalizer equalizer = Create(20, 20000, 100, 10);
equalizer.DownsampleLogarithmically(1024, 20, 20000);
IReadOnlyList<(int startInclusive, int stopExclusive)> valleys = equalizer.GetValleys(10, 1);
Assert.AreEqual(15, valleys.Count);
Assert.AreEqual(492, valleys[0].startInclusive);
Assert.AreEqual(617, valleys[0].stopExclusive);
}

/// <summary>
/// Tests if <see cref="Equalizer.LimitPeaks(double)"/> works as intended.
/// </summary>
Expand Down

0 comments on commit 56e87e1

Please sign in to comment.