JPL Spatial
Sound spatialization and propagation library
Loading...
Searching...
No Matches
ShelfEQ.h
Go to the documentation of this file.
1//
2// ██╗██████╗ ██╗ ██╗██████╗ ███████╗
3// ██║██╔══██╗ ██║ ██║██╔══██╗██╔════╝ ** JPL Spatial **
4// ██║██████╔╝ ██║ ██║██████╔╝███████╗
5// ██ ██║██╔═══╝ ██║ ██║██╔══██╗╚════██║ https://github.com/Jaytheway/JPLSpatial
6// ╚█████╔╝██║ ███████╗██║██████╔╝███████║
7// ╚════╝ ╚═╝ ╚══════╝╚═╝╚═════╝ ╚══════╝
8//
9// Copyright Jaroslav Pevno 2026, JPL Spatial is offered under the terms of the ISC license:
10//
11// Permission to use, copy, modify, and/or distribute this software for any purpose with or
12// without fee is hereby granted, provided that the above copyright notice and this permission
13// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
14// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
16// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
17// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19
20#pragma once
21
25
26#include <array>
27#include <cmath>
28#include <complex>
29#include <vector>
30#include <span>
31
32//==============================================================================
35
36namespace JPL
37{
38 //==========================================================================
39 template<std::size_t N> requires(N > 1)
41 {
42 std::span<const float, N - 1> Splits; // Band split frequencies
43 std::span<const float, N> G; // Target gains at band centres
45 };
46
47 //==========================================================================
48 // Forward declaring
49 template<std::size_t NumEQBands> requires(NumEQBands > 1)
50 class ShelfEQ;
51
54
55 //==========================================================================
57 template<std::size_t NumEQBands> requires(NumEQBands > 1)
58 class ShelfEQ : public Topology::Cascade<NumEQBands - 1>
59 {
60 public:
61 static constexpr std::size_t NumBands = NumEQBands;
62 static constexpr std::size_t NumFilters = NumBands - 1;
63
64 using Base = Topology::Cascade<NumEQBands - 1>;
67
68 using GainVec = std::array<float, NumBands>;
69 using GainMat = std::array<float, NumBands * NumBands>;
70
71 public:
74 JPL_INLINE bool Prepare(const Params& params);
75
77 bool SetFrequencies(std::span<const float, NumFilters> frequencies);
78
80 void SetGainsDb(std::span<const float, NumBands> gainsDb);
81
82 private:
83 [[nodiscard]] static bool Inverse(const GainMat& A, GainMat& outInvA);
84
85#ifdef JPL_ENABLE_ASSERTS
86 void ValidateFrequencies(std::span<const float, NumFilters> frequencies) const;
87 void ValidateBandCentres(std::span<const float, NumBands> centres) const;
88#endif
89
90 private:
91 // Caching terms for easier updates of frequencies or gain
92 float mInvSampleRate = -1.0f; // 1 / samplerate
93 GainVec mG; // target gains at the band centres
94 GainMat mGinv; // inverse gain conversion matrix
95 std::array<Cache::BiquadShelf, NumFilters> mCache;
96 };
97} // namespace JPL
98
99//==============================================================================
100//
101// Code beyond this point is implementation detail...
102//
103//==============================================================================
104
105namespace JPL
106{
107#ifdef JPL_ENABLE_ASSERTS
108 template<std::size_t NumEQBands> requires(NumEQBands > 1)
109 inline void ShelfEQ<NumEQBands>::ValidateFrequencies(std::span<const float, NumFilters> frequencies) const
110 {
112 std::isfinite(mInvSampleRate) and
113 mInvSampleRate > 0.0f,
114 "Prepare must supply a valid sample rate.");
115
116 static constexpr double minBandHz = 20.0;
117 static constexpr double maxBandHz = 22050.0;
120
121 double previousHz = minBandHz;
122
123 for (float frequency : frequencies)
124 {
126 std::isfinite(frequency) and
127 static_cast<double>(frequency) > previousHz and
128 static_cast<double>(frequency) < maxBandHz,
129 "Splits must be finite, strictly increasing, "
130 "and inside the propagation frequency range.");
131
132 const double normalized =
133 static_cast<double>(frequency) * static_cast<double>(mInvSampleRate);
134
136 normalized > 0.0 and normalized < 0.5,
137 "Shelf frequencies must be below Nyquist.");
138
139 previousHz = frequency;
140 }
141 }
142
143 template<std::size_t NumEQBands> requires(NumEQBands > 1)
144 inline void ShelfEQ<NumEQBands>::ValidateBandCentres(std::span<const float, NumBands> centres) const
145 {
146 float previousCentre = 0.0f;
147 for (float centre : centres)
148 {
150 std::isfinite(centre) and
151 centre > previousCentre and
152 centre < 0.5f,
153 "Target centres must be finite, strictly increasing, "
154 "and below Nyquist.");
155
156 previousCentre = centre;
157 }
158 }
159#endif
160
161 //==========================================================================
162 template<std::size_t NumEQBands> requires(NumEQBands > 1)
163 JPL_INLINE bool ShelfEQ<NumEQBands>::Prepare(const Params& params)
164 {
166 std::isfinite(params.SampleRate) and
167 params.SampleRate > 0.0f,
168 "Sample rate must be finite and positive.");
169
170 std::ranges::copy(params.G, mG.begin());
171 mInvSampleRate = 1.0f / params.SampleRate;
172 return SetFrequencies(params.Splits);
173 }
174
175 template<std::size_t NumEQBands> requires(NumEQBands > 1)
176 inline bool ShelfEQ<NumEQBands>::SetFrequencies(std::span<const float, NumFilters> frequencies)
177 {
178#ifdef JPL_ENABLE_ASSERTS
179 ValidateFrequencies(frequencies);
180#endif
181
182 std::array<Cache::BiquadShelf, NumFilters> newCache;
183 std::array<Topology::Biquad, NumFilters> sh;
184
185 // Make prototype shelfs
186 for (uint32 i = 0; i < frequencies.size(); ++i)
187 {
188 newCache[i] = Cache::BiquadShelf::From(frequencies[i] * mInvSampleRate);
189 sh[i] = Topology::Biquad::MakeHighShelf(newCache[i], /* gain_dB */1.0f);
190 }
191
192 std::array<float, NumBands> centres = ComputeBandCenters(frequencies); // using default nyquist 22050 Hz for band centres
193 for (float& c : centres)
194 c *= mInvSampleRate;
195
196#ifdef JPL_ENABLE_ASSERTS
197 ValidateBandCentres(centres);
198#endif
199 // Compute gain conversion matrix
200 GainMat A;
201 for (uint32 row = 0; row < NumBands; ++row)
202 {
203 A[row * NumBands] = 1.0f;
204 for (uint32 col = 1; col < NumBands; ++col)
205 A[row * NumBands + col] = static_cast<float>(-GainTodB(std::abs(sh[col - 1].CalculateResponse(centres[row]))));
206 }
207
208 // Store inverse gain conversion matrix
209 const bool bInverted = Inverse(A, mGinv);
210
211 if (not JPL_ENSURE(bInverted, "Gain conversion matrix doesn't have inverse."))
212 return false;
213
214 // Cache for later, when gain only changes
215 mCache = newCache;
216
217 // We need to update gains when frequencies change
218 SetGainsDb(mG);
219
220 return true;
221 }
222
223 template<std::size_t NumEQBands> requires(NumEQBands > 1)
224 inline bool ShelfEQ<NumEQBands>::Inverse(const GainMat& A, GainMat& outInvA)
225 {
226 static constexpr uint32 N = NumBands;
227 static constexpr uint32 Width = N * 2;
228
229 std::array<double, N * Width> augmented{};
230
231 auto At = [&augmented](uint32 row, uint32 col) -> double&
232 {
233 return augmented[row * Width + col];
234 };
235
236 double scale = 0.0;
237
238 for (uint32 row = 0; row < N; ++row)
239 {
240 for (uint32 col = 0; col < N; ++col)
241 {
242 const double value = A[row * N + col];
243 if (not std::isfinite(value))
244 return false;
245
246 At(row, col) = value;
247 scale = std::max(scale, std::abs(value));
248 }
249
250 At(row, N + row) = 1.0;
251 }
252
253 if (scale == 0.0)
254 return false;
255
256 const double pivotTolerance =
257 64.0 * static_cast<double>(N) * std::numeric_limits<double>::epsilon() * scale;
258
259 for (uint32 col = 0; col < N; ++col)
260 {
261 uint32 pivotRow = col;
262
263 double pivotMagnitude = std::abs(At(col, col));
264
265 for (uint32 row = col + 1; row < N; ++row)
266 {
267 const double magnitude = std::abs(At(row, col));
268
269 if (magnitude > pivotMagnitude)
270 {
271 pivotMagnitude = magnitude;
272 pivotRow = row;
273 }
274 }
275
276 if (not std::isfinite(pivotMagnitude) or pivotMagnitude <= pivotTolerance)
277 return false;
278
279
280 if (pivotRow != col)
281 {
282 for (uint32 j = 0; j < Width; ++j)
283 std::swap(At(col, j), At(pivotRow, j));
284 }
285
286 const double inversePivot = 1.0 / At(col, col);
287
288 for (uint32 j = 0; j < Width; ++j)
289 At(col, j) *= inversePivot;
290
291 At(col, col) = 1.0;
292
293 for (uint32 row = 0; row < N; ++row)
294 {
295 if (row == col)
296 continue;
297
298 const double factor = At(row, col);
299
300 for (uint32 j = 0; j < Width; ++j)
301 At(row, j) -= factor * At(col, j);
302
303 At(row, col) = 0.0;
304 }
305 }
306
307 GainMat candidate{};
308
309 for (uint32 row = 0; row < N; ++row)
310 {
311 for (uint32 col = 0; col < N; ++col)
312 {
313 const double value = At(row, N + col);
314
315 if (not std::isfinite(value) or std::abs(value) > std::numeric_limits<float>::max())
316 return false;
317
318 candidate[row * N + col] = static_cast<float>(value);
319 }
320 }
321
322 outInvA = candidate;
323 return true;
324 }
325
326 template<std::size_t NumEQBands> requires(NumEQBands > 1)
327 inline void ShelfEQ<NumEQBands>::SetGainsDb(std::span<const float, NumBands> gainsDb)
328 {
329 JPL_ASSERT(mInvSampleRate > 0.0f);
330
331 std::ranges::copy(gainsDb, mG.begin());
332
333 GainVec Ks{};
334
335 for (uint32 col = 0, invIdx = 0; col < NumBands; ++col)
336 {
337 for (uint32 row = 0; row < NumBands; ++row)
338 Ks[col] += mGinv[invIdx++] * mG[row];
339 }
340
341 const float broadbandGain = dBToGain(Ks[0]);
342
343 for (uint32 i = 0; i < Base::Stages.size(); ++i)
344 Base::Stages[i] = Topology::Biquad::MakeHighShelf(mCache[i], Ks[i + 1]);
345
346 // Apply broadband gain
347 auto& firstStage = Base::Stages[0];
348 firstStage.b0 *= broadbandGain;
349 firstStage.b1 *= broadbandGain;
350 firstStage.b2 *= broadbandGain;
351
352 }
353} // namespace JPL
#define JPL_ASSERT(inExpression,...)
Main assert macro, usage: JPL_ASSERT(condition, message) or JPL_ASSERT(condition)
Definition ErrorReporting.h:80
#define JPL_ENSURE(inExpression,...)
Define ENSURE.
Definition ErrorReporting.h:94
Multi-Shelf 2nd-order graphic equalizer.
Definition ShelfEQ.h:59
std::array< float, NumBands > GainVec
Definition ShelfEQ.h:68
std::array< float, NumBands *NumBands > GainMat
Definition ShelfEQ.h:69
Definition AcousticMaterial.h:36
@ Inverse
Definition DistanceAttenuation.h:35
std::uint32_t uint32
Definition Core.h:311
JPL_INLINE T dBToGain(const T &dB) noexcept
Definition DecibelsAndGain.h:40
JPL_INLINE T GainTodB(const T &gainFactor) noexcept
Definition DecibelsAndGain.h:75
JPL_INLINE FreqBandCenters ComputeBandCenters(const SplitFrequencies &splitFrequenciesHz, float nyquist=22050.0f)
Definition FrequencyBands.h:56
static JPL_INLINE BiquadShelf From(double normalizedFrequency)
Definition FilterComponents.h:495
Definition ShelfEQ.h:41
std::span< const float, N > G
Definition ShelfEQ.h:43
float SampleRate
Definition ShelfEQ.h:44
std::span< const float, N - 1 > Splits
Definition ShelfEQ.h:42
Definition FilterComponents.h:196
static JPL_INLINE Biquad MakeHighShelf(double normalizedFrequency, double gainDb)
Definition FilterComponents.h:490
Sample b0
Definition FilterComponents.h:104