DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
WDF.h
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
90 [[nodiscard]] T getResistance() const noexcept { return value_; }
91
92 void prepare(double) noexcept {}
93 void updatePorts() noexcept { R_ = static_cast<double>(value_); }
94 void reset() noexcept { a_ = 0; }
95
96 [[nodiscard]] double portResistance() const noexcept { return R_; }
97 [[nodiscard]] T reflected() noexcept { return T(0); }
98 void incident(T a) noexcept { a_ = a; }
99
101 [[nodiscard]] T getVoltage() const noexcept { return a_ * T(0.5); }
103 [[nodiscard]] T getCurrent() const noexcept
104 {
105 return static_cast<T>(static_cast<double>(a_) * 0.5 / R_);
106 }
107
108private:
109 T value_;
110 double R_ = 1.0;
111 T a_ = 0;
112};
113
115template <FloatType T>
117{
118public:
119 explicit Capacitor(T farads) noexcept : value_(farads) {}
120
121 void setCapacitance(T farads) noexcept { value_ = std::max(farads, T(1e-18)); }
122
124 [[nodiscard]] T getCapacitance() const noexcept { return value_; }
125
126 void prepare(double sampleRate) noexcept { fs_ = sampleRate; }
127 void updatePorts() noexcept { R_ = 1.0 / (2.0 * fs_ * static_cast<double>(value_)); }
128 void reset() noexcept { state_ = 0; a_ = 0; b_ = 0; }
129
133 void offsetVoltage(T offset) noexcept
134 {
135 state_ += offset;
136 a_ += offset;
137 b_ += offset;
138 }
139
140 [[nodiscard]] double portResistance() const noexcept { return R_; }
141 [[nodiscard]] T reflected() noexcept { b_ = state_; return b_; }
142 void incident(T a) noexcept { a_ = a; state_ = a; }
143
144 [[nodiscard]] T getVoltage() const noexcept { return (a_ + b_) * T(0.5); }
145 [[nodiscard]] T getCurrent() const noexcept
146 {
147 return static_cast<T>(static_cast<double>(a_ - b_) * 0.5 / R_);
148 }
149
150private:
151 T value_;
152 double fs_ = 48000.0;
153 double R_ = 1.0;
154 T state_ = 0, a_ = 0, b_ = 0;
155};
156
158template <FloatType T>
160{
161public:
162 explicit Inductor(T henries) noexcept : value_(henries) {}
163
164 void setInductance(T henries) noexcept { value_ = std::max(henries, T(1e-12)); }
165
166 void prepare(double sampleRate) noexcept { fs_ = sampleRate; }
167 void updatePorts() noexcept { R_ = 2.0 * fs_ * static_cast<double>(value_); }
168 void reset() noexcept { state_ = 0; a_ = 0; b_ = 0; }
169
170 [[nodiscard]] double portResistance() const noexcept { return R_; }
171 [[nodiscard]] T reflected() noexcept { b_ = -state_; return b_; }
172 void incident(T a) noexcept { a_ = a; state_ = a; }
173
174 [[nodiscard]] T getVoltage() const noexcept { return (a_ + b_) * T(0.5); }
175 [[nodiscard]] T getCurrent() const noexcept
176 {
177 return static_cast<T>(static_cast<double>(a_ - b_) * 0.5 / R_);
178 }
179
180private:
181 T value_;
182 double fs_ = 48000.0;
183 double R_ = 1.0;
184 T state_ = 0, a_ = 0, b_ = 0;
185};
186
193template <FloatType T>
195{
196public:
197 explicit ResistiveVoltageSource(T seriesResistanceOhms) noexcept
198 : value_(seriesResistanceOhms) {}
199
200 void setResistance(T ohms) noexcept { value_ = std::max(ohms, T(1e-9)); }
201 void setVoltage(T volts) noexcept { vs_ = volts; }
202
203 void prepare(double) noexcept {}
204 void updatePorts() noexcept { R_ = static_cast<double>(value_); }
205 void reset() noexcept { a_ = 0; }
206
207 [[nodiscard]] double portResistance() const noexcept { return R_; }
208 [[nodiscard]] T reflected() noexcept { return vs_; }
209 void incident(T a) noexcept { a_ = a; }
210
212 [[nodiscard]] T getVoltage() const noexcept { return (a_ + vs_) * T(0.5); }
213
214private:
215 T value_;
216 double R_ = 1.0;
217 T vs_ = 0, a_ = 0;
218};
219
220// ============================================================================
221// Connectors
222// ============================================================================
223
238template <FloatType T, typename Child1, typename Child2>
240{
241public:
242 Series(Child1& c1, Child2& c2) noexcept : c1_(c1), c2_(c2) {}
243
244 void prepare(double sampleRate) noexcept
245 {
246 c1_.prepare(sampleRate);
247 c2_.prepare(sampleRate);
248 }
249 void updatePorts() noexcept
250 {
251 c1_.updatePorts();
252 c2_.updatePorts();
253 const double r1 = c1_.portResistance();
254 const double r2 = c2_.portResistance();
255 R_ = r1 + r2;
256 gamma1_ = static_cast<T>(r1 / R_);
257 }
258 void reset() noexcept { c1_.reset(); c2_.reset(); a1_ = 0; a2_ = 0; }
259
260 [[nodiscard]] double portResistance() const noexcept { return R_; }
261
262 [[nodiscard]] T reflected() noexcept
263 {
264 a1_ = c1_.reflected();
265 a2_ = c2_.reflected();
266 return a1_ + a2_; // -(a1+a2) with the up-port inversion folded in
267 }
268
269 void incident(T a3) noexcept
270 {
271 // b_k = a_k - gamma_k * (a1 + a2 + a3'), a3' = -a3 (folded inversion),
272 // with gamma1 + gamma2 = 1 on the adapted port.
273 const T sum = a1_ + a2_ - a3;
274 c1_.incident(a1_ - gamma1_ * sum);
275 c2_.incident(a2_ - (sum - gamma1_ * sum)); // a2 - gamma2 * sum
276 }
277
278private:
279 Child1& c1_;
280 Child2& c2_;
281 double R_ = 2.0;
282 T gamma1_ = T(0.5);
283 T a1_ = 0, a2_ = 0;
284};
285
292template <FloatType T, typename Child1, typename Child2>
294{
295public:
296 Parallel(Child1& c1, Child2& c2) noexcept : c1_(c1), c2_(c2) {}
297
298 void prepare(double sampleRate) noexcept
299 {
300 c1_.prepare(sampleRate);
301 c2_.prepare(sampleRate);
302 }
303 void updatePorts() noexcept
304 {
305 c1_.updatePorts();
306 c2_.updatePorts();
307 const double g1 = 1.0 / c1_.portResistance();
308 const double g2 = 1.0 / c2_.portResistance();
309 R_ = 1.0 / (g1 + g2);
310 d1_ = static_cast<T>(g1 / (g1 + g2));
311 }
312 void reset() noexcept { c1_.reset(); c2_.reset(); a1_ = 0; a2_ = 0; bUp_ = 0; }
313
314 [[nodiscard]] double portResistance() const noexcept { return R_; }
315
316 [[nodiscard]] T reflected() noexcept
317 {
318 a1_ = c1_.reflected();
319 a2_ = c2_.reflected();
320 bUp_ = d1_ * a1_ + (T(1) - d1_) * a2_; // weighted node wave
321 return bUp_;
322 }
323
324 void incident(T a3) noexcept
325 {
326 // Common node wave: bNode = bUp + a3; then b_k = bNode - a_k.
327 const T bNode = bUp_ + a3;
328 c1_.incident(bNode - a1_);
329 c2_.incident(bNode - a2_);
330 }
331
332private:
333 Child1& c1_;
334 Child2& c2_;
335 double R_ = 0.5;
336 T d1_ = T(0.5);
337 T a1_ = 0, a2_ = 0, bUp_ = 0;
338};
339
341template <FloatType T, typename Child>
343{
344public:
345 explicit Inverter(Child& c) noexcept : c_(c) {}
346
347 void prepare(double sampleRate) noexcept { c_.prepare(sampleRate); }
348 void updatePorts() noexcept { c_.updatePorts(); }
349 void reset() noexcept { c_.reset(); }
350
351 [[nodiscard]] double portResistance() const noexcept { return c_.portResistance(); }
352 [[nodiscard]] T reflected() noexcept { return -c_.reflected(); }
353 void incident(T a) noexcept { c_.incident(-a); }
354
355private:
356 Child& c_;
357};
358
359// ============================================================================
360// Roots
361// ============================================================================
362
369template <FloatType T, typename Tree>
371{
372public:
373 explicit IdealVoltageSourceRoot(Tree& tree) noexcept : tree_(tree) {}
374
375 void setVoltage(T volts) noexcept { vs_ = volts; }
376
377 void prepare(double sampleRate) noexcept
378 {
379 tree_.prepare(sampleRate);
380 tree_.updatePorts();
381 tree_.reset();
382 }
383 void reset() noexcept { tree_.reset(); }
384
385 void process() noexcept
386 {
387 const T a = tree_.reflected();
388 tree_.incident(T(2) * vs_ - a);
389 }
390
391private:
392 Tree& tree_;
393 T vs_ = 0;
394};
395
396namespace detail {
397
409template <typename F>
410[[nodiscard]] inline double solveMonotonic(double a, double seed, F&& evaluate) noexcept
411{
412 double lo = std::min(0.0, a);
413 double hi = std::max(0.0, a);
414 double v = std::clamp(seed, lo, hi);
415 double prevAbsF = 1e300;
416
417 for (int it = 0; it < 48; ++it)
418 {
419 double f = 0.0, fp = 1.0;
420 evaluate(v, f, fp);
421
422 const double absF = std::abs(f);
423 if (absF < 1e-10)
424 break;
425 if (f > 0.0) hi = v;
426 else lo = v;
427
428 // Accept the Newton step only while it makes real progress. Deep in
429 // the exponential region Newton crawls at ~nVt per step (the classic
430 // diode pathology), so a stalled |f| switches to bisection, which
431 // halves the bracket unconditionally. Quadratic near the root,
432 // bisection-guaranteed everywhere.
433 const double vn = v - f / fp;
434 if (vn > lo && vn < hi && absF < 0.7 * prevAbsF)
435 v = vn;
436 else
437 v = 0.5 * (lo + hi);
438 prevAbsF = absF;
439 }
440 return v;
441}
442
443} // namespace detail
444
451template <FloatType T, typename Tree>
453{
454public:
455 explicit DiodePairRoot(Tree& tree, T saturationCurrent = T(2.52e-9),
456 T idealityTimesVt = T(1.752 * 0.02585)) noexcept
457 : tree_(tree), is_(saturationCurrent), nvt_(idealityTimesVt) {}
458
459 void setSaturationCurrent(T amps) noexcept { is_ = std::max(amps, T(1e-15)); }
460 void setIdealityTimesVt(T volts) noexcept { nvt_ = std::max(volts, T(1e-4)); }
461
462 void prepare(double sampleRate) noexcept
463 {
464 tree_.prepare(sampleRate);
465 tree_.updatePorts();
466 tree_.reset();
467 v_ = 0.0;
468 }
469 void reset() noexcept { tree_.reset(); v_ = 0.0; }
470
471 void process() noexcept
472 {
473 const double a = static_cast<double>(tree_.reflected());
474 const double k = 2.0 * tree_.portResistance() * static_cast<double>(is_);
475 const double nvt = static_cast<double>(nvt_);
476
477 // Far-field analytic seed: neglecting the linear term in
478 // v + k sinh(v/nVt) = a gives v0 = nVt asinh(a/k), within ~nVt of the
479 // root at any drive level (deep in the exponential, plain Newton
480 // would crawl at ~nVt per iteration). Newton then converges in 1-3.
481 const double seed = nvt * std::asinh(a / k);
482
483 v_ = detail::solveMonotonic(a, seed, [k, nvt, a](double v, double& f, double& fp)
484 {
485 // Guard the argument: cosh overflows around |x| > 710 in double,
486 // far outside any solution the bracket allows anyway.
487 const double x = std::clamp(v / nvt, -700.0, 700.0);
488 f = v + k * std::sinh(x) - a;
489 fp = 1.0 + (k / nvt) * std::cosh(x);
490 });
491
492 tree_.incident(static_cast<T>(2.0 * v_ - a));
493 }
494
496 [[nodiscard]] T getVoltage() const noexcept { return static_cast<T>(v_); }
497
498private:
499 Tree& tree_;
500 T is_, nvt_;
501 double v_ = 0.0;
502};
503
509template <FloatType T, typename Tree>
511{
512public:
513 explicit DiodeRoot(Tree& tree, T saturationCurrent = T(2.52e-9),
514 T idealityTimesVt = T(1.752 * 0.02585)) noexcept
515 : tree_(tree), is_(saturationCurrent), nvt_(idealityTimesVt) {}
516
517 void setSaturationCurrent(T amps) noexcept { is_ = std::max(amps, T(1e-15)); }
518 void setIdealityTimesVt(T volts) noexcept { nvt_ = std::max(volts, T(1e-4)); }
519
520 void prepare(double sampleRate) noexcept
521 {
522 tree_.prepare(sampleRate);
523 tree_.updatePorts();
524 tree_.reset();
525 v_ = 0.0;
526 }
527 void reset() noexcept { tree_.reset(); v_ = 0.0; }
528
529 void process() noexcept
530 {
531 const double a = static_cast<double>(tree_.reflected());
532 const double k = tree_.portResistance() * static_cast<double>(is_);
533 const double nvt = static_cast<double>(nvt_);
534
535 // Far-field analytic seed: forward drive lands near
536 // nVt ln(1 + a/k); reverse drive blocks, so the root sits near a.
537 const double seed = (a > 0.0) ? nvt * std::log1p(a / k) : a;
538
539 v_ = detail::solveMonotonic(a, seed, [k, nvt, a](double v, double& f, double& fp)
540 {
541 const double x = std::clamp(v / nvt, -700.0, 700.0);
542 f = v + k * std::expm1(x) - a;
543 fp = 1.0 + (k / nvt) * std::exp(x);
544 });
545
546 tree_.incident(static_cast<T>(2.0 * v_ - a));
547 }
548
550 [[nodiscard]] T getVoltage() const noexcept { return static_cast<T>(v_); }
551
552private:
553 Tree& tree_;
554 T is_, nvt_;
555 double v_ = 0.0;
556};
557
558// ============================================================================
559// R-type adaptor (arbitrary topologies)
560// ============================================================================
561
577template <FloatType T, typename... Children>
578class RType
579{
580 template <FloatType> friend class ToneStackFMV;
581public:
582 static constexpr int kNumPorts = static_cast<int>(sizeof...(Children)) + 1;
583 static constexpr int kMaxNodes = 12;
584 static_assert(sizeof...(Children) >= 1, "RType needs at least one child");
585
591 RType(const std::array<std::pair<int, int>, static_cast<size_t>(kNumPorts)>& portNodes,
592 int numNodes, Children&... children) noexcept
593 : children_(children...), portNodes_(portNodes),
594 numNodes_(std::clamp(numNodes, 1, kMaxNodes))
595 {
596 assert(numNodes >= 1 && numNodes <= kMaxNodes);
597 // Release-safe node sanitising: an out-of-range node index would
598 // stamp the conductance/RHS arrays out of bounds. Clamp into
599 // [-1, numNodes_-1] (-1 = ground); a clamped topology is wrong but
600 // cannot corrupt memory.
601 for (auto& [p, m] : portNodes_)
602 {
603 assert(p >= -1 && p < numNodes_ && m >= -1 && m < numNodes_);
604 p = std::clamp(p, -1, numNodes_ - 1);
605 m = std::clamp(m, -1, numNodes_ - 1);
606 }
607 }
608
609 void prepare(double sampleRate) noexcept
610 {
611 std::apply([&](auto&... ch) { (ch.prepare(sampleRate), ...); }, children_);
612 }
613
614 void updatePorts() noexcept
615 {
616 std::apply([&](auto&... ch) { (ch.updatePorts(), ...); }, children_);
617
618 // Gather child port resistances (port 0 resolved by adaptation).
619 {
620 int i = 1;
621 std::apply([&](auto&... ch)
622 { ((portR_[static_cast<size_t>(i++)] = ch.portResistance()), ...); },
623 children_);
624 }
625
626 // 1) Thevenin resistance looking into the network from port 0:
627 // assemble MNA without port 0, inject 1 A across its nodes.
628 assembleConductance(false);
629 factor();
630 double rhs[kMaxNodes] = {};
631 stampCurrent(rhs, 0, 1.0);
632 solve(rhs);
633 double rth = portVoltage(rhs, 0);
634 portR_[0] = std::clamp(rth, 1e-6, 1e12);
635
636 // 2) Full MNA with every port loaded; S = 2M - I where column j of M
637 // is the port-voltage response to a_j = 1 (Norton: 1/R_j).
638 assembleConductance(true);
639 factor();
640 for (int j = 0; j < kNumPorts; ++j)
641 {
642 double v[kMaxNodes] = {};
643 stampCurrent(v, j, 1.0 / portR_[static_cast<size_t>(j)]);
644 solve(v);
645 for (int i = 0; i < kNumPorts; ++i)
646 {
647 const double m = portVoltage(v, i);
648 s_[static_cast<size_t>(i)][static_cast<size_t>(j)] =
649 2.0 * m - (i == j ? 1.0 : 0.0);
650 }
651 }
652 // Adaptation leaves no instantaneous reflection at the up port.
653 s_[0][0] = 0.0;
654 }
655
656 void reset() noexcept
657 {
658 std::apply([&](auto&... ch) { (ch.reset(), ...); }, children_);
659 a_.fill(0.0);
660 }
661
662 [[nodiscard]] double portResistance() const noexcept { return portR_[0]; }
663
664 [[nodiscard]] T reflected() noexcept
665 {
666 int i = 1;
667 std::apply([&](auto&... ch)
668 { ((a_[static_cast<size_t>(i++)] = static_cast<double>(ch.reflected())), ...); },
669 children_);
670 double b0 = 0.0;
671 for (int j = 1; j < kNumPorts; ++j)
672 b0 += s_[0][static_cast<size_t>(j)] * a_[static_cast<size_t>(j)];
673 return static_cast<T>(b0);
674 }
675
676 void incident(T aUp) noexcept
677 {
678 a_[0] = static_cast<double>(aUp);
679 int i = 1;
680 std::apply([&](auto&... ch)
681 {
682 ((ch.incident(static_cast<T>(rowDot(i))), ++i), ...);
683 },
684 children_);
685 }
686
687private:
688 // Preserve a topology-proven zero-current voltage mode after the nodal
689 // solve: S*v = v. For the FMV network the source and three coupling
690 // capacitors share a common DC voltage; every resistor has zero voltage.
691 // Correcting only the final nonzero column removes factorization roundoff
692 // in this identity without adding arithmetic to sample processing.
693 void preserveDcMode(const std::array<double, static_cast<size_t>(kNumPorts)>& mode) noexcept
694 {
695 int pivot = kNumPorts - 1;
696 while (pivot > 0 && mode[static_cast<size_t>(pivot)] == 0.0) --pivot;
697 if (pivot == 0) return; // Keep the adapted up-port reflection at zero.
698 for (int i = 0; i < kNumPorts; ++i)
699 {
700 double remainder = 0.0;
701 for (int j = 0; j < kNumPorts; ++j)
702 if (j != pivot)
703 remainder += s_[static_cast<size_t>(i)][static_cast<size_t>(j)]
704 * mode[static_cast<size_t>(j)];
705 s_[static_cast<size_t>(i)][static_cast<size_t>(pivot)] =
706 (mode[static_cast<size_t>(i)] - remainder) / mode[static_cast<size_t>(pivot)];
707 }
708 }
709
710 [[nodiscard]] double rowDot(int row) const noexcept
711 {
712 double acc = 0.0;
713 for (int j = 0; j < kNumPorts; ++j)
714 acc += s_[static_cast<size_t>(row)][static_cast<size_t>(j)] * a_[static_cast<size_t>(j)];
715 return acc;
716 }
717
718 void assembleConductance(bool includePort0) noexcept
719 {
720 for (auto& row : g_) row.fill(0.0);
721 for (int j = includePort0 ? 0 : 1; j < kNumPorts; ++j)
722 {
723 const double g = 1.0 / portR_[static_cast<size_t>(j)];
724 const int p = portNodes_[static_cast<size_t>(j)].first;
725 const int m = portNodes_[static_cast<size_t>(j)].second;
726 if (p >= 0) g_[static_cast<size_t>(p)][static_cast<size_t>(p)] += g;
727 if (m >= 0) g_[static_cast<size_t>(m)][static_cast<size_t>(m)] += g;
728 if (p >= 0 && m >= 0)
729 {
730 g_[static_cast<size_t>(p)][static_cast<size_t>(m)] -= g;
731 g_[static_cast<size_t>(m)][static_cast<size_t>(p)] -= g;
732 }
733 }
734 }
735
736 void stampCurrent(double* rhs, int port, double amps) const noexcept
737 {
738 const int p = portNodes_[static_cast<size_t>(port)].first;
739 const int m = portNodes_[static_cast<size_t>(port)].second;
740 if (p >= 0) rhs[p] += amps;
741 if (m >= 0) rhs[m] -= amps;
742 }
743
744 [[nodiscard]] double portVoltage(const double* v, int port) const noexcept
745 {
746 const int p = portNodes_[static_cast<size_t>(port)].first;
747 const int m = portNodes_[static_cast<size_t>(port)].second;
748 return (p >= 0 ? v[p] : 0.0) - (m >= 0 ? v[m] : 0.0);
749 }
750
752 void factor() noexcept
753 {
754 const int n = numNodes_;
755 for (int i = 0; i < n; ++i)
756 {
757 for (int j = 0; j < n; ++j)
758 lu_[static_cast<size_t>(i)][static_cast<size_t>(j)] =
759 g_[static_cast<size_t>(i)][static_cast<size_t>(j)];
760 piv_[static_cast<size_t>(i)] = i;
761 }
762 for (int k = 0; k < n; ++k)
763 {
764 int p = k;
765 for (int i = k + 1; i < n; ++i)
766 if (std::abs(lu_[static_cast<size_t>(i)][static_cast<size_t>(k)])
767 > std::abs(lu_[static_cast<size_t>(p)][static_cast<size_t>(k)]))
768 p = i;
769 if (p != k)
770 {
771 std::swap(lu_[static_cast<size_t>(p)], lu_[static_cast<size_t>(k)]);
772 std::swap(piv_[static_cast<size_t>(p)], piv_[static_cast<size_t>(k)]);
773 }
774 double d = lu_[static_cast<size_t>(k)][static_cast<size_t>(k)];
775 if (std::abs(d) < 1e-300)
776 d = (d >= 0.0 ? 1e-300 : -1e-300);
777 const double invD = 1.0 / d;
778 for (int i = k + 1; i < n; ++i)
779 {
780 const double f = lu_[static_cast<size_t>(i)][static_cast<size_t>(k)] * invD;
781 lu_[static_cast<size_t>(i)][static_cast<size_t>(k)] = f;
782 for (int j = k + 1; j < n; ++j)
783 lu_[static_cast<size_t>(i)][static_cast<size_t>(j)]
784 -= f * lu_[static_cast<size_t>(k)][static_cast<size_t>(j)];
785 }
786 }
787 }
788
790 void solve(double* rhs) const noexcept
791 {
792 const int n = numNodes_;
793 double y[kMaxNodes];
794 for (int i = 0; i < n; ++i)
795 y[i] = rhs[piv_[static_cast<size_t>(i)]];
796 for (int i = 0; i < n; ++i)
797 for (int j = 0; j < i; ++j)
798 y[i] -= lu_[static_cast<size_t>(i)][static_cast<size_t>(j)] * y[j];
799 for (int i = n - 1; i >= 0; --i)
800 {
801 for (int j = i + 1; j < n; ++j)
802 y[i] -= lu_[static_cast<size_t>(i)][static_cast<size_t>(j)] * y[j];
803 double d = lu_[static_cast<size_t>(i)][static_cast<size_t>(i)];
804 if (std::abs(d) < 1e-300)
805 d = (d >= 0.0 ? 1e-300 : -1e-300);
806 y[i] /= d;
807 }
808 for (int i = 0; i < n; ++i)
809 rhs[i] = y[i];
810 }
811
812 std::tuple<Children&...> children_;
813 std::array<std::pair<int, int>, static_cast<size_t>(kNumPorts)> portNodes_;
814 int numNodes_;
815
816 std::array<double, static_cast<size_t>(kNumPorts)> portR_ {};
817 std::array<double, static_cast<size_t>(kNumPorts)> a_ {};
818 std::array<std::array<double, static_cast<size_t>(kNumPorts)>,
819 static_cast<size_t>(kNumPorts)> s_ {};
820
821 std::array<std::array<double, kMaxNodes>, kMaxNodes> g_ {};
822 std::array<std::array<double, kMaxNodes>, kMaxNodes> lu_ {};
823 std::array<int, kMaxNodes> piv_ {};
824};
825
826// ============================================================================
827// Fender '59 Bassman FMV tone stack (reference R-type circuit)
828// ============================================================================
829
845template <FloatType T>
847{
848public:
853 explicit ToneStackFMV(double sourceResistance = 1e3, double loadResistance = 1e6)
854 : rOut_(static_cast<T>(sourceResistance)), c1_(T(0.25e-9)),
855 r1Top_(T(125e3)), r1Bot_(T(125e3)), r4_(T(56e3)), c2_(T(20e-9)),
856 r2_(T(500e3)), c3_(T(20e-9)), r3Top_(T(12.5e3)), r3Bot_(T(12.5e3)),
857 rLoad_(static_cast<T>(loadResistance)),
858 rtype_({ { { kSrc, -1 }, // port 0: ideal source root
859 { kSrc, kVi }, // rOut
860 { kVi, kA }, // C1
861 { kA, kVo }, // (1-t) R1
862 { kVo, kB }, // t R1
863 { kVi, kS }, // R4
864 { kS, kB }, // C2
865 { kB, kC }, // l R2 (rheostat)
866 { kS, kW }, // C3 to the middle wiper
867 { kC, kW }, // (1-m) R3
868 { kW, -1 }, // m R3 to ground
869 { kVo, -1 } } }, // grid load
870 kNumNodes,
871 rOut_, c1_, r1Top_, r1Bot_, r4_, c2_, r2_, c3_, r3Top_, r3Bot_, rLoad_),
872 root_(rtype_)
873 {
874 }
875
877 void prepare(double sampleRate) noexcept
878 {
879 root_.prepare(sampleRate);
880 setControls(treble_, bass_, middle_);
881 }
882
884 void reset() noexcept { root_.reset(); }
885
895 void copyStateFrom(const ToneStackFMV& source, T inputOffset = T(0)) noexcept
896 {
897 c1_ = source.c1_;
898 c2_ = source.c2_;
899 c3_ = source.c3_;
900 if (inputOffset != T(0))
901 {
902 c1_.offsetVoltage(inputOffset);
903 c2_.offsetVoltage(inputOffset);
904 c3_.offsetVoltage(inputOffset);
905 }
906 }
907
914 void setControls(T treble, T bass, T middle) noexcept
915 {
916 treble_ = std::clamp(treble, T(0), T(1));
917 bass_ = std::clamp(bass, T(0), T(1));
918 middle_ = std::clamp(middle, T(0), T(1));
919
920 const double t = static_cast<double>(treble_);
921 const double l = static_cast<double>(bass_) * static_cast<double>(bass_);
922 const double m = static_cast<double>(middle_);
923 constexpr double kRmin = 0.5; // keeps the node matrix well-posed
924
925 r1Top_.setResistance(static_cast<T>((1.0 - t) * 250e3 + kRmin));
926 r1Bot_.setResistance(static_cast<T>(t * 250e3 + kRmin));
927 r2_.setResistance(static_cast<T>(l * 1e6 + kRmin));
928 r3Top_.setResistance(static_cast<T>((1.0 - m) * 25e3 + kRmin));
929 r3Bot_.setResistance(static_cast<T>(m * 25e3 + kRmin));
930 rtype_.updatePorts(); // rebuild scattering, keep states
931 rtype_.preserveDcMode({1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0});
932 }
933
935 [[nodiscard]] T processSample(T input) noexcept
936 {
937 root_.setVoltage(input);
938 root_.process();
939 return rLoad_.getVoltage();
940 }
941
955 {
956 double a[3][3];
957 double b[3];
958 double c[3];
959 double d;
960 double capacitance[3];
961 };
962
963 [[nodiscard]] AnalogStateSpace analogStateSpace() const noexcept
964 {
965 // Unknowns: node voltages kVi..kW (7) then the three capacitor currents.
966 constexpr int kN = 10;
967 struct Element { int p, m; double value; bool capacitor; };
968 const Element elements[] = {
969 { kSrc, kVi, static_cast<double>(rOut_.getResistance()), false },
970 { kVi, kA, static_cast<double>(c1_.getCapacitance()), true },
971 { kA, kVo, static_cast<double>(r1Top_.getResistance()), false },
972 { kVo, kB, static_cast<double>(r1Bot_.getResistance()), false },
973 { kVi, kS, static_cast<double>(r4_.getResistance()), false },
974 { kS, kB, static_cast<double>(c2_.getCapacitance()), true },
975 { kB, kC, static_cast<double>(r2_.getResistance()), false },
976 { kS, kW, static_cast<double>(c3_.getCapacitance()), true },
977 { kC, kW, static_cast<double>(r3Top_.getResistance()), false },
978 { kW, -1, static_cast<double>(r3Bot_.getResistance()), false },
979 { kVo, -1, static_cast<double>(rLoad_.getResistance()), false } };
980 double m[kN][kN] {};
981 double rhs[kN][4] {}; // columns: unit C1, C2, C3 voltages, unit source
982 int capIndex = 0;
983 AnalogStateSpace out {};
984 for (const auto& e : elements)
985 {
986 const int p = e.p - 1, q = e.m < 0 ? -1 : e.m - 1; // kSrc maps to -1
987 if (e.capacitor)
988 {
989 const int row = 7 + capIndex;
990 if (p >= 0) { m[p][row] += 1.0; m[row][p] += 1.0; }
991 if (q >= 0) { m[q][row] -= 1.0; m[row][q] -= 1.0; }
992 rhs[row][capIndex] = 1.0;
993 out.capacitance[capIndex] = e.value;
994 ++capIndex;
995 continue;
996 }
997 const double g = 1.0 / e.value;
998 if (e.p == kSrc)
999 {
1000 // Source node is driven: its conductance stamps the RHS.
1001 m[q][q] += g;
1002 rhs[q][3] += g;
1003 continue;
1004 }
1005 if (p >= 0) m[p][p] += g;
1006 if (q >= 0) m[q][q] += g;
1007 if (p >= 0 && q >= 0) { m[p][q] -= g; m[q][p] -= g; }
1008 }
1009 // Gaussian elimination with partial pivoting, four right-hand sides.
1010 for (int k = 0; k < kN; ++k)
1011 {
1012 int piv = k;
1013 for (int i = k + 1; i < kN; ++i)
1014 if (std::abs(m[i][k]) > std::abs(m[piv][k])) piv = i;
1015 if (piv != k)
1016 for (int j = 0; j < kN; ++j) std::swap(m[piv][j], m[k][j]);
1017 if (piv != k)
1018 for (int j = 0; j < 4; ++j) std::swap(rhs[piv][j], rhs[k][j]);
1019 for (int i = k + 1; i < kN; ++i)
1020 {
1021 const double f = m[i][k] / m[k][k];
1022 if (f == 0.0) continue;
1023 for (int j = k; j < kN; ++j) m[i][j] -= f * m[k][j];
1024 for (int j = 0; j < 4; ++j) rhs[i][j] -= f * rhs[k][j];
1025 }
1026 }
1027 for (int i = kN - 1; i >= 0; --i)
1028 for (int j = 0; j < 4; ++j)
1029 {
1030 double v = rhs[i][j];
1031 for (int k = i + 1; k < kN; ++k) v -= m[i][k] * rhs[k][j];
1032 rhs[i][j] = v / m[i][i];
1033 }
1034 // Capacitor current flows from its first node to its second node.
1035 for (int j = 0; j < 4; ++j)
1036 {
1037 for (int k = 0; k < 3; ++k)
1038 {
1039 const double dv = rhs[7 + k][j] / out.capacitance[k];
1040 if (j < 3) out.a[k][j] = dv;
1041 else out.b[k] = dv;
1042 }
1043 if (j < 3) out.c[j] = rhs[kVo - 1][j];
1044 else out.d = rhs[kVo - 1][j];
1045 }
1046 return out;
1047 }
1048
1049private:
1050 static constexpr int kSrc = 0, kVi = 1, kA = 2, kVo = 3,
1051 kB = 4, kS = 5, kC = 6, kW = 7;
1052 static constexpr int kNumNodes = 8;
1053
1054 Resistor<T> rOut_;
1055 Capacitor<T> c1_;
1056 Resistor<T> r1Top_, r1Bot_, r4_;
1057 Capacitor<T> c2_;
1058 Resistor<T> r2_;
1059 Capacitor<T> c3_;
1060 Resistor<T> r3Top_, r3Bot_, rLoad_;
1061
1065 StackRType rtype_;
1067
1068 T treble_ = T(0.5), bass_ = T(0.5), middle_ = T(0.5);
1069};
1070
1071} // namespace wdf
1072} // namespace dspark
Capacitor, bilinear discretization: b[n] = a[n-1], Rp = 1/(2 fs C).
Definition WDF.h:117
double portResistance() const noexcept
Definition WDF.h:140
void incident(T a) noexcept
Definition WDF.h:142
void setCapacitance(T farads) noexcept
Definition WDF.h:121
void prepare(double sampleRate) noexcept
Definition WDF.h:126
void reset() noexcept
Definition WDF.h:128
T getCurrent() const noexcept
Definition WDF.h:145
T reflected() noexcept
Definition WDF.h:141
T getVoltage() const noexcept
Definition WDF.h:144
T getCapacitance() const noexcept
Current capacitance in farads.
Definition WDF.h:124
void offsetVoltage(T offset) noexcept
Shifts the voltage reference without changing capacitor current. Stream-owner only; the finite offset...
Definition WDF.h:133
Capacitor(T farads) noexcept
Definition WDF.h:119
void updatePorts() noexcept
Definition WDF.h:127
Antiparallel diode pair root (the classic clipper nonlinearity).
Definition WDF.h:453
DiodePairRoot(Tree &tree, T saturationCurrent=T(2.52e-9), T idealityTimesVt=T(1.752 *0.02585)) noexcept
Definition WDF.h:455
void process() noexcept
Definition WDF.h:471
void setIdealityTimesVt(T volts) noexcept
Definition WDF.h:460
T getVoltage() const noexcept
Voltage across the pair (the clipper output).
Definition WDF.h:496
void prepare(double sampleRate) noexcept
Definition WDF.h:462
void reset() noexcept
Definition WDF.h:469
void setSaturationCurrent(T amps) noexcept
Definition WDF.h:459
Single Shockley diode root: i(v) = Is (e^{v/(n Vt)} - 1).
Definition WDF.h:511
DiodeRoot(Tree &tree, T saturationCurrent=T(2.52e-9), T idealityTimesVt=T(1.752 *0.02585)) noexcept
Definition WDF.h:513
void setIdealityTimesVt(T volts) noexcept
Definition WDF.h:518
void setSaturationCurrent(T amps) noexcept
Definition WDF.h:517
T getVoltage() const noexcept
Voltage across the diode.
Definition WDF.h:550
void prepare(double sampleRate) noexcept
Definition WDF.h:520
void reset() noexcept
Definition WDF.h:527
void process() noexcept
Definition WDF.h:529
Ideal voltage source closing a linear tree: b = 2 Vs - a.
Definition WDF.h:371
void setVoltage(T volts) noexcept
Definition WDF.h:375
IdealVoltageSourceRoot(Tree &tree) noexcept
Definition WDF.h:373
void prepare(double sampleRate) noexcept
Definition WDF.h:377
Inductor, bilinear discretization: b[n] = -a[n-1], Rp = 2 fs L.
Definition WDF.h:160
T getVoltage() const noexcept
Definition WDF.h:174
Inductor(T henries) noexcept
Definition WDF.h:162
void updatePorts() noexcept
Definition WDF.h:167
void setInductance(T henries) noexcept
Definition WDF.h:164
void reset() noexcept
Definition WDF.h:168
void incident(T a) noexcept
Definition WDF.h:172
void prepare(double sampleRate) noexcept
Definition WDF.h:166
T reflected() noexcept
Definition WDF.h:171
double portResistance() const noexcept
Definition WDF.h:170
T getCurrent() const noexcept
Definition WDF.h:175
Two-port polarity inverter (flips the connected subtree's polarity).
Definition WDF.h:343
void updatePorts() noexcept
Definition WDF.h:348
Inverter(Child &c) noexcept
Definition WDF.h:345
void incident(T a) noexcept
Definition WDF.h:353
void prepare(double sampleRate) noexcept
Definition WDF.h:347
T reflected() noexcept
Definition WDF.h:352
void reset() noexcept
Definition WDF.h:349
double portResistance() const noexcept
Definition WDF.h:351
Adapted three-port parallel connector.
Definition WDF.h:294
void incident(T a3) noexcept
Definition WDF.h:324
Parallel(Child1 &c1, Child2 &c2) noexcept
Definition WDF.h:296
void prepare(double sampleRate) noexcept
Definition WDF.h:298
T reflected() noexcept
Definition WDF.h:316
double portResistance() const noexcept
Definition WDF.h:314
void updatePorts() noexcept
Definition WDF.h:303
void reset() noexcept
Definition WDF.h:312
N-port R-type adaptor for non-series/parallel interconnections.
Definition WDF.h:579
void reset() noexcept
Definition WDF.h:656
void updatePorts() noexcept
Definition WDF.h:614
RType(const std::array< std::pair< int, int >, static_cast< size_t >(kNumPorts)> &portNodes, int numNodes, Children &... children) noexcept
Definition WDF.h:591
static constexpr int kNumPorts
Definition WDF.h:582
static constexpr int kMaxNodes
Definition WDF.h:583
void prepare(double sampleRate) noexcept
Definition WDF.h:609
T reflected() noexcept
Definition WDF.h:664
double portResistance() const noexcept
Definition WDF.h:662
void incident(T aUp) noexcept
Definition WDF.h:676
Voltage source with series resistance (Thevenin leaf): b = Vs.
Definition WDF.h:195
void setVoltage(T volts) noexcept
Definition WDF.h:201
double portResistance() const noexcept
Definition WDF.h:207
void setResistance(T ohms) noexcept
Definition WDF.h:200
void prepare(double) noexcept
Definition WDF.h:203
T getVoltage() const noexcept
Voltage at the source terminals (after the series resistance).
Definition WDF.h:212
void incident(T a) noexcept
Definition WDF.h:209
ResistiveVoltageSource(T seriesResistanceOhms) noexcept
Definition WDF.h:197
void updatePorts() noexcept
Definition WDF.h:204
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:94
double portResistance() const noexcept
Definition WDF.h:96
void updatePorts() noexcept
Definition WDF.h:93
Resistor(T resistanceOhms) noexcept
Definition WDF.h:85
void prepare(double) noexcept
Definition WDF.h:92
void incident(T a) noexcept
Definition WDF.h:98
T reflected() noexcept
Definition WDF.h:97
T getCurrent() const noexcept
Current through the resistor.
Definition WDF.h:103
T getResistance() const noexcept
Current resistance in ohms.
Definition WDF.h:90
T getVoltage() const noexcept
Voltage across the resistor (valid after the root scattered).
Definition WDF.h:101
Adapted three-port series connector.
Definition WDF.h:240
void reset() noexcept
Definition WDF.h:258
double portResistance() const noexcept
Definition WDF.h:260
void updatePorts() noexcept
Definition WDF.h:249
void prepare(double sampleRate) noexcept
Definition WDF.h:244
T reflected() noexcept
Definition WDF.h:262
void incident(T a3) noexcept
Definition WDF.h:269
Series(Child1 &c1, Child2 &c2) noexcept
Definition WDF.h:242
Exact Fender '59 Bassman treble/bass/middle tone stack.
Definition WDF.h:847
void reset() noexcept
Clears capacitor states. RT-safe.
Definition WDF.h:884
void setControls(T treble, T bass, T middle) noexcept
Sets the three controls, [0, 1] each.
Definition WDF.h:914
AnalogStateSpace analogStateSpace() const noexcept
Definition WDF.h:963
ToneStackFMV(double sourceResistance=1e3, double loadResistance=1e6)
Definition WDF.h:853
void copyStateFrom(const ToneStackFMV &source, T inputOffset=T(0)) noexcept
Copies the capacitor history from an identically prepared stack.
Definition WDF.h:895
void prepare(double sampleRate) noexcept
Prepares the network (allocates nothing).
Definition WDF.h:877
T processSample(T input) noexcept
Processes one sample (input volts -> wiper volts).
Definition WDF.h:935
Constrains a type to IEEE floating-point (float or double).
Definition DspMath.h:30
Main namespace for the DSPark framework.
Continuous-time state space of the same network.
Definition WDF.h:955