JPL Spatial
Sound spatialization and propagation library
Loading...
Searching...
No Matches
DualShelfFilter.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
24
25#include <array>
26#include <cmath>
27#include <concepts>
28#include <vector>
29#include <span>
30#include <type_traits>
31
32//==================================================================================
36
37namespace JPL
38{
39 namespace Impl
40 {
41 //==========================================================================
44 struct DualTopo
45 {
48 float BroadbandGain = 0.0f;
49
50 [[nodiscard]] JPL_INLINE float CalculateResponse(float normalizedFrequency) const
51 {
52 return BroadbandGain * A.CalculateResponse(normalizedFrequency) * B.CalculateResponse(normalizedFrequency);
53 }
54
55 [[nodiscard]] JPL_INLINE void SetCoefficients(const DualTopo& other)
56 {
57 *this = other;
58 }
59 };
60
63 struct DualState
64 {
67
68 [[nodiscard]] JPL_INLINE float Process(float x, const DualTopo& topo)
69 {
70 return topo.BroadbandGain * A.Process(B.Process(x, topo.B), topo.A);
71 }
72 };
73
74 //==========================================================================
76 template<class T>
77 concept CDSFTopo = std::same_as<T, DualTopo> or std::same_as<T, Topology::Biquad>;
78
79 //==========================================================================
80 template<CDSFTopo TopoType>
82 {
83 protected:
84 static constexpr bool bDualTopo = std::same_as<TopoType, DualTopo>;
85 using StateType = std::conditional_t<bDualTopo, DualState, State::DirectFormI>;
86
87 protected:
88 [[nodiscard]] JPL_INLINE TopoType BuildTopo(float broadbandGain) const;
89
90 protected:
93 };
94 } // namespace Impl
95
96 //==========================================================================
98 {
99 float Fl, Fm, Fh; // Low, Mid, High control point frequencies
100 float Gl, Gm, Gh; // Low, Mid, High control point gains, in dB
102 float PrototypeControlFrequency = 640.0f; // Decent default for 44.1/48 kHz sample rate (choose roughly midpoint)
103 };
104
105 template<Impl::CDSFTopo TopoType>
106 class DualShelfEQBase;
107
110
112 // This version uses dual One-Pole topologies/state instead of Biquad,
113 // which allows inspecting indiviDual-Shelfs, which can be
114 // useful for diagnostics and GUI.
116
117 //==========================================================================
118 template<Impl::CDSFTopo TopoType>
119 class DualShelfEQBase : public TopoType, private Impl::DSFCommonBase<TopoType>
120 {
121 public:
123 using StateType = typename Base::StateType;
124
125 public:
126 [[nodiscard]] JPL_INLINE std::span<const float> GetPrototypeLUT() const { return mProtoLUT; }
127
130 JPL_INLINE void Prepare(const DualShelfEQParams& params);
131
133 inline void SetFrequencies(float Fl, float Fm, float Fh);
134
136 inline void SetGainsDb(float Gl, float Gm, float Gh);
137
138 private:
141 static inline void BuildProtoLUT(float controlFrequency, float sampleRate, std::vector<float>& outLUT);
142
144 [[nodiscard]] static JPL_INLINE int GetLUTOffset(float frequency, float controlFrequency);
145
147 [[nodiscard]] inline float ReadPrototype(int index) const;
148
149 private:
150 std::vector<float> mProtoLUT;
151 int mControlFrequencyIdx = 0;
152
153 // Caching terms for easier updates of frequencies or gain
154 float mInvSampleRate = -1.0f; // 1 / samplerate
155 float mGl = 0.0f, mGm = 0.0f, mGh = 0.0f; // gains at the three frequency control points
156 std::array<float, 9> mGinv; // inverse gain conversion matrix
157 };
158
159 //==========================================================================
161 {
162 float Fl, Fh; // Low, High control point frequencies
163 float Gain; // Desired gain at nyquist
165
166 // Fraction of the total dB transition assigned to the lower-frequency shelf, in [0, 1].
167 // 0: transition at Fh; 1: transition at Fl; 0.5: equal split.
168 float Weight;
169 };
170
171 template<Impl::CDSFTopo TopoType>
173
175 // To interpolate parameter changes, copy initial object of this class
176 // to persistent Biquad storage and use object of this class
177 // as the "target".
179
181 // This version uses dual One-Pole topologies/state instead of Biquad,
182 // which allows inspecting individual shelfs, which can be
183 // useful for diagnostics and GUI.
185
186 //==========================================================================
187 template<Impl::CDSFTopo TopoType>
188 class DualShelfFilterBase : public TopoType, private Impl::DSFCommonBase<TopoType>
189 {
190 public:
192 using StateType = typename Base::StateType;
193
194 public:
197 JPL_INLINE void Prepare(DualShelfFilterParams params);
198
202 inline void PrepareLinear(const DualShelfFilterParams& params);
203
205 JPL_INLINE void SetFrequencies(float Fl, float Fh);
206
209 JPL_INLINE void SetGainLinear(float gain, float weight);
210
212 inline void SetGainLinear(float gain);
213
214 private:
215 // Caching terms for easier updates of frequencies or gain
216 float mInvSampleRate = 0.0f; // 1 / samplerate
217 float mWeight = 0.0f;
218 float mEarlyGain = 0.0f;
219 };
220
221} // namespace JPL
222
223//==============================================================================
224//
225// Code beyond this point is implementation detail...
226//
227//==============================================================================
228
229namespace JPL
230{
231 //==========================================================================
232 template<Impl::CDSFTopo TopoType>
233 JPL_INLINE TopoType Impl::DSFCommonBase<TopoType>::BuildTopo(float broadbandGain) const
234 {
235 if constexpr (bDualTopo)
236 {
237 return DualTopo{
238 .A = Topology::OnePole::MakeLowShelf(mLowShelfCache),
239 .B = Topology::OnePole::MakeHighShelf(mHighShelfCache),
240 .BroadbandGain = broadbandGain
241 };
242 }
243 else
244 {
246 Topology::OnePole::MakeLowShelf(mLowShelfCache),
247 Topology::OnePole::MakeHighShelf(mHighShelfCache),
248 broadbandGain
249 );
250 }
251 }
252
253 //==========================================================================
254 template<Impl::CDSFTopo TopoType>
256 {
257 JPL_ASSERT(params.SampleRate > 0.0f);
258 JPL_ASSERT(params.PrototypeControlFrequency > 20.0f and params.PrototypeControlFrequency < (params.SampleRate * 0.5f));
259 JPL_ASSERT(params.Fl < params.Fm and params.Fm < params.Fh and params.Fh < (params.SampleRate * 0.5f));
260
261 BuildProtoLUT(params.PrototypeControlFrequency, params.SampleRate, mProtoLUT);
262 mControlFrequencyIdx = GetLUTOffset(params.PrototypeControlFrequency, /* minFrequency */ 20.0f);
263
264 mGl = params.Gl;
265 mGm = params.Gm;
266 mGh = params.Gh;
267 mInvSampleRate = 1.0f / params.SampleRate;
268 SetFrequencies(params.Fl, params.Fm, params.Fh);
269 }
270
271 template<Impl::CDSFTopo TopoType>
272 inline void DualShelfEQBase<TopoType>::SetFrequencies(float Fl, float Fm, float Fh)
273 {
274 JPL_ASSERT(mInvSampleRate > 0.0f);
275 JPL_ASSERT(0.0f < Fl and Fl < Fm and Fm < Fh and Fh < (0.5f / mInvSampleRate));
276
277 // Cache for later, when gain only changes
278 Base::mLowShelfCache.t = ::tanf(JPL_PI * Fl * mInvSampleRate);
279 Base::mHighShelfCache.t = ::tanf(JPL_PI * Fh * mInvSampleRate);
280
281 const int center = mControlFrequencyIdx;
282 const float Glm = ReadPrototype(center + GetLUTOffset(Fm, Fl));
283 const float Glh = ReadPrototype(center + GetLUTOffset(Fh, Fl));
284 const float Ghl = ReadPrototype(center - GetLUTOffset(Fl, Fh));
285 const float Ghm = ReadPrototype(center - GetLUTOffset(Fm, Fh));
286
287 // Gain covnersion matrix
288#if 0
289 std::array<float, 9> G
290 {
291 1.0f, 1.0f, Ghl,
292 1.0f, Glm, Ghm,
293 1.0f, Glh, 1.0f
294 };
295#endif
296
297 std::array<float, 9> G1{
298 Glm - Ghm * Glh, Ghl * Glh - 1.0f, Ghm - Ghl * Glm,
299 Ghm - 1.0f, 1.0f - Ghl, Ghl - Ghm,
300 Glh - Glm, 1.0f - Glh, Glm - 1.0f
301 };
302 const float detG = (Glm + Ghm - Ghm * Glh + Ghl * Glh - Ghl * Glm - 1.0f);
303
304 JPL_ASSERT(std::isfinite(detG) and detG != 0.0f,
305 "Dual-shelf gain matrix is singular or non-finite. "
306 "Control frequencies must produce independent LUT constraints; "
307 "increase their spacing.");
308
309 const float invDetG = 1.0f / detG;
310 for (uint32 i = 0; i < G1.size(); ++i)
311 mGinv[i] = G1[i] * invDetG;
312
313 // We need to update gains when frequencies change
314 SetGainsDb(mGl, mGm, mGh);
315 }
316
317 template<Impl::CDSFTopo TopoType>
318 inline void DualShelfEQBase<TopoType>::SetGainsDb(float Gl, float Gm, float Gh)
319 {
320 JPL_ASSERT(mInvSampleRate > 0.0f);
321
322 mGl = Gl;
323 mGm = Gm;
324 mGh = Gh;
325
326 std::array<float, 3> Ks = {
327 mGinv[0] * mGl + mGinv[1] * mGm + mGinv[2] * mGh,
328 mGinv[3] * mGl + mGinv[4] * mGm + mGinv[5] * mGh,
329 mGinv[6] * mGl + mGinv[7] * mGm + mGinv[8] * mGh
330 };
331
332 // Kl and Kh are the dB gains of low and high shelving filters
333 // at their control frequencies;
334 // K is an additional broadband gain.
335 auto& [K, Kl, Kh] = Ks;
336
337 // Convert to gain
338 K = dBToGain(K);
339 Kl = dBToGain(2.0f * Kl);
340 Kh = dBToGain(2.0f * Kh);
341
342 Base::mLowShelfCache.sqrtK = ::sqrtf(Kl);
343 Base::mLowShelfCache.gain = Kl;
344
345 Base::mHighShelfCache.sqrtK = ::sqrtf(Kh);
346 Base::mHighShelfCache.gain = Kh;
347
348 static_cast<TopoType&>(*this) = Base::BuildTopo(K);
349 }
350
351 template<Impl::CDSFTopo TopoType>
352 inline void DualShelfEQBase<TopoType>::BuildProtoLUT(float controlFrequency, float sampleRate, std::vector<float>& outLUT)
353 {
354 static constexpr float cMinFrequency = 20.0f;
355 JPL_ASSERT(controlFrequency > cMinFrequency);
356
357 const float k = dBToGain(2.0f);
358 const auto lsf = Topology::OnePole::MakeLowShelf(controlFrequency, sampleRate, k);
359 FilterUtils::ComputeFilterResponse_dB(lsf, sampleRate, outLUT);
360 }
361
362 template<Impl::CDSFTopo TopoType>
363 JPL_INLINE int DualShelfEQBase<TopoType>::GetLUTOffset(float frequency, float controlFrequency)
364 {
365 return static_cast<int>(std::lround(12.0 * std::log2(frequency / controlFrequency)));
366 }
367
368 template<Impl::CDSFTopo TopoType>
369 inline float DualShelfEQBase<TopoType>::ReadPrototype(int index) const
370 {
371 JPL_ASSERT(not mProtoLUT.empty());
372
373 if (index < 0)
374 return 2.0f; // Prototype's low-frequency limit, in dB
375
376 if (static_cast<std::size_t>(index) >= mProtoLUT.size())
377 return 0.0f; // Prototype's high-frequency limit, in dB
378
379 return mProtoLUT[static_cast<std::size_t>(index)];
380 }
381
382 //==========================================================================
383 template<Impl::CDSFTopo TopoType>
385 {
386 params.Gain = dBToGain(params.Gain);
387 PrepareLinear(params);
388 }
389
390 template<Impl::CDSFTopo TopoType>
392 {
393 JPL_ASSERT(params.SampleRate > 0.0f);
394 JPL_ASSERT(params.Gain > 0.0f);
395 JPL_ASSERT(0.0f < params.Fl and params.Fl < params.Fh and params.Fh < (params.SampleRate * 0.5f));
396 JPL_ASSERT(params.Weight >= 0.0f and params.Weight <= 1.0f);
397
398 mInvSampleRate = 1.0f / params.SampleRate;
399 mWeight = params.Weight;
400
401 Base::mLowShelfCache.t = ::tanf(JPL_PI * params.Fl * mInvSampleRate);
402 Base::mHighShelfCache.t = ::tanf(JPL_PI * params.Fh * mInvSampleRate);
403
404 SetGainLinear(params.Gain);
405 }
406
407 template<Impl::CDSFTopo TopoType>
408 JPL_INLINE void DualShelfFilterBase<TopoType>::SetFrequencies(float Fl, float Fh)
409 {
410 JPL_ASSERT(mInvSampleRate > 0.0f);
411 JPL_ASSERT(0.0f < Fl and Fl < Fh and Fh < (0.5f / mInvSampleRate));
412
413 Base::mLowShelfCache.t = ::tanf(JPL_PI * Fl * mInvSampleRate);
414 Base::mHighShelfCache.t = ::tanf(JPL_PI * Fh * mInvSampleRate);
415
416 static_cast<TopoType&>(*this) = Base::BuildTopo(mEarlyGain);
417 }
418
419 template<Impl::CDSFTopo TopoType>
420 JPL_INLINE void DualShelfFilterBase<TopoType>::SetGainLinear(float gain, float weight)
421 {
422 JPL_ASSERT(mInvSampleRate > 0.0f);
423 JPL_ASSERT(gain > 0.0f);
424 JPL_ASSERT(weight >= 0.0f and weight <= 1.0f);
425
426 mWeight = weight;
427 SetGainLinear(gain);
428 }
429
430 template<Impl::CDSFTopo TopoType>
432 {
433 JPL_ASSERT(mInvSampleRate > 0.0f);
434 JPL_ASSERT(gain > 0.0f);
435
436 mEarlyGain = ::powf(gain, mWeight);
437
438 const float lowShelfGain = 1.0f / mEarlyGain;
439 const float highShelfGain = gain * lowShelfGain;
440
441 Base::mLowShelfCache.sqrtK = ::sqrtf(lowShelfGain);
442 Base::mLowShelfCache.gain = lowShelfGain;
443
444 Base::mHighShelfCache.sqrtK = ::sqrtf(highShelfGain);
445 Base::mHighShelfCache.gain = highShelfGain;
446
447 static_cast<TopoType&>(*this) = Base::BuildTopo(mEarlyGain);
448 }
449
450} // namespace JPL
#define JPL_ASSERT(inExpression,...)
Main assert macro, usage: JPL_ASSERT(condition, message) or JPL_ASSERT(condition)
Definition ErrorReporting.h:80
Definition DualShelfFilter.h:120
void SetFrequencies(float Fl, float Fm, float Fh)
Set the desired frequencies of the control points.
Definition DualShelfFilter.h:272
typename Base::StateType StateType
Definition DualShelfFilter.h:123
void SetGainsDb(float Gl, float Gm, float Gh)
Set the desired gains at the control points.
Definition DualShelfFilter.h:318
JPL_INLINE void Prepare(const DualShelfEQParams &params)
Definition DualShelfFilter.h:255
JPL_INLINE std::span< const float > GetPrototypeLUT() const
Definition DualShelfFilter.h:126
Definition DualShelfFilter.h:189
JPL_INLINE void SetGainLinear(float gain, float weight)
Definition DualShelfFilter.h:420
JPL_INLINE void SetFrequencies(float Fl, float Fh)
Set the desired frequencies of the control points.
Definition DualShelfFilter.h:408
JPL_INLINE void Prepare(DualShelfFilterParams params)
Definition DualShelfFilter.h:384
typename Base::StateType StateType
Definition DualShelfFilter.h:192
void PrepareLinear(const DualShelfFilterParams &params)
Definition DualShelfFilter.h:391
Definition DualShelfFilter.h:82
Cache::Shelf mLowShelfCache
Definition DualShelfFilter.h:91
Cache::Shelf mHighShelfCache
Definition DualShelfFilter.h:92
static constexpr bool bDualTopo
Definition DualShelfFilter.h:84
JPL_INLINE TopoType BuildTopo(float broadbandGain) const
Definition DualShelfFilter.h:233
std::conditional_t< bDualTopo, DualState, State::DirectFormI > StateType
Definition DualShelfFilter.h:85
DSF (EQ) can be either Biquad, or DualTopo (Dual One-Pole)
Definition DualShelfFilter.h:77
void ComputeFilterResponse_dB(const FilterType &filter, float sampleRate, std::vector< float > &outMagnitudes)
Definition FilterComponents.h:383
Definition AcousticMaterial.h:36
std::uint32_t uint32
Definition Core.h:311
JPL_INLINE T dBToGain(const T &dB) noexcept
Definition DecibelsAndGain.h:40
Definition FilterComponents.h:59
Definition DualShelfFilter.h:98
float Gm
Definition DualShelfFilter.h:100
float Fh
Definition DualShelfFilter.h:99
float SampleRate
Definition DualShelfFilter.h:101
float Fm
Definition DualShelfFilter.h:99
float Gl
Definition DualShelfFilter.h:100
float PrototypeControlFrequency
Definition DualShelfFilter.h:102
float Fl
Definition DualShelfFilter.h:99
float Gh
Definition DualShelfFilter.h:100
Definition DualShelfFilter.h:161
float Weight
Definition DualShelfFilter.h:168
float Gain
Definition DualShelfFilter.h:163
float Fl
Definition DualShelfFilter.h:162
float SampleRate
Definition DualShelfFilter.h:164
float Fh
Definition DualShelfFilter.h:162
Definition DualShelfFilter.h:64
JPL_INLINE float Process(float x, const DualTopo &topo)
Definition DualShelfFilter.h:68
State::DirectFormIOnePole A
Definition DualShelfFilter.h:65
State::DirectFormIOnePole B
Definition DualShelfFilter.h:66
Definition DualShelfFilter.h:45
JPL_INLINE void SetCoefficients(const DualTopo &other)
Definition DualShelfFilter.h:55
Topology::OnePole A
Definition DualShelfFilter.h:46
float BroadbandGain
Definition DualShelfFilter.h:48
JPL_INLINE float CalculateResponse(float normalizedFrequency) const
Definition DualShelfFilter.h:50
Topology::OnePole B
Definition DualShelfFilter.h:47
Definition FilterComponents.h:137
JPL_INLINE Sample Process(Sample x, const Topology::OnePole &onePole)
Definition FilterComponents.h:143
static Biquad Combine(const OnePole &low, const OnePole &high, float broadbandGain)
Definition FilterComponents.h:479
Definition FilterComponents.h:81
static JPL_INLINE OnePole MakeHighShelf(float frequency, float sampleRate, float gain)
Definition FilterComponents.h:428
Sample CalculateResponse(float normalizedFrequency) const
Definition FilterComponents.h:390
static JPL_INLINE OnePole MakeLowShelf(float frequency, float sampleRate, float gain)
Definition FilterComponents.h:399