721 prepared_.store(
false, std::memory_order_relaxed);
724 mixMaxStep_ =
static_cast<T
>(1.0 / std::max(1.0, sampleRate_ * 0.02));
727 fs2_ =
static_cast<double>(osFactor_) * sampleRate_;
730 driveLogSmoother_.
prepare(fs2_, 30.0);
731 for (
auto& smoother : outputLogSmoothers_)
732 smoother.prepare(fs2_, 30.0);
734 stageBlend_.
prepare(fs2_, 20.0);
735 for (
auto& ramp : gainRamps_)
736 ramp.resize(
static_cast<size_t>(maxBlock_) *
static_cast<size_t>(osFactor_));
737 for (
auto& scratch : compensationScratch_)
738 scratch.resize(
static_cast<size_t>(maxBlock_) *
static_cast<size_t>(osFactor_));
739 stageRamp_.resize(
static_cast<size_t>(maxBlock_) *
static_cast<size_t>(osFactor_));
743 oversampler_ = std::make_unique<Oversampling<T>>(
745 oversampler_->prepare(spec);
746 design_ = std::make_unique<detail::TubePreampCoreDesign>(osFactor_);
750 oversampler_.reset();
756 loadTable_ = std::make_unique<LoadTable>(
757 design_ ? std::numeric_limits<double>::infinity() : fs2_);
758 for (
auto& lane : channels_)
761 lane.resize(
static_cast<size_t>(numChannels_));
762 for (
auto& ch : lane)
763 ch = std::make_unique<ChannelState>(fs2_, loadTable_.get(), design_.get());
766 latency_ = (oversampler_ ? oversampler_->getLatency() : 0) + (design_ ? design_->latency : 0);
768 while (drySize_ < latency_ + maxBlock_ + 1) drySize_ <<= 1;
769 dryRing_.assign(
static_cast<size_t>(numChannels_),
770 std::vector<T>(
static_cast<size_t>(drySize_), T(0)));
773 calibrateReference();
775 prepared_.store(
true, std::memory_order_relaxed);
776 dirty_.store(
true, std::memory_order_release);
783 if (!prepared_.load(std::memory_order_relaxed))
return;
784 const double sagR =
static_cast<double>(sag_.load(std::memory_order_relaxed)) * 40e3;
785 const int stages = stages_.load(std::memory_order_relaxed);
786 for (
int st = 1; st <= 2; ++st)
787 for (
auto& ch : channels_[
static_cast<size_t>(st - 1)])
789 numStagesActive_ = stages;
790 stageBlend_.
reset(
static_cast<double>(stages - 1));
791 stageWarmupRemaining_ = 0;
792 bothStagesActive_ =
false;
793 for (
auto& d : dryRing_)
794 std::fill(d.begin(), d.end(), T(0));
796 if (oversampler_) oversampler_->reset();
798 gainsInitialized_ =
false;
799 currentMix_ = mix_.load(std::memory_order_relaxed);
808 if (!std::isfinite(db))
return;
809 driveDb_.store(std::clamp(db, T(-12), T(36)), std::memory_order_relaxed);
810 dirty_.store(
true, std::memory_order_release);
816 if (!std::isfinite(treble))
return;
817 treble_.store(std::clamp(treble, T(0), T(1)), std::memory_order_relaxed);
818 dirty_.store(
true, std::memory_order_release);
825 if (!std::isfinite(bass))
return;
826 bass_.store(std::clamp(bass, T(0), T(1)), std::memory_order_relaxed);
827 dirty_.store(
true, std::memory_order_release);
833 if (!std::isfinite(middle))
return;
834 middle_.store(std::clamp(middle, T(0), T(1)), std::memory_order_relaxed);
835 dirty_.store(
true, std::memory_order_release);
841 if (!std::isfinite(sag))
return;
842 sag_.store(std::clamp(sag, T(0), T(1)), std::memory_order_relaxed);
843 dirty_.store(
true, std::memory_order_release);
852 stages_.store(std::clamp(stages, 1, 2), std::memory_order_relaxed);
853 dirty_.store(
true, std::memory_order_release);
871 if (factor < 1 || factor > 16 || (factor & (factor - 1)) != 0)
return;
872 if (factor == osFactor_)
return;
877 if (prepared_.load(std::memory_order_relaxed))
887 if (!std::isfinite(db))
return;
888 outputDb_.store(std::clamp(db, T(-24), T(12)), std::memory_order_relaxed);
895 if (!std::isfinite(mix))
return;
896 mix_.store(std::clamp(mix, T(0), T(1)), std::memory_order_relaxed);
899 [[nodiscard]] T
getDrive() const noexcept {
return driveDb_.load(std::memory_order_relaxed); }
900 [[nodiscard]] T
getTreble() const noexcept {
return treble_.load(std::memory_order_relaxed); }
901 [[nodiscard]] T
getBass() const noexcept {
return bass_.load(std::memory_order_relaxed); }
902 [[nodiscard]] T
getMiddle() const noexcept {
return middle_.load(std::memory_order_relaxed); }
903 [[nodiscard]] T
getSag() const noexcept {
return sag_.load(std::memory_order_relaxed); }
904 [[nodiscard]]
int getStages() const noexcept {
return stages_.load(std::memory_order_relaxed); }
905 [[nodiscard]] T
getOutput() const noexcept {
return outputDb_.load(std::memory_order_relaxed); }
906 [[nodiscard]] T
getMix() const noexcept {
return mix_.load(std::memory_order_relaxed); }
911 [[nodiscard]]
int getLatency() const noexcept {
return latency_; }
920 return supplyNow_.load(std::memory_order_relaxed);
924 [[nodiscard]] std::vector<uint8_t>
getState()
const
929 w.
write(
"drive",
static_cast<float>(driveDb_.load(std::memory_order_relaxed)));
930 w.
write(
"treble",
static_cast<float>(treble_.load(std::memory_order_relaxed)));
931 w.
write(
"bass",
static_cast<float>(bass_.load(std::memory_order_relaxed)));
932 w.
write(
"middle",
static_cast<float>(middle_.load(std::memory_order_relaxed)));
933 w.
write(
"sag",
static_cast<float>(sag_.load(std::memory_order_relaxed)));
934 w.
write(
"stages", stages_.load(std::memory_order_relaxed));
935 w.
write(
"output",
static_cast<float>(outputDb_.load(std::memory_order_relaxed)));
936 w.
write(
"mix",
static_cast<float>(mix_.load(std::memory_order_relaxed)));
937 w.
write(
"oversampling", osFactor_);
965 if (!prepared_.load(std::memory_order_relaxed))
return;
968 const int nCh = std::min(buffer.getNumChannels(), numChannels_);
969 const int nS = buffer.getNumSamples();
970 if (nCh == 0 || nS == 0)
return;
978 for (
int ch = 0; ch < nCh; ++ch)
980 T* d = buffer.getChannel(ch);
981 for (
int i = 0; i < nS; ++i)
982 if (!std::isfinite(d[i])) d[i] = T(0);
987 if (dirty_.load(std::memory_order_relaxed)
988 && dirty_.exchange(
false, std::memory_order_acquire))
993 const T mixTarget = mix_.load(std::memory_order_relaxed);
994 const T mixStart = currentMix_;
995 const double trim = std::pow(10.0,
996 static_cast<double>(outputDb_.load(std::memory_order_relaxed)) / 20.0);
997 const std::array<double, 2> outGain { mScale_[0] * trim, mScale_[1] * trim };
1000 for (
int ch = 0; ch < nCh; ++ch)
1002 const T* in = buffer.getChannel(ch);
1003 auto& dry = dryRing_[
static_cast<size_t>(ch)];
1005 for (
int i = 0; i < nS; ++i)
1007 dry[
static_cast<size_t>(dp)] = in[i];
1008 dp = (dp + 1) & (drySize_ - 1);
1015 const bool osOn = (oversampler_ !=
nullptr);
1016 auto osView = osOn ? oversampler_->upsample(buffer) : buffer;
1017 const int osN = osView.getNumSamples();
1023 const double driveLog = std::log(hScale_);
1024 if (!gainsInitialized_)
1026 driveLogSmoother_.
reset(driveLog);
1027 for (
size_t st = 0; st < 2; ++st)
1028 outputLogSmoothers_[st].
reset(std::log(outGain[st]));
1029 gainsInitialized_ =
true;
1032 const bool driveRamping = driveLogSmoother_.
isSmoothing();
1036 std::span<double>(gainRamps_[0].data(),
static_cast<size_t>(osN)));
1037 for (
int i = 0; i < osN; ++i)
1039 auto& value = gainRamps_[0][
static_cast<size_t>(i)];
1040 value = value == driveLog ? hScale_ : std::exp(value);
1043 std::array<bool, 2> outputRamping {};
1044 for (
size_t st = 0; st < 2; ++st)
1046 const double outputLog = std::log(outGain[st]);
1047 auto& smoother = outputLogSmoothers_[st];
1048 smoother.setTargetValue(outputLog);
1049 outputRamping[st] = smoother.isSmoothing();
1050 if (!outputRamping[st])
continue;
1051 smoother.processBlock(
1052 std::span<double>(gainRamps_[st + 1].data(),
static_cast<size_t>(osN)));
1053 for (
int i = 0; i < osN; ++i)
1055 auto& value = gainRamps_[st + 1][
static_cast<size_t>(i)];
1056 value = value == outputLog ? outGain[st] : std::exp(value);
1063 int transitionSamples = 0;
1064 while (bothStagesActive_ && transitionSamples < osN)
1067 if (stageWarmupRemaining_ > 0) --stageWarmupRemaining_;
1069 stageRamp_[
static_cast<size_t>(transitionSamples++)] = w * w * (3.0 - 2.0 * w);
1070 if (stageWarmupRemaining_ == 0 && !stageBlend_.
isSmoothing())
1071 bothStagesActive_ =
false;
1073 const size_t target =
static_cast<size_t>(numStagesActive_ - 1);
1074 for (
int ch = 0; ch < nCh; ++ch)
1076 T* d = osView.getChannel(ch);
1077 for (
int i = 0; i < osN; ++i)
1079 const double h = driveRamping ? gainRamps_[0][
static_cast<size_t>(i)] : hScale_;
1080 const double x = h *
static_cast<double>(d[i]);
1081 for (
size_t st = 0; st < 2; ++st)
1083 if (st != target && i >= transitionSamples)
continue;
1084 compensationScratch_[st][
static_cast<size_t>(i)] =
1085 channels_[st][
static_cast<size_t>(ch)]->template processSample<false>(
1086 x,
static_cast<int>(st) + 1, sagR_);
1091 for (
size_t st = 0; st < 2; ++st)
1093 const int count = st == target ? osN : transitionSamples;
1095 channels_[st][
static_cast<size_t>(ch)]->compensateBlock(
1096 compensationScratch_[st].data(), count);
1099 for (
int i = 0; i < osN; ++i)
1101 const auto index =
static_cast<size_t>(i);
1102 const double g = outputRamping[target] ? gainRamps_[target + 1][index] : outGain[target];
1103 double value = g * compensationScratch_[target][index];
1104 if (i < transitionSamples)
1106 const size_t other = 1 - target;
1107 const double gOther = outputRamping[other] ? gainRamps_[other + 1][index] : outGain[other];
1108 const double alternate = gOther * compensationScratch_[other][index];
1109 const double w = target == 1 ? stageRamp_[index] : 1.0 - stageRamp_[index];
1110 value = alternate + w * (value - alternate);
1112 d[i] =
static_cast<T
>(value);
1115 if (osOn) oversampler_->downsample(buffer);
1117 const double w = position * position * (3.0 - 2.0 * position);
1118 const double current = (1.0 - w) * channels_[0][0]->supplyCurrent()
1119 + w * channels_[1][0]->supplyCurrent();
1120 supplyNow_.store(
static_cast<T
>(kBplus - sagR_ * current),
1121 std::memory_order_relaxed);
1126 for (
int ch = 0; ch < nCh; ++ch)
1128 T* d = buffer.getChannel(ch);
1129 const auto& dry = dryRing_[
static_cast<size_t>(ch)];
1130 for (
int i = 0; i < nS; ++i)
1132 const int idx = (dryPos_ + i - latency_) & (drySize_ - 1);
1133 const T drySample = dry[
static_cast<size_t>(idx)];
1134 const T mixVal =
moveTowards(mixStart, mixTarget, mixMaxStep_ *
static_cast<T
>(i + 1));
1135 d[i] = drySample + (d[i] - drySample) * mixVal;
1138 currentMix_ =
moveTowards(mixStart, mixTarget, mixMaxStep_ *
static_cast<T
>(nS));
1139 dryPos_ = (dryPos_ + nS) & (drySize_ - 1);
1144 static constexpr double kBplus = 300.0;
1145 static constexpr double kRL = detail::TubePreampCurrentTable::kRL;
1146 static constexpr double kRk = detail::TubePreampCurrentTable::kRk;
1147 static constexpr double kCk = detail::TubePreampCurrentTable::kCk;
1148 static constexpr double kInterstage = 0.12;
1150 using LoadTable = detail::TubePreampCurrentTable;
1151 using Clamp = detail::TubePreampGridClamp;
1153 static void koren(
double vpk,
double vgk,
double& ip,
1154 double& dIpdVpk,
double& dIpdVgk)
noexcept
1156 LoadTable::koren(vpk, vgk, ip, dIpdVpk, dIpdVgk);
1160 static double settleCurrent(
double bplus,
double grid = 0.0) noexcept
1163 for (
int it = 0; it < 60; ++it)
1165 const double vkS = i * kRk;
1166 const double vpk = bplus - i * kRL - vkS;
1167 double ipK = 0.0, dVpk = 0.0, dVgk = 0.0;
1168 koren(vpk, grid - vkS, ipK, dVpk, dVgk);
1169 const double f = i - ipK;
1170 const double fp = 1.0 - (dVpk * (-(kRL + kRk)) + dVgk * (-kRk));
1171 const double di = f / fp;
1173 i = std::clamp(i, 0.0, bplus / (kRL + kRk));
1174 if (std::abs(di) < 1e-15)
break;
1183 const LoadTable* table =
nullptr;
1184 double fs2 = 96000.0;
1188 double vpDC = 200.0;
1190 void settleDC(
double bplus)
noexcept
1192 ip = settleCurrent(bplus);
1195 vpDC = bplus - ip * kRL;
1199 [[nodiscard]]
double processSample(
double vg,
double bplusEff)
noexcept
1201 vg = Clamp::value(vg);
1205 const double h2c = 0.5 / (fs2 * kCk);
1206 const double denom = 1.0 + h2c / kRk;
1207 const double kA = (vk + h2c * fPrev) / denom;
1208 const double kB = h2c / denom;
1210 const double iMax = bplusEff / kRL + 1e-3;
1211 double i = std::clamp(ip, 0.0, iMax);
1214 if (table->covers(bplusEff - kA, vg - kA))
1215 i = table->eval(bplusEff - kA, vg - kA);
1216 else for (
int it = 0; it < 8; ++it)
1218 const double vkN = kA + kB * i;
1219 const double vpk = bplusEff - i * kRL - vkN;
1220 const double vgk = vg - vkN;
1221 double ipK = 0.0, dVpk = 0.0, dVgk = 0.0;
1222 koren(vpk, vgk, ipK, dVpk, dVgk);
1224 const double f = i - ipK;
1225 const double fp = 1.0 - (dVpk * (-kRL - kB) + dVgk * (-kB));
1226 const double di = f / std::max(fp, 1e-6);
1228 i = std::clamp(i, 0.0, iMax);
1229 if (std::abs(di) < 1e-12)
break;
1232 const double vkN = kA + kB * i;
1233 fPrev = i - vkN / kRk;
1236 return (bplusEff - i * kRL) - vpDC;
1245 struct ContinuousCore
1247 static constexpr int kLevels = 5;
1248 static constexpr int kMaxPieces = 48;
1249 static constexpr int kOrder = detail::TubePreampCoreDesign::kKernelOrder;
1250 static constexpr double kKneeRange = 1.0;
1251 static constexpr double kSmallZ = 0.3, kStiffZ = 40.0;
1252 static constexpr double kLongPiece = 0.5;
1253 static constexpr double kShortPiece = 0.03;
1255 const LoadTable* table =
nullptr;
1256 const detail::TubePreampCoreDesign* design =
nullptr;
1257 double period = 1.0 / 96000.0, cathodeDecay = 0.0, sagDecay = 0.0;
1258 detail::TubePreampToneModes modes;
1259 std::array<double, 16> ring {};
1262 double vk1 = 0.0, vk2 = 0.0, ipLP = 0.0, i1Prev = 0.0, i2Prev = 0.0;
1263 double vp1 = 0.0, vp2 = 0.0;
1264 double out[kOrder] {};
1265 double cutSupply[2] = {-1.0, -1.0}, cutVgk[2] {};
1267 typename LoadTable::Slice slice1, slice2;
1268 double supply = 0.0, a1 = 0.0, a2 = 0.0, s1 = 0.0, s2 = 0.0;
1269 double lev1[kLevels] {}, lev2[kLevels] {};
1270 double integral1 = 0.0, integral2 = 0.0,
moments[kOrder] {};
1271 double hi1 = 0.0, hi2 = 0.0;
1272 bool haveHi1 =
false, haveHi2 =
false;
1275 static constexpr double kG4x[4] = {0.069431844202973713, 0.33000947820757187,
1276 0.66999052179242813, 0.93056815579702629};
1277 static constexpr double kG4w[4] = {0.17392742256872692, 0.32607257743127308,
1278 0.32607257743127308, 0.17392742256872692};
1279 static constexpr double kG3x[3] = {0.11270166537925831, 0.5, 0.88729833462074169};
1280 static constexpr double kG3w[3] = {5.0 / 18.0, 4.0 / 9.0, 5.0 / 18.0};
1281 double lagrange4[4][4] {};
1282 double lagrange3[3][3] {};
1284 void init(
double sampleRate,
const LoadTable* tableIn,
1285 const detail::TubePreampCoreDesign* designIn)
noexcept
1289 period = 1.0 / sampleRate;
1290 cathodeDecay = std::exp(-period / (kRk * kCk));
1291 sagDecay = std::exp(-period / 0.07);
1292 for (
int k = 0; k < 4; ++k)
1294 double poly[4] = {1.0, 0.0, 0.0, 0.0};
1297 for (
int o = 0; o < 4; ++o)
1299 if (o == k)
continue;
1300 double next[4] = {0.0, 0.0, 0.0, 0.0};
1301 for (
int e = 0; e <= deg; ++e)
1303 next[e + 1] += poly[e];
1304 next[e] -= kG4x[o] * poly[e];
1307 for (
int e = 0; e < 4; ++e) poly[e] = next[e];
1308 den *= kG4x[k] - kG4x[o];
1310 for (
int e = 0; e < 4; ++e) lagrange4[k][e] = poly[e] / den;
1312 for (
int k = 0; k < 3; ++k)
1314 const int o1 = (k + 1) % 3, o2 = (k + 2) % 3;
1315 const double den = (kG3x[k] - kG3x[o1]) * (kG3x[k] - kG3x[o2]);
1316 lagrange3[k][0] = kG3x[o1] * kG3x[o2] / den;
1317 lagrange3[k][1] = -(kG3x[o1] + kG3x[o2]) / den;
1318 lagrange3[k][2] = 1.0 / den;
1323 void setTone(
const wdf::ToneStackFMV<double>::AnalogStateSpace& ss)
noexcept
1325 double physical[3] = {0.0, 0.0, 0.0};
1326 for (
int i = 0; i < 3; ++i)
1327 for (
int m = 0; m < 3; ++m)
1328 physical[i] += modes.toPhysical[i][m] * q[m];
1329 modes.design(ss, 1.0 / period);
1330 for (
int m = 0; m < 3; ++m)
1333 for (
int i = 0; i < 3; ++i) q[m] += modes.toModal[m][i] * physical[i];
1337 static std::array<double, 3> dcBias(
double sagR,
int numStages,
double grid = 0.0) noexcept
1339 double bp = kBplus, i1 = 0.0, i2 = 0.0, iTotal = 0.0;
1340 for (
int it = 0; it < 40; ++it)
1342 i1 = settleCurrent(bp, grid);
1343 i2 = numStages > 1 && grid != 0.0 ? settleCurrent(bp) : i1;
1344 iTotal = i1 + (numStages > 1 ? i2 : 0.0);
1345 const double next = kBplus - sagR * iTotal;
1346 if (std::abs(next - bp) < 1e-12) { bp = next;
break; }
1349 return {bp, i1, i2};
1352 void reset(
double sagR,
int numStages)
noexcept
1354 const auto bias = dcBias(sagR, numStages);
1355 const double bp = bias[0], i1 = bias[1];
1356 vk1 = vk2 = i1 * kRk;
1357 vp1 = vp2 = bp - i1 * kRL;
1358 ipLP = i1 * numStages;
1359 i1Prev = i2Prev = i1;
1360 q[0] = q[1] = q[2] = 0.0;
1363 for (
double& v : out) v = 0.0;
1364 cutSupply[0] = cutSupply[1] = -1.0;
1367 static double cutoffVgk(
double s)
noexcept
1371 return LoadTable::gridForCurrent(s, 1e-10);
1374 static double currentSlow(
double s,
double g)
noexcept
1377 double i = 0.5 * std::max(s, 0.0) / kRL, p = 0.0, dp = 0.0, dg = 0.0;
1378 for (
int it = 0; it < 60; ++it)
1380 koren(s - kRL * i, g, p, dp, dg);
1381 const double next = std::clamp(i - (i - p) / (1.0 + kRL * dp), 0.0,
1382 std::max(s, 0.0) / kRL);
1383 if (std::abs(next - i) < 1e-16) { i = next;
break; }
1390 void currents(
const typename LoadTable::Slice& sl,
double cathode,
1391 const double* x,
double* result)
const noexcept
1394 for (
int j = 0; j < N; ++j) g[j] = Clamp::value(x[j]) - cathode;
1395 for (
int j = 0; j < N; ++j)
1396 result[j] = (sl.inside && g[j] < 1.0) ? sl.eval(g[j]) : currentSlow(sl.s, g[j]);
1399 double current(
const typename LoadTable::Slice& sl,
double cathode,
double x)
const noexcept
1402 currents<1>(sl, cathode, &x, &r);
1406 static int classify(
const double* lev,
double v)
noexcept
1409 while (k < kLevels && v > lev[k]) ++k;
1412 static int classifyEdges(
const double* lev,
double v)
noexcept
1414 return v <= lev[0] ? 0 : (v > lev[kLevels - 1] ? kLevels : 1);
1417 void addMoments(
double tau,
double weight,
double y)
noexcept
1419 const double d = tau - 0.5;
1420 double t = weight * y;
1421 for (
int p = 0; p < kOrder; ++p) {
moments[p] += t; t *= d; }
1423 void addConstantMoments(
double a,
double b,
double y)
noexcept
1425 const double pa = a - 0.5, pb = b - 0.5;
1426 double ea = pa, eb = pb;
1427 for (
int p = 0; p < kOrder; ++p)
1429 moments[p] += y * (eb - ea) / (p + 1);
1435 void addPolynomialMoments(
double a,
double h,
const double* c,
int degree)
noexcept
1437 static constexpr double inv[16] = {1.0, 1.0 / 2, 1.0 / 3, 1.0 / 4, 1.0 / 5, 1.0 / 6,
1438 1.0 / 7, 1.0 / 8, 1.0 / 9, 1.0 / 10, 1.0 / 11, 1.0 / 12, 1.0 / 13, 1.0 / 14,
1439 1.0 / 15, 1.0 / 16};
1440 static constexpr double binom[kOrder][kOrder] = {{1}, {1, 1}, {1, 2, 1},
1441 {1, 3, 3, 1}, {1, 4, 6, 4, 1}, {1, 5, 10, 10, 5, 1}};
1442 double sums[kOrder], hk = 1.0, dk[kOrder];
1444 for (
int k = 1; k < kOrder; ++k) dk[k] = dk[k - 1] * (a - 0.5);
1445 for (
int k = 0; k < kOrder; ++k)
1448 for (
int e = 0; e <= degree; ++e) acc += c[e] * inv[e + k];
1452 for (
int p = 0; p < kOrder; ++p)
1455 for (
int k = 0; k <= p; ++k) acc += binom[p][k] * dk[p - k] * sums[k];
1470 double exactTerm(
int k,
double xi,
double& derivative)
const noexcept
1473 detail::tubePreampPhi(
z[k] * xi, p);
1474 static constexpr double fact[4] = {1.0, 1.0, 2.0, 6.0};
1475 double forced = 0.0, xp = xi, ui = 0.0, xe = 1.0;
1476 for (
int e = 0; e <
uTerms; ++e)
1478 forced +=
u[e] * fact[e] * xp * p[e + 1];
1483 const double qv = p[0] *
q0[k] +
bt[k] * forced;
1484 derivative =
g[k] * (
z[k] * qv +
bt[k] * ui);
1489 double v = ((((
c[5] * xi +
c[4]) * xi +
c[3]) * xi +
c[2]) * xi +
c[1]) * xi +
c[0];
1497 double eval(
double xi,
double& derivative)
const noexcept
1499 double v =
c[5], d = 0.0;
1500 for (
int p = 4; p >= 0; --p)
1519 void propagate(
double h,
const double* u,
int terms, Trajectory& tr)
noexcept
1521 static constexpr double fact[4] = {1.0, 1.0, 2.0, 6.0};
1524 for (
int e = 0; e < 4; ++e) tr.u[e] = e < terms ? u[e] : 0.0;
1525 for (
double& x : tr.c) x = 0.0;
1526 const double u0 = u[0], u0p = terms > 1 ? u[1] : 0.0;
1527 double u1 = 0.0, u1p = 0.0;
1528 for (
int e = 0; e < terms; ++e)
1535 double h0 = 0.0, h0p = 0.0, h0pp = 0.0, h1 = 0.0, h1p = 0.0, h1pp = 0.0;
1536 for (
int m = 0; m < 3; ++m)
1538 const double z = modes.lambda[m] * h, bt = modes.beta[m] * h, g = modes.gamma[m];
1539 const double az = std::abs(z);
1544 const double iz = 1.0 / z;
1545 double der[4] = {0.0, 0.0, 0.0, 0.0};
1546 for (
int e = 0; e < terms; ++e) der[e] = u[e];
1547 double izp = 1.0, endValue = 0.0;
1548 for (
int k = 0; k < terms; ++k)
1550 double sumAtEnd = 0.0;
1551 for (
int e = 0; e < 4; ++e)
1553 tr.c[e] += -g * bt * iz * izp * der[e];
1556 endValue += izp * sumAtEnd;
1557 for (
int e = 0; e < 3; ++e) der[e] = der[e + 1] * (e + 1);
1561 q[m] = -bt * iz * endValue;
1565 detail::tubePreampPhi(z, p);
1566 double forced = 0.0;
1567 for (
int e = 0; e < terms; ++e) forced += u[e] * fact[e] * p[e + 1];
1568 const double qEnd = p[0] * q[m] + bt * forced;
1571 const int k = tr.exactCount++;
1579 const double d0 = z * q[m] + bt * u0, d1 = z * qEnd + bt * u1;
1582 h0pp += g * (z * d0 + bt * u0p);
1585 h1pp += g * (z * d1 + bt * u1p);
1588 const double quad = 0.5 * h0pp;
1589 const double r0 = h1 - (h0 + h0p + quad), r1 = h1p - (h0p + 2.0 * quad), r2 = h1pp - 2.0 * quad;
1593 tr.c[3] += 10.0 * r0 - 4.0 * r1 + 0.5 * r2;
1594 tr.c[4] += -15.0 * r0 + 7.0 * r1 - r2;
1595 tr.c[5] += 6.0 * r0 - 3.0 * r1 + 0.5 * r2;
1596 for (
int e = 0; e < terms; ++e) tr.c[e] += modes.direct * u[e];
1600 void stage2(
double a,
double h,
const Trajectory& tr)
noexcept
1604 cuts[count++] = 0.0;
1605 const int samples = h > 0.5 ? 4 : 2;
1607 double lo = std::numeric_limits<double>::max(), hi = -lo;
1608 for (
int k = 0; k <= samples; ++k)
1610 sv[k] = kInterstage * tr(
double(k) / samples);
1611 lo = std::min(lo, sv[k]);
1612 hi = std::max(hi, sv[k]);
1614 const bool edges = hi - lo < kKneeRange;
1615 const auto cls = [&](
double v) {
return edges ? classifyEdges(lev2, v) : classify(lev2, v); };
1616 double pt = 0.0, pv = sv[0];
1618 const int first = pc;
1619 bool changed =
false;
1620 for (
int k = 1; k <= samples; ++k)
1622 const double t = double(k) / samples, v = sv[k];
1623 const int cc = cls(v);
1627 const int low = std::min(cc, pc), high = std::max(cc, pc);
1628 for (
int r = 0; r < high - low; ++r)
1630 int li = cc > pc ? low + r : high - 1 - r;
1631 if (edges) li = li == 0 ? 0 : kLevels - 1;
1632 const double level = lev2[li];
1633 double tt = pt + (t - pt) * (pv - level) / (pv - v);
1634 for (
int it = 0; it < 2; ++it)
1637 const double f = kInterstage * tr.eval(tt, dv) - level;
1639 if (dv == 0.0)
break;
1640 const double next = tt - f / dv;
1641 if (!(next > pt && next < t))
break;
1644 if (tt > cuts[count - 1] + 1e-12 && count < 14) cuts[count++] = tt;
1651 cuts[count++] = 1.0;
1652 for (
int k = 0; k + 1 < count; ++k)
1654 const double xa = cuts[k], xb = cuts[k + 1], len = xb - xa;
1655 if (len <= 0.0)
continue;
1656 const int cm = changed ? cls(kInterstage * tr(0.5 * (xa + xb))) : first;
1657 if (cm == 0 || cm == kLevels)
1662 if (!haveHi2) { hi2 = current(slice2, a2, 1e3); haveHi2 =
true; }
1665 integral2 += h * len * i2;
1666 addConstantMoments(a + h * xa, a + h * xb, supply - kRL * i2 - vp2);
1669 if (h * len > kLongPiece)
1673 double xs[4], is[4], y[4], poly[4];
1674 for (
int j = 0; j < 4; ++j) xs[j] = kInterstage * tr(xa + len * kG4x[j]);
1675 currents<4>(slice2, a2, xs, is);
1676 for (
int j = 0; j < 4; ++j)
1678 integral2 += h * len * kG4w[j] * is[j];
1679 y[j] = supply - kRL * is[j] - vp2;
1681 for (
int e = 0; e < 4; ++e)
1682 poly[e] = lagrange4[0][e] * y[0] + lagrange4[1][e] * y[1]
1683 + lagrange4[2][e] * y[2] + lagrange4[3][e] * y[3];
1684 addPolynomialMoments(a + h * xa, h * len, poly, 3);
1687 if (h * len < kShortPiece)
1691 constexpr double g0 = 0.21132486540518713, g1 = 0.78867513459481287;
1692 double xs[2] = {kInterstage * tr(xa + len * g0), kInterstage * tr(xa + len * g1)}, is[2];
1693 currents<2>(slice2, a2, xs, is);
1694 integral2 += h * len * 0.5 * (is[0] + is[1]);
1695 const double y0 = supply - kRL * is[0] - vp2, y1 = supply - kRL * is[1] - vp2;
1697 poly[1] = (y1 - y0) / (g1 - g0);
1698 poly[0] = y0 - poly[1] * g0;
1699 addPolynomialMoments(a + h * xa, h * len, poly, 1);
1702 double xs[3], is[3];
1703 for (
int j = 0; j < 3; ++j) xs[j] = kInterstage * tr(xa + len * kG3x[j]);
1704 currents<3>(slice2, a2, xs, is);
1708 double y[3], poly[3];
1709 for (
int j = 0; j < 3; ++j)
1711 integral2 += h * len * kG3w[j] * is[j];
1712 y[j] = supply - kRL * is[j] - vp2;
1714 for (
int e = 0; e < 3; ++e)
1715 poly[e] = lagrange3[0][e] * y[0] + lagrange3[1][e] * y[1] + lagrange3[2][e] * y[2];
1716 addPolynomialMoments(a + h * xa, h * len, poly, 2);
1720 void plate(
double a,
double h,
const double* u,
int terms)
noexcept
1723 propagate(h, u, terms, tr);
1726 else if (tr.exactCount == 0)
1727 addPolynomialMoments(a, h, tr.c, 5);
1732 const double y[3] = {tr(kG3x[0]), tr(kG3x[1]), tr(kG3x[2])};
1734 for (
int e = 0; e < 3; ++e)
1735 poly[e] = lagrange3[0][e] * y[0] + lagrange3[1][e] * y[1] + lagrange3[2][e] * y[2];
1736 addPolynomialMoments(a, h, poly, 2);
1740 double process(
double x,
int numStages,
double sagR)
noexcept
1743 const int taps = design->taps;
1744 ring[
static_cast<size_t>(ringPos)] = x;
1745 ringPos = ringPos + 1 == taps ? 0 : ringPos + 1;
1747 const double result = out[0];
1748 for (
int k = 0; k + 1 < kOrder; ++k) out[k] = out[k + 1];
1749 out[kOrder - 1] = 0.0;
1753 void interval(
double sagR)
noexcept
1756 const int taps = design->taps, deg = design->degree;
1759 for (
int j = 0; j < taps; ++j)
1761 const double v = ring[
static_cast<size_t>(idx)];
1762 idx = idx + 1 == taps ? 0 : idx + 1;
1763 const double* row = design->farrow + j * (deg + 1);
1764 for (
int p = 0; p <= deg; ++p) c[p] += row[p] * v;
1766 const auto xAt = [&](
double tau) {
1768 for (
int p = deg - 1; p >= 0; --p) v = v * tau + c[p];
1772 const double iPrev = i1Prev + (stages > 1 ? i2Prev : 0.0);
1773 supply = kBplus - sagR * (ipLP + 0.5 * (1.0 - sagDecay) * (iPrev - ipLP));
1774 a1 = vk1 + 0.5 * (1.0 - cathodeDecay) * (i1Prev * kRk - vk1);
1775 a2 = vk2 + 0.5 * (1.0 - cathodeDecay) * (i2Prev * kRk - vk2);
1778 slice1.set(*table, s1);
1779 slice2.set(*table, s2);
1780 if (std::abs(s1 - cutSupply[0]) > 0.05) { cutSupply[0] = s1; cutVgk[0] = cutoffVgk(s1); }
1781 if (std::abs(s2 - cutSupply[1]) > 0.05) { cutSupply[1] = s2; cutVgk[1] = cutoffVgk(s2); }
1782 lev1[0] = a1 + cutVgk[0]; lev1[1] = a1 - 3.0; lev1[2] = a1 - 1.0; lev1[3] = 1.0; lev1[4] = 6.0;
1783 lev2[0] = a2 + cutVgk[1]; lev2[1] = a2 - 3.0; lev2[2] = a2 - 1.0; lev2[3] = 1.0; lev2[4] = 6.0;
1784 for (
int j = 1; j < kLevels; ++j)
1786 lev1[j] = std::max(lev1[j], lev1[j - 1] + 1e-6);
1787 lev2[j] = std::max(lev2[j], lev2[j - 1] + 1e-6);
1792 double lo = std::numeric_limits<double>::max(), hi = -lo;
1793 for (
int k = 0; k <= 4; ++k)
1795 qv[k] = xAt(0.25 * k);
1796 lo = std::min(lo, qv[k]);
1797 hi = std::max(hi, qv[k]);
1799 const bool edges = hi - lo < kKneeRange;
1800 const auto cls = [&](
double v) {
return edges ? classifyEdges(lev1, v) : classify(lev1, v); };
1801 double cuts[kMaxPieces];
1803 cuts[count++] = 0.0;
1804 double pt = 0.0, pv = qv[0];
1806 const int first = pc;
1807 bool changed =
false;
1808 for (
int k = 1; k <= 4; ++k)
1810 const double t = 0.25 * k, v = qv[k];
1811 const int cc = cls(v);
1815 const int low = std::min(cc, pc), high = std::max(cc, pc);
1816 for (
int r = 0; r < high - low; ++r)
1818 int li = cc > pc ? low + r : high - 1 - r;
1819 if (edges) li = li == 0 ? 0 : kLevels - 1;
1820 const double level = lev1[li];
1821 double tt = pt + (t - pt) * (pv - level) / (pv - v);
1822 for (
int it = 0; it < 2; ++it)
1824 double f = c[deg], df = 0.0;
1825 for (
int p = deg - 1; p >= 0; --p) { df = df * tt + f; f = f * tt + c[p]; }
1827 if (df == 0.0)
break;
1828 const double next = tt - f / df;
1829 if (!(next > pt && next < t))
break;
1832 if (tt > cuts[count - 1] + 1e-12 && count < kMaxPieces - 2) cuts[count++] = tt;
1839 cuts[count++] = 1.0;
1840 integral1 = integral2 = 0.0;
1841 for (
double& m :
moments) m = 0.0;
1842 haveHi1 = haveHi2 =
false;
1843 for (
int k = 0; k + 1 < count; ++k)
1845 const double a = cuts[k], b = cuts[k + 1], h = b - a;
1846 if (h <= 0.0)
continue;
1847 const int cm = changed ? cls(xAt(0.5 * (a + b))) : first;
1848 if (cm == 0 || cm == kLevels)
1854 if (!haveHi1) { hi1 = current(slice1, a1, 1e3); haveHi1 =
true; }
1857 integral1 += h * i1;
1858 const double u[1] = {supply - kRL * i1 - vp1};
1862 double xs[4], is[4];
1864 double t0 = a + h * kG4x[0], t1 = a + h * kG4x[1];
1865 double t2 = a + h * kG4x[2], t3 = a + h * kG4x[3];
1866 double v0 = c[deg], v1 = c[deg], v2 = c[deg], v3 = c[deg];
1867 for (
int p = deg - 1; p >= 0; --p)
1869 v0 = v0 * t0 + c[p];
1870 v1 = v1 * t1 + c[p];
1871 v2 = v2 * t2 + c[p];
1872 v3 = v3 * t3 + c[p];
1874 xs[0] = v0; xs[1] = v1; xs[2] = v2; xs[3] = v3;
1876 currents<4>(slice1, a1, xs, is);
1878 for (
int j = 0; j < 4; ++j)
1880 y[j] = supply - kRL * is[j] - vp1;
1881 integral1 += h * kG4w[j] * is[j];
1884 for (
int e = 0; e < 4; ++e)
1885 u[e] = lagrange4[0][e] * y[0] + lagrange4[1][e] * y[1]
1886 + lagrange4[2][e] * y[2] + lagrange4[3][e] * y[3];
1890 vk1 = vk1 * cathodeDecay + (1.0 - cathodeDecay) * kRk * integral1;
1893 vk2 = vk2 * cathodeDecay + (1.0 - cathodeDecay) * kRk * integral2;
1896 ipLP = ipLP * sagDecay + (1.0 - sagDecay) * (integral1 + (stages > 1 ? integral2 : 0.0));
1898 for (
int k = 0; k < kOrder; ++k)
1900 const auto& piece = design->kernel[
static_cast<size_t>(k)];
1902 for (
int p = 0; p < kOrder; ++p) acc += piece[static_cast<size_t>(p)] *
moments[p];
1911 explicit ChannelState(
double fs2In,
const LoadTable* table,
1912 const detail::TubePreampCoreDesign* designIn)
1913 : design(designIn), fmv(38e3, 1e6)
1915 stage1.table = table;
1916 stage2.table = table;
1923 core.init(fs2In, table, design);
1924 core.setTone(fmv.analogStateSpace());
1925 compensation.prepare(
static_cast<int>(design->compensation.size()), 1);
1926 compensation.setCoefficients(design->compensation);
1936 void reset(
double sagR,
int numStages)
noexcept
1942 double iTotal = 0.0;
1943 for (
int it = 0; it < 8; ++it)
1945 stage1.settleDC(bp);
1946 stage2.settleDC(bp);
1947 iTotal = stage1.ip + (numStages > 1 ? stage2.ip : 0.0);
1948 const double bpNew = kBplus - sagR * iTotal;
1949 if (std::abs(bpNew - bp) < 1e-9)
break;
1953 outHpX = outHpY = 0.0;
1955 for (
auto& set : flatten)
1960 core.reset(sagR, numStages);
1961 compensation.reset();
1971 void resumeFrom(
const ChannelState& source,
int numStages,
double sagR)
noexcept
1977 const double current1 = (design ? source.core.vk1 : source.stage1.vk) / kRk;
1978 const double current2 = numStages == 1
1979 ? (design ? source.core.vk2 : source.stage2.vk) / kRk : 0.0;
1980 const double supply = kBplus - sagR * (current1 + current2);
1981 const double plate = std::max(1.0, supply - (kRL + kRk) * current1);
1982 const double grid = kRk * current1
1983 + LoadTable::gridForCurrent(plate, std::max(current1, 1e-18));
1984 const auto after = ContinuousCore::dcBias(sagR, numStages, grid);
1985 const double di = after[1] - current1;
1986 const double dTotal = after[1] + (numStages == 2 ? after[2] : 0.0) - current1 - current2;
1987 const double dPlate = after[0] - supply - kRL * di;
1988 const double reference = after[0] - kRL * after[1];
1989 const double previousReference = design ? source.core.vp1 : source.stage1.vpDC;
1990 const double inputOffset = previousReference + dPlate - reference;
1991 stage1 = source.stage1;
1992 stage1.vk += kRk * di;
1994 stage1.vpDC = reference;
1995 stage2.ip = after[2];
1996 stage2.vk = kRk * after[2];
1997 stage2.vpDC = after[0] - kRL * after[2];
1999 ipLP = source.ipLP + dTotal;
2000 if (!design) fmv.copyStateFrom(source.fmv, inputOffset);
2002 core.vk1 += kRk * di;
2004 core.vp1 = reference;
2006 for (
int mode = 0; mode < 3; ++mode)
2007 for (
int capacitor = 0; capacitor < 3; ++capacitor)
2008 core.q[mode] += core.modes.toModal[mode][capacitor] * inputOffset;
2013 core.ipLP += dTotal;
2014 core.vk2 = stage2.vk;
2015 core.vp2 = stage2.vpDC;
2016 core.i2Prev = stage2.ip;
2017 for (
double& value : core.out) value = 0.0;
2018 outHpX = outHpY = 0.0;
2019 for (
auto& filter : flatten[static_cast<size_t>(numStages - 1)])
2021 if (design) compensation.reset();
2025 void setFlattenCoeffs(
int stageCount,
2026 const std::array<BiquadCoeffs, 3>& c)
noexcept
2028 auto& set = flatten[
static_cast<size_t>(stageCount - 1)];
2029 for (
int k = 0; k < 3; ++k)
2030 set[
static_cast<size_t>(k)].setCoeffs(c[
static_cast<size_t>(k)]);
2033 void setToneControls(
double t,
double b,
double m)
noexcept
2035 fmv.setControls(t, b, m);
2036 if (design) core.setTone(fmv.analogStateSpace());
2040 [[nodiscard]]
double supplyCurrent() const noexcept {
return design ? core.ipLP : ipLP; }
2042 template <
bool Compensate = true>
2043 [[nodiscard]]
double processSample(
double vgIn,
int numStages,
double sagR)
noexcept
2047 v = core.process(vgIn, numStages, sagR);
2051 const double sagAlpha = 1.0 - std::exp(-1.0 / (0.07 * fs2));
2052 const double iTotal = stage1.ip + (numStages > 1 ? stage2.ip : 0.0);
2053 ipLP += sagAlpha * (iTotal - ipLP);
2054 const double bplusEff = kBplus - sagR * ipLP;
2056 v = stage1.processSample(vgIn, bplusEff);
2057 v = fmv.processSample(v);
2059 v = stage2.processSample(v * kInterstage, bplusEff);
2063 const double a = 1.0 - 2.0 * std::numbers::pi * 8.0 / fs2;
2064 const double y = a * (outHpY + v - outHpX);
2068 double out = (numStages > 1) ? y : -y;
2075 auto& fl = flatten[numStages > 1 ? 1 : 0];
2076 out = fl[0].processSample(out, 0);
2077 out = fl[1].processSample(out, 0);
2078 out = fl[2].processSample(out, 0);
2079 if constexpr (!Compensate)
return out;
2080 if (!design)
return out;
2081 return compensation.processSample(out, 0);
2084 void compensateBlock(
double* data,
int count)
noexcept
2088 compensation.processBlock(AudioBufferView<double>(&data, 1, count));
2091 double fs2 = 96000.0;
2092 TriodeStage stage1, stage2;
2093 double ipLP = 1.6e-3;
2094 double outHpX = 0.0, outHpY = 0.0;
2095 std::array<std::array<Biquad<double, 1>, 3>, 2> flatten;
2096 const detail::TubePreampCoreDesign* design;
2097 ContinuousCore core;
2098 FIRFilter<double> compensation;
2100 wdf::ToneStackFMV<double> fmv;
2104 void recompute() noexcept
2106 const double drive = std::pow(10.0,
static_cast<double>(
2107 driveDb_.load(std::memory_order_relaxed)) / 20.0);
2108 const double t =
static_cast<double>(treble_.load(std::memory_order_relaxed));
2109 const double b =
static_cast<double>(bass_.load(std::memory_order_relaxed));
2110 const double m =
static_cast<double>(middle_.load(std::memory_order_relaxed));
2111 const double sag =
static_cast<double>(sag_.load(std::memory_order_relaxed));
2112 const int requestedStages = stages_.load(std::memory_order_relaxed);
2121 for (
auto& lane : channels_)
2122 for (auto& ch : lane)
2123 ch->setToneControls(t, b, m);
2125 if (!gainsInitialized_)
2129 for (
auto& ch : channels_[static_cast<size_t>(requestedStages - 1)])
2130 ch->
reset(sagR_, requestedStages);
2131 stageBlend_.
reset(
static_cast<double>(requestedStages - 1));
2132 bothStagesActive_ =
false;
2133 stageWarmupRemaining_ = 0;
2135 else if (requestedStages != numStagesActive_)
2137 if (!bothStagesActive_)
2139 auto& incoming = channels_[
static_cast<size_t>(requestedStages - 1)];
2140 const auto& outgoing = channels_[
static_cast<size_t>(numStagesActive_ - 1)];
2141 for (
size_t ch = 0; ch < incoming.size(); ++ch)
2142 incoming[ch]->resumeFrom(*outgoing[ch], requestedStages, sagR_);
2143 stageWarmupRemaining_ = std::max(1,
static_cast<int>(std::ceil(0.005 * fs2_)));
2144 bothStagesActive_ =
true;
2146 stageBlend_.
setTargetValue(
static_cast<double>(requestedStages - 1));
2149 bothStagesActive_ =
false;
2150 stageWarmupRemaining_ = 0;
2153 numStagesActive_ = requestedStages;
2174 const double driveDbNow =
static_cast<double>(driveDb_.load(std::memory_order_relaxed));
2175 for (
int st = 1; st <= 2; ++st)
2176 mScale_[
static_cast<size_t>(st - 1)] = std::pow(drive, 0.25)
2177 / std::max(programGainAt(driveDbNow, st), 1e-9);
2181 [[nodiscard]]
double programGainAt(
double driveDb,
int stages)
const noexcept
2183 const auto& lut = gProgLut_[
static_cast<size_t>(stages - 1)];
2187 const double pos = std::min(std::max(0.0, (driveDb - kDriveLutMinDb) / kDriveLutStepDb),
2188 static_cast<double>(kDriveLutN - 1));
2189 const int idx = std::min(
static_cast<int>(pos), kDriveLutN - 2);
2190 const double frac = pos -
static_cast<double>(idx);
2191 const double a = std::max(lut[
static_cast<size_t>(idx)], 1e-9);
2192 const double b = std::max(lut[
static_cast<size_t>(idx) + 1], 1e-9);
2193 return a * std::pow(b / a, frac);
2211 void calibrateReference() noexcept
2213 constexpr double kRefTones[] = { 100.0, 200.0, 400.0, 800.0, 1600.0, 3200.0, 6400.0 };
2214 constexpr double kSagRef = 0.3 * 40e3;
2215 const int settle =
static_cast<int>(0.060 * fs2_);
2216 const int meas =
static_cast<int>(0.050 * fs2_);
2217 constexpr double kAmpPerTone = 0.095 / 2.6457513;
2219 for (
int st = 1; st <= 2; ++st)
2221 ChannelState cal(fs2_, loadTable_.get(), design_.get());
2222 cal.setToneControls(0.5, 0.5, 0.5);
2223 cal.reset(kSagRef, st);
2226 std::array<double, 7> gs1 {}, gs2 {}, gc {};
2227 for (
int k = 0; k < 7; ++k)
2228 gc[
static_cast<size_t>(k)] =
2229 2.0 * std::cos(2.0 * std::numbers::pi * kRefTones[k] / fs2_);
2230 for (
int i = 0; i < settle + meas; ++i)
2233 for (
int k = 0; k < 7; ++k)
2234 x += std::sin(2.0 * std::numbers::pi * kRefTones[k] * i / fs2_ + k * 1.7);
2236 const double y = cal.processSample(x, st, kSagRef);
2238 for (
int k = 0; k < 7; ++k)
2240 auto& s1 = gs1[
static_cast<size_t>(k)];
2241 auto& s2 = gs2[
static_cast<size_t>(k)];
2242 const double s0 = y + gc[
static_cast<size_t>(k)] * s1 - s2;
2246 std::array<double, 7> gainDb {};
2247 for (
int k = 0; k < 7; ++k)
2249 const double c = gc[
static_cast<size_t>(k)] * 0.5;
2250 const double s1 = gs1[
static_cast<size_t>(k)], s2 = gs2[
static_cast<size_t>(k)];
2251 const double mag = std::sqrt(std::max(s1 * s1 + s2 * s2 - 2.0 * c * s1 * s2, 0.0))
2253 gainDb[
static_cast<size_t>(k)] =
2254 20.0 * std::log10(std::max(mag / kAmpPerTone, 1e-9));
2259 for (
double g : gainDb) mean += g / 7.0;
2260 const double lo = 0.5 * (gainDb[0] + gainDb[1]) - mean;
2261 const double mid = (gainDb[2] + gainDb[3] + gainDb[4]) / 3.0 - mean;
2262 const double hi = 0.5 * (gainDb[5] + gainDb[6]) - mean;
2264 std::array<BiquadCoeffs, 3> fc = {
2269 for (
auto& lane : channels_)
2270 for (auto& ch : lane)
2271 ch->setFlattenCoeffs(st, fc);
2279 ChannelState sweep(fs2_, loadTable_.get(), design_.get());
2280 sweep.setToneControls(0.5, 0.5, 0.5);
2281 sweep.setFlattenCoeffs(st, fc);
2282 sweep.reset(kSagRef, st);
2283 const int settle2 =
static_cast<int>(0.030 * fs2_);
2284 const int meas2 =
static_cast<int>(0.030 * fs2_);
2286 for (
int kd = 0; kd < kDriveLutN; ++kd)
2288 const double d = std::pow(10.0,
2289 (kDriveLutMinDb + kd * kDriveLutStepDb) / 20.0);
2290 double inSq = 0.0, outSq = 0.0;
2291 for (
int i = 0; i < settle2 + meas2; ++i, ++t)
2294 for (
int k = 0; k < 7; ++k)
2295 x += std::sin(2.0 * std::numbers::pi * kRefTones[k] * t / fs2_ + k * 1.7);
2297 const double y = sweep.processSample(d * x, st, kSagRef);
2304 gProgLut_[
static_cast<size_t>(st - 1)][
static_cast<size_t>(kd)] =
2305 (outSq > 0.0 && inSq > 0.0) ? std::sqrt(outSq / inSq) : 1.0;
2312 double sampleRate_ = 48000.0;
2313 double fs2_ = 96000.0;
2314 int numChannels_ = 0;
2316 std::atomic<bool> prepared_ {
false };
2321 std::unique_ptr<LoadTable> loadTable_;
2322 std::unique_ptr<detail::TubePreampCoreDesign> design_;
2323 std::unique_ptr<Oversampling<T>> oversampler_;
2324 std::array<std::vector<std::unique_ptr<ChannelState>>, 2> channels_;
2326 std::vector<std::vector<T>> dryRing_;
2329 static constexpr int kDriveLutN = 13;
2330 static constexpr double kDriveLutMinDb = -12.0;
2331 static constexpr double kDriveLutStepDb = 4.0;
2333 double hScale_ = 1.0;
2334 std::array<double, 2> mScale_ { 1.0, 1.0 };
2336 int numStagesActive_ = 1;
2338 std::array<std::array<double, kDriveLutN>, 2> gProgLut_ {
2339 { { 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 },
2340 { 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 } } };
2341 SmoothedValue<double> driveLogSmoother_;
2342 std::array<SmoothedValue<double>, 2> outputLogSmoothers_;
2343 std::array<std::vector<double>, 3> gainRamps_;
2344 std::array<std::vector<double>, 2> compensationScratch_;
2345 SmoothedValue<double> stageBlend_;
2346 std::vector<double> stageRamp_;
2347 int stageWarmupRemaining_ = 0;
2348 bool bothStagesActive_ =
false;
2349 bool gainsInitialized_ =
false;
2350 T currentMix_ = T(1);
2351 T mixMaxStep_ = T(1.0 / 960.0);
2353 std::atomic<T> driveDb_ { T(0) };
2354 std::atomic<T> treble_ { T(0.5) };
2355 std::atomic<T> bass_ { T(0.5) };
2356 std::atomic<T> middle_ { T(0.5) };
2357 std::atomic<T> sag_ { T(0.3) };
2358 std::atomic<int> stages_ { 1 };
2359 std::atomic<T> outputDb_ { T(0) };
2360 std::atomic<T> mix_ { T(1) };
2361 std::atomic<T> supplyNow_ { T(300) };
2362 std::atomic<bool> dirty_ {
true };