Skip to content

Commit 0797f09

Browse files
authored
Merge pull request #5 from LukasB97/codex/optimize-exported-eq
Optimize exported GraphicEQ points directly
2 parents 0635617 + 23648a9 commit 0797f09

7 files changed

Lines changed: 392 additions & 67 deletions

File tree

‎README.md‎

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ to improve the in-room FR of a loudspeaker.
88
This algorithm creates a config for a detailed graphical equalizer that can be
99
imported into software equalizers like EQApo or Wavelet.
1010

11-
This is especially useful when you want to do room correction for a bluetooth speaker
11+
This is especially useful when you want to do room correction for a Bluetooth speaker
1212
(Wavelet)
1313

1414
You can customize the target curve and certain parameters of equalizer you want to create.
@@ -26,7 +26,7 @@ If you want to use diagnostic plots, install the optional plotting dependency:
2626

2727
pip install -e ".[plot]"
2828

29-
The simplest way is to either supply a list of paths to req files or a directory with the files.
29+
The simplest way is to either supply a list of paths to REW files or a directory with the files.
3030

3131
from orca.RewToGraphEq import get_graph_eq_str
3232

@@ -45,8 +45,14 @@ in the 20-20000 Hz range.
4545

4646
Measurements are level-normalized against the shared 100-10000 Hz range by default. This avoids
4747
letting bass roll-off, room modes, or high-frequency directivity dominate the level reference.
48-
All supplied measurements must use the same logarithmic frequency grid. ORCA keeps that native
49-
measurement resolution throughout the calculation instead of resampling it to a fixed grid.
48+
All supplied measurements must use the same logarithmic frequency grid. ORCA evaluates the
49+
equalizer at every point of that native measurement grid while optimizing only the GraphicEQ
50+
control points that will actually be exported.
51+
52+
With `verbose=True` (or the CLI's `--verbose` flag), ORCA reports the level-aligned mean absolute
53+
deviation from the target and its 95th percentile for both the original and estimated equalized
54+
response. Level alignment excludes the overall playback-volume change introduced by anchoring the
55+
maximum EQ gain at 0 dB while retaining every frequency-dependent error.
5056

5157
More examples on the usage, are in the examples.py
5258

@@ -77,13 +83,13 @@ EQ-Config
7783
eq_res=128: The number of eq points.
7884
If supplied, eq_res log-spaced points will be computed between eq_from and eq_to.
7985
80-
eq_points: Can be supplied instead of eq_from, eq_to and eq_res. In this case,
81-
the eq points are supplied instead of being computed
86+
eq_points: Can be supplied instead of eq_from, eq_to and eq_res. They must be
87+
finite, positive, unique, and strictly increasing.
8288
set_max_zero=True: Determines, if the max boost value of the created eq
8389
gets anchored at 0 dB, in order not to introduce distortion. The complete
8490
curve is shifted, so the relative differences between EQ points are preserved.
8591
max_boost=10: the maximum dB boost that will be applied when set_max_zero=False.
86-
weighting_fun: function that applies weighting based on smooting factor and frequency
92+
weighting_fun: function that applies weighting based on smoothing factor and frequency
8793

8894
## Development
8995

@@ -94,25 +100,29 @@ Run the complete test suite from the repository root with:
94100

95101
## The Algorithm
96102

97-
To compute an equalizer, one boost level is evaluated for every frequency point in the
98-
original measurement grid.
103+
To compute an equalizer, ORCA optimizes the configured GraphicEQ control-point gains directly.
104+
The piecewise-linear curve produced by those points is evaluated at every frequency in the
105+
original measurement grid. If that grid is too sparse to constrain every control-point gain,
106+
ORCA supplements it with the configured control frequencies. This ensures that the optimization
107+
sees every degree of freedom in the same finite-resolution filter that will later be exported. If
108+
a direct API caller supplies a grid outside the normal 100-10000 Hz reference band, ORCA uses the
109+
complete available grid for level alignment.
99110

100111
For each of the frequencies, the process is as follows:
101112

102113
We take a strongly smoothed version of each measurement. Smoothing widths are defined in
103114
octaves, so they do not change when the measurement grid has a different point density,
104115
and compare the level of our target curve to the current spl.
105116

106-
We look for a dB adjustment at this frequency to minimize the
107-
error between target-spl and equalized-spl. This is a classical example of a
108-
convex optimization problem. You can find more on this in
109-
the Algorithm PDF.
117+
We look for a dB adjustment at every measured frequency to minimize the error between target SPL
118+
and equalized SPL. A least-squares projection then finds the GraphicEQ point updates whose
119+
interpolated curve best realizes those adjustments over the complete measurement grid.
110120

111121

112122
In multiple iterations, the boost gets adjusted, by
113123
repeating the above process with decreasingly smoothed measurements.
114124

115-
In each iteration, we take the difference, between the current level + dB adjustment
125+
In each iteration, we take the difference between the current level + dB adjustment
116126
and the target level.
117127

118128
This difference gets weighted and added to the current dB adjustment.

‎orca/BoostComputation.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import math
22
from typing import List
33

4-
from scipy.optimize import shgo
4+
from scipy.optimize import minimize_scalar
55

66
from .EQConfig import EQConfig
77
from .Measurement import Measurement
@@ -36,10 +36,14 @@ def minimize(target, spl):
3636
if math.isclose(lower, upper):
3737
return lower
3838

39-
result = shgo(fun_to_minimize, bounds=[(lower, upper)])
39+
result = minimize_scalar(
40+
fun_to_minimize,
41+
bounds=(lower, upper),
42+
method="bounded",
43+
)
4044
if not result.success:
4145
raise RuntimeError(f"Boost optimization failed: {result.message}")
42-
return result.x[0]
46+
return result.x
4347

4448

4549
def calc_boost(

‎orca/EQConfig.py‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import math
2+
from numbers import Real
13
from typing import Callable
24

35
from . import WeightingFuns
@@ -18,12 +20,28 @@ def __init__(self,
1820
if not eq_res:
1921
raise ValueError("Either eq_res or eq_points must be specified")
2022
eq_points = log_spaced_ints(eq_from, eq_to, eq_res)
21-
if not eq_points:
22-
raise ValueError("eq_points cannot be empty")
23+
try:
24+
eq_points = tuple(float(point) for point in eq_points)
25+
except (TypeError, ValueError) as exc:
26+
raise ValueError("eq_points must contain numeric frequencies") from exc
27+
if len(eq_points) < 2:
28+
raise ValueError("eq_points must contain at least two frequencies")
29+
if any(not math.isfinite(point) or point <= 0 for point in eq_points):
30+
raise ValueError("eq_points must contain finite frequencies greater than 0")
31+
if any(left >= right for left, right in zip(eq_points, eq_points[1:])):
32+
raise ValueError("eq_points must be strictly increasing without duplicates")
33+
if not isinstance(set_max_zero, bool):
34+
raise ValueError("set_max_zero must be a boolean")
35+
if isinstance(max_boost, bool) or not isinstance(max_boost, Real):
36+
raise ValueError("max_boost must be a finite number")
37+
if not math.isfinite(max_boost):
38+
raise ValueError("max_boost must be a finite number")
39+
if not callable(weighting_fun):
40+
raise ValueError("weighting_fun must be callable")
2341

2442
self.eq_points = eq_points
2543
self.set_max_zero = set_max_zero
26-
self.max_boost = max_boost
44+
self.max_boost = float(max_boost)
2745
self.weighting_fun = weighting_fun
2846

2947

0 commit comments

Comments
 (0)