DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
WDF.h
Go to the documentation of this file.
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
64#include "DspMath.h"
65
66#include <algorithm>
67#include <array>
68#include <cassert>
69#include <cmath>
70#include <tuple>
71#include <utility>
72
73namespace dspark {
74namespace wdf {
75
76// ============================================================================
77// One-port leaf elements
78// ============================================================================
79
81template <FloatType T>
83{
84public:
85 explicit Resistor(T resistanceOhms) noexcept : value_(resistanceOhms) {}
86
87 void setResistance(T ohms) noexcept { value_ = std::max(ohms, T(1e-9)); }
88
89 void prepare(double) noexcept {}
90 void updatePorts() noexcept { R_ = static_cast<double>(value_); }
91 void reset() noexcept { a_ = 0; }
92
93 [[nodiscard]] double portResistance() const noexcept { return R_; }
94 [[nodiscard]] T reflected() noexcept { return T(0); }
95 void incident(T a) noexcept { a_ = a; }
96
98 [[nodiscard]] T getVoltage() const noexcept { return a_ * T(0.5); }
100 [[nodiscard]] T getCurrent() const noexcept
101 {
102 return static_cast<T>(static_cast<double>(a_) * 0.5 / R_);
103 }
104
105private:
106 T value_;
107 double R_ = 1.0;
108 T a_ = 0;
109};
110
112template <FloatType T>
114{
115public:
116 explicit Capacitor(T farads) noexcept : value_(farads) {}
117
118 void setCapacitance(T farads) noexcept { value_ = std::max(farads, T(1e-18)); }
119
120 void prepare(double sampleRate) noexcept { fs_ = sampleRate; }
121 void updatePorts() noexcept { R_ = 1.0 / (2.0 * fs_ * static_cast<double>(value_)); }
122 void reset() noexcept { state_ = 0; a_ = 0; b_ = 0; }
123
124 [[nodiscard]] double portResistance() const noexcept { return R_; }
125 [[nodiscard]] T reflected() noexcept { b_ = state_; return b_; }
126 void incident(T a) noexcept { a_ = a; state_ = a; }
127
128 [[nodiscard]] T getVoltage() const noexcept { return (a_ + b_) * T(0.5); }
129 [[nodiscard]] T getCurrent() const noexcept
130 {
131 return static_cast<T>(static_cast<double>(a_ - b_) * 0.5 / R_);
132 }
133
134private:
135 T value_;
136 double fs_ = 48000.0;
137 double R_ = 1.0;
138 T state_ = 0, a_ = 0, b_ = 0;
139};
140
142template <FloatType T>
144{
145public:
146 explicit Inductor(T henries) noexcept : value_(henries) {}
147
148 void setInductance(T henries) noexcept { value_ = std::max(henries, T(1e-12)); }
149
150 void prepare(double sampleRate) noexcept { fs_ = sampleRate; }
151 void updatePorts() noexcept { R_ = 2.0 * fs_ * static_cast<double>(value_); }
152 void reset() noexcept { state_ = 0; a_ = 0; b_ = 0; }
153
154 [[nodiscard]] double portResistance() const noexcept { return R_; }
155 [[nodiscard]] T reflected() noexcept { b_ = -state_; return b_; }
156 void incident(T a) noexcept { a_ = a; state_ = a; }
157
158 [[nodiscard]] T getVoltage() const noexcept { return (a_ + b_) * T(0.5); }
159 [[nodiscard]] T getCurrent() const noexcept
160 {
161 return static_cast<T>(static_cast<double>(a_ - b_) * 0.5 / R_);
162 }
163
164private:
165 T value_;
166 double fs_ = 48000.0;
167 double R_ = 1.0;
168 T state_ = 0, a_ = 0, b_ = 0;
169};
170
177template <FloatType T>
179{
180public:
181 explicit ResistiveVoltageSource(T seriesResistanceOhms) noexcept
182 : value_(seriesResistanceOhms) {}
183
184 void setResistance(T ohms) noexcept { value_ = std::max(ohms, T(1e-9)); }
185 void setVoltage(T volts) noexcept { vs_ = volts; }
186
187 void prepare(double) noexcept {}
188 void updatePorts() noexcept { R_ = static_cast<double>(value_); }
189 void reset() noexcept { a_ = 0; }
190
191 [[nodiscard]] double portResistance() const noexcept { return R_; }
192 [[nodiscard]] T reflected() noexcept { return vs_; }
193 void incident(T a) noexcept { a_ = a; }
194
196 [[nodiscard]] T getVoltage() const noexcept { return (a_ + vs_) * T(0.5); }
197
198private:
199 T value_;
200 double R_ = 1.0;
201 T vs_ = 0, a_ = 0;
202};
203
204// ============================================================================
205// Connectors
206// ============================================================================
207
222template <FloatType T, typename Child1, typename Child2>
224{
225public:
226 Series(Child1& c1, Child2& c2) noexcept : c1_(c1), c2_(c2) {}
227
228 void prepare(double sampleRate) noexcept
229 {
230 c1_.prepare(sampleRate);
231 c2_.prepare(sampleRate);
232 }
233 void updatePorts() noexcept
234 {
235 c1_.updatePorts();
236 c2_.updatePorts();
237 const double r1 = c1_.portResistance();
238 const double r2 = c2_.portResistance();
239 R_ = r1 + r2;
240 gamma1_ = static_cast<T>(r1 / R_);
241 }
242 void reset() noexcept { c1_.reset(); c2_.reset(); a1_ = 0; a2_ = 0; }
243
244 [[nodiscard]] double portResistance() const noexcept { return R_; }
245
246 [[nodiscard]] T reflected() noexcept
247 {
248 a1_ = c1_.reflected();
249 a2_ = c2_.reflected();
250 return a1_ + a2_; // -(a1+a2) with the up-port inversion folded in
251 }
252
253 void incident(T a3) noexcept
254 {
255 // b_k = a_k - gamma_k * (a1 + a2 + a3'), a3' = -a3 (folded inversion),
256 // with gamma1 + gamma2 = 1 on the adapted port.
257 const T sum = a1_ + a2_ - a3;
258 c1_.incident(a1_ - gamma1_ * sum);
259 c2_.incident(a2_ - (sum - gamma1_ * sum)); // a2 - gamma2 * sum
260 }
261
262private:
263 Child1& c1_;
264 Child2& c2_;
265 double R_ = 2.0;
266 T gamma1_ = T(0.5);
267 T a1_ = 0, a2_ = 0;
268};
269
276template <FloatType T, typename Child1, typename Child2>
278{
279public:
280 Parallel(Child1& c1, Child2& c2) noexcept : c1_(c1), c2_(c2) {}
281
282 void prepare(double sampleRate) noexcept
283 {
284 c1_.prepare(sampleRate);
285 c2_.prepare(sampleRate);
286 }
287 void updatePorts() noexcept
288 {
289 c1_.updatePorts();
290 c2_.updatePorts();
291 const double g1 = 1.0 / c1_.portResistance();
292 const double g2 = 1.0 / c2_.portResistance();
293 R_ = 1.0 / (g1 + g2);
294 d1_ = static_cast<T>(g1 / (g1 + g2));
295 }
296 void reset() noexcept { c1_.reset(); c2_.reset(); a1_ = 0; a2_ = 0; bUp_ = 0; }
297
298 [[nodiscard]] double portResistance() const noexcept { return R_; }
299
300 [[nodiscard]] T reflected() noexcept
301 {
302 a1_ = c1_.reflected();
303 a2_ = c2_.reflected();
304 bUp_ = d1_ * a1_ + (T(1) - d1_) * a2_; // weighted node wave
305 return bUp_;
306 }
307
308 void incident(T a3) noexcept
309 {
310 // Common node wave: bNode = bUp + a3; then b_k = bNode - a_k.
311 const T bNode = bUp_ + a3;
312 c1_.incident(bNode - a1_);
313 c2_.incident(bNode - a2_);
314 }
315
316private:
317 Child1& c1_;
318 Child2& c2_;
319 double R_ = 0.5;
320 T d1_ = T(0.5);
321 T a1_ = 0, a2_ = 0, bUp_ = 0;
322};
323
325template <FloatType T, typename Child>
327{
328public:
329 explicit Inverter(Child& c) noexcept : c_(c) {}
330
331 void prepare(double sampleRate) noexcept { c_.prepare(sampleRate); }
332 void updatePorts() noexcept { c_.updatePorts(); }
333 void reset() noexcept { c_.reset(); }
334
335 [[nodiscard]] double portResistance() const noexcept { return c_.portResistance(); }
336 [[nodiscard]] T reflected() noexcept { return -c_.reflected(); }
337 void incident(T a) noexcept { c_.incident(-a); }
338
339private:
340 Child& c_;
341};
342
343// ============================================================================
344// Roots
345// ============================================================================
346
353template <FloatType T, typename Tree>
355{
356public:
357 explicit IdealVoltageSourceRoot(Tree& tree) noexcept : tree_(tree) {}
358
359 void setVoltage(T volts) noexcept { vs_ = volts; }
360
361 void prepare(double sampleRate) noexcept
362 {
363 tree_.prepare(sampleRate);
364 tree_.updatePorts();
365 tree_.reset();
366 }
367 void reset() noexcept { tree_.reset(); }
368
369 void process() noexcept
370 {
371 const T a = tree_.reflected();
372 tree_.incident(T(2) * vs_ - a);
373 }
374
375private:
376 Tree& tree_;
377 T vs_ = 0;
378};
379
380namespace detail {
381
393template <typename F>
394[[nodiscard]] inline double solveMonotonic(double a, double seed, F&& evaluate) noexcept
395{
396 double lo = std::min(0.0, a);
397 double hi = std::max(0.0, a);
398 double v = std::clamp(seed, lo, hi);
399 double prevAbsF = 1e300;
400
401 for (int it = 0; it < 48; ++it)
402 {
403 double f = 0.0, fp = 1.0;
404 evaluate(v, f, fp);
405
406 const double absF = std::abs(f);
407 if (absF < 1e-10)
408 break;
409 if (f > 0.0) hi = v;
410 else lo = v;
411
412 // Accept the Newton step only while it makes real progress. Deep in
413 // the exponential region Newton crawls at ~nVt per step (the classic
414 // diode pathology), so a stalled |f| switches to bisection, which
415 // halves the bracket unconditionally. Quadratic near the root,
416 // bisection-guaranteed everywhere.
417 const double vn = v - f / fp;
418 if (vn > lo && vn < hi && absF < 0.7 * prevAbsF)
419 v = vn;
420 else
421 v = 0.5 * (lo + hi);
422 prevAbsF = absF;
423 }
424 return v;
425}
426
427} // namespace detail
428
435template <FloatType T, typename Tree>
437{
438public:
439 explicit DiodePairRoot(Tree& tree, T saturationCurrent = T(2.52e-9),
440 T idealityTimesVt = T(1.752 * 0.02585)) noexcept
441 : tree_(tree), is_(saturationCurrent), nvt_(idealityTimesVt) {}
442
443 void setSaturationCurrent(T amps) noexcept { is_ = std::max(amps, T(1e-15)); }
444 void setIdealityTimesVt(T volts) noexcept { nvt_ = std::max(volts, T(1e-4)); }
445
446 void prepare(double sampleRate) noexcept
447 {
448 tree_.prepare(sampleRate);
449 tree_.updatePorts();
450 tree_.reset();
451 v_ = 0.0;
452 }
453 void reset() noexcept { tree_.reset(); v_ = 0.0; }
454
455 void process() noexcept
456 {
457 const double a = static_cast<double>(tree_.reflected());
458 const double k = 2.0 * tree_.portResistance() * static_cast<double>(is_);
459 const double nvt = static_cast<double>(nvt_);
460
461 // Far-field analytic seed: neglecting the linear term in
462 // v + k sinh(v/nVt) = a gives v0 = nVt asinh(a/k), within ~nVt of the
463 // root at any drive level (deep in the exponential, plain Newton
464 // would crawl at ~nVt per iteration). Newton then converges in 1-3.
465 const double seed = nvt * std::asinh(a / k);
466
467 v_ = detail::solveMonotonic(a, seed, [k, nvt, a](double v, double& f, double& fp)
468 {
469 // Guard the argument: cosh overflows around |x| > 710 in double,
470 // far outside any solution the bracket allows anyway.
471 const double x = std::clamp(v / nvt, -700.0, 700.0);
472 f = v + k * std::sinh(x) - a;
473 fp = 1.0 + (k / nvt) * std::cosh(x);
474 });
475
476 tree_.incident(static_cast<T>(2.0 * v_ - a));
477 }
478
480 [[nodiscard]] T getVoltage() const noexcept { return static_cast<T>(v_); }
481
482private:
483 Tree& tree_;
484 T is_, nvt_;
485 double v_ = 0.0;
486};
487
493template <FloatType T, typename Tree>
495{
496public:
497 explicit DiodeRoot(Tree& tree, T saturationCurrent = T(2.52e-9),
498 T idealityTimesVt = T(1.752 * 0.02585)) noexcept
499 : tree_(tree), is_(saturationCurrent), nvt_(idealityTimesVt) {}
500
501 void setSaturationCurrent(T amps) noexcept { is_ = std::max(amps, T(1e-15)); }
502 void setIdealityTimesVt(T volts) noexcept { nvt_ = std::max(volts, T(1e-4)); }
503
504 void prepare(double sampleRate) noexcept
505 {
506 tree_.prepare(sampleRate);
507 tree_.updatePorts();
508 tree_.reset();
509 v_ = 0.0;
510 }
511 void reset() noexcept { tree_.reset(); v_ = 0.0; }
512
513 void process() noexcept
514 {
515 const double a = static_cast<double>(tree_.reflected());
516 const double k = tree_.portResistance() * static_cast<double>(is_);
517 const double nvt = static_cast<double>(nvt_);
518
519 // Far-field analytic seed: forward drive lands near
520 // nVt ln(1 + a/k); reverse drive blocks, so the root sits near a.
521 const double seed = (a > 0.0) ? nvt * std::log1p(a / k) : a;
522
523 v_ = detail::solveMonotonic(a, seed, [k, nvt, a](double v, double& f, double& fp)
524 {
525 const double x = std::clamp(v / nvt, -700.0, 700.0);
526 f = v + k * std::expm1(x) - a;
527 fp = 1.0 + (k / nvt) * std::exp(x);
528 });
529
530 tree_.incident(static_cast<T>(2.0 * v_ - a));
531 }
532
534 [[nodiscard]] T getVoltage() const noexcept { return static_cast<T>(v_); }
535
536private:
537 Tree& tree_;
538 T is_, nvt_;
539 double v_ = 0.0;
540};
541
542// ============================================================================
543// R-type adaptor (arbitrary topologies)
544// ============================================================================
545
561template <FloatType T, typename... Children>
562class RType
563{
564public:
565 static constexpr int kNumPorts = static_cast<int>(sizeof...(Children)) + 1;
566 static constexpr int kMaxNodes = 12;
567 static_assert(sizeof...(Children) >= 1, "RType needs at least one child");
568
574 RType(const std::array<std::pair<int, int>, static_cast<size_t>(kNumPorts)>& portNodes,
575 int numNodes, Children&... children) noexcept
576 : children_(children...), portNodes_(portNodes),
577 numNodes_(std::clamp(numNodes, 1, kMaxNodes))
578 {
579 assert(numNodes >= 1 && numNodes <= kMaxNodes);
580 // Release-safe node sanitising: an out-of-range node index would
581 // stamp the conductance/RHS arrays out of bounds. Clamp into
582 // [-1, numNodes_-1] (-1 = ground); a clamped topology is wrong but
583 // cannot corrupt memory.
584 for (auto& [p, m] : portNodes_)
585 {
586 assert(p >= -1 && p < numNodes_ && m >= -1 && m < numNodes_);
587 p = std::clamp(p, -1, numNodes_ - 1);
588 m = std::clamp(m, -1, numNodes_ - 1);
589 }
590 }
591
592 void prepare(double sampleRate) noexcept
593 {
594 std::apply([&](auto&... ch) { (ch.prepare(sampleRate), ...); }, children_);
595 }
596
597 void updatePorts() noexcept
598 {
599 std::apply([&](auto&... ch) { (ch.updatePorts(), ...); }, children_);
600
601 // Gather child port resistances (port 0 resolved by adaptation).
602 {
603 int i = 1;
604 std::apply([&](auto&... ch)
605 { ((portR_[static_cast<size_t>(i++)] = ch.portResistance()), ...); },
606 children_);
607 }
608
609 // 1) Thévenin resistance looking into the network from port 0:
610 // assemble MNA without port 0, inject 1 A across its nodes.
611 assembleConductance(false);
612 factor();
613 double rhs[kMaxNodes] = {};
614 stampCurrent(rhs, 0, 1.0);
615 solve(rhs);
616 double rth = portVoltage(rhs, 0);
617 portR_[0] = std::clamp(rth, 1e-6, 1e12);
618
619 // 2) Full MNA with every port loaded; S = 2M - I where column j of M
620 // is the port-voltage response to a_j = 1 (Norton: 1/R_j).
621 assembleConductance(true);
622 factor();
623 for (int j = 0; j < kNumPorts; ++j)
624 {
625 double v[kMaxNodes] = {};
626 stampCurrent(v, j, 1.0 / portR_[static_cast<size_t>(j)]);
627 solve(v);
628 for (int i = 0; i < kNumPorts; ++i)
629 {
630 const double m = portVoltage(v, i);
631 s_[static_cast<size_t>(i)][static_cast<size_t>(j)] =
632 2.0 * m - (i == j ? 1.0 : 0.0);
633 }
634 }
635 // Adaptation leaves no instantaneous reflection at the up port.
636 s_[0][0] = 0.0;
637 }
638
639 void reset() noexcept
640 {
641 std::apply([&](auto&... ch) { (ch.reset(), ...); }, children_);
642 a_.fill(0.0);
643 }
644
645 [[nodiscard]] double portResistance() const noexcept { return portR_[0]; }
646
647 [[nodiscard]] T reflected() noexcept
648 {
649 int i = 1;
650 std::apply([&](auto&... ch)
651 { ((a_[static_cast<size_t>(i++)] = static_cast<double>(ch.reflected())), ...); },
652 children_);
653 double b0 = 0.0;
654 for (int j = 1; j < kNumPorts; ++j)
655 b0 += s_[0][static_cast<size_t>(j)] * a_[static_cast<size_t>(j)];
656 return static_cast<T>(b0);
657 }
658
659 void incident(T aUp) noexcept
660 {
661 a_[0] = static_cast<double>(aUp);
662 int i = 1;
663 std::apply([&](auto&... ch)
664 {
665 ((ch.incident(static_cast<T>(rowDot(i))), ++i), ...);
666 },
667 children_);
668 }
669
670private:
671 [[nodiscard]] double rowDot(int row) const noexcept
672 {
673 double acc = 0.0;
674 for (int j = 0; j < kNumPorts; ++j)
675 acc += s_[static_cast<size_t>(row)][static_cast<size_t>(j)] * a_[static_cast<size_t>(j)];
676 return acc;
677 }
678
679 void assembleConductance(bool includePort0) noexcept
680 {
681 for (auto& row : g_) row.fill(0.0);
682 for (int j = includePort0 ? 0 : 1; j < kNumPorts; ++j)
683 {
684 const double g = 1.0 / portR_[static_cast<size_t>(j)];
685 const int p = portNodes_[static_cast<size_t>(j)].first;
686 const int m = portNodes_[static_cast<size_t>(j)].second;
687 if (p >= 0) g_[static_cast<size_t>(p)][static_cast<size_t>(p)] += g;
688 if (m >= 0) g_[static_cast<size_t>(m)][static_cast<size_t>(m)] += g;
689 if (p >= 0 && m >= 0)
690 {
691 g_[static_cast<size_t>(p)][static_cast<size_t>(m)] -= g;
692 g_[static_cast<size_t>(m)][static_cast<size_t>(p)] -= g;
693 }
694 }
695 }
696
697 void stampCurrent(double* rhs, int port, double amps) const noexcept
698 {
699 const int p = portNodes_[static_cast<size_t>(port)].first;
700 const int m = portNodes_[static_cast<size_t>(port)].second;
701 if (p >= 0) rhs[p] += amps;
702 if (m >= 0) rhs[m] -= amps;
703 }
704
705 [[nodiscard]] double portVoltage(const double* v, int port) const noexcept
706 {
707 const int p = portNodes_[static_cast<size_t>(port)].first;
708 const int m = portNodes_[static_cast<size_t>(port)].second;
709 return (p >= 0 ? v[p] : 0.0) - (m >= 0 ? v[m] : 0.0);
710 }
711
713 void factor() noexcept
714 {
715 const int n = numNodes_;
716 for (int i = 0; i < n; ++i)
717 {
718 for (int j = 0; j < n; ++j)
719 lu_[static_cast<size_t>(i)][static_cast<size_t>(j)] =
720 g_[static_cast<size_t>(i)][static_cast<size_t>(j)];
721 piv_[static_cast<size_t>(i)] = i;
722 }
723 for (int k = 0; k < n; ++k)
724 {
725 int p = k;
726 for (int i = k + 1; i < n; ++i)
727 if (std::abs(lu_[static_cast<size_t>(i)][static_cast<size_t>(k)])
728 > std::abs(lu_[static_cast<size_t>(p)][static_cast<size_t>(k)]))
729 p = i;
730 if (p != k)
731 {
732 std::swap(lu_[static_cast<size_t>(p)], lu_[static_cast<size_t>(k)]);
733 std::swap(piv_[static_cast<size_t>(p)], piv_[static_cast<size_t>(k)]);
734 }
735 double d = lu_[static_cast<size_t>(k)][static_cast<size_t>(k)];
736 if (std::abs(d) < 1e-300)
737 d = (d >= 0.0 ? 1e-300 : -1e-300);
738 const double invD = 1.0 / d;
739 for (int i = k + 1; i < n; ++i)
740 {
741 const double f = lu_[static_cast<size_t>(i)][static_cast<size_t>(k)] * invD;
742 lu_[static_cast<size_t>(i)][static_cast<size_t>(k)] = f;
743 for (int j = k + 1; j < n; ++j)
744 lu_[static_cast<size_t>(i)][static_cast<size_t>(j)]
745 -= f * lu_[static_cast<size_t>(k)][static_cast<size_t>(j)];
746 }
747 }
748 }
749
751 void solve(double* rhs) const noexcept
752 {
753 const int n = numNodes_;
754 double y[kMaxNodes];
755 for (int i = 0; i < n; ++i)
756 y[i] = rhs[piv_[static_cast<size_t>(i)]];
757 for (int i = 0; i < n; ++i)
758 for (int j = 0; j < i; ++j)
759 y[i] -= lu_[static_cast<size_t>(i)][static_cast<size_t>(j)] * y[j];
760 for (int i = n - 1; i >= 0; --i)
761 {
762 for (int j = i + 1; j < n; ++j)
763 y[i] -= lu_[static_cast<size_t>(i)][static_cast<size_t>(j)] * y[j];
764 double d = lu_[static_cast<size_t>(i)][static_cast<size_t>(i)];
765 if (std::abs(d) < 1e-300)
766 d = (d >= 0.0 ? 1e-300 : -1e-300);
767 y[i] /= d;
768 }
769 for (int i = 0; i < n; ++i)
770 rhs[i] = y[i];
771 }
772
773 std::tuple<Children&...> children_;
774 std::array<std::pair<int, int>, static_cast<size_t>(kNumPorts)> portNodes_;
775 int numNodes_;
776
777 std::array<double, static_cast<size_t>(kNumPorts)> portR_ {};
778 std::array<double, static_cast<size_t>(kNumPorts)> a_ {};
779 std::array<std::array<double, static_cast<size_t>(kNumPorts)>,
780 static_cast<size_t>(kNumPorts)> s_ {};
781
782 std::array<std::array<double, kMaxNodes>, kMaxNodes> g_ {};
783 std::array<std::array<double, kMaxNodes>, kMaxNodes> lu_ {};
784 std::array<int, kMaxNodes> piv_ {};
785};
786
787// ============================================================================
788// Fender '59 Bassman FMV tone stack (reference R-type circuit)
789// ============================================================================
790
806template <FloatType T>
808{
809public:
814 explicit ToneStackFMV(double sourceResistance = 1e3, double loadResistance = 1e6)
815 : rOut_(static_cast<T>(sourceResistance)), c1_(T(0.25e-9)),
816 r1Top_(T(125e3)), r1Bot_(T(125e3)), r4_(T(56e3)), c2_(T(20e-9)),
817 r2_(T(500e3)), c3_(T(20e-9)), r3Top_(T(12.5e3)), r3Bot_(T(12.5e3)),
818 rLoad_(static_cast<T>(loadResistance)),
819 rtype_({ { { kSrc, -1 }, // port 0: ideal source root
820 { kSrc, kVi }, // rOut
821 { kVi, kA }, // C1
822 { kA, kVo }, // (1-t) R1
823 { kVo, kB }, // t R1
824 { kVi, kS }, // R4
825 { kS, kB }, // C2
826 { kB, kC }, // l R2 (rheostat)
827 { kS, kW }, // C3 to the middle wiper
828 { kC, kW }, // (1-m) R3
829 { kW, -1 }, // m R3 to ground
830 { kVo, -1 } } }, // grid load
831 kNumNodes,
832 rOut_, c1_, r1Top_, r1Bot_, r4_, c2_, r2_, c3_, r3Top_, r3Bot_, rLoad_),
833 root_(rtype_)
834 {
835 }
836
838 void prepare(double sampleRate) noexcept
839 {
840 root_.prepare(sampleRate);
841 setControls(treble_, bass_, middle_);
842 }
843
845 void reset() noexcept { root_.reset(); }
846
853 void setControls(T treble, T bass, T middle) noexcept
854 {
855 treble_ = std::clamp(treble, T(0), T(1));
856 bass_ = std::clamp(bass, T(0), T(1));
857 middle_ = std::clamp(middle, T(0), T(1));
858
859 const double t = static_cast<double>(treble_);
860 const double l = static_cast<double>(bass_) * static_cast<double>(bass_);
861 const double m = static_cast<double>(middle_);
862 constexpr double kRmin = 0.5; // keeps the node matrix well-posed
863
864 r1Top_.setResistance(static_cast<T>((1.0 - t) * 250e3 + kRmin));
865 r1Bot_.setResistance(static_cast<T>(t * 250e3 + kRmin));
866 r2_.setResistance(static_cast<T>(l * 1e6 + kRmin));
867 r3Top_.setResistance(static_cast<T>((1.0 - m) * 25e3 + kRmin));
868 r3Bot_.setResistance(static_cast<T>(m * 25e3 + kRmin));
869 rtype_.updatePorts(); // rebuild scattering, keep states
870 }
871
873 [[nodiscard]] T processSample(T input) noexcept
874 {
875 root_.setVoltage(input);
876 root_.process();
877 return rLoad_.getVoltage();
878 }
879
880private:
881 static constexpr int kSrc = 0, kVi = 1, kA = 2, kVo = 3,
882 kB = 4, kS = 5, kC = 6, kW = 7;
883 static constexpr int kNumNodes = 8;
884
885 Resistor<T> rOut_;
886 Capacitor<T> c1_;
887 Resistor<T> r1Top_, r1Bot_, r4_;
888 Capacitor<T> c2_;
889 Resistor<T> r2_;
890 Capacitor<T> c3_;
891 Resistor<T> r3Top_, r3Bot_, rLoad_;
892
896 StackRType rtype_;
898
899 T treble_ = T(0.5), bass_ = T(0.5), middle_ = T(0.5);
900};
901
902} // namespace wdf
903} // namespace dspark
Core mathematical utilities for digital signal processing.
Capacitor, bilinear discretization: b[n] = a[n-1], Rp = 1/(2 fs C).
Definition WDF.h:114
double portResistance() const noexcept
Definition WDF.h:124
void incident(T a) noexcept
Definition WDF.h:126
void setCapacitance(T farads) noexcept
Definition WDF.h:118
void prepare(double sampleRate) noexcept
Definition WDF.h:120
void reset() noexcept
Definition WDF.h:122
T getCurrent() const noexcept
Definition WDF.h:129
T reflected() noexcept
Definition WDF.h:125
T getVoltage() const noexcept
Definition WDF.h:128
Capacitor(T farads) noexcept
Definition WDF.h:116
void updatePorts() noexcept
Definition WDF.h:121
Antiparallel diode pair root (the classic clipper nonlinearity).
Definition WDF.h:437
DiodePairRoot(Tree &tree, T saturationCurrent=T(2.52e-9), T idealityTimesVt=T(1.752 *0.02585)) noexcept
Definition WDF.h:439
void process() noexcept
Definition WDF.h:455
void setIdealityTimesVt(T volts) noexcept
Definition WDF.h:444
T getVoltage() const noexcept
Voltage across the pair (the clipper output).
Definition WDF.h:480
void prepare(double sampleRate) noexcept
Definition WDF.h:446
void reset() noexcept
Definition WDF.h:453
void setSaturationCurrent(T amps) noexcept
Definition WDF.h:443
Single Shockley diode root: i(v) = Is (e^{v/(n Vt)} - 1).
Definition WDF.h:495
DiodeRoot(Tree &tree, T saturationCurrent=T(2.52e-9), T idealityTimesVt=T(1.752 *0.02585)) noexcept
Definition WDF.h:497
void setIdealityTimesVt(T volts) noexcept
Definition WDF.h:502
void setSaturationCurrent(T amps) noexcept
Definition WDF.h:501
T getVoltage() const noexcept
Voltage across the diode.
Definition WDF.h:534
void prepare(double sampleRate) noexcept
Definition WDF.h:504
void reset() noexcept
Definition WDF.h:511
void process() noexcept
Definition WDF.h:513
Ideal voltage source closing a linear tree: b = 2 Vs - a.
Definition WDF.h:355
void setVoltage(T volts) noexcept
Definition WDF.h:359
IdealVoltageSourceRoot(Tree &tree) noexcept
Definition WDF.h:357
void prepare(double sampleRate) noexcept
Definition WDF.h:361
Inductor, bilinear discretization: b[n] = -a[n-1], Rp = 2 fs L.
Definition WDF.h:144
T getVoltage() const noexcept
Definition WDF.h:158
Inductor(T henries) noexcept
Definition WDF.h:146
void updatePorts() noexcept
Definition WDF.h:151
void setInductance(T henries) noexcept
Definition WDF.h:148
void reset() noexcept
Definition WDF.h:152
void incident(T a) noexcept
Definition WDF.h:156
void prepare(double sampleRate) noexcept
Definition WDF.h:150
T reflected() noexcept
Definition WDF.h:155
double portResistance() const noexcept
Definition WDF.h:154
T getCurrent() const noexcept
Definition WDF.h:159
Two-port polarity inverter (flips the connected subtree's polarity).
Definition WDF.h:327
void updatePorts() noexcept
Definition WDF.h:332
Inverter(Child &c) noexcept
Definition WDF.h:329
void incident(T a) noexcept
Definition WDF.h:337
void prepare(double sampleRate) noexcept
Definition WDF.h:331
T reflected() noexcept
Definition WDF.h:336
void reset() noexcept
Definition WDF.h:333
double portResistance() const noexcept
Definition WDF.h:335
Adapted three-port parallel connector.
Definition WDF.h:278
void incident(T a3) noexcept
Definition WDF.h:308
Parallel(Child1 &c1, Child2 &c2) noexcept
Definition WDF.h:280
void prepare(double sampleRate) noexcept
Definition WDF.h:282
T reflected() noexcept
Definition WDF.h:300
double portResistance() const noexcept
Definition WDF.h:298
void updatePorts() noexcept
Definition WDF.h:287
void reset() noexcept
Definition WDF.h:296
N-port R-type adaptor for non-series/parallel interconnections.
Definition WDF.h:563
void reset() noexcept
Definition WDF.h:639
void updatePorts() noexcept
Definition WDF.h:597
RType(const std::array< std::pair< int, int >, static_cast< size_t >(kNumPorts)> &portNodes, int numNodes, Children &... children) noexcept
Definition WDF.h:574
static constexpr int kNumPorts
Definition WDF.h:565
static constexpr int kMaxNodes
Definition WDF.h:566
void prepare(double sampleRate) noexcept
Definition WDF.h:592
T reflected() noexcept
Definition WDF.h:647
double portResistance() const noexcept
Definition WDF.h:645
void incident(T aUp) noexcept
Definition WDF.h:659
Voltage source with series resistance (Thévenin leaf): b = Vs.
Definition WDF.h:179
void setVoltage(T volts) noexcept
Definition WDF.h:185
double portResistance() const noexcept
Definition WDF.h:191
void setResistance(T ohms) noexcept
Definition WDF.h:184
void prepare(double) noexcept
Definition WDF.h:187
T getVoltage() const noexcept
Voltage at the source terminals (after the series resistance).
Definition WDF.h:196
void incident(T a) noexcept
Definition WDF.h:193
ResistiveVoltageSource(T seriesResistanceOhms) noexcept
Definition WDF.h:181
void updatePorts() noexcept
Definition WDF.h:188
Ideal resistor. Absorbs its incident wave (b = 0).
Definition WDF.h:83
void setResistance(T ohms) noexcept
Definition WDF.h:87
void reset() noexcept
Definition WDF.h:91
double portResistance() const noexcept
Definition WDF.h:93
void updatePorts() noexcept
Definition WDF.h:90
Resistor(T resistanceOhms) noexcept
Definition WDF.h:85
void prepare(double) noexcept
Definition WDF.h:89
void incident(T a) noexcept
Definition WDF.h:95
T reflected() noexcept
Definition WDF.h:94
T getCurrent() const noexcept
Current through the resistor.
Definition WDF.h:100
T getVoltage() const noexcept
Voltage across the resistor (valid after the root scattered).
Definition WDF.h:98
Adapted three-port series connector.
Definition WDF.h:224
void reset() noexcept
Definition WDF.h:242
double portResistance() const noexcept
Definition WDF.h:244
void updatePorts() noexcept
Definition WDF.h:233
void prepare(double sampleRate) noexcept
Definition WDF.h:228
T reflected() noexcept
Definition WDF.h:246
void incident(T a3) noexcept
Definition WDF.h:253
Series(Child1 &c1, Child2 &c2) noexcept
Definition WDF.h:226
Exact Fender '59 Bassman treble/bass/middle tone stack.
Definition WDF.h:808
void reset() noexcept
Clears capacitor states. RT-safe.
Definition WDF.h:845
void setControls(T treble, T bass, T middle) noexcept
Sets the three controls, [0, 1] each.
Definition WDF.h:853
ToneStackFMV(double sourceResistance=1e3, double loadResistance=1e6)
Definition WDF.h:814
void prepare(double sampleRate) noexcept
Prepares the network (allocates nothing).
Definition WDF.h:838
T processSample(T input) noexcept
Processes one sample (input volts -> wiper volts).
Definition WDF.h:873
Constrains a type to IEEE floating-point (float or double).
Definition DspMath.h:29
Main namespace for the DSPark framework.