From 3de67bef039ec8baa80fee89ca90c3153b18417a Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Wed, 14 Oct 2026 22:43:28 +0300 Subject: [PATCH 01/18] m6 X1: exact J4 coupling (load port at y on both roots), HOT and full acceptance at 384 kHz --- docs/PROGRESS.md | 9 + engine/include/amp2203/Amp2203.h | 2 + engine/include/amp2203/Params.h | 4 + engine/include/amp2203/PowerAmp.h | 11 +- engine/include/amp2203/Preamp.h | 18 +- engine/src/Amp2203.cpp | 30 ++- engine/src/PowerAmp.cpp | 23 +- engine/src/Preamp.cpp | 91 +++++++- engine/src/devices/FourTriodes.h | 33 ++- engine/src/devices/TriodesPentodes.h | 35 ++++ engine/src/stage1234/Stage1234Network.h | 5 +- engine/src/stage56/Stage56Network.h | 3 +- harness/config.py | 2 + harness/m4_acceptance.py | 7 +- ...261014_full_acceptance_fs384000_j4exact.md | 197 ++++++++++++++++++ ...14_full_acceptance_fs384000_j4exact_hot.md | 52 +++++ tools/ampwav/src/Cli.cpp | 17 +- tools/ampwav/src/Cli.h | 1 + tools/ampwav/src/Runner.cpp | 4 + tools/ampwav/src/Runner.h | 2 + 20 files changed, 517 insertions(+), 29 deletions(-) create mode 100644 harness/reports/M6/20261014_full_acceptance_fs384000_j4exact.md create mode 100644 harness/reports/M6/20261014_full_acceptance_fs384000_j4exact_hot.md diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 95dc0ba..ed7d418 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -635,6 +635,15 @@ Notes: knob-robust, usable with other plugins). Written: `docs/ARCHITECTURE_STUDY.md` (literature, products, candidates A–E against latency / authenticity / CPU / knobs / verifiability / effort). M6 waits for the owner's direction. +- 2026-10-14 Owner: run experiments X1 and X3 (ARCHITECTURE_STUDY §6); PyTorch 2.14.1 approved for X3. +- X1 (branch `m6-x1-j4`): exact J4 coupling (`EngineConfig::exactJ4`, ampwav `--j4 exact`, harness + `AMP2203_J4=exact`): a load port rY at y on both roots (preamp: given current; power amp: the preamp's + linearised Thevenin source, rTh = Z_yy − Z_yp·D·(I + Z_pp·D)⁻¹·Z_py, every sample), preamp solved, power amp + solved against it, preamp re-solved (warm) with the actual load current. At 384 kHz: HOT 220 Hz ESR −12.6 → + −58.9 dB, harmonics 11.6 → 0.01 dB, HOT DI −13 → −41.5 … −42.7 dB (the HOT DI clips keep 5–8 rescued + first-solve failures each: 4 FAIL on newton_failures, `…_j4exact_hot.md`); full acceptance 107 PASS, CLEAN + (Master 1.0) −31.2 → −60.1 dB, the rest within 0.5 dB (`…_j4exact.md`). Cost at 384 kHz (10 s DI): 3352 / 4497 + ns default / hot (main 2242 / 3016; the extra open port alone +6 %). ## Questions for the owner _(Claude Code: add questions here and also ask them in the session.)_ diff --git a/engine/include/amp2203/Amp2203.h b/engine/include/amp2203/Amp2203.h index 5b8020a..6fccaa7 100644 --- a/engine/include/amp2203/Amp2203.h +++ b/engine/include/amp2203/Amp2203.h @@ -86,6 +86,8 @@ class Amp2203 { const Halfband halfband_{10.0}; detail::FirWindow decim_, interp_; bool powerPhase_ = false; // true: the power amp runs at this sample + double iY_ = 0.0; // exact J4: the load current at y of the last sample (A) + bool exactJ4() const noexcept { return cfg_.exactJ4 && cfg_.powerAmpRateDivisor == 1; } double vIn_ = 0.0; // V at the input jack double vOut_ = 0.0; // V across the 16 Ω load diff --git a/engine/include/amp2203/Params.h b/engine/include/amp2203/Params.h index f974b3b..73123d5 100644 --- a/engine/include/amp2203/Params.h +++ b/engine/include/amp2203/Params.h @@ -21,6 +21,10 @@ struct EngineConfig { /// 1: the power amp (B56) runs at sampleRate; 2: at sampleRate / 2 behind a halfband decimator, its output /// interpolated back (M6). The engine then delays the output by latencySamples(). int powerAmpRateDivisor = 1; + /// M6 X1: the preamp → power amp junction at the master wiper solved exactly (a load port on both roots, + /// the preamp linearised as a Thevenin source each sample) instead of v_oc(y) behind R_th4 (D-015). + /// Needs powerAmpRateDivisor = 1. + bool exactJ4 = false; }; } // namespace amp2203 diff --git a/engine/include/amp2203/PowerAmp.h b/engine/include/amp2203/PowerAmp.h index c90ae20..db1c357 100644 --- a/engine/include/amp2203/PowerAmp.h +++ b/engine/include/amp2203/PowerAmp.h @@ -30,7 +30,7 @@ class PowerAmp { using PI = PhaseInverter; using PS = PowerStage; static constexpr int kParamUpdateInterval = 32; - static constexpr int kNodes = 21, kElements = 42, kRootPorts = 10; + static constexpr int kNodes = 21, kElements = 42, kRootPorts = 11; // 10 tube ports + the preamp at y static constexpr double kDefaultHalfStepTrigger = 100.0; // V; D-043 (0 = off) PowerAmp(); @@ -46,6 +46,14 @@ class PowerAmp { vScr_ = vScreen; vCt_ = vCt; } + /// The preamp as a Thevenin source at y on the root port rY (M6 X1, exact J4 coupling; g = 0 leaves the + /// port open and J4 is the element source). Real-time safe; for the next process() / reset(). + void setPreampPort(double vTh, double g) noexcept { + portVTh_ = vTh; + portG_ = g; + } + /// Current the root port rY draws from the preamp (A, out of the preamp's y). + double preampPortCurrent() const noexcept { return -root_.i[kRootPorts - 1]; } /// R22 value (default PI::R22; 1 TΩ opens the loop for the M4 NFB check). Not real-time. void setFeedbackResistance(double r); /// DC operating point for a constant J4 source voltage (capacitors open, windings shorted). Not real-time. @@ -134,6 +142,7 @@ class PowerAmp { detail::RootNetworkState half_; // matrices at T/2 and the half-step solver state double halfStepTrigger_ = kDefaultHalfStepTrigger; double residualTol_ = 1e-9; + double portVTh_ = 0.0, portG_ = 0.0, vThPrev_ = 0.0; // preamp port at y (M6 X1) long halfSteps_ = 0; double xPrev_ = 0.0; // the previous sample's J4 source voltage (half-step midpoint) std::array bLast_{}; // reflected waves of the reactive elements before this sample's reflected() diff --git a/engine/include/amp2203/Preamp.h b/engine/include/amp2203/Preamp.h index a0c044b..a018916 100644 --- a/engine/include/amp2203/Preamp.h +++ b/engine/include/amp2203/Preamp.h @@ -28,7 +28,7 @@ class Preamp { static constexpr double kSupplyBpA = 389.349603; // CIRCUIT.md §5; B7 sets them in M4 static constexpr double kSupplyBpX = 402.335999; static constexpr int kParamUpdateInterval = 32; - static constexpr int kNodes = 20, kElements = 38, kRootPorts = 8; + static constexpr int kNodes = 20, kElements = 38, kRootPorts = 9; // 8 triode ports + the load at y // Component values, CIRCUIT.md §1 and tubes.lib (SI units). static constexpr double R1 = 2.7e3, R3 = 68e3, R4 = 100e3, R5 = 470e3, R6 = 10e3, R7 = 100e3; @@ -54,6 +54,13 @@ class Preamp { void reset(double vInDc); /// One sample: guitar source voltage in, v_oc(y) out. Real-time safe. double process(double vIn) noexcept; + /// The same sample in steps, with a load current drawn at y (M6 X1, exact J4 coupling): beginSample, then + /// solveLoaded (repeatable: each call restarts from the state before the sample), then finishSample. + void beginSample(double vIn) noexcept; + bool solveLoaded(double iY) noexcept; + /// Linearised Thevenin source at y around the last solveLoaded(iY): v_y = vTh − rTh·i_y. + void theveninAtY(double iY, double& vTh, double& rTh) const noexcept; + void finishSample() noexcept; double output() const noexcept { return out_; } /// Output resistance at the master wiper for J4 (DECISIONS D-015 formula, as ToneStackMaster). @@ -75,6 +82,15 @@ class Preamp { private: using Wave = std::array; double residualTol_ = 1e-9; + // a sample in steps (beginSample / solveLoaded / finishSample) + Wave reflected_{}, incident_{}, eOff_{}; + std::array u_{}; + std::array vOc_{}; + std::array, 4> saved_{}; // v, vPrev, i, vOcPrev before the sample + std::array savedNode_{}; + double vInNow_ = 0.0; + bool ok_ = true; + bool solvedOnce_ = false; // solveLoaded() ran in this sample void applyKnobs() noexcept; Wave conductances(bool dc) const noexcept; diff --git a/engine/src/Amp2203.cpp b/engine/src/Amp2203.cpp index f082093..1012081 100644 --- a/engine/src/Amp2203.cpp +++ b/engine/src/Amp2203.cpp @@ -16,6 +16,7 @@ namespace { constexpr int kDcIterations = 50; constexpr double kDcTolerance = 1e-9; // V, supply node change between fixed-point rounds constexpr double kOpenLoopR22 = 1e12; // Ω: "R22 → 1 TΩ" (SPEC §7 M4 NFB check) +constexpr double kOpenJ4 = 1e12; // Ω: the J4 element source in the exact-J4 mode (M6 X1) /// Appends `src` to `dst`, skipping `skip` and renaming per `rename` ({from, to} pairs). void appendProbes(std::vector& dst, std::span src, std::initializer_list skip, @@ -92,8 +93,15 @@ void Amp2203::solveDc() { const double before[4] = {supply_->ct(), supply_->screen(), supply_->bpX(), supply_->bpA()}; applySupplies(); preamp_->reset(0.0); - power_->setSourceResistance(preamp_->rth4()); - power_->reset(preamp_->output()); + if (exactJ4()) { // J4 element open; the preamp on the root port (at DC no current through C9) + power_->setSourceResistance(kOpenJ4); + power_->setPreampPort(preamp_->output(), 1.0 / preamp_->rth4()); + power_->reset(0.0); + } else { + power_->setPreampPort(0.0, 0.0); + power_->setSourceResistance(preamp_->rth4()); + power_->reset(preamp_->output()); + } supply_->reset({power_->centreTapCurrent(), power_->screenCurrent(), preamp_->supplyCurrentX() + power_->supplyCurrentX(), preamp_->supplyCurrentA()}); const double after[4] = {supply_->ct(), supply_->screen(), supply_->bpX(), supply_->bpA()}; @@ -105,6 +113,7 @@ void Amp2203::solveDc() { } applySupplies(); vOut_ = power_->output(); + iY_ = 0.0; // rate-change filters at the DC point: the decimator holds the preamp output, the interpolator the // zero-stuffed power amp output (2·v at the samples where the power amp runs, 0 between; both polyphase // branches have unity DC gain) @@ -124,6 +133,23 @@ void Amp2203::reset() { double Amp2203::processSample(double x) noexcept { vIn_ = x * cfg_.inputVoltsPerFullScale; applySupplies(); + if (exactJ4()) { + // preamp with the last load current, its linearised Thevenin source at y, the power amp against it, + // then the preamp again with the power amp's actual load current + preamp_->beginSample(vIn_); + preamp_->solveLoaded(iY_); + double vTh = 0.0, rTh = 0.0; + preamp_->theveninAtY(iY_, vTh, rTh); + power_->setPreampPort(vTh, 1.0 / rTh); + vOut_ = power_->process(0.0); + iY_ = power_->preampPortCurrent(); + preamp_->solveLoaded(iY_); + preamp_->finishSample(); + supply_->process({power_->centreTapCurrent(), power_->screenCurrent(), + preamp_->supplyCurrentX() + power_->supplyCurrentX(), preamp_->supplyCurrentA()}); + const double o = vOut_ / cfg_.outputVoltsPerFullScale; + return std::isfinite(o) ? o : 0.0; + } const double y = preamp_->process(vIn_); if (cfg_.powerAmpRateDivisor == 1) { power_->setSourceResistance(preamp_->rth4()); diff --git a/engine/src/PowerAmp.cpp b/engine/src/PowerAmp.cpp index e3e3b32..22a3ef0 100644 --- a/engine/src/PowerAmp.cpp +++ b/engine/src/PowerAmp.cpp @@ -12,10 +12,12 @@ namespace { using namespace stage56; using devices::TriodesPentodes; +using devices::TriodesPentodesLoad; constexpr stage::RootNetwork kNet{kElements, kRoot}; -const solver::MultiportNewton<10> kRootSolver = TriodesPentodes::solver(16); // D-045 (owner decision Q21) -const solver::MultiportNewton<10> kDcSolver = TriodesPentodes::solver(500); // reset() only +// D-045 (owner decision Q21) +const solver::MultiportNewton kRootSolver = TriodesPentodes::solverK(16); +const solver::MultiportNewton kDcSolver = TriodesPentodes::solverK(500); // reset() only constexpr std::array kNoRootOffsets{}; constexpr double kWindingDcConductance = 1e6; // S: the windings are shorts at DC (as PowerStage) constexpr Element kWindings[3] = {eLp1, eLp2, eLs}; @@ -166,11 +168,12 @@ void PowerAmp::halfStep(double vIn) noexcept { tuned.config.residualTol = residualTol_; const auto& newton = residualTol_ == kRootSolver.config.residualTol ? kRootSolver : tuned; double iterations = 0.0; - for (const double x : {0.5 * (xPrev_ + vIn), vIn}) { - j4_.setVoltage(x); + for (const int step : {0, 1}) { + j4_.setVoltage(step == 0 ? 0.5 * (xPrev_ + vIn) : vIn); + const TriodesPentodesLoad dev{step == 0 ? 0.5 * (vThPrev_ + portVTh_) : portVTh_, portG_}; Wave wave; stage::reflected(elements(), wave); - kNet.process(h, newton, TriodesPentodes{}, wave, elementOffsets(), kNoRootOffsets); + kNet.process(h, newton, dev, wave, elementOffsets(), kNoRootOffsets); stage::incident(elements(), wave); iterations += h.iterations; } @@ -216,8 +219,10 @@ void PowerAmp::reset(double vInDc) { sources[eRsB] = vScr_; const Wave eOff = elementOffsets(); const double vb = PS::kBiasVoltage; - kNet.solveDc(root_, kDcSolver, TriodesPentodes{}, conductances(true), sources, eOff, kNoRootOffsets, - {-1.5, 0.5 * vbpX_, -1.5, 0.5 * vbpX_, vb, vScr_ - 5.0, vCt_ - 1.0, vb, vScr_ - 5.0, vCt_ - 1.0}); + kNet.solveDc(root_, kDcSolver, TriodesPentodesLoad{portVTh_, portG_}, conductances(true), sources, eOff, + kNoRootOffsets, + {-1.5, 0.5 * vbpX_, -1.5, 0.5 * vbpX_, vb, vScr_ - 5.0, vCt_ - 1.0, vb, vScr_ - 5.0, vCt_ - 1.0, + portVTh_}); auto across = [&](Element e) { return kNet.elementVoltage(root_, e, eOff); }; c9_.setDcVoltage(across(eC9)); @@ -231,6 +236,7 @@ void PowerAmp::reset(double vInDc) { const auto& v = root_.node; ot_.setDcCurrents({(v[pA] - v[pA1]) / PS::Rwp1, (v[pB1] - v[pB]) / PS::Rwp2, v[sl] / PS::Rws}); xPrev_ = vInDc; + vThPrev_ = portVTh_; updateOutputs(vInDc); } @@ -273,12 +279,13 @@ double PowerAmp::process(double vIn) noexcept { halfStep(vIn); updateOutputs(vIn); } else { - const bool ok = kNet.solve(root_, newton, TriodesPentodes{}, u, vOc, wave, eOff); + const bool ok = kNet.solve(root_, newton, TriodesPentodesLoad{portVTh_, portG_}, u, vOc, wave, eOff); stage::incident(elements(), wave); // on failure wave = reflected: states unchanged if (ok) updateOutputs(vIn); } xPrev_ = vIn; + vThPrev_ = portVTh_; return out_; } diff --git a/engine/src/Preamp.cpp b/engine/src/Preamp.cpp index 3bd7edc..c18cd57 100644 --- a/engine/src/Preamp.cpp +++ b/engine/src/Preamp.cpp @@ -10,10 +10,11 @@ namespace { using namespace stage1234; using devices::FourTriodes; +using devices::FourTriodesLoad; constexpr stage::RootNetwork kNet{kElements, kRoot}; -const solver::MultiportNewton<8> kRootSolver = FourTriodes::solver(12); -const solver::MultiportNewton<8> kDcSolver = FourTriodes::solver(500); // reset() only +const solver::MultiportNewton kRootSolver = FourTriodes::solverK(12); +const solver::MultiportNewton kDcSolver = FourTriodes::solverK(500); // reset() only } // namespace @@ -144,8 +145,8 @@ void Preamp::reset(double vInDc) { sources[eR7] = vbpA_; sources[eR12] = vbpX_; const Wave eOff = elementOffsets(); - kNet.solveDc(root_, kDcSolver, FourTriodes{}, conductances(true), sources, eOff, rootOffsets(), - {-1.0, 0.5 * vbpA_, -1.0, 0.5 * vbpA_, -1.5, 0.5 * vbpX_, -1.5, 0.5 * vbpX_}); + kNet.solveDc(root_, kDcSolver, FourTriodesLoad{}, conductances(true), sources, eOff, rootOffsets(), + {-1.0, 0.5 * vbpA_, -1.0, 0.5 * vbpA_, -1.5, 0.5 * vbpX_, -1.5, 0.5 * vbpX_, 0.0}); auto across = [&](Element e) { return kNet.elementVoltage(root_, e, eOff); }; c1_.setDcVoltage(across(eK1a)); @@ -173,6 +174,13 @@ void Preamp::reset(double vInDc) { } double Preamp::process(double vIn) noexcept { + beginSample(vIn); + solveLoaded(0.0); + finishSample(); + return out_; +} + +void Preamp::beginSample(double vIn) noexcept { bool moving = volume_.step(); moving |= treble_.step(); moving |= middle_.step(); @@ -190,16 +198,77 @@ double Preamp::process(double vIn) noexcept { src4_.setVoltage(vbpA_); src7_.setVoltage(vbpA_); src12_.setVoltage(vbpX_); - Wave wave; - stage::reflected(elements(), wave); + vInNow_ = vIn; + solvedOnce_ = false; + stage::reflected(elements(), reflected_); + eOff_ = elementOffsets(); + kNet.openCircuit(root_, reflected_, eOff_, rootOffsets(), u_, vOc_); + saved_ = {root_.v, root_.vPrev, root_.i, root_.vOcPrev}; + savedNode_ = root_.node; +} + +bool Preamp::solveLoaded(double iY) noexcept { + // the first call of a sample predicts from the last samples; a repeated call (corrected load current) + // starts at the first call's solution with no trend, and the trend is then restored to the last sample's + const bool repeat = solvedOnce_ && ok_; + const auto failures = root_.stats.failures; + if (repeat) { + root_.vPrev = root_.v; // no extrapolation: v is already this sample's solution for the old current + } else { + root_.v = saved_[0]; + root_.vPrev = saved_[1]; + root_.i = saved_[2]; + root_.vOcPrev = saved_[3]; + root_.node = savedNode_; + } auto tuned = kRootSolver; // the M6 tolerance study only (a copy per sample when not the default) tuned.config.residualTol = residualTol_; const auto& newton = residualTol_ == kRootSolver.config.residualTol ? kRootSolver : tuned; - const bool ok = kNet.process(root_, newton, FourTriodes{}, wave, elementOffsets(), rootOffsets()); - stage::incident(elements(), wave); // on failure wave = reflected: states unchanged - if (ok) - updateOutputs(vIn); - return out_; + root_.i[rY] = iY; + incident_ = reflected_; + ok_ = kNet.solve(root_, newton, FourTriodesLoad{iY}, u_, vOc_, incident_, eOff_); + if (repeat && root_.stats.failures == failures) + root_.vPrev = saved_[0]; // the trend for the next sample: this sample's solution vs the last one + solvedOnce_ = true; + return ok_; +} + +void Preamp::theveninAtY(double iY, double& vTh, double& rTh) const noexcept { + // linearised at the solution: v = vOc − Z·i, i_p = i_p(v_p), i_y given. A change of i_y moves the triode + // ports by dv_p = −(I + Z_pp·D)⁻¹·Z_py·di_y, so v_y = vTh − rTh·i_y with + // rTh = Z_yy − Z_yp·D·(I + Z_pp·D)⁻¹·Z_py + constexpr int P = 8; + solver::Vec i; + solver::Mat d9; + FourTriodes::eval(root_.v, i, d9); + solver::Mat

A; + solver::Vec

x; + for (int r = 0; r < P; ++r) { + for (int c = 0; c < P; ++c) { + double s = r == c ? 1.0 : 0.0; + for (int m = FourTriodes::kBlockBegin[c]; m < FourTriodes::kBlockEnd[c]; ++m) + s += root_.Z[r][m] * d9[m][c]; + A[r][c] = s; + } + x[r] = root_.Z[r][rY]; + } + rTh = root_.Z[rY][rY]; + if (solver::solveLinear

(A, x)) { + for (int r = 0; r < P; ++r) { + double dx = 0.0; + for (int m = FourTriodes::kBlockBegin[r]; m < FourTriodes::kBlockEnd[r]; ++m) + dx += d9[r][m] * x[m]; + rTh -= root_.Z[rY][r] * dx; + } + } + vTh = root_.v[rY] + rTh * iY; +} + +void Preamp::finishSample() noexcept { + // on failure incident_ = reflected: states unchanged + stage::incident(elements(), ok_ ? incident_ : reflected_); + if (ok_) + updateOutputs(vInNow_); } } // namespace amp2203 diff --git a/engine/src/devices/FourTriodes.h b/engine/src/devices/FourTriodes.h index 77168b4..267cbd3 100644 --- a/engine/src/devices/FourTriodes.h +++ b/engine/src/devices/FourTriodes.h @@ -11,7 +11,11 @@ struct FourTriodes { static constexpr int kBlockBegin[8] = {0, 0, 2, 2, 4, 4, 6, 6}; // 2×2 blocks (solver::BlockDevice) static constexpr int kBlockEnd[8] = {2, 2, 4, 4, 6, 6, 8, 8}; - void operator()(const solver::Vec<8>& v, solver::Vec<8>& i, solver::Mat<8>& d) const noexcept { + void operator()(const solver::Vec<8>& v, solver::Vec<8>& i, solver::Mat<8>& d) const noexcept { eval<8>(v, i, d); } + + /// The four triodes on ports 0–7 of a K-port root. + template + static void eval(const solver::Vec& v, solver::Vec& i, solver::Mat& d) noexcept { for (int t = 0; t < 4; ++t) { const int g = 2 * t, p = 2 * t + 1; const auto c = k12AX7.eval(v[g], v[p]); @@ -25,6 +29,19 @@ struct FourTriodes { } /// Newton solver with the triode step limits of D-037 on every grid / plate port. + template + static solver::MultiportNewton solverK(int maxIter) noexcept { + const auto s = solver(maxIter); + solver::MultiportNewton r; + r.config = s.config; + for (int k = 0; k < 8; ++k) { + r.maxStep[k] = s.maxStep[k]; + r.maxStepDown[k] = s.maxStepDown[k]; + } + for (int k = 8; k < K; ++k) + r.maxStep[k] = 1e4; // linear ports: no limit in practice + return r; + } static solver::MultiportNewton<8> solver(int maxIter) noexcept { solver::MultiportNewton<8> r; r.config.maxIter = maxIter; @@ -38,4 +55,18 @@ struct FourTriodes { } }; +/// FourTriodes plus a ninth port at the master wiper y that draws a given current (the power amp's input, +/// M6 X1: exact J4 coupling): i = iY, ∂i/∂v = 0. +struct FourTriodesLoad { + static constexpr int kBlockBegin[9] = {0, 0, 2, 2, 4, 4, 6, 6, 8}; + static constexpr int kBlockEnd[9] = {2, 2, 4, 4, 6, 6, 8, 8, 9}; + double iY = 0.0; + + void operator()(const solver::Vec<9>& v, solver::Vec<9>& i, solver::Mat<9>& d) const noexcept { + FourTriodes::eval<9>(v, i, d); + i[8] = iY; + d[8][8] = 0.0; + } +}; + } // namespace amp2203::devices diff --git a/engine/src/devices/TriodesPentodes.h b/engine/src/devices/TriodesPentodes.h index 6402e2e..ae20158 100644 --- a/engine/src/devices/TriodesPentodes.h +++ b/engine/src/devices/TriodesPentodes.h @@ -13,6 +13,12 @@ struct TriodesPentodes { static constexpr int kBlockEnd[10] = {2, 2, 4, 4, 7, 7, 7, 10, 10, 10}; void operator()(const solver::Vec<10>& v, solver::Vec<10>& i, solver::Mat<10>& d) const noexcept { + eval<10>(v, i, d); + } + + /// The two triodes and the two pentode pairs on ports 0–9 of a K-port root. + template + static void eval(const solver::Vec& v, solver::Vec& i, solver::Mat& d) noexcept { for (int t = 0; t < 2; ++t) { const int g = 2 * t, p = 2 * t + 1; const auto c = k12AX7.eval(v[g], v[p]); @@ -41,6 +47,21 @@ struct TriodesPentodes { } } + /// Step limits of both device groups; ports from 10 on (linear) without a practical limit. + template + static solver::MultiportNewton solverK(int maxIter) noexcept { + const auto s = solver(maxIter); + solver::MultiportNewton r; + r.config = s.config; + for (int k = 0; k < 10; ++k) { + r.maxStep[k] = s.maxStep[k]; + r.maxStepDown[k] = s.maxStepDown[k]; + } + for (int k = 10; k < K; ++k) + r.maxStep[k] = 1e4; + return r; + } + /// Step limits of both device groups (D-034, D-037, PowerStage). static solver::MultiportNewton<10> solver(int maxIter) noexcept { const auto t = TwoTriodes::solver(maxIter); @@ -59,4 +80,18 @@ struct TriodesPentodes { } }; +/// TriodesPentodes plus an eleventh port at the PI input y: the preamp as a Thevenin source (vTh behind 1/g), +/// seen as a linear device drawing i = g·(v − vTh) (M6 X1: exact J4 coupling; g = 0: no port). +struct TriodesPentodesLoad { + static constexpr int kBlockBegin[11] = {0, 0, 2, 2, 4, 4, 4, 7, 7, 7, 10}; + static constexpr int kBlockEnd[11] = {2, 2, 4, 4, 7, 7, 7, 10, 10, 10, 11}; + double vTh = 0.0, g = 0.0; + + void operator()(const solver::Vec<11>& v, solver::Vec<11>& i, solver::Mat<11>& d) const noexcept { + TriodesPentodes::eval<11>(v, i, d); + i[10] = g * (v[10] - vTh); + d[10][10] = g; + } +}; + } // namespace amp2203::devices diff --git a/engine/src/stage1234/Stage1234Network.h b/engine/src/stage1234/Stage1234Network.h index 9930c33..3cb8868 100644 --- a/engine/src/stage1234/Stage1234Network.h +++ b/engine/src/stage1234/Stage1234Network.h @@ -22,7 +22,7 @@ enum Element : int { /// Grid and plate port of each triode against its cathode. V2b's plate port is bpX − k2b: stored /// gnd − k2b, offset +v(bpX). -enum RootPort : int { rG1a, rP1a, rG1b, rP1b, rG2a, rP2a, rG2b, rP2b, kNumRootPorts }; +enum RootPort : int { rG1a, rP1a, rG1b, rP1b, rG2a, rP2a, rG2b, rP2b, rY, kNumRootPorts }; // rY: load at y (M6 X1) inline constexpr mna::PortNetwork kElements{{{ {g1a, mna::kGround}, // Vin behind R3 @@ -74,6 +74,7 @@ inline constexpr mna::PortNetwork kRoot{{{ {p2a, k2a}, {p2a, k2b}, {mna::kGround, k2b}, + {y, mna::kGround}, }}}; /// Sign of v(bpX) in each port's offset: (supply at node+) − (supply at node−). @@ -81,6 +82,6 @@ inline constexpr int kElementSupplySign[kNumElements] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B12 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, +1, // B3 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // B4 -inline constexpr int kRootSupplySign[kNumRootPorts] = {0, 0, 0, 0, 0, 0, 0, +1}; +inline constexpr int kRootSupplySign[kNumRootPorts] = {0, 0, 0, 0, 0, 0, 0, +1, 0}; } // namespace amp2203::stage1234 diff --git a/engine/src/stage56/Stage56Network.h b/engine/src/stage56/Stage56Network.h index 7086321..05c1450 100644 --- a/engine/src/stage56/Stage56Network.h +++ b/engine/src/stage56/Stage56Network.h @@ -46,7 +46,7 @@ enum Element : int { }; /// Root: V3a / V3b grid and plate ports against k3, then (g, s, p) of each EL34 pair against ground. -enum RootPort : int { rG3a, rP3a, rG3b, rP3b, rGA, rSA, rPA, rGB, rSB, rPB, kNumRootPorts }; +enum RootPort : int { rG3a, rP3a, rG3b, rP3b, rGA, rSA, rPA, rGB, rSB, rPB, rY, kNumRootPorts }; // rY: preamp at y (M6 X1) inline constexpr mna::PortNetwork kElements{{{ {y, mna::kGround}, @@ -104,6 +104,7 @@ inline constexpr mna::PortNetwork kRoot{{{ {gB, mna::kGround}, {sB, mna::kGround}, {pB, mna::kGround}, + {y, mna::kGround}, }}}; } // namespace amp2203::stage56 diff --git a/harness/config.py b/harness/config.py index 3415ac4..05a41c0 100644 --- a/harness/config.py +++ b/harness/config.py @@ -10,6 +10,8 @@ POWER_DIVISOR = int(os.environ.get('AMP2203_POWER_DIVISOR', 1)) # AMP2203_NEWTON_TOL sets the engine's Newton residual tolerance in volts (M6 study; ampwav --newton-tol) NEWTON_TOL = os.environ.get('AMP2203_NEWTON_TOL') +# AMP2203_J4=exact runs the full amp with the exact preamp -> power amp junction (M6 X1; ampwav --j4 exact) +J4 = os.environ.get('AMP2203_J4', 'thevenin') INPUT_VOLTS_PER_FS = 4.361 # 0 dBFS = +12 dBu at the jack (D-010); scales DI recordings ROOT = Path(__file__).resolve().parent.parent diff --git a/harness/m4_acceptance.py b/harness/m4_acceptance.py index aa82d56..da0bb67 100644 --- a/harness/m4_acceptance.py +++ b/harness/m4_acceptance.py @@ -72,6 +72,8 @@ def engine(knobs, x, nodes=(), nfb=None, seconds=None): args += ['--power-rate-divisor', str(config.POWER_DIVISOR)] if config.NEWTON_TOL: args += ['--newton-tol', config.NEWTON_TOL] + if config.J4 != 'thevenin': + args += ['--j4', config.J4] _ampwav(args) res = {'out': read_wav(d / 'out.wav')[0], 'stats': json.loads((d / 'stats.json').read_text())} for n in nodes: @@ -217,7 +219,7 @@ def one(item): def write_report(rep, res, notes, only=None): # rate studies (AMP2203_FS, M5) go to the M5 folder with the rate in the name; power-amp rate studies # (AMP2203_POWER_DIVISOR, M6) to M6 with the divisor too - m6 = config.POWER_DIVISOR != 1 or config.NEWTON_TOL or (only and 'hot' in only) + m6 = config.POWER_DIVISOR != 1 or config.NEWTON_TOL or config.J4 != 'thevenin' or (only and 'hot' in only) out = config.REPORT_DIR / ('M6' if m6 else 'M4' if config.FS == config.DEFAULT_FS else 'M5') out.mkdir(parents=True, exist_ok=True) stem = f'{datetime.date.today().strftime("%Y%m%d")}_full_acceptance' @@ -227,6 +229,8 @@ def write_report(rep, res, notes, only=None): stem += f'_pdiv{config.POWER_DIVISOR}' if config.NEWTON_TOL: stem += f'_tol{config.NEWTON_TOL}' + if config.J4 != 'thevenin': + stem += f'_j4{config.J4}' if only: # a subset of sections gets its own file stem += '_' + '-'.join(only) npass = sum(r[6] == 'PASS' for r in rep.rows) @@ -235,6 +239,7 @@ def write_report(rep, res, notes, only=None): f'- Date: {datetime.date.today().isoformat()}, engine commit `{_git()}`, ngspice {spice.ngspice_version()}, ' f'fs = {config.FS} Hz' + (f' (power amp at fs / {config.POWER_DIVISOR})' if config.POWER_DIVISOR != 1 else '') + (f', Newton residual tolerance {config.NEWTON_TOL} V' if config.NEWTON_TOL else '') + + (f', J4 {config.J4}' if config.J4 != 'thevenin' else '') + f', SPICE reltol {config.SPICE_RELTOL}', f'- Netlist sha256 `{_sha(config.NETLIST)}`, tubes.lib sha256 `{_sha(config.TUBES)}`', '- Engine: `ampwav --block full` (volts at the jack and across the 16 Ω load), the checks of SPEC §7 M4.', diff --git a/harness/reports/M6/20261014_full_acceptance_fs384000_j4exact.md b/harness/reports/M6/20261014_full_acceptance_fs384000_j4exact.md new file mode 100644 index 0000000..0f3cbdb --- /dev/null +++ b/harness/reports/M6/20261014_full_acceptance_fs384000_j4exact.md @@ -0,0 +1,197 @@ +# M4 acceptance: full amplifier vs the full netlist + +- Date: 2026-10-14, engine commit `f1f3778 (dirty)`, ngspice 47, fs = 384000 Hz, J4 exact, SPICE reltol 1e-08 +- Netlist sha256 `7bea8385e4adb04e`, tubes.lib sha256 `9ec33154be8127d7` +- Engine: `ampwav --block full` (volts at the jack and across the 16 Ω load), the checks of SPEC §7 M4. + +## Summary + +PASS 107 · FAIL 0 + +| Section | Check | Item | Measured | Tolerance | Status | +|---|---|---|---|---|---| +| dc | dc_rel_error | v(ct) | 4.141e-07 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(nscr) | 4.454e-07 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(bpx) | 3.65e-06 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(bpa) | 5.682e-06 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(nbias) | 4.636e-08 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(p1a) | 1.996e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(k1a) | 3.983e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(p1b) | 1.688e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(k1b) | 0.0001012 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(p2a) | 2.198e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(k2a) | 2.11e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(k2b) | 2.143e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(p3a) | 1.578e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(p3b) | 1.709e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(k3) | 2.143e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(nf) | 2.143e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(ng) | 2.143e-05 | ≤ 0.005 | PASS | +| dc | el34_plate_ma | ip_tube_a | 1.194e-05 mA | ≤ 1 | PASS | +| dc | el34_plate_ma | ip_tube_b | 1.195e-05 mA | ≤ 1 | PASS | +| dc | el34_screen_ma | is_tube_a | 1.046e-06 mA | ≤ 0.3 | PASS | +| dc | el34_screen_ma | is_tube_b | 1.044e-06 mA | ≤ 0.3 | PASS | +| dc | ct_rel | v(ct) | 4.141e-07 | ≤ 0.01 | PASS | +| dc | nbias_v | v(nbias) | 2.15e-06 V | ≤ 0.2 | PASS | +| nfb | nfb_gain_db | closed 100 Hz | 0.001954 dB | ≤ 0.5 | PASS | +| nfb | newton_failures | closed 100 Hz | 0 | ≤ 0 | PASS | +| nfb | newton_unrescued | closed 100 Hz | 0 | — | INFO | +| nfb | newton_median_iterations | closed 100 Hz | 1 | ≤ 3 | PASS | +| nfb | newton_max_iterations | closed 100 Hz | 2 | ≤ 16 | PASS | +| nfb | nfb_gain_db | closed 1000 Hz | 0.004996 dB | ≤ 0.5 | PASS | +| nfb | newton_failures | closed 1000 Hz | 0 | ≤ 0 | PASS | +| nfb | newton_unrescued | closed 1000 Hz | 0 | — | INFO | +| nfb | newton_median_iterations | closed 1000 Hz | 2 | ≤ 3 | PASS | +| nfb | newton_max_iterations | closed 1000 Hz | 2 | ≤ 16 | PASS | +| nfb | nfb_gain_db | closed 5000 Hz | 0.01361 dB | ≤ 0.5 | PASS | +| nfb | newton_failures | closed 5000 Hz | 0 | ≤ 0 | PASS | +| nfb | newton_unrescued | closed 5000 Hz | 0 | — | INFO | +| nfb | newton_median_iterations | closed 5000 Hz | 2 | ≤ 3 | PASS | +| nfb | newton_max_iterations | closed 5000 Hz | 2 | ≤ 16 | PASS | +| nfb | nfb_gain_db | open 100 Hz | 0.002342 dB | ≤ 0.5 | PASS | +| nfb | newton_failures | open 100 Hz | 0 | ≤ 0 | PASS | +| nfb | newton_unrescued | open 100 Hz | 0 | — | INFO | +| nfb | newton_median_iterations | open 100 Hz | 1 | ≤ 3 | PASS | +| nfb | newton_max_iterations | open 100 Hz | 2 | ≤ 16 | PASS | +| nfb | nfb_gain_db | open 1000 Hz | 0.0057 dB | ≤ 0.5 | PASS | +| nfb | newton_failures | open 1000 Hz | 0 | ≤ 0 | PASS | +| nfb | newton_unrescued | open 1000 Hz | 0 | — | INFO | +| nfb | newton_median_iterations | open 1000 Hz | 2 | ≤ 3 | PASS | +| nfb | newton_max_iterations | open 1000 Hz | 2 | ≤ 16 | PASS | +| nfb | nfb_gain_db | open 5000 Hz | 0.02095 dB | ≤ 0.5 | PASS | +| nfb | newton_failures | open 5000 Hz | 0 | ≤ 0 | PASS | +| nfb | newton_unrescued | open 5000 Hz | 0 | — | INFO | +| nfb | newton_median_iterations | open 5000 Hz | 2 | ≤ 3 | PASS | +| nfb | newton_max_iterations | open 5000 Hz | 2 | ≤ 16 | PASS | +| clean | total_esr_db | CLEAN SIN-1000Hz-0.025V | -60.06 dB | ≤ -20 | PASS | +| clean | total_harmonics_2_9_db | CLEAN SIN-1000Hz-0.025V | 0.02042 dB | ≤ 1.5 | PASS | +| clean | newton_failures | CLEAN SIN-1000Hz-0.025V | 0 | ≤ 0 | PASS | +| clean | newton_unrescued | CLEAN SIN-1000Hz-0.025V | 0 | — | INFO | +| clean | newton_median_iterations | CLEAN SIN-1000Hz-0.025V | 2 | ≤ 3 | PASS | +| clean | newton_max_iterations | CLEAN SIN-1000Hz-0.025V | 6 | ≤ 16 | PASS | +| clean | total_power_rel | CLEAN SIN-1000Hz-0.025V | 0.001105 | ≤ 0.05 | PASS | +| clean | total_thd_pp | CLEAN SIN-1000Hz-0.025V | 0.0006695 pp | ≤ 0.5 | PASS | +| stability | stability_out_v | max |out| | 4.428e-05 V | ≤ 0.001 | PASS | +| stability | stability_growth | RMS last 10 s − first 10 s | -3.968e-12 V | ≤ 1e-06 | PASS | +| stability | newton_failures | 60 s silence | 0 | ≤ 0 | PASS | +| stability | newton_unrescued | 60 s silence | 0 | — | INFO | +| stability | newton_median_iterations | 60 s silence | 1 | ≤ 3 | PASS | +| stability | newton_max_iterations | 60 s silence | 1 | ≤ 16 | PASS | +| overdrive | total_esr_db | OVERDRIVE SIN-220Hz-0.3V | -67.66 dB | ≤ -20 | PASS | +| overdrive | total_harmonics_2_9_db | OVERDRIVE SIN-220Hz-0.3V | 0.008332 dB | ≤ 1.5 | PASS | +| overdrive | sag_min_v | OVERDRIVE SIN-220Hz-0.3V | 0.004485 V | ≤ 2 | PASS | +| overdrive | newton_failures | OVERDRIVE SIN-220Hz-0.3V | 0 | ≤ 0 | PASS | +| overdrive | newton_unrescued | OVERDRIVE SIN-220Hz-0.3V | 0 | — | INFO | +| overdrive | newton_median_iterations | OVERDRIVE SIN-220Hz-0.3V | 2 | ≤ 3 | PASS | +| overdrive | newton_max_iterations | OVERDRIVE SIN-220Hz-0.3V | 6 | ≤ 16 | PASS | +| crunch_drive | total_esr_db | CRUNCH SIN-220Hz-0.3V | -69.99 dB | ≤ -20 | PASS | +| crunch_drive | total_harmonics_2_9_db | CRUNCH SIN-220Hz-0.3V | 0.01118 dB | ≤ 1.5 | PASS | +| crunch_drive | newton_failures | CRUNCH SIN-220Hz-0.3V | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | CRUNCH SIN-220Hz-0.3V | 0 | — | INFO | +| crunch_drive | newton_median_iterations | CRUNCH SIN-220Hz-0.3V | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | CRUNCH SIN-220Hz-0.3V | 5 | ≤ 16 | PASS | +| crunch_drive | total_esr_db | CRUNCH DI-GTR L | -61.26 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | CRUNCH DI-GTR L | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | CRUNCH DI-GTR L | 0 | — | INFO | +| crunch_drive | newton_median_iterations | CRUNCH DI-GTR L | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | CRUNCH DI-GTR L | 8 | ≤ 16 | PASS | +| crunch_drive | total_esr_db | CRUNCH DI-GTR LL | -61.57 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | CRUNCH DI-GTR LL | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | CRUNCH DI-GTR LL | 0 | — | INFO | +| crunch_drive | newton_median_iterations | CRUNCH DI-GTR LL | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | CRUNCH DI-GTR LL | 8 | ≤ 16 | PASS | +| crunch_drive | total_esr_db | CRUNCH DI-GTR R | -62.34 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | CRUNCH DI-GTR R | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | CRUNCH DI-GTR R | 0 | — | INFO | +| crunch_drive | newton_median_iterations | CRUNCH DI-GTR R | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | CRUNCH DI-GTR R | 8 | ≤ 16 | PASS | +| crunch_drive | total_esr_db | CRUNCH DI-GTR RR | -61.11 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | CRUNCH DI-GTR RR | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | CRUNCH DI-GTR RR | 0 | — | INFO | +| crunch_drive | newton_median_iterations | CRUNCH DI-GTR RR | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | CRUNCH DI-GTR RR | 11 | ≤ 16 | PASS | +| crunch_drive | total_esr_db | DRIVE SIN-220Hz-0.3V | -63.6 dB | ≤ -20 | PASS | +| crunch_drive | total_harmonics_2_9_db | DRIVE SIN-220Hz-0.3V | 0.03526 dB | ≤ 1.5 | PASS | +| crunch_drive | newton_failures | DRIVE SIN-220Hz-0.3V | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | DRIVE SIN-220Hz-0.3V | 0 | — | INFO | +| crunch_drive | newton_median_iterations | DRIVE SIN-220Hz-0.3V | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | DRIVE SIN-220Hz-0.3V | 6 | ≤ 16 | PASS | +| crunch_drive | total_esr_db | DRIVE DI-GTR L | -60.62 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | DRIVE DI-GTR L | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | DRIVE DI-GTR L | 0 | — | INFO | +| crunch_drive | newton_median_iterations | DRIVE DI-GTR L | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | DRIVE DI-GTR L | 9 | ≤ 16 | PASS | +| crunch_drive | total_esr_db | DRIVE DI-GTR LL | -61.56 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | DRIVE DI-GTR LL | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | DRIVE DI-GTR LL | 0 | — | INFO | +| crunch_drive | newton_median_iterations | DRIVE DI-GTR LL | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | DRIVE DI-GTR LL | 9 | ≤ 16 | PASS | +| crunch_drive | total_esr_db | DRIVE DI-GTR R | -61.32 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | DRIVE DI-GTR R | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | DRIVE DI-GTR R | 0 | — | INFO | +| crunch_drive | newton_median_iterations | DRIVE DI-GTR R | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | DRIVE DI-GTR R | 9 | ≤ 16 | PASS | +| crunch_drive | total_esr_db | DRIVE DI-GTR RR | -60.57 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | DRIVE DI-GTR RR | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | DRIVE DI-GTR RR | 0 | — | INFO | +| crunch_drive | newton_median_iterations | DRIVE DI-GTR RR | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | DRIVE DI-GTR RR | 9 | ≤ 16 | PASS | + +## DC operating point + +| Node / current | SPICE | Engine | +|---|---|---| +| ct | 488.7084 V | 488.7086 V | +| nscr | 485.6493 V | 485.6495 V | +| bpx | 402.3360 V | 402.3375 V | +| bpa | 389.3496 V | 389.3518 V | +| nbias | -46.3636 V | -46.3636 V | +| p1a | 296.4025 V | 296.4084 V | +| k1a | 2.5096 V | 2.5095 V | +| p1b | 352.4328 V | 352.4387 V | +| k1b | 3.6917 V | 3.6913 V | +| p2a | 231.1295 V | 231.1346 V | +| k2a | 1.4039 V | 1.4039 V | +| k2b | 231.6858 V | 231.6908 V | +| p3a | 270.8809 V | 270.8852 V | +| p3b | 262.2706 V | 262.2751 V | +| k3 | 44.9334 V | 44.9324 V | +| nf | 43.5216 V | 43.5207 V | +| ng | 13.4840 V | 13.4837 V | +| ip_tube_a | 34.0783 mA | 34.0783 mA | +| ip_tube_b | 34.0780 mA | 34.0780 mA | +| is_tube_a | 4.5674 mA | 4.5674 mA | +| is_tube_b | 4.5675 mA | 4.5675 mA | + +## NFB (gain in → out, dB) + +| f | SPICE closed | engine closed | SPICE open | engine open | NFB SPICE | NFB engine | +|---|---|---|---|---|---|---| +| 100 Hz | 43.44 | 43.45 | 50.07 | 50.07 | 6.63 | 6.63 | +| 1000 Hz | 55.58 | 55.59 | 61.74 | 61.74 | 6.16 | 6.16 | +| 5000 Hz | 68.07 | 68.09 | 74.12 | 74.14 | 6.04 | 6.05 | + +## Signals + +| Preset | Signal | ESR | best lag | harm. 2–9 | THD F / E | Power F / E | min v(ct) F / E | +|---|---|---|---|---|---|---|---| +| CLEAN | SIN-1000Hz-0.025V | -60.1 dB | 0 | 0.02 dB | 3.47 / 3.47 % | 100.2 / 100.3 W | — | +| OVERDRIVE | SIN-220Hz-0.3V | -67.7 dB | 0 | 0.01 dB | 23.35 / 23.35 % | 33.8 / 33.8 W | 469.82 / 469.81 V | +| CRUNCH | SIN-220Hz-0.3V | -70.0 dB | 0 | 0.01 dB | 15.04 / 15.04 % | 32.9 / 32.9 W | — | +| CRUNCH | DI-GTR L | -61.3 dB | 0 | — | — | — | — | +| CRUNCH | DI-GTR LL | -61.6 dB | 0 | — | — | — | — | +| CRUNCH | DI-GTR R | -62.3 dB | 0 | — | — | — | — | +| CRUNCH | DI-GTR RR | -61.1 dB | 0 | — | — | — | — | +| DRIVE | SIN-220Hz-0.3V | -63.6 dB | 0 | 0.04 dB | 25.98 / 25.99 % | 36.2 / 36.2 W | — | +| DRIVE | DI-GTR L | -60.6 dB | 0 | — | — | — | — | +| DRIVE | DI-GTR LL | -61.6 dB | 0 | — | — | — | — | +| DRIVE | DI-GTR R | -61.3 dB | 0 | — | — | — | — | +| DRIVE | DI-GTR RR | -60.6 dB | 0 | — | — | — | — | + +## Stability + +60 s silence at Preamp / Master / Presence 1.0: max |out| 4.43e-05 V, RMS first / last 10 s 4.23e-05 / 4.23e-05 V. + +## Delayed-NFB break + +Not applicable: since D-041 the global NFB (R22) is part of the B56 tree and has no delay (SPEC §7 M4 asked for a 1- vs 2-sample comparison of the delayed design). diff --git a/harness/reports/M6/20261014_full_acceptance_fs384000_j4exact_hot.md b/harness/reports/M6/20261014_full_acceptance_fs384000_j4exact_hot.md new file mode 100644 index 0000000..879b42e --- /dev/null +++ b/harness/reports/M6/20261014_full_acceptance_fs384000_j4exact_hot.md @@ -0,0 +1,52 @@ +# M4 acceptance: full amplifier vs the full netlist + +- Date: 2026-10-14, engine commit `f1f3778 (dirty)`, ngspice 47, fs = 384000 Hz, J4 exact, SPICE reltol 1e-08 +- Netlist sha256 `7bea8385e4adb04e`, tubes.lib sha256 `9ec33154be8127d7` +- Engine: `ampwav --block full` (volts at the jack and across the 16 Ω load), the checks of SPEC §7 M4. + +## Summary + +PASS 17 · FAIL 4 + +| Section | Check | Item | Measured | Tolerance | Status | +|---|---|---|---|---|---| +| hot | total_esr_db | HOT SIN-220Hz-0.3V | -58.9 dB | ≤ -20 | PASS | +| hot | total_harmonics_2_9_db | HOT SIN-220Hz-0.3V | 0.01274 dB | ≤ 1.5 | PASS | +| hot | newton_failures | HOT SIN-220Hz-0.3V | 0 | ≤ 0 | PASS | +| hot | newton_unrescued | HOT SIN-220Hz-0.3V | 0 | — | INFO | +| hot | newton_median_iterations | HOT SIN-220Hz-0.3V | 2 | ≤ 3 | PASS | +| hot | newton_max_iterations | HOT SIN-220Hz-0.3V | 7 | ≤ 16 | PASS | +| hot | total_esr_db | HOT DI-GTR L | -41.45 dB | ≤ -20 | PASS | +| hot | newton_failures | HOT DI-GTR L | 5 | ≤ 0 | FAIL | +| hot | newton_unrescued | HOT DI-GTR L | 0 | — | INFO | +| hot | newton_median_iterations | HOT DI-GTR L | 2 | ≤ 3 | PASS | +| hot | newton_max_iterations | HOT DI-GTR L | 16 | ≤ 16 | PASS | +| hot | total_esr_db | HOT DI-GTR LL | -42.13 dB | ≤ -20 | PASS | +| hot | newton_failures | HOT DI-GTR LL | 6 | ≤ 0 | FAIL | +| hot | newton_unrescued | HOT DI-GTR LL | 0 | — | INFO | +| hot | newton_median_iterations | HOT DI-GTR LL | 2 | ≤ 3 | PASS | +| hot | newton_max_iterations | HOT DI-GTR LL | 16 | ≤ 16 | PASS | +| hot | total_esr_db | HOT DI-GTR R | -42.66 dB | ≤ -20 | PASS | +| hot | newton_failures | HOT DI-GTR R | 6 | ≤ 0 | FAIL | +| hot | newton_unrescued | HOT DI-GTR R | 0 | — | INFO | +| hot | newton_median_iterations | HOT DI-GTR R | 2 | ≤ 3 | PASS | +| hot | newton_max_iterations | HOT DI-GTR R | 16 | ≤ 16 | PASS | +| hot | total_esr_db | HOT DI-GTR RR | -41.83 dB | ≤ -20 | PASS | +| hot | newton_failures | HOT DI-GTR RR | 8 | ≤ 0 | FAIL | +| hot | newton_unrescued | HOT DI-GTR RR | 0 | — | INFO | +| hot | newton_median_iterations | HOT DI-GTR RR | 2 | ≤ 3 | PASS | +| hot | newton_max_iterations | HOT DI-GTR RR | 16 | ≤ 16 | PASS | + +## Signals + +| Preset | Signal | ESR | best lag | harm. 2–9 | THD F / E | Power F / E | min v(ct) F / E | +|---|---|---|---|---|---|---|---| +| HOT | SIN-220Hz-0.3V | -58.9 dB | 0 | 0.01 dB | 46.17 / 46.17 % | 183.4 / 183.4 W | — | +| HOT | DI-GTR L | -41.5 dB | 0 | — | — | — | — | +| HOT | DI-GTR LL | -42.1 dB | 0 | — | — | — | — | +| HOT | DI-GTR R | -42.7 dB | 0 | — | — | — | — | +| HOT | DI-GTR RR | -41.8 dB | 0 | — | — | — | — | + +## Delayed-NFB break + +Not applicable: since D-041 the global NFB (R22) is part of the B56 tree and has no delay (SPEC §7 M4 asked for a 1- vs 2-sample comparison of the delayed design). diff --git a/tools/ampwav/src/Cli.cpp b/tools/ampwav/src/Cli.cpp index 006ce8b..b718abb 100644 --- a/tools/ampwav/src/Cli.cpp +++ b/tools/ampwav/src/Cli.cpp @@ -58,7 +58,8 @@ void writeStats(const std::string& path, const Options& o, double rate, size_t s << " \"newton_rescued\": " << st.newtonRescued << ",\n" << " \"newton_unrescued\": " << st.newtonUnrescued << ",\n" << " \"power_rate_divisor\": " << o.powerRateDivisor << ",\n" - << " \"newton_residual_tol\": " << o.newtonTol.value_or(1e-9) << "\n" + << " \"newton_residual_tol\": " << o.newtonTol.value_or(1e-9) << ",\n" + << " \"exact_j4\": " << (o.exactJ4 ? "true" : "false") << "\n" << "}\n"; } @@ -70,6 +71,7 @@ void printUsage(std::ostream& os) { " [--units volts|fs] [--block-size N] [--stats stats.json] [--list-nodes]\n" " [--knob-ramp knob=start,duration,from,to ...] [--supply node=file.wav ...]\n" " [--nfb closed|open] [--power-rate-divisor 1|2] [--newton-tol V]\n" + " [--j4 thevenin|exact]\n" "\n" " --block full, preamp (= b1234), b12, b3, b34, b4, b5, b6, b56; b7 is a pass-through\n" " --in mono WAV; 64-bit float is read bit-exact, other formats via 32-bit float\n" @@ -90,6 +92,7 @@ void printUsage(std::ostream& os) { " --power-rate-divisor full amp: 2 runs the power amp at half the rate behind halfband filters (M6);\n" " the filters' delay is removed from the output and the dumps\n" " --newton-tol full amp: Newton residual tolerance in volts (default 1e-9, M6 study)\n" + " --j4 full amp: preamp -> power amp junction: thevenin (default, D-015) or exact (M6 X1)\n" " --supply drive a recorded input of the block from a WAV of the same length and rate, set every\n" " sample: supply nodes (bpa, bpx, ct, nscr), the NFB speaker voltage (b5: out) or the\n" " second PI output (b6: p3b) (partition studies and B5 / B6 tests, VERIFICATION §1)\n"; @@ -172,6 +175,14 @@ std::optional parseArgs(const std::vector& args, std::stri return std::nullopt; } o.halfStepTrigger = x; + } else if (a == "--j4") { + std::string v; + if (!value(v)) return std::nullopt; + if (v != "thevenin" && v != "exact") { + error = "--j4 expects thevenin or exact, got '" + v + "'"; + return std::nullopt; + } + o.exactJ4 = v == "exact"; } else if (a == "--newton-tol") { std::string v; if (!value(v)) return std::nullopt; @@ -287,6 +298,10 @@ int run(const Options& o, std::ostream& log) { } const double rate = in.sampleRate; + if (o.exactJ4 && !runner->setExactJ4(true)) { + log << "ampwav: block '" << o.block << "' has no J4 option\n"; + return 1; + } if (o.powerRateDivisor != 1 && !runner->setPowerRateDivisor(o.powerRateDivisor)) { log << "ampwav: block '" << o.block << "' has no power-amp rate option\n"; return 1; diff --git a/tools/ampwav/src/Cli.h b/tools/ampwav/src/Cli.h index d4f240a..c1ac014 100644 --- a/tools/ampwav/src/Cli.h +++ b/tools/ampwav/src/Cli.h @@ -42,6 +42,7 @@ struct Options { std::string nfb; // --nfb closed | open (full amp only; empty = closed) int powerRateDivisor = 1; // --power-rate-divisor 1 | 2 (full amp, M6) std::optional newtonTol; // --newton-tol V (full amp, M6 study) + bool exactJ4 = false; // --j4 exact (full amp, M6 X1) Units units = Units::Volts; int blockSize = 512; bool listNodes = false; diff --git a/tools/ampwav/src/Runner.cpp b/tools/ampwav/src/Runner.cpp index d554206..9f0675c 100644 --- a/tools/ampwav/src/Runner.cpp +++ b/tools/ampwav/src/Runner.cpp @@ -36,6 +36,10 @@ class FullRunner final : public Runner { amp_.setNewtonResidualTolerance(v); return true; } + bool setExactJ4(bool on) override { + cfg_.exactJ4 = on; + return true; + } bool setPowerRateDivisor(int d) override { cfg_.powerAmpRateDivisor = d; return true; diff --git a/tools/ampwav/src/Runner.h b/tools/ampwav/src/Runner.h index 81d1b73..5362f44 100644 --- a/tools/ampwav/src/Runner.h +++ b/tools/ampwav/src/Runner.h @@ -28,6 +28,8 @@ class Runner { virtual bool setHalfStepTrigger(double) { return false; } /// Newton residual tolerance in volts (full amp, M6 study). False if the block has none. virtual bool setNewtonResidualTolerance(double) { return false; } + /// Exact J4 coupling (full amp, M6 X1); call before prepare(). False if the block has none. + virtual bool setExactJ4(bool) { return false; } /// Power amp at sampleRate / divisor (full amp, M6); call before prepare(). False if the block has none. virtual bool setPowerRateDivisor(int) { return false; } /// Output delay in samples added by the block (full amp at divisor 2); ampwav removes it. From 313e065aa8b57f9e848f48ba5cd98c31ef0389ee Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Wed, 14 Oct 2026 22:45:51 +0300 Subject: [PATCH 02/18] m6 X3: experiment script, PyTorch pin (D-050) --- CLAUDE.md | 1 + docs/DECISIONS.md | 8 + harness/x3_neural.py | 362 +++++++++++++++++++++++++++++++++++++++++++ requirements-ml.txt | 2 + 4 files changed, 373 insertions(+) create mode 100644 harness/x3_neural.py create mode 100644 requirements-ml.txt diff --git a/CLAUDE.md b/CLAUDE.md index 73c6dab..b33f538 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,6 +78,7 @@ python -m harness.render_spice --block --preset # SPICE reference (ca python -m harness.run_engine --block --preset python -m harness.compare --block --preset --milestone M # renders + runs + report python -m unittest discover -s harness/tests -t . # harness unit tests +python -m harness.x3_neural render|train|evaluate # M6 X3: LSTM trained on the engine (needs requirements-ml.txt, D-050) ``` Planned: none (all M0 commands exist; later milestones add theirs here). diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 26e74b7..e7857c7 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -874,3 +874,11 @@ Decision: Consequences: SPEC §6.3 lists Power Amp Rate. The HOT preset study (PROGRESS.md M6) found the J4 partition error at high Master, independent of these changes. +## D-050 PyTorch 2.14.1 for the neural-model experiment (X3) +Date: 2026-10-14 · Status: accepted (owner decision: "PyTorch 2.14.1") +Context: ARCHITECTURE_STUDY.md §6 X3 trains a small recurrent network on the engine's output. +Decision: `torch==2.14.1`, pinned in `requirements-ml.txt` (separate from `requirements.txt`; the harness and +the engine do not need it). Inference cost is measured with hand-written C++ in the experiment, so the engine and +plugin have no new dependency. +Consequences: `pip install -r requirements-ml.txt` in the venv before `python -m harness.x3_neural`. + diff --git a/harness/x3_neural.py b/harness/x3_neural.py new file mode 100644 index 0000000..91941b8 --- /dev/null +++ b/harness/x3_neural.py @@ -0,0 +1,362 @@ +"""M6 experiment X3 (docs/ARCHITECTURE_STUDY.md §6): a small recurrent network trained on the engine's output at +one knob setting (CRUNCH), evaluated against SPICE, timed in C++, checked for aliasing. + + python -m harness.x3_neural render # teacher: ampwav full amp at 768 kHz on the DI tracks, 4 input gains + python -m harness.x3_neural train # LSTM-32 at 48 kHz (PyTorch, MPS or CPU) + python -m harness.x3_neural evaluate # vs SPICE CRUNCH DI references, aliasing, C++ cost; writes the report + +Data: the four DI tracks resampled to 48 kHz (the student's rate) and from there to 768 kHz for the teacher, so +both see the same band-limited input. Teacher output decimated to 48 kHz. Held out: the first 5 s of every track +(the SPICE CRUNCH DI references) and the last 20 s (validation). Units: full-scale samples as in the plugin +(input × 4.361 V, output / 60 V). +""" +import argparse +import datetime +import json +import os +import subprocess +import sys +import tempfile +import time +from concurrent.futures import ThreadPoolExecutor +from math import gcd +from pathlib import Path + +import numpy as np +from scipy.signal import resample_poly + +from . import config, metrics +from .wavio import read_wav, write_wav + +RATE = 48000 +TEACHER_FACTOR = 16 +KNOBS = {'preampVolume': 0.6, 'treble': 0.5, 'middle': 0.5, 'bass': 0.5, 'master': 0.3, 'presence': 0.5} # CRUNCH +GAINS_DB = (-6.0, 0.0, 6.0, 12.0) +HOLD_HEAD_S, HOLD_TAIL_S = 5.0, 20.0 +CACHE = config.HARNESS / 'cache' / 'x3' +AMPWAV = config.ROOT / 'build' / 'tools' / 'ampwav' / 'ampwav' +HIDDEN = 32 + + +def _tracks(): + out = {} + for f in sorted(Path(config.DI_DIR).glob('*.wav')): + x, rate = read_wav(f, mono=True) + g = gcd(int(rate), RATE) + out[f.stem] = resample_poly(x, RATE // g, int(rate) // g) + return out + + +def _teacher(x48): + """Full amp at 768 kHz on a 48 kHz full-scale input; output decimated to 48 kHz (full scale).""" + hi = resample_poly(x48, TEACHER_FACTOR, 1) + with tempfile.TemporaryDirectory(prefix='amp2203_x3_') as d: + d = Path(d) + write_wav(d / 'in.wav', hi, RATE * TEACHER_FACTOR) + args = [str(AMPWAV), '--block', 'full', '--in', str(d / 'in.wav'), '--out', str(d / 'out.wav'), + '--units', 'fs', '--rate', str(RATE * TEACHER_FACTOR)] + args += [a for k, v in KNOBS.items() for a in ('--param', f'{k}={v!r}')] + r = subprocess.run(args, capture_output=True, text=True) + if r.returncode != 0: + raise RuntimeError(r.stderr) + y, _ = read_wav(d / 'out.wav') + return resample_poly(y, 1, TEACHER_FACTOR) + + +def render(): + CACHE.mkdir(parents=True, exist_ok=True) + tracks = _tracks() + jobs = [(name, gdb) for name in tracks for gdb in GAINS_DB] + + def one(job): + name, gdb = job + path = CACHE / f'{name}_{gdb:+.0f}dB.npz' + if path.exists(): + return path, 0.0 + x = tracks[name] * 10 ** (gdb / 20) + t = time.time() + y = _teacher(x) + np.savez(path, x=x, y=y[:len(x)]) + return path, time.time() - t + + with ThreadPoolExecutor(max_workers=os.cpu_count() - 2) as ex: + for path, secs in ex.map(one, jobs): + print(f'x3: {path.name} ({secs:.0f} s)') + + +def _load(split): + """Arrays (x, y) for 'train' (between the held-out head and tail) or 'val' (the tail).""" + xs, ys = [], [] + h, t = int(HOLD_HEAD_S * RATE), int(HOLD_TAIL_S * RATE) + for f in sorted(CACHE.glob('*dB.npz')): + d = np.load(f) + x, y = d['x'], d['y'] + s = slice(h, len(x) - t) if split == 'train' else slice(len(x) - t, len(x)) + xs.append(x[s].astype(np.float32)) + ys.append(y[s].astype(np.float32)) + return xs, ys + + +def _model(): + import torch + + class Rnn(torch.nn.Module): + """LSTM-32 + linear output + input skip (Wright et al. 2020).""" + + def __init__(self): + super().__init__() + self.rnn = torch.nn.LSTM(1, HIDDEN, batch_first=True) + self.lin = torch.nn.Linear(HIDDEN, 1) + + def forward(self, x, state=None): + h, state = self.rnn(x, state) + return self.lin(h) + x, state + + return Rnn() + + +def _esr_loss(y, t): + import torch + # pre-emphasis 1 − 0.85 z⁻¹ (Wright et al.) plus a DC term + pe = lambda s: s[:, 1:] - 0.85 * s[:, :-1] + e = torch.sum((pe(t) - pe(y)) ** 2) / (torch.sum(pe(t) ** 2) + 1e-10) + dc = torch.mean(torch.mean(t - y, dim=1) ** 2) / (torch.mean(t ** 2) + 1e-10) + return e + dc + + +def train(epochs=400, seg=RATE // 2, batch=48, tbptt=2048, warm=1000): + import torch + torch.manual_seed(0) + np.random.seed(0) + dev = 'mps' if torch.backends.mps.is_available() else 'cpu' + xs, ys = _load('train') + vx, vy = _load('val') + model = _model().to(dev) + opt = torch.optim.Adam(model.parameters(), lr=5e-3) + sched = torch.optim.lr_scheduler.ReduceLROnPlateau(opt, factor=0.5, patience=10) + best, log = float('inf'), [] + t0 = time.time() + for ep in range(epochs): + model.train() + bx, by = [], [] + for _ in range(batch): + k = np.random.randint(len(xs)) + s = np.random.randint(0, len(xs[k]) - seg) + bx.append(xs[k][s:s + seg]) + by.append(ys[k][s:s + seg]) + bx = torch.tensor(np.stack(bx))[:, :, None].to(dev) + by = torch.tensor(np.stack(by))[:, :, None].to(dev) + with torch.no_grad(): # warm the state, not scored + _, state = model(bx[:, :warm]) + total = 0.0 + for s in range(warm, seg, tbptt): + state = tuple(z.detach() for z in state) + out, state = model(bx[:, s:s + tbptt], state) + loss = _esr_loss(out, by[:, s:s + tbptt]) + opt.zero_grad() + loss.backward() + opt.step() + total += float(loss) + if ep % 5 == 4 or ep == epochs - 1: + v = validate(model, vx, vy, dev) + sched.step(v) + log.append((ep + 1, total, v, time.time() - t0)) + print(f'x3: epoch {ep + 1} train {total:.4f} val ESR {10 * np.log10(v):.1f} dB ({time.time() - t0:.0f} s)', + flush=True) + if v < best: + best = v + torch.save(model.state_dict(), CACHE / 'lstm32.pt') + (CACHE / 'train_log.json').write_text(json.dumps(log)) + + +def validate(model, vx, vy, dev): + import torch + model.eval() + num = den = 0.0 + with torch.no_grad(): + for x, y in zip(vx, vy): + out, _ = model(torch.tensor(x)[None, :, None].to(dev)) + o = out[0, :, 0].cpu().numpy() + num += np.sum((o[RATE:] - y[RATE:]) ** 2) + den += np.sum(y[RATE:] ** 2) + return num / den + + +def _student(x): + import torch + model = _model() + model.load_state_dict(torch.load(CACHE / 'lstm32.pt', map_location='cpu')) + model.eval() + with torch.no_grad(): + out, _ = model(torch.tensor(x.astype(np.float32))[None, :, None]) + return out[0, :, 0].numpy().astype(np.float64) + + +def _band48(y, rate): + g = gcd(int(rate), RATE) + return resample_poly(y, RATE // g, int(rate) // g) + + +def _spice_crunch_di(): + """SPICE CRUNCH DI references (5 s, full netlist at config.FS) as {track: (x48 full scale, out48 full scale)}.""" + from . import cascade, signals + from .blocks import BLOCKS + from .presets import PRESETS + texts = cascade._texts() + knobs = PRESETS['CRUNCH'].all_knobs() + out = {} + for sig in signals.di_signals(): + f = cascade.render_f(texts, knobs, sig) + name = sig.id.replace('DI-', '') + out[name] = (f['out'] / 60.0) + return out + + +def _alias_test(student_fn): + """5 kHz and 9 kHz sines at two levels: power at non-harmonic frequencies relative to the total (48 kHz), + student vs teacher (768 kHz, decimated).""" + rows = [] + n = RATE + t = np.arange(2 * n) / RATE + for f0 in (5000.0, 9000.0): + for amp in (0.1, 0.4): + x = amp * np.sin(2 * np.pi * f0 * t) + ys = student_fn(x)[n:] + yt = _teacher(x)[n:2 * n] + + def nonharm(y): + w = np.hanning(len(y)) + Y = np.abs(np.fft.rfft(y * w)) ** 2 + fr = np.fft.rfftfreq(len(y), 1 / RATE) + harm = np.zeros_like(Y, bool) + for k in range(0, int(RATE / 2 / f0) + 1): + harm |= np.abs(fr - k * f0) < 30 + return 10 * np.log10(Y[~harm].sum() / Y.sum()) + rows.append((f0, amp, nonharm(yt), nonharm(ys))) + return rows + + +def _cpp_cost(): + """ns per sample of a hand-written LSTM-32 (same weights) in C++ (-O3), single thread.""" + import torch + sd = torch.load(CACHE / 'lstm32.pt', map_location='cpu') + w = np.concatenate([sd[k].numpy().ravel() for k in + ('rnn.weight_ih_l0', 'rnn.weight_hh_l0', 'rnn.bias_ih_l0', 'rnn.bias_hh_l0', 'lin.weight', + 'lin.bias')]).astype(np.float32) + src = CACHE / 'lstm_bench.cpp' + src.write_text(r''' +#include +#include +#include +#include +constexpr int H = %d; +int main(int, char** argv) { + std::vector w(4 * H + 4 * H * H + 8 * H + H + 1); + FILE* f = std::fopen(argv[1], "rb"); std::fread(w.data(), 4, w.size(), f); std::fclose(f); + const float* wih = w.data(); const float* whh = wih + 4 * H; const float* bih = whh + 4 * H * H; + const float* bhh = bih + 4 * H; const float* lw = bhh + 4 * H; const float lb = lw[H]; + float b[4 * H]; for (int k = 0; k < 4 * H; ++k) b[k] = bih[k] + bhh[k]; + float h[H] = {}, c[H] = {}, g[4 * H]; + const int n = 48000 * 20; double sink = 0; + auto t0 = std::chrono::steady_clock::now(); + for (int s = 0; s < n; ++s) { + const float x = 0.3f * std::sin(0.01f * s); + for (int k = 0; k < 4 * H; ++k) { + float a = b[k] + wih[k] * x; + const float* r = whh + k * H; + for (int j = 0; j < H; ++j) a += r[j] * h[j]; + g[k] = a; + } + float y = lb + x; + for (int j = 0; j < H; ++j) { + const float i = 1 / (1 + std::exp(-g[j])), fg = 1 / (1 + std::exp(-g[H + j])); + const float gg = std::tanh(g[2 * H + j]), o = 1 / (1 + std::exp(-g[3 * H + j])); + c[j] = fg * c[j] + i * gg; + h[j] = o * std::tanh(c[j]); + y += lw[j] * h[j]; + } + sink += y; + } + const double ns = std::chrono::duration(std::chrono::steady_clock::now() - t0).count() * 1e9 / n; + std::printf("%%.1f %%g\n", ns, sink); +} +''' % HIDDEN) + wb = CACHE / 'lstm32.bin' + w.tofile(wb) + exe = CACHE / 'lstm_bench' + subprocess.run(['clang++', '-std=c++20', '-O3', '-ffast-math', str(src), '-o', str(exe)], check=True) + ns = min(float(subprocess.run([str(exe), str(wb)], capture_output=True, text=True).stdout.split()[0]) + for _ in range(3)) + return ns + + +def evaluate(): + tracks = _tracks() + spice = _spice_crunch_di() + rows = [] + for name, ref_hi in spice.items(): + n5 = int(HOLD_HEAD_S * RATE) + x = tracks[name][:n5] + ref = _band48(ref_hi, config.FS)[:n5] + teach = _teacher(x)[:n5] + stud = _student(x)[:n5] + s = slice(int(0.1 * RATE), n5) # the student starts from a zero state + rows.append((name, metrics.esr_db(ref[s], teach[s]), metrics.esr_db(ref[s], stud[s]), + metrics.esr_db(teach[s], stud[s]))) + print(f'x3: {name}: teacher vs SPICE {rows[-1][1]:.1f} dB, student vs SPICE {rows[-1][2]:.1f} dB, ' + f'student vs teacher {rows[-1][3]:.1f} dB', flush=True) + vx, vy = _load('val') + import torch + model = _model() + model.load_state_dict(torch.load(CACHE / 'lstm32.pt', map_location='cpu')) + val = validate(model, vx, vy, 'cpu') + alias = _alias_test(_student) + ns = _cpp_cost() + log = json.loads((CACHE / 'train_log.json').read_text()) + write_report(rows, val, alias, ns, log) + + +def write_report(rows, val, alias, ns, log): + out = config.REPORT_DIR / 'M6' + out.mkdir(parents=True, exist_ok=True) + path = out / f'{datetime.date.today().strftime("%Y%m%d")}_x3_neural.md' + lines = ['# M6 experiment X3: LSTM-32 trained on the engine (CRUNCH), vs SPICE', '', + f'- Date {datetime.date.today().isoformat()}; teacher `ampwav --block full` at {RATE * TEACHER_FACTOR} Hz, ' + f'knobs CRUNCH {KNOBS}; DI tracks at input gains {GAINS_DB} dB; student at {RATE} Hz ' + f'(LSTM {HIDDEN} + linear + input skip, PyTorch, pre-emphasised ESR loss).', + f'- Held out: first {HOLD_HEAD_S:g} s (SPICE references, full netlist at {config.FS} Hz) and last ' + f'{HOLD_TAIL_S:g} s of every track (validation). Training: {log[-1][0]} epochs, {log[-1][3]:.0f} s.', + '', '## Accuracy (48 kHz band, first 0.1 s excluded)', '', + '| Track | Teacher vs SPICE | Student vs SPICE | Student vs teacher |', '|---|---|---|---|'] + for name, a, b, c in rows: + lines.append(f'| {name} | {a:.1f} dB | {b:.1f} dB | {c:.1f} dB |') + lines += ['', f'Validation (last {HOLD_TAIL_S:g} s of every track, 4 gains): student vs teacher ESR ' + f'{10 * np.log10(val):.1f} dB.', '', + '## Aliasing (sines; power outside the harmonics of f0, relative to the total)', '', + '| f0 | amplitude (FS) | teacher (768 kHz → 48 kHz) | student (48 kHz) |', '|---|---|---|---|'] + for f0, amp, t, s in alias: + lines.append(f'| {f0:.0f} Hz | {amp:g} | {t:.1f} dB | {s:.1f} dB |') + lines += ['', '## Cost', '', + f'Hand-written LSTM-{HIDDEN} in C++ (float, -O3 -ffast-math, one core of the M4 Pro): {ns:.1f} ns per ' + f'48 kHz sample = {ns / 20833.3 * 100:.2f} % of one core at 48 kHz (no oversampling). The engine at 8x: ' + '≈ 86 % (D-049 baseline).', '', '## Training log (epoch, train loss sum, validation ESR)', ''] + lines += [f'- {e}: {tl:.4f}, {10 * np.log10(v):.1f} dB ({t:.0f} s)' for e, tl, v, t in log[::4] + [log[-1]]] + path.write_text('\n'.join(lines) + '\n') + print(f'x3: wrote {path.relative_to(config.ROOT)}') + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument('step', choices=('render', 'train', 'evaluate')) + ap.add_argument('--epochs', type=int, default=400) + a = ap.parse_args() + if a.step == 'render': + render() + elif a.step == 'train': + train(a.epochs) + else: + evaluate() + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/requirements-ml.txt b/requirements-ml.txt new file mode 100644 index 0000000..8a7ee9a --- /dev/null +++ b/requirements-ml.txt @@ -0,0 +1,2 @@ +# M6 experiment X3 only (D-050): the neural-model study (python -m harness.x3_neural). Not needed by the harness. +torch==2.14.1 From 135101d51f5a101338e955f0b4ae1353dcfefd7d Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 00:51:56 +0300 Subject: [PATCH 03/18] m6 X3: LSTM-32 trained on the engine (CRUNCH), vs SPICE, aliasing, C++ cost; resumable training --- docs/PROGRESS.md | 6 +++ harness/reports/M6/20261015_x3_neural.md | 47 ++++++++++++++++++++++++ harness/x3_neural.py | 46 ++++++++++++++++------- 3 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 harness/reports/M6/20261015_x3_neural.md diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index ed7d418..e24f899 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -644,6 +644,12 @@ Notes: first-solve failures each: 4 FAIL on newton_failures, `…_j4exact_hot.md`); full acceptance 107 PASS, CLEAN (Master 1.0) −31.2 → −60.1 dB, the rest within 0.5 dB (`…_j4exact.md`). Cost at 384 kHz (10 s DI): 3352 / 4497 ns default / hot (main 2242 / 3016; the extra open port alone +6 %). +- X3 (`harness/x3_neural.py`, report `harness/reports/M6/20261015_x3_neural.md`): LSTM-32 at 48 kHz trained on + the engine (768 kHz, CRUNCH, four DI tracks × four input gains; CPU, Adam 1e-3 with gradient clipping 1.0 after + 5e-3 diverged at epoch ≈ 80; 1200 epochs, 78 min). Vs SPICE on the held-out 5 s CRUNCH DI clips: −16.5 … + −17.1 dB (teacher −48.7 … −49.1 dB); validation vs teacher −14.5 dB. Non-harmonic power on 5 / 9 kHz sines: + −6.3 … −17.0 dB (teacher −25.7 … −39.9 dB). Cost: 439 ns per 48 kHz sample in plain C++ (2.1 % of one core). + Training is resumable (`train_state.pt`, written at every validation). ## Questions for the owner _(Claude Code: add questions here and also ask them in the session.)_ diff --git a/harness/reports/M6/20261015_x3_neural.md b/harness/reports/M6/20261015_x3_neural.md new file mode 100644 index 0000000..320e521 --- /dev/null +++ b/harness/reports/M6/20261015_x3_neural.md @@ -0,0 +1,47 @@ +# M6 experiment X3: LSTM-32 trained on the engine (CRUNCH), vs SPICE + +- Date 2026-10-15; teacher `ampwav --block full` at 768000 Hz, knobs CRUNCH {'preampVolume': 0.6, 'treble': 0.5, 'middle': 0.5, 'bass': 0.5, 'master': 0.3, 'presence': 0.5}; DI tracks at input gains (-6.0, 0.0, 6.0, 12.0) dB; student at 48000 Hz (LSTM 32 + linear + input skip, PyTorch, pre-emphasised ESR loss). +- Held out: first 5 s (SPICE references, full netlist at 768000 Hz) and last 20 s of every track (validation). Training: 1200 epochs, 4694 s. + +## Accuracy (48 kHz band, first 0.1 s excluded) + +| Track | Teacher vs SPICE | Student vs SPICE | Student vs teacher | +|---|---|---|---| +| GTR L | -49.1 dB | -16.5 dB | -16.5 dB | +| GTR LL | -48.7 dB | -16.8 dB | -16.8 dB | +| GTR R | -48.9 dB | -17.1 dB | -17.1 dB | +| GTR RR | -49.0 dB | -16.7 dB | -16.7 dB | + +Validation (last 20 s of every track, 4 gains): student vs teacher ESR -14.5 dB. + +## Aliasing (sines; power outside the harmonics of f0, relative to the total) + +| f0 | amplitude (FS) | teacher (768 kHz → 48 kHz) | student (48 kHz) | +|---|---|---|---| +| 5000 Hz | 0.1 | -26.0 dB | -9.4 dB | +| 5000 Hz | 0.4 | -25.7 dB | -17.0 dB | +| 9000 Hz | 0.1 | -39.9 dB | -6.3 dB | +| 9000 Hz | 0.4 | -38.5 dB | -11.5 dB | + +## Cost + +Hand-written LSTM-32 in C++ (float, -O3 -ffast-math, one core of the M4 Pro): 439.4 ns per 48 kHz sample = 2.11 % of one core at 48 kHz (no oversampling). The engine at 8x: ≈ 86 % (D-049 baseline). + +## Training log (epoch, train loss sum, validation ESR) + +- 20: 10.9631, -0.8 dB (81 s) +- 100: 9.8592, -2.3 dB (391 s) +- 180: 8.6409, -3.2 dB (702 s) +- 260: 7.4055, -4.7 dB (1000 s) +- 340: 5.7680, -6.9 dB (1313 s) +- 420: 5.0419, -8.4 dB (1624 s) +- 500: 4.2288, -9.0 dB (1931 s) +- 580: 3.5665, -9.9 dB (2247 s) +- 660: 3.0526, -10.5 dB (2573 s) +- 740: 2.5862, -11.4 dB (2882 s) +- 820: 2.6778, -12.3 dB (3210 s) +- 900: 1.9964, -13.7 dB (3531 s) +- 980: 1.7416, -13.9 dB (3832 s) +- 1060: 1.7047, -14.9 dB (4151 s) +- 1140: 1.7517, -12.5 dB (4456 s) +- 1200: 1.5472, -14.0 dB (4694 s) diff --git a/harness/x3_neural.py b/harness/x3_neural.py index 91941b8..da41a2e 100644 --- a/harness/x3_neural.py +++ b/harness/x3_neural.py @@ -14,6 +14,7 @@ import datetime import json import os +import resource import subprocess import sys import tempfile @@ -124,19 +125,32 @@ def _esr_loss(y, t): return e + dc -def train(epochs=400, seg=RATE // 2, batch=48, tbptt=2048, warm=1000): +def train(epochs=1200, seg=RATE // 2, batch=48, tbptt=2048, warm=1000): import torch torch.manual_seed(0) np.random.seed(0) - dev = 'mps' if torch.backends.mps.is_available() else 'cpu' + dev = 'cpu' # an LSTM-32 trains ≈ 17× faster per step on the CPU than on MPS (0.18 vs ≈ 3 s, batch 48 × 2048) + torch.set_num_threads(8) xs, ys = _load('train') vx, vy = _load('val') + vx, vy = [x[:5 * RATE] for x in vx], [y[:5 * RATE] for y in vy] # 5 s per validation track while training model = _model().to(dev) - opt = torch.optim.Adam(model.parameters(), lr=5e-3) - sched = torch.optim.lr_scheduler.ReduceLROnPlateau(opt, factor=0.5, patience=10) - best, log = float('inf'), [] + opt = torch.optim.Adam(model.parameters(), lr=1e-3) # 5e-3 diverged at epoch ≈ 80 + sched = torch.optim.lr_scheduler.ReduceLROnPlateau(opt, factor=0.5, patience=3) + best, log, start = float('inf'), [], 0 t0 = time.time() - for ep in range(epochs): + ckpt = CACHE / 'train_state.pt' + if ckpt.exists(): # resume after an interruption (written at every validation) + st = torch.load(ckpt, weights_only=False) + model.load_state_dict(st['model']) + opt.load_state_dict(st['opt']) + sched.load_state_dict(st['sched']) + best, log, start = st['best'], st['log'], st['ep'] + np.random.set_state(st['np_rng']) + torch.set_rng_state(st['torch_rng']) + t0 -= log[-1][3] if log else 0.0 + print(f'x3: resuming at epoch {start}', flush=True) + for ep in range(start, epochs): model.train() bx, by = [], [] for _ in range(batch): @@ -155,18 +169,24 @@ def train(epochs=400, seg=RATE // 2, batch=48, tbptt=2048, warm=1000): loss = _esr_loss(out, by[:, s:s + tbptt]) opt.zero_grad() loss.backward() + torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) opt.step() - total += float(loss) - if ep % 5 == 4 or ep == epochs - 1: - v = validate(model, vx, vy, dev) - sched.step(v) + total += loss.item() + if ep % 20 == 19 or ep == epochs - 1: + v = float(validate(model, vx, vy, dev)) + sched.step(v) # patience counts validations (20 epochs each) log.append((ep + 1, total, v, time.time() - t0)) - print(f'x3: epoch {ep + 1} train {total:.4f} val ESR {10 * np.log10(v):.1f} dB ({time.time() - t0:.0f} s)', - flush=True) + rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 2 ** 30 # bytes on macOS + print(f'x3: epoch {ep + 1} train {total:.4f} val ESR {10 * np.log10(v):.1f} dB ({time.time() - t0:.0f} s, ' + f'peak RSS {rss:.1f} GB)', flush=True) if v < best: best = v torch.save(model.state_dict(), CACHE / 'lstm32.pt') + torch.save({'model': model.state_dict(), 'opt': opt.state_dict(), 'sched': sched.state_dict(), + 'best': best, 'log': log, 'ep': ep + 1, 'np_rng': np.random.get_state(), + 'torch_rng': torch.get_rng_state()}, ckpt) (CACHE / 'train_log.json').write_text(json.dumps(log)) + ckpt.unlink(missing_ok=True) def validate(model, vx, vy, dev): @@ -348,7 +368,7 @@ def write_report(rows, val, alias, ns, log): def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument('step', choices=('render', 'train', 'evaluate')) - ap.add_argument('--epochs', type=int, default=400) + ap.add_argument('--epochs', type=int, default=1200) a = ap.parse_args() if a.step == 'render': render() From 4783155be8cbc4956ed20a1060bedadb38dc0854 Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 05:17:04 +0300 Subject: [PATCH 04/18] m6 X4: LSTM-32 with Preamp Volume, Master, Presence as inputs, vs SPICE at CRUNCH / DRIVE / HOT --- docs/PROGRESS.md | 15 + harness/reports/M6/20261015_x4_conditioned.md | 80 +++++ harness/x3_neural.py | 15 +- harness/x4_conditioned.py | 327 ++++++++++++++++++ 4 files changed, 430 insertions(+), 7 deletions(-) create mode 100644 harness/reports/M6/20261015_x4_conditioned.md create mode 100644 harness/x4_conditioned.py diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index e24f899..b734360 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -650,6 +650,21 @@ Notes: −17.1 dB (teacher −48.7 … −49.1 dB); validation vs teacher −14.5 dB. Non-harmonic power on 5 / 9 kHz sines: −6.3 … −17.0 dB (teacher −25.7 … −39.9 dB). Cost: 439 ns per 48 kHz sample in plain C++ (2.1 % of one core). Training is resumable (`train_state.pt`, written at every validation). +- X4 plan (owner 2026-10-15: knobs Preamp Volume, Master, Presence as inputs; tone stack at 0.5): + `harness/x4_conditioned.py`. LSTM-32 as X3 with the three knob values as extra inputs (constant per segment). + Teacher: ampwav full amp at 768 kHz with `--j4 exact` (X1; valid at high Master). Data: 96 training settings + (each knob uniform in [0, 1], one in five at 0 or 1 so the corners are covered), each a 20 s segment of a random + DI track (between the held-out first 5 s and last 20 s) at a random input gain; 16 validation settings on the + last 20 s. Training as X3 (Adam 1e-3, clipping 1.0), 2400 epochs, resumable. Evaluation: vs SPICE on the + held-out 5 s DI clips at CRUNCH, DRIVE and HOT (cached 384 kHz references, 48 kHz band), X3 alongside at + CRUNCH; vs the teacher at the 16 unseen settings; aliasing at CRUNCH and HOT; C++ cost. +- X4 result (report `harness/reports/M6/20261015_x4_conditioned.md`): render 112 × 20 s (the Mac slept from 01:16; + jobs now run under `caffeinate -s -i`), training 2400 epochs, 2 h 26 min, validation flat at −8.1 dB from epoch + ≈ 1300. Vs SPICE (held-out 5 s DI, 48 kHz band): CRUNCH −3.0 … −3.8 dB (X3 at the same clips −16.5 … −17.1 dB), + DRIVE −5.0 … −5.4 dB, HOT −6.8 … −7.1 dB; teacher −48.7 … −49.0 / −55.7 … −56.1 / −39.6 … −40.1 dB. Unseen + settings vs teacher −8.4 dB overall (best −10.9 dB); at Preamp Volume 0 or Master 0 (near-silent output) +23.7 … + +62.2 dB: the input skip (output = network + input) has to cancel the input there. Non-harmonic power on 5 / 9 kHz + sines −18.7 … −23.3 dB (teacher −25.7 … −39.9 dB). Cost 442 ns per 48 kHz sample (static knobs folded into the bias). ## Questions for the owner _(Claude Code: add questions here and also ask them in the session.)_ diff --git a/harness/reports/M6/20261015_x4_conditioned.md b/harness/reports/M6/20261015_x4_conditioned.md new file mode 100644 index 0000000..5cd84f0 --- /dev/null +++ b/harness/reports/M6/20261015_x4_conditioned.md @@ -0,0 +1,80 @@ +# M6 experiment X4: LSTM-32 with Preamp Volume, Master, Presence as inputs, vs SPICE + +- Date 2026-10-15; teacher `ampwav --block full --j4 exact` at 768000 Hz; tone stack 0.5; 96 training settings (20 s each, random DI track, offset and input gain in (-6.0, 0.0, 6.0, 12.0) dB), 16 validation settings on the held-out last 20 s; student LSTM 32 on (audio, 3 knobs) at 48000 Hz. +- SPICE: full netlist at 384000 Hz on the held-out first 5 s of every DI track. Training: 2400 epochs, 8786 s. + +## Accuracy vs SPICE (48 kHz band, first 0.1 s excluded) + +| Preset | Track | Teacher vs SPICE | Student vs SPICE | Student vs teacher | X3 vs SPICE | +|---|---|---|---|---|---| +| CRUNCH | GTR L | -49.0 dB | -3.3 dB | -3.3 dB | -16.5 dB | +| CRUNCH | GTR LL | -48.7 dB | -3.1 dB | -3.1 dB | -16.8 dB | +| CRUNCH | GTR R | -48.9 dB | -3.8 dB | -3.8 dB | -17.1 dB | +| CRUNCH | GTR RR | -49.0 dB | -3.0 dB | -3.0 dB | -16.7 dB | +| DRIVE | GTR L | -55.7 dB | -5.1 dB | -5.1 dB | — | +| DRIVE | GTR LL | -56.1 dB | -5.0 dB | -5.0 dB | — | +| DRIVE | GTR R | -56.0 dB | -5.4 dB | -5.4 dB | — | +| DRIVE | GTR RR | -55.9 dB | -5.0 dB | -5.0 dB | — | +| HOT | GTR L | -40.0 dB | -7.0 dB | -7.0 dB | — | +| HOT | GTR LL | -39.6 dB | -6.8 dB | -6.8 dB | — | +| HOT | GTR R | -39.6 dB | -6.8 dB | -6.8 dB | — | +| HOT | GTR RR | -40.1 dB | -7.1 dB | -7.1 dB | — | + +## Unseen settings: student vs teacher (16 validation settings, 20 s, first 1 s excluded) + +Overall -8.4 dB. + +| Preamp Volume | Master | Presence | ESR | +|---|---|---|---| +| 0.992 | 0.789 | 0.654 | -10.9 dB | +| 1.000 | 0.824 | 0.943 | -10.6 dB | +| 0.755 | 0.405 | 0.820 | -10.4 dB | +| 0.758 | 0.971 | 0.715 | -9.8 dB | +| 0.408 | 0.792 | 0.000 | -9.7 dB | +| 0.224 | 0.853 | 0.080 | -8.9 dB | +| 0.278 | 0.861 | 0.374 | -8.8 dB | +| 0.124 | 0.985 | 0.351 | -8.7 dB | +| 1.000 | 0.413 | 0.993 | -7.5 dB | +| 0.566 | 0.084 | 0.519 | -5.2 dB | +| 0.507 | 0.265 | 0.000 | -4.3 dB | +| 0.123 | 0.576 | 1.000 | -1.9 dB | +| 0.000 | 0.526 | 0.269 | 23.7 dB | +| 0.000 | 0.330 | 0.849 | 26.1 dB | +| 0.591 | 0.000 | 0.919 | 34.6 dB | +| 0.000 | 0.013 | 0.398 | 62.2 dB | + +## Aliasing (sines; power outside the harmonics of f0, relative to the total) + +| Preset | f0 | amplitude (FS) | teacher (768 kHz → 48 kHz) | student (48 kHz) | +|---|---|---|---|---| +| CRUNCH | 5000 Hz | 0.1 | -26.0 dB | -19.8 dB | +| CRUNCH | 5000 Hz | 0.4 | -25.7 dB | -23.3 dB | +| CRUNCH | 9000 Hz | 0.1 | -39.9 dB | -18.7 dB | +| CRUNCH | 9000 Hz | 0.4 | -38.5 dB | -18.9 dB | +| HOT | 5000 Hz | 0.1 | -30.0 dB | -19.3 dB | +| HOT | 5000 Hz | 0.4 | -29.8 dB | -22.3 dB | +| HOT | 9000 Hz | 0.1 | -38.2 dB | -20.0 dB | +| HOT | 9000 Hz | 0.4 | -36.0 dB | -19.1 dB | + +## Cost + +Static knobs folded into the LSTM bias, same C++ kernel as X3: 442.4 ns per 48 kHz sample = 2.12 % of one core at 48 kHz. A knob change costs one 4·32 × 3 product. + +## Training log (epoch, train loss sum, validation ESR) + +- 20: 11.3881, -0.5 dB (71 s) +- 180: 8.6594, -3.0 dB (664 s) +- 340: 6.5097, -4.8 dB (1247 s) +- 500: 5.6850, -5.6 dB (1832 s) +- 660: 4.8720, -6.7 dB (2411 s) +- 820: 4.6264, -7.1 dB (3001 s) +- 980: 4.3583, -7.6 dB (3587 s) +- 1140: 3.7460, -7.8 dB (4176 s) +- 1300: 3.8545, -8.0 dB (4765 s) +- 1460: 3.8782, -8.0 dB (5351 s) +- 1620: 3.7328, -8.1 dB (5933 s) +- 1780: 3.4030, -8.1 dB (6518 s) +- 1940: 3.7793, -8.1 dB (7108 s) +- 2100: 4.2810, -8.1 dB (7696 s) +- 2260: 3.1078, -8.1 dB (8279 s) +- 2400: 3.9575, -8.1 dB (8786 s) diff --git a/harness/x3_neural.py b/harness/x3_neural.py index da41a2e..133ac47 100644 --- a/harness/x3_neural.py +++ b/harness/x3_neural.py @@ -48,7 +48,7 @@ def _tracks(): return out -def _teacher(x48): +def _teacher(x48, knobs=KNOBS, j4=None): """Full amp at 768 kHz on a 48 kHz full-scale input; output decimated to 48 kHz (full scale).""" hi = resample_poly(x48, TEACHER_FACTOR, 1) with tempfile.TemporaryDirectory(prefix='amp2203_x3_') as d: @@ -56,7 +56,8 @@ def _teacher(x48): write_wav(d / 'in.wav', hi, RATE * TEACHER_FACTOR) args = [str(AMPWAV), '--block', 'full', '--in', str(d / 'in.wav'), '--out', str(d / 'out.wav'), '--units', 'fs', '--rate', str(RATE * TEACHER_FACTOR)] - args += [a for k, v in KNOBS.items() for a in ('--param', f'{k}={v!r}')] + args += [a for k, v in knobs.items() for a in ('--param', f'{k}={v!r}')] + args += ['--j4', j4] if j4 else [] r = subprocess.run(args, capture_output=True, text=True) if r.returncode != 0: raise RuntimeError(r.stderr) @@ -232,7 +233,7 @@ def _spice_crunch_di(): return out -def _alias_test(student_fn): +def _alias_test(student_fn, teacher_fn=_teacher): """5 kHz and 9 kHz sines at two levels: power at non-harmonic frequencies relative to the total (48 kHz), student vs teacher (768 kHz, decimated).""" rows = [] @@ -242,7 +243,7 @@ def _alias_test(student_fn): for amp in (0.1, 0.4): x = amp * np.sin(2 * np.pi * f0 * t) ys = student_fn(x)[n:] - yt = _teacher(x)[n:2 * n] + yt = teacher_fn(x)[n:2 * n] def nonharm(y): w = np.hanning(len(y)) @@ -256,10 +257,10 @@ def nonharm(y): return rows -def _cpp_cost(): - """ns per sample of a hand-written LSTM-32 (same weights) in C++ (-O3), single thread.""" +def _cpp_cost(sd=None): + """ns per sample of a hand-written LSTM-32 (same weights; one audio input) in C++ (-O3), single thread.""" import torch - sd = torch.load(CACHE / 'lstm32.pt', map_location='cpu') + sd = torch.load(CACHE / 'lstm32.pt', map_location='cpu') if sd is None else sd w = np.concatenate([sd[k].numpy().ravel() for k in ('rnn.weight_ih_l0', 'rnn.weight_hh_l0', 'rnn.bias_ih_l0', 'rnn.bias_hh_l0', 'lin.weight', 'lin.bias')]).astype(np.float32) diff --git a/harness/x4_conditioned.py b/harness/x4_conditioned.py new file mode 100644 index 0000000..a9eb0ac --- /dev/null +++ b/harness/x4_conditioned.py @@ -0,0 +1,327 @@ +"""M6 experiment X4 (docs/ARCHITECTURE_STUDY.md §6): X3's LSTM-32 with three knobs as extra inputs (Preamp Volume, +Master, Presence; tone stack at 0.5), trained on the engine with the exact J4 coupling (X1), evaluated against SPICE +at CRUNCH, DRIVE and HOT and against the engine at unseen knob settings. + + python -m harness.x4_conditioned render # teacher segments at random knob settings (768 kHz, --j4 exact) + python -m harness.x4_conditioned train # conditioned LSTM-32 at 48 kHz (PyTorch, CPU), resumable + python -m harness.x4_conditioned evaluate # vs SPICE (cached 384 kHz references), vs teacher, aliasing, cost + +Data: 96 training settings, each knob uniform in [0, 1] with one in five at 0 or 1, each a 20 s segment of a random +DI track (between X3's held-out first 5 s and last 20 s) at a random input gain; 16 validation settings on the last +20 s of a track. Units as X3 (full-scale samples). +""" +import argparse +import datetime +import json +import os +import subprocess +import sys +import time +from concurrent.futures import ThreadPoolExecutor + +import numpy as np + +from . import config, metrics +from . import x3_neural as x3 + +RATE = x3.RATE +KNOB_NAMES = ('preampVolume', 'master', 'presence') +N_TRAIN, N_VAL = 96, 16 +SEG_S, WARM_S = 20.0, 0.5 # scored segment; teacher warm-up before it (dropped) +WORKERS = 6 # ≈ 1 GB per teacher render at 768 kHz +CACHE = config.HARNESS / 'cache' / 'x4' +EVAL_PRESETS = ('CRUNCH', 'DRIVE', 'HOT') +SPICE_FS = 384000 # the HOT DI references exist at 384 kHz only (X1) + + +def _all_knobs(k): + return {**config.DEFAULT_KNOBS, **dict(zip(KNOB_NAMES, (float(v) for v in k)))} + + +def _settings(): + """[(split, index, track, start sample, gain dB, knob vector)], deterministic.""" + rng = np.random.default_rng(4) + tracks = x3._tracks() + names = sorted(tracks) + seg, warm = int(SEG_S * RATE), int(WARM_S * RATE) + h, t = int(x3.HOLD_HEAD_S * RATE), int(x3.HOLD_TAIL_S * RATE) + out = [] + for split, n in (('train', N_TRAIN), ('val', N_VAL)): + for i in range(n): + k = rng.uniform(0.0, 1.0, 3) + edge = rng.uniform(size=3) < 0.2 + k[edge] = rng.integers(0, 2, edge.sum()) + name = names[rng.integers(len(names))] + n_all = len(tracks[name]) + if split == 'train': + start = int(rng.integers(h + warm, n_all - t - seg)) + else: + start = n_all - t # the held-out tail; its warm-up reaches back into the training region + gdb = float(rng.choice(x3.GAINS_DB)) + out.append((split, i, name, start, gdb, np.round(k, 4))) + return out, tracks + + +def render(): + CACHE.mkdir(parents=True, exist_ok=True) + jobs, tracks = _settings() + warm = int(WARM_S * RATE) + seg = int(SEG_S * RATE) + + def one(job): + split, i, name, start, gdb, k = job + path = CACHE / f'{split}_{i:03d}.npz' + if path.exists(): + return path, 0.0 + x = tracks[name][start - warm:start + seg] * 10 ** (gdb / 20) + t0 = time.time() + y = x3._teacher(x, _all_knobs(k), 'exact')[:len(x)] + np.savez(path, x=x[warm:].astype(np.float32), y=y[warm:].astype(np.float32), k=k.astype(np.float32), + track=name, start=start, gain_db=gdb) + return path, time.time() - t0 + + with ThreadPoolExecutor(max_workers=WORKERS) as ex: + for path, secs in ex.map(one, jobs): + print(f'x4: {path.name} ({secs:.0f} s)', flush=True) + + +def _load(split): + xs, ys, ks = [], [], [] + for f in sorted(CACHE.glob(f'{split}_*.npz')): + with np.load(f) as d: + xs.append(d['x']) + ys.append(d['y']) + ks.append(d['k']) + return xs, ys, ks + + +def _model(): + import torch + + class Rnn(torch.nn.Module): + """LSTM-32 on (audio, knobs) + linear output + audio skip (Wright et al. 2020, conditioned).""" + + def __init__(self): + super().__init__() + self.rnn = torch.nn.LSTM(1 + len(KNOB_NAMES), x3.HIDDEN, batch_first=True) + self.lin = torch.nn.Linear(x3.HIDDEN, 1) + + def forward(self, x, k, state=None): + inp = torch.cat([x, k[:, None, :].expand(-1, x.shape[1], -1)], dim=2) + h, state = self.rnn(inp, state) + return self.lin(h) + x, state + + return Rnn() + + +def validate(model, vx, vy, vk, per_setting=False): + import torch + model.eval() + num = den = 0.0 + rows = [] + with torch.no_grad(): + for x, y, k in zip(vx, vy, vk): + out, _ = model(torch.tensor(x)[None, :, None], torch.tensor(k)[None]) + o = out[0, :, 0].numpy() + e, r = np.sum((o[RATE:] - y[RATE:]) ** 2), np.sum(y[RATE:] ** 2) + num, den = num + e, den + r + rows.append(10 * np.log10(e / r)) + return (float(num / den), rows) if per_setting else float(num / den) + + +def train(epochs=2400, seg=RATE // 2, batch=48, tbptt=2048, warm=1000): + import torch + torch.manual_seed(0) + np.random.seed(0) + torch.set_num_threads(8) + xs, ys, ks = _load('train') + vx, vy, vk = _load('val') + vx, vy = [x[:5 * RATE] for x in vx], [y[:5 * RATE] for y in vy] # 5 s per validation setting while training + model = _model() + opt = torch.optim.Adam(model.parameters(), lr=1e-3) + sched = torch.optim.lr_scheduler.ReduceLROnPlateau(opt, factor=0.5, patience=3) + best, log, start = float('inf'), [], 0 + t0 = time.time() + ckpt = CACHE / 'train_state.pt' + if ckpt.exists(): # resume after an interruption (written at every validation) + st = torch.load(ckpt, weights_only=False) + model.load_state_dict(st['model']) + opt.load_state_dict(st['opt']) + sched.load_state_dict(st['sched']) + best, log, start = st['best'], st['log'], st['ep'] + np.random.set_state(st['np_rng']) + torch.set_rng_state(st['torch_rng']) + t0 -= log[-1][3] if log else 0.0 + print(f'x4: resuming at epoch {start}', flush=True) + for ep in range(start, epochs): + model.train() + bx, by, bk = [], [], [] + for _ in range(batch): + j = np.random.randint(len(xs)) + s = np.random.randint(0, len(xs[j]) - seg) + bx.append(xs[j][s:s + seg]) + by.append(ys[j][s:s + seg]) + bk.append(ks[j]) + bx = torch.tensor(np.stack(bx))[:, :, None] + by = torch.tensor(np.stack(by))[:, :, None] + bk = torch.tensor(np.stack(bk)) + with torch.no_grad(): # warm the state, not scored + _, state = model(bx[:, :warm], bk) + total = 0.0 + for s in range(warm, seg, tbptt): + state = tuple(z.detach() for z in state) + out, state = model(bx[:, s:s + tbptt], bk, state) + loss = x3._esr_loss(out, by[:, s:s + tbptt]) + opt.zero_grad() + loss.backward() + torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) + opt.step() + total += loss.item() + if ep % 20 == 19 or ep == epochs - 1: + v = validate(model, vx, vy, vk) + sched.step(v) # patience counts validations (20 epochs each) + log.append((ep + 1, total, v, time.time() - t0)) + print(f'x4: epoch {ep + 1} train {total:.4f} val ESR {10 * np.log10(v):.1f} dB ({time.time() - t0:.0f} s)', + flush=True) + if v < best: + best = v + torch.save(model.state_dict(), CACHE / 'lstm32k.pt') + torch.save({'model': model.state_dict(), 'opt': opt.state_dict(), 'sched': sched.state_dict(), + 'best': best, 'log': log, 'ep': ep + 1, 'np_rng': np.random.get_state(), + 'torch_rng': torch.get_rng_state()}, ckpt) + (CACHE / 'train_log.json').write_text(json.dumps(log)) + ckpt.unlink(missing_ok=True) + + +def _load_model(): + import torch + model = _model() + model.load_state_dict(torch.load(CACHE / 'lstm32k.pt', map_location='cpu')) + model.eval() + return model + + +def _student(x, k, model=None): + import torch + model = model or _load_model() + with torch.no_grad(): + out, _ = model(torch.tensor(x.astype(np.float32))[None, :, None], + torch.tensor(np.asarray(k, np.float32))[None]) + return out[0, :, 0].numpy().astype(np.float64) + + +def spice_refs(): + """Cached SPICE DI references (full netlist at SPICE_FS, the first 5 s of every track) at EVAL_PRESETS, in the + 48 kHz band, full scale. Run with AMP2203_FS = SPICE_FS (evaluate does). Never renders: a missing reference + is an error (each takes hours).""" + from . import cascade, signals, spice + from .presets import PRESETS + assert config.FS == SPICE_FS, config.FS + cached = spice._cached + + def only_cached(key, compute): + if not (config.CACHE_DIR / f'{key}.npz').exists(): + raise RuntimeError('x4: SPICE reference not cached; refusing to render it here') + return cached(key, compute) + + spice._cached = only_cached + texts = cascade._texts() + out = {} + for pid in EVAL_PRESETS: + for sig in signals.di_signals(): + f = cascade.render_f(texts, PRESETS[pid].all_knobs(), sig) + out[f'{pid}|{sig.id.replace("DI-", "")}'] = x3._band48(f['out'] / 60.0, config.FS) + CACHE.mkdir(parents=True, exist_ok=True) + np.savez(CACHE / 'spice_refs.npz', **out) + + +def evaluate(): + from .presets import PRESETS + env = {**os.environ, 'AMP2203_FS': str(SPICE_FS)} + subprocess.run([sys.executable, '-m', 'harness.x4_conditioned', 'spice-refs'], env=env, check=True) + refs = dict(np.load(CACHE / 'spice_refs.npz')) + tracks = x3._tracks() + model = _load_model() + n5 = int(x3.HOLD_HEAD_S * RATE) + s = slice(int(0.1 * RATE), n5) # the students start from a zero state + rows = [] + for key, ref in refs.items(): + pid, name = key.split('|') + knobs = PRESETS[pid].all_knobs() + k = [knobs[n] for n in KNOB_NAMES] + x = tracks[name][:n5] + ref = ref[:n5] + teach = x3._teacher(x, knobs, 'exact')[:n5] + stud = _student(x, k, model)[:n5] + x3s = metrics.esr_db(ref[s], x3._student(x)[:n5][s]) if pid == 'CRUNCH' else None + rows.append((pid, name, metrics.esr_db(ref[s], teach[s]), metrics.esr_db(ref[s], stud[s]), + metrics.esr_db(teach[s], stud[s]), x3s)) + print(f'x4: {pid} {name}: teacher vs SPICE {rows[-1][2]:.1f} dB, student vs SPICE {rows[-1][3]:.1f} dB, ' + f'student vs teacher {rows[-1][4]:.1f} dB' + (f', X3 vs SPICE {x3s:.1f} dB' if x3s is not None else ''), + flush=True) + vx, vy, vk = _load('val') + val, val_rows = validate(model, vx, vy, vk, per_setting=True) + print(f'x4: validation (16 unseen settings) {10 * np.log10(val):.1f} dB', flush=True) + alias = {} + for pid in ('CRUNCH', 'HOT'): + knobs = PRESETS[pid].all_knobs() + k = [knobs[n] for n in KNOB_NAMES] + alias[pid] = x3._alias_test(lambda x: _student(x, k, model), lambda x: x3._teacher(x, knobs, 'exact')) + import torch + sd = {n: v.clone() for n, v in torch.load(CACHE / 'lstm32k.pt', map_location='cpu').items()} + knobs = PRESETS['CRUNCH'].all_knobs() + kc = torch.tensor([knobs[n] for n in KNOB_NAMES]) + w = sd['rnn.weight_ih_l0'] + sd['rnn.bias_ih_l0'] = sd['rnn.bias_ih_l0'] + w[:, 1:] @ kc # static knobs fold into the bias + sd['rnn.weight_ih_l0'] = w[:, :1].contiguous() + ns = x3._cpp_cost(sd) + log = json.loads((CACHE / 'train_log.json').read_text()) + write_report(rows, val, val_rows, vk, alias, ns, log) + + +def write_report(rows, val, val_rows, vk, alias, ns, log): + out = config.REPORT_DIR / 'M6' + out.mkdir(parents=True, exist_ok=True) + path = out / f'{datetime.date.today().strftime("%Y%m%d")}_x4_conditioned.md' + lines = ['# M6 experiment X4: LSTM-32 with Preamp Volume, Master, Presence as inputs, vs SPICE', '', + f'- Date {datetime.date.today().isoformat()}; teacher `ampwav --block full --j4 exact` at ' + f'{RATE * x3.TEACHER_FACTOR} Hz; tone stack 0.5; {N_TRAIN} training settings ({SEG_S:g} s each, ' + f'random DI track, offset and input gain in {x3.GAINS_DB} dB), {N_VAL} validation settings on the ' + f'held-out last {x3.HOLD_TAIL_S:g} s; student LSTM {x3.HIDDEN} on (audio, 3 knobs) at {RATE} Hz.', + f'- SPICE: full netlist at {SPICE_FS} Hz on the held-out first {x3.HOLD_HEAD_S:g} s of every DI track. ' + f'Training: {int(log[-1][0])} epochs, {log[-1][3]:.0f} s.', + '', '## Accuracy vs SPICE (48 kHz band, first 0.1 s excluded)', '', + '| Preset | Track | Teacher vs SPICE | Student vs SPICE | Student vs teacher | X3 vs SPICE |', + '|---|---|---|---|---|---|'] + for pid, name, a, b, c, d in rows: + lines.append(f'| {pid} | {name} | {a:.1f} dB | {b:.1f} dB | {c:.1f} dB | ' + f'{"—" if d is None else f"{d:.1f} dB"} |') + lines += ['', f'## Unseen settings: student vs teacher ({N_VAL} validation settings, 20 s, first 1 s excluded)', '', + f'Overall {10 * np.log10(val):.1f} dB.', '', + '| Preamp Volume | Master | Presence | ESR |', '|---|---|---|---|'] + for k, e in sorted(zip(vk, val_rows), key=lambda r: r[1]): + lines.append(f'| {k[0]:.3f} | {k[1]:.3f} | {k[2]:.3f} | {e:.1f} dB |') + lines += ['', '## Aliasing (sines; power outside the harmonics of f0, relative to the total)', '', + '| Preset | f0 | amplitude (FS) | teacher (768 kHz → 48 kHz) | student (48 kHz) |', '|---|---|---|---|---|'] + for pid, rs in alias.items(): + for f0, amp, t, s in rs: + lines.append(f'| {pid} | {f0:.0f} Hz | {amp:g} | {t:.1f} dB | {s:.1f} dB |') + lines += ['', '## Cost', '', + f'Static knobs folded into the LSTM bias, same C++ kernel as X3: {ns:.1f} ns per 48 kHz sample = ' + f'{ns / 20833.3 * 100:.2f} % of one core at 48 kHz. A knob change costs one 4·{x3.HIDDEN} × 3 ' + 'product.', '', '## Training log (epoch, train loss sum, validation ESR)', ''] + lines += [f'- {int(e)}: {tl:.4f}, {10 * np.log10(v):.1f} dB ({t:.0f} s)' for e, tl, v, t in log[::8] + [log[-1]]] + path.write_text('\n'.join(lines) + '\n') + print(f'x4: wrote {path.relative_to(config.ROOT)}') + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument('step', choices=('render', 'train', 'evaluate', 'spice-refs')) + ap.add_argument('--epochs', type=int, default=2400) + a = ap.parse_args() + {'render': render, 'train': lambda: train(a.epochs), 'evaluate': evaluate, 'spice-refs': spice_refs}[a.step]() + + +if __name__ == '__main__': + sys.exit(main()) From a76ced346d3b09bafa2d9f9ea2fb6769d1604a5b Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 05:17:09 +0300 Subject: [PATCH 05/18] m6 X4: command in CLAUDE.md --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index b33f538..98ea2ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,6 +79,7 @@ python -m harness.run_engine --block --preset python -m harness.compare --block --preset --milestone M # renders + runs + report python -m unittest discover -s harness/tests -t . # harness unit tests python -m harness.x3_neural render|train|evaluate # M6 X3: LSTM trained on the engine (needs requirements-ml.txt, D-050) +python -m harness.x4_conditioned render|train|evaluate # M6 X4: X3 with Preamp Volume, Master, Presence as inputs (run long jobs under caffeinate -s -i) ``` Planned: none (all M0 commands exist; later milestones add theirs here). From c67fb3502c0b00b95b7f04db0d204761aaca1050 Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 09:57:01 +0300 Subject: [PATCH 06/18] m6: one ESR tolerance of -20 dB for every ESR check, plugin model check added (D-051) --- docs/DECISIONS.md | 18 ++++++++++++++++++ docs/PROGRESS.md | 2 ++ docs/VERIFICATION.md | 11 ++++++----- harness/knob_jump.py | 4 ++-- harness/tests/test_scoring.py | 7 ++++--- harness/tolerances.py | 9 +++++---- 6 files changed, 37 insertions(+), 14 deletions(-) diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index e7857c7..4eb4365 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -882,3 +882,21 @@ the engine do not need it). Inference cost is measured with hand-written C++ in plugin have no new dependency. Consequences: `pip install -r requirements-ml.txt` in the venv before `python -m harness.x3_neural`. + +## D-051 One ESR tolerance of −20 dB for every ESR check, engine and real-time plugin model +Date: 2026-10-15 · Status: accepted (owner decisions: "change the acceptance level to what the literature say +−17 to −27 dB ESR"; threshold "−20 dB"; engine block tolerances "loosen them too") +Context: M6 experiments X3 / X4 (neural models trained on the engine) and the owner's aim of "good enough" tonal +accuracy at a low CPU and memory cost. The literature on real-time amp models reports ESR −17 … −27 dB with good +listening scores (Wright et al. 2019/2020; ARCHITECTURE_STUDY §2.4, measured there against hardware recordings, +here against SPICE). The whole-amp DRIVE / CRUNCH check was already −20 dB; the engine's per-block checks were +−40 / −30 / −60 dB. +Decision: every ESR tolerance in VERIFICATION §7 is −20 dB: linear block (was −60), nonlinear block Small / Nominal +/ Hot (was −40), Overdrive (was −30), knob jump (was −40), partition at y, sines (was −26); DI partition and the +whole-amp DRIVE / CRUNCH check stay −20. New check `plugin_model_esr_db`: a real-time plugin model (any +architecture) vs SPICE, held-out DI clips at CRUNCH, DRIVE and HOT, 48 kHz band, first 0.1 s excluded, ≤ −20 dB. +Tolerances that are not ESR (DC, magnitude, harmonics, power / THD, NFB, stability, Newton) are unchanged. +Consequences: the engine's per-block checks flag only errors above −20 dB; an error such as the J4 partition error +at HOT before X1 (−13 dB) is still caught, smaller ones (e.g. −25 dB) are not. Earlier reports keep their recorded +tolerances; reports generated from now on score against −20 dB. Against the new check: X3 (CRUNCH only) −16.5 … +−17.1 dB, X4 −3.0 … −7.1 dB, the engine −39.6 … −56.1 dB. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index b734360..38dc89f 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -737,3 +737,5 @@ _(Claude Code: add questions here and also ask them in the session.)_ optimise the solver first (accuracy unchanged; measured per step). Done (D-046): real-time factor 0.42 → 0.53; re-verification (B56, cascade, M4 acceptance, null test) unchanged; automation 90 first-solve failures, all rescued. M5 stops here for review. +- Q25 (M6): acceptance target for the real-time plugin? — Answered 2026-10-15: −20 dB ESR (literature −17 … −27 dB) for + every ESR check, the engine's per-block checks included (D-051). diff --git a/docs/VERIFICATION.md b/docs/VERIFICATION.md index 23d4d87..b3afb5b 100644 --- a/docs/VERIFICATION.md +++ b/docs/VERIFICATION.md @@ -131,15 +131,16 @@ See `WDF_DESIGN.md` §5.5. |---|---| | Device DC sweeps | 0.1 % / 1 nA | | Linear block magnitude (B4) | ≤ 0.1 dB (20 Hz–10 kHz), ≤ 0.3 dB (10–20 kHz) | -| Linear block time-domain ESR | ≤ −60 dB | -| Knob jump (SPEC §6.2: each knob 0→1 in 10 ms during a 1 kHz tone) | No NaN/Inf; ESR vs SPICE with time-varying pots on the same smoothed rotation trajectory ≤ −40 dB over the sweep and the 20 ms after it (D-025) | +| Linear block time-domain ESR | ≤ −20 dB (D-051; was −60 dB) | +| Knob jump (SPEC §6.2: each knob 0→1 in 10 ms during a 1 kHz tone) | No NaN/Inf; ESR vs SPICE with time-varying pots on the same smoothed rotation trajectory ≤ −20 dB over the sweep and the 20 ms after it (D-025; D-051, was −40 dB) | | DC operating point (any block) | ≤ 0.5 % per node (EL34 currents: ±1 mA plate, ±0.3 mA screen) | -| Block-exact ESR, nonlinear, Small/Nominal/Hot | ≤ −40 dB | -| Block-exact ESR, Overdrive | ≤ −30 dB | +| Block-exact ESR, nonlinear, Small/Nominal/Hot | ≤ −20 dB (D-051; was −40 dB) | +| Block-exact ESR, Overdrive | ≤ −20 dB (D-051; was −30 dB) | | Block-exact harmonics 2–5 | within 0.5 dB | -| Partition error at y (M3) | ≤ −26 dB sines, ≤ −20 dB DI, at the loaded y: the SPICE chain's v_oc(y) through the J4 source into the PI (b56) vs F (D-036; report; triggers upgrade path if exceeded) | +| Partition error at y (M3) | ≤ −20 dB sines (D-051; was −26 dB), ≤ −20 dB DI, at the loaded y: the SPICE chain's v_oc(y) through the J4 source into the PI (b56) vs F (D-036; report; triggers upgrade path if exceeded) | | Total, CLEAN power | ±5 % of SPICE, THD ±0.5 pp | | Total, DRIVE/CRUNCH ESR | ≤ −20 dB; harmonics 2–9 within 1.5 dB | +| Real-time plugin model (any architecture, M6), whole amp vs SPICE | ESR ≤ −20 dB on the held-out DI clips at CRUNCH, DRIVE and HOT, 48 kHz band, first 0.1 s excluded (D-051) | | NFB amount | ±0.5 dB at 100 Hz / 1 kHz / 5 kHz | | Stability | 60 s silence at max settings: |out| < 1 mV, no growth | | Newton | 0 failures on the standard signal set; max iterations ≤ the configured limit (12; 16 for the power amp B56 and the full amp, D-045) | diff --git a/harness/knob_jump.py b/harness/knob_jump.py index adcb915..92a984a 100644 --- a/harness/knob_jump.py +++ b/harness/knob_jump.py @@ -7,7 +7,7 @@ nominal 0.1 V at the guitar input). The engine runs through `ampwav --knob-ramp` (it smooths the targets and updates its scattering matrix every kParamUpdateInterval samples). The reference is SPICE with the pot resistances as expressions of recorded -rotation nodes that follow the same smoothed trajectory continuously. Pass: no NaN/Inf and ESR ≤ −40 dB +rotation nodes that follow the same smoothed trajectory continuously. Pass: no NaN/Inf and ESR ≤ −20 dB (D-051; was −40 dB) over the ramp and the 20 ms after it. Peak error and error energy above 5 kHz are reported as information. Writes harness/reports/M/__knob_jump.md. @@ -155,7 +155,7 @@ def main(): x = dc + setup['tone_v'] * np.sin(2 * np.pi * TONE_HZ * t) with tempfile.TemporaryDirectory(prefix='amp2203_knob_') as d, ThreadPoolExecutor() as ex: rows = list(ex.map(lambda c: run_case(block_id, c, x, d), setup['cases'])) - limit = -40.0 + limit = -20.0 # D-051 (was -40) ms = setup['milestone'] lines = [f'# {ms} report: {block_id.upper()} knob-jump test', '', f'- Date: {datetime.date.today().isoformat()}, engine commit `{_git()}`, ngspice ' diff --git a/harness/tests/test_scoring.py b/harness/tests/test_scoring.py index f160edd..4ffcf45 100644 --- a/harness/tests/test_scoring.py +++ b/harness/tests/test_scoring.py @@ -10,9 +10,10 @@ class TestM2Scoring(unittest.TestCase): def test_b12_checks_are_scored_in_m2_only(self): - self.assertEqual(tolerances.status('M2', 'b12', 'nonlinear_esr_db', -45.0)[1], 'PASS') - self.assertEqual(tolerances.status('M2', 'b12', 'nonlinear_esr_db', -35.0)[1], 'FAIL') - self.assertEqual(tolerances.status('M2', 'b12', 'nonlinear_esr_db_overdrive', -35.0)[1], 'PASS') + self.assertEqual(tolerances.status('M2', 'b12', 'nonlinear_esr_db', -25.0)[1], 'PASS') # limit -20 dB (D-051) + self.assertEqual(tolerances.status('M2', 'b12', 'nonlinear_esr_db', -15.0)[1], 'FAIL') + self.assertEqual(tolerances.status('M2', 'b12', 'nonlinear_esr_db_overdrive', -25.0)[1], 'PASS') + self.assertEqual(tolerances.status('M2', 'b12', 'nonlinear_esr_db_overdrive', -15.0)[1], 'FAIL') self.assertEqual(tolerances.status('M2', 'b12', 'newton_median_iterations', 3)[1], 'PASS') self.assertEqual(tolerances.status('M2', 'b12', 'newton_median_iterations', 4)[1], 'FAIL') self.assertEqual(tolerances.status('M2', 'b12', 'newton_max_iterations', 12)[1], 'PASS') diff --git a/harness/tolerances.py b/harness/tolerances.py index d28a0dc..3f75c7a 100644 --- a/harness/tolerances.py +++ b/harness/tolerances.py @@ -9,16 +9,17 @@ 'tb_dc_rel_error': (0.005, 'max'), # M0 testbench sanity vs F (0.5 %) 'linear_mag_db_20_10k': (0.1, 'max'), 'linear_mag_db_10k_20k': (0.3, 'max'), - 'linear_esr_db': (-60.0, 'max'), - 'nonlinear_esr_db': (-40.0, 'max'), # Small / Nominal / Hot - 'nonlinear_esr_db_overdrive': (-30.0, 'max'), + 'linear_esr_db': (-20.0, 'max'), # D-051 (was -60) + 'nonlinear_esr_db': (-20.0, 'max'), # Small / Nominal / Hot; D-051 (was -40) + 'nonlinear_esr_db_overdrive': (-20.0, 'max'), # D-051 (was -30) 'harmonics_2_5_db': (0.5, 'max'), 'total_power_rel': (0.05, 'max'), 'total_thd_pp': (0.5, 'max'), 'total_esr_db': (-20.0, 'max'), + 'plugin_model_esr_db': (-20.0, 'max'), # real-time plugin model vs SPICE, DI, 48 kHz band (D-051) 'total_harmonics_2_9_db': (1.5, 'max'), 'newton_failures': (0, 'max'), - 'partition_esr_db_sines': (-26.0, 'max'), # at y (M3), VERIFICATION §7 + 'partition_esr_db_sines': (-20.0, 'max'), # at y (M3), VERIFICATION §7; D-051 (was -26) 'partition_esr_db_di': (-20.0, 'max'), 'newton_median_iterations': (3, 'max'), # SPEC §7 M2 acceptance 'newton_max_iterations': (12, 'max'), # the configured limit (WDF_DESIGN §4 maxIter) From 76d7b158a47bb4d3c5c7a59a53ce3b95ffc7e3f7 Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 10:06:15 +0300 Subject: [PATCH 07/18] m6 X6: block-boundary study for per-block models (engine only) --- docs/PROGRESS.md | 16 + harness/boundary_study.py | 176 +++++++++++ harness/reports/M6/20261015_x6_boundaries.md | 298 +++++++++++++++++++ 3 files changed, 490 insertions(+) create mode 100644 harness/boundary_study.py create mode 100644 harness/reports/M6/20261015_x6_boundaries.md diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 38dc89f..3714a44 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -665,6 +665,22 @@ Notes: settings vs teacher −8.4 dB overall (best −10.9 dB); at Preamp Volume 0 or Master 0 (near-silent output) +23.7 … +62.2 dB: the input skip (output = network + input) has to cancel the input there. Non-harmonic power on 5 / 9 kHz sines −18.7 … −23.3 dB (teacher −25.7 … −39.9 dB). Cost 442 ns per 48 kHz sample (static knobs folded into the bias). +- X6 plan, block boundaries for per-block models (owner 2026-10-15: "run the boundary measurement"): + `harness/boundary_study.py`, engine only (ampwav full amp, 768 kHz, `--j4 exact`). Candidate boundaries p1a, p1b, + p2a, k2b, y and the supply nodes bpa, bpx, dumped in one run per knob setting. Settings: Preamp Volume 0.2 / 0.6 / + 1.0, each with every downstream knob (Treble, Middle, Bass, Master, Presence) at 0 and at 1 one at a time, all at 0, + all at 1, and the defaults (13 per Volume); signals: DI GTR L and GTR RR, first 5 s, at 0 and +12 dB. Metric: ESR + in the 48 kHz band of a boundary node at a changed setting vs the same node at the default downstream knobs (each + minus its value at t = 0). A boundary is clean for a knob when that change stays ≤ −20 dB (D-051) and is also + reported against −40 dB. +- X6 result (`harness/reports/M6/20261015_x6_boundaries.md`, 156 renders, 5 min): worst ESR of each boundary vs + downstream knobs, raw / above 20 Hz (20 Hz high-pass added: the raw figure is dominated by sub-audio B+ sag drift). + p1a (no knobs upstream): Volume / tone / Presence ≤ −30.7 dB, Master −27.5 dB above 20 Hz (raw up to +15 dB). + p1b: each knob ≤ −20.5 dB above 20 Hz, all downstream knobs at once −19.4 dB (raw up to +29 dB). p2a / k2b: Master + −17.3 / −17.4 dB above 20 Hz (coupled, via the supply bpx); k2b Middle / Bass −20.5 / −22.5 dB (J3 loading); + treble and Presence ≤ −35 dB. y: Presence −36.4 dB. Supply nodes bpa / bpx change by +3 … +26 dB with the knobs + (they carry the coupling). Implication: a split at y and at p1a / p1b is within −20 dB in the audio band; any + preamp block reaching p2a or later needs the supply (power-amp current draw) as an input, or Master as a proxy. ## Questions for the owner _(Claude Code: add questions here and also ask them in the session.)_ diff --git a/harness/boundary_study.py b/harness/boundary_study.py new file mode 100644 index 0000000..884c302 --- /dev/null +++ b/harness/boundary_study.py @@ -0,0 +1,176 @@ +"""M6 experiment X6: where can the amp be split into per-block models? Engine only. + + python -m harness.boundary_study # renders (cached in harness/cache/boundary/) and writes the report + +For each candidate boundary node b (signal handed from one block to the next) the knobs downstream of b must not +change v_b, or the model of the block ending at b needs those knobs as inputs. Measured with the full amp +(ampwav, 768 kHz, --j4 exact): v_b at a changed downstream setting vs v_b at the default downstream knobs, same +upstream knobs, same input. ESR in the 48 kHz band, each signal minus its value at t = 0 (VERIFICATION §6). +""" +import datetime +import os +import subprocess +import sys +import tempfile +import time +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path + +import numpy as np +from scipy.signal import butter, resample_poly, sosfiltfilt + +from . import config, metrics +from . import x3_neural as x3 +from .wavio import read_wav, write_wav + +RATE, FACTOR = x3.RATE, x3.TEACHER_FACTOR +CACHE = config.HARNESS / 'cache' / 'boundary' +NODES = ('p1a', 'p1b', 'p2a', 'k2b', 'y', 'bpa', 'bpx') +# knobs downstream of each boundary (signal order: V1a, Volume, V1b, V2a, V2b, tone stack, Master -> y, PI + Presence) +DOWNSTREAM = { + 'p1a': ('preampVolume', 'treble', 'middle', 'bass', 'master', 'presence'), + 'p1b': ('treble', 'middle', 'bass', 'master', 'presence'), + 'p2a': ('treble', 'middle', 'bass', 'master', 'presence'), + 'k2b': ('treble', 'middle', 'bass', 'master', 'presence'), + 'y': ('presence',), + 'bpa': ('preampVolume', 'treble', 'middle', 'bass', 'master', 'presence'), # supply: every knob (information) + 'bpx': ('preampVolume', 'treble', 'middle', 'bass', 'master', 'presence'), +} +VOLUMES = (0.2, 0.6, 1.0) +DOWN_KNOBS = ('treble', 'middle', 'bass', 'master', 'presence') +TRACKS = ('GTR L', 'GTR RR') +GAINS_DB = (0.0, 12.0) +SECONDS = 5.0 +WORKERS = 6 +LIMITS = (-20.0, -40.0) +HP_HZ = 20.0 # second figure: both signals high-passed (sub-audio sag drift removed) + + +def _settings(): + """{label: knobs}: per Volume the defaults, each downstream knob at 0 and 1 alone, all at 0, all at 1.""" + out = {} + for v in VOLUMES: + base = {**config.DEFAULT_KNOBS, 'preampVolume': v} + out[f'V{v:g} base'] = base + for k in DOWN_KNOBS: + for val in (0.0, 1.0): + out[f'V{v:g} {k}={val:g}'] = {**base, k: val} + out[f'V{v:g} all=0'] = {**base, **{k: 0.0 for k in DOWN_KNOBS}} + out[f'V{v:g} all=1'] = {**base, **{k: 1.0 for k in DOWN_KNOBS}} + return out + + +def _signals(): + tracks = x3._tracks() + n = int(SECONDS * RATE) + return {f'{t} {g:+.0f}dB': tracks[t][:n] * 10 ** (g / 20) for t in TRACKS for g in GAINS_DB} + + +def _render(x48, knobs): + """Full amp at 768 kHz (exact J4) on a 48 kHz full-scale input; NODES in volts, decimated to 48 kHz.""" + hi = resample_poly(x48, FACTOR, 1) + with tempfile.TemporaryDirectory(prefix='amp2203_x6_') as d: + d = Path(d) + write_wav(d / 'in.wav', hi, RATE * FACTOR) + args = [str(x3.AMPWAV), '--block', 'full', '--in', str(d / 'in.wav'), '--out', str(d / 'out.wav'), + '--units', 'fs', '--rate', str(RATE * FACTOR), '--j4', 'exact'] + args += [a for k, v in knobs.items() for a in ('--param', f'{k}={v!r}')] + args += [a for n in NODES for a in ('--dump-node', f'{n}={d / n}.wav')] + r = subprocess.run(args, capture_output=True, text=True) + if r.returncode != 0: + raise RuntimeError(r.stderr) + out = {} + for n in NODES: + v, _ = read_wav(d / f'{n}.wav') + out[n] = v[0] + resample_poly(v - v[0], 1, FACTOR) # band-limit the signal, keep the t = 0 value + return out + + +def render(): + CACHE.mkdir(parents=True, exist_ok=True) + sigs, sets = _signals(), _settings() + jobs = [(s, k) for s in sigs for k in sets] + + def one(job): + s, k = job + path = CACHE / f'{s}|{k}.npz'.replace(' ', '_') + if path.exists(): + return path, 0.0 + t0 = time.time() + np.savez(path, **_render(sigs[s], sets[k])) + return path, time.time() - t0 + + with ThreadPoolExecutor(max_workers=WORKERS) as ex: + for i, (path, secs) in enumerate(ex.map(one, jobs)): + print(f'x6: {i + 1}/{len(jobs)} {path.name} ({secs:.0f} s)', flush=True) + + +def _load(s, k): + with np.load(CACHE / f'{s}|{k}.npz'.replace(' ', '_')) as z: + return {n: z[n] for n in z.files} + + +def analyse(): + sigs, sets = _signals(), _settings() + first = int(0.1 * RATE) + hp = butter(4, HP_HZ, 'highpass', fs=RATE, output='sos') + rows = [] # (node, setting label, changed knobs, worst ESR, signal at worst, worst ESR after the high-pass) + for node in NODES: + down = set(DOWNSTREAM[node]) + for label, knobs in sets.items(): + v = label.split()[0] + base_label = f'{v} base' if node != 'p1a' and node not in ('bpa', 'bpx') else 'V0.6 base' + base = sets[base_label] + changed = sorted(k for k in knobs if knobs[k] != base[k]) + if not changed or not set(changed) <= down: + continue # an upstream knob differs: not a test of this boundary + worst, worst_hp = (-np.inf, ''), -np.inf + for s in sigs: + a, b = _load(s, base_label)[node], _load(s, label)[node] + a, b = a - a[0], b - b[0] + worst = max(worst, (metrics.esr_db(a[first:], b[first:]), s)) + worst_hp = max(worst_hp, metrics.esr_db(sosfiltfilt(hp, a)[first:], sosfiltfilt(hp, b)[first:])) + rows.append((node, label, changed, worst[0], worst[1], worst_hp)) + return rows + + +def write_report(rows): + out = config.REPORT_DIR / 'M6' + out.mkdir(parents=True, exist_ok=True) + path = out / f'{datetime.date.today().strftime("%Y%m%d")}_x6_boundaries.md' + lines = ['# M6 experiment X6: block boundaries for per-block models (engine only)', '', + f'- Date {datetime.date.today().isoformat()}; full amp `ampwav --j4 exact` at {RATE * FACTOR} Hz; ' + f'signals {", ".join(f"DI {t}" for t in TRACKS)} (first {SECONDS:g} s) at {GAINS_DB} dB; Preamp ' + f'Volume {VOLUMES}; tone 0.5, Master 0.3, Presence 0.5 unless changed.', + '- Value: ESR (48 kHz band, first 0.1 s excluded) of the boundary node at the changed setting vs the ' + 'same node at the default downstream knobs, worst of the four signals. Lower = the downstream knob ' + 'changes the boundary less. `clean` ≤ −40 dB, `ok` ≤ −20 dB (D-051), `coupled` above −20 dB.', + '- Supply nodes bpa / bpx are compared against Volume 0.6 with every knob counted as downstream ' + '(information: how far the power amp\'s load reaches back through the supply).', '', + '## Summary: worst case per boundary and knob', '', + f'| Boundary | Knob(s) changed | Worst ESR | Setting | Signal | Class | Worst ESR > {HP_HZ:g} Hz | Class > {HP_HZ:g} Hz |', + '|---|---|---|---|---|---|---|---|'] + cls = lambda e: 'clean' if e <= LIMITS[1] else 'ok' if e <= LIMITS[0] else 'coupled' + groups = {} + for node, label, changed, e, s, ehp in rows: + key = (node, '+'.join(changed) if len(changed) < 5 else 'all downstream') + if key not in groups or e > groups[key][0]: + groups[key] = (e, label, s, max(ehp, groups.get(key, (0, 0, 0, -np.inf))[3])) + else: + groups[key] = (*groups[key][:3], max(ehp, groups[key][3])) + for (node, knobs), (e, label, s, ehp) in groups.items(): + lines.append(f'| {node} | {knobs} | {e:.1f} dB | {label} | {s} | {cls(e)} | {ehp:.1f} dB | {cls(ehp)} |') + lines += ['', '## All comparisons', '', f'| Boundary | Setting | Worst ESR | Signal | Worst ESR > {HP_HZ:g} Hz |', + '|---|---|---|---|---|'] + lines += [f'| {n} | {label} | {e:.1f} dB | {s} | {ehp:.1f} dB |' for n, label, _, e, s, ehp in rows] + path.write_text('\n'.join(lines) + '\n') + print(f'x6: wrote {path.relative_to(config.ROOT)}') + + +def main(): + render() + write_report(analyse()) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/harness/reports/M6/20261015_x6_boundaries.md b/harness/reports/M6/20261015_x6_boundaries.md new file mode 100644 index 0000000..74cee83 --- /dev/null +++ b/harness/reports/M6/20261015_x6_boundaries.md @@ -0,0 +1,298 @@ +# M6 experiment X6: block boundaries for per-block models (engine only) + +- Date 2026-10-15; full amp `ampwav --j4 exact` at 768000 Hz; signals DI GTR L, DI GTR RR (first 5 s) at (0.0, 12.0) dB; Preamp Volume (0.2, 0.6, 1.0); tone 0.5, Master 0.3, Presence 0.5 unless changed. +- Value: ESR (48 kHz band, first 0.1 s excluded) of the boundary node at the changed setting vs the same node at the default downstream knobs, worst of the four signals. Lower = the downstream knob changes the boundary less. `clean` ≤ −40 dB, `ok` ≤ −20 dB (D-051), `coupled` above −20 dB. +- Supply nodes bpa / bpx are compared against Volume 0.6 with every knob counted as downstream (information: how far the power amp's load reaches back through the supply). + +## Summary: worst case per boundary and knob + +| Boundary | Knob(s) changed | Worst ESR | Setting | Signal | Class | Worst ESR > 20 Hz | Class > 20 Hz | +|---|---|---|---|---|---|---|---| +| p1a | preampVolume | -3.9 dB | V1 base | GTR RR +0dB | coupled | -30.8 dB | ok | +| p1a | preampVolume+treble | -2.1 dB | V1 treble=0 | GTR RR +0dB | coupled | -30.8 dB | ok | +| p1a | middle+preampVolume | -0.5 dB | V1 middle=1 | GTR RR +0dB | coupled | -30.8 dB | ok | +| p1a | bass+preampVolume | -1.7 dB | V1 bass=1 | GTR RR +0dB | coupled | -30.7 dB | ok | +| p1a | master+preampVolume | 14.8 dB | V1 master=1 | GTR RR +0dB | coupled | -25.4 dB | ok | +| p1a | preampVolume+presence | -1.1 dB | V1 presence=1 | GTR RR +0dB | coupled | -30.8 dB | ok | +| p1a | all downstream | 15.0 dB | V1 all=1 | GTR RR +0dB | coupled | -25.4 dB | ok | +| p1a | treble | -23.9 dB | V0.6 treble=0 | GTR RR +0dB | ok | -60.6 dB | clean | +| p1a | middle | -14.3 dB | V0.6 middle=1 | GTR RR +0dB | coupled | -53.0 dB | clean | +| p1a | bass | -15.7 dB | V0.6 bass=0 | GTR L +0dB | coupled | -49.9 dB | clean | +| p1a | master | 14.2 dB | V0.6 master=1 | GTR RR +0dB | coupled | -27.5 dB | ok | +| p1a | presence | -12.4 dB | V0.6 presence=1 | GTR RR +0dB | coupled | -52.5 dB | clean | +| p1b | treble | -23.0 dB | V0.2 treble=0 | GTR RR +12dB | ok | -52.1 dB | clean | +| p1b | middle | -13.2 dB | V0.2 middle=1 | GTR RR +12dB | coupled | -45.0 dB | clean | +| p1b | bass | -15.9 dB | V0.2 bass=0 | GTR L +12dB | coupled | -39.5 dB | ok | +| p1b | master | 27.9 dB | V0.2 master=1 | GTR RR +0dB | coupled | -20.5 dB | ok | +| p1b | presence | -10.2 dB | V0.2 presence=1 | GTR RR +12dB | coupled | -46.2 dB | clean | +| p1b | all downstream | 28.9 dB | V0.2 all=1 | GTR RR +0dB | coupled | -19.4 dB | coupled | +| p2a | treble | -41.0 dB | V1 treble=0 | GTR L +0dB | clean | -47.2 dB | clean | +| p2a | middle | -35.1 dB | V1 middle=1 | GTR RR +12dB | ok | -36.2 dB | ok | +| p2a | bass | -29.5 dB | V1 bass=0 | GTR L +12dB | ok | -35.0 dB | ok | +| p2a | master | -2.1 dB | V0.2 master=1 | GTR RR +0dB | coupled | -17.3 dB | coupled | +| p2a | presence | -31.7 dB | V0.6 presence=1 | GTR RR +12dB | ok | -39.3 dB | ok | +| p2a | all downstream | -1.1 dB | V0.2 all=1 | GTR RR +0dB | coupled | -17.6 dB | coupled | +| k2b | treble | -38.6 dB | V1 treble=0 | GTR L +12dB | ok | -47.1 dB | clean | +| k2b | middle | -17.9 dB | V1 middle=0 | GTR RR +12dB | coupled | -20.5 dB | ok | +| k2b | bass | -18.9 dB | V1 bass=0 | GTR RR +12dB | coupled | -22.5 dB | ok | +| k2b | master | -1.6 dB | V0.2 master=1 | GTR RR +0dB | coupled | -17.4 dB | coupled | +| k2b | presence | -29.7 dB | V0.6 presence=1 | GTR RR +12dB | ok | -39.3 dB | ok | +| k2b | all downstream | -0.6 dB | V0.2 all=1 | GTR RR +0dB | coupled | -18.5 dB | coupled | +| y | presence | -36.5 dB | V0.6 presence=1 | GTR RR +12dB | ok | -36.4 dB | ok | +| bpa | preampVolume | 2.7 dB | V1 base | GTR RR +0dB | coupled | 8.4 dB | coupled | +| bpa | preampVolume+treble | 4.5 dB | V1 treble=0 | GTR RR +0dB | coupled | 10.1 dB | coupled | +| bpa | middle+preampVolume | 6.1 dB | V1 middle=1 | GTR RR +0dB | coupled | 10.7 dB | coupled | +| bpa | bass+preampVolume | 4.9 dB | V1 bass=1 | GTR RR +0dB | coupled | 10.7 dB | coupled | +| bpa | master+preampVolume | 21.4 dB | V1 master=1 | GTR RR +0dB | coupled | 25.5 dB | coupled | +| bpa | preampVolume+presence | 5.5 dB | V1 presence=1 | GTR RR +0dB | coupled | 9.6 dB | coupled | +| bpa | all downstream | 21.6 dB | V1 all=1 | GTR RR +0dB | coupled | 25.7 dB | coupled | +| bpa | treble | -17.3 dB | V0.6 treble=0 | GTR RR +0dB | coupled | -17.2 dB | coupled | +| bpa | middle | -7.7 dB | V0.6 middle=1 | GTR RR +0dB | coupled | -9.2 dB | coupled | +| bpa | bass | -8.3 dB | V0.6 bass=0 | GTR L +12dB | coupled | -6.4 dB | coupled | +| bpa | master | 20.8 dB | V0.6 master=1 | GTR RR +0dB | coupled | 24.8 dB | coupled | +| bpa | presence | -5.8 dB | V0.6 presence=1 | GTR RR +0dB | coupled | -7.4 dB | coupled | +| bpx | preampVolume | 3.2 dB | V1 base | GTR RR +0dB | coupled | 3.7 dB | coupled | +| bpx | preampVolume+treble | 5.0 dB | V1 treble=0 | GTR RR +0dB | coupled | 5.1 dB | coupled | +| bpx | middle+preampVolume | 6.4 dB | V1 middle=1 | GTR RR +0dB | coupled | 6.6 dB | coupled | +| bpx | bass+preampVolume | 5.4 dB | V1 bass=1 | GTR RR +0dB | coupled | 5.4 dB | coupled | +| bpx | master+preampVolume | 21.7 dB | V1 master=1 | GTR RR +0dB | coupled | 21.9 dB | coupled | +| bpx | preampVolume+presence | 5.7 dB | V1 presence=1 | GTR RR +0dB | coupled | 6.3 dB | coupled | +| bpx | all downstream | 21.9 dB | V1 all=1 | GTR RR +0dB | coupled | 21.9 dB | coupled | +| bpx | treble | -17.0 dB | V0.6 treble=0 | GTR RR +0dB | coupled | -17.9 dB | coupled | +| bpx | middle | -7.6 dB | V0.6 middle=1 | GTR RR +0dB | coupled | -7.9 dB | coupled | +| bpx | bass | -7.8 dB | V0.6 bass=0 | GTR L +12dB | coupled | -8.2 dB | coupled | +| bpx | master | 21.1 dB | V0.6 master=1 | GTR RR +0dB | coupled | 21.4 dB | coupled | +| bpx | presence | -5.4 dB | V0.6 presence=1 | GTR RR +0dB | coupled | -3.9 dB | coupled | + +## All comparisons + +| Boundary | Setting | Worst ESR | Signal | Worst ESR > 20 Hz | +|---|---|---|---|---| +| p1a | V0.2 base | -7.5 dB | GTR RR +0dB | -37.8 dB | +| p1a | V0.2 treble=0 | -7.7 dB | GTR RR +0dB | -37.8 dB | +| p1a | V0.2 treble=1 | -7.6 dB | GTR RR +0dB | -37.8 dB | +| p1a | V0.2 middle=0 | -7.1 dB | GTR RR +0dB | -37.7 dB | +| p1a | V0.2 middle=1 | -7.9 dB | GTR L +0dB | -37.9 dB | +| p1a | V0.2 bass=0 | -7.3 dB | GTR L +0dB | -37.7 dB | +| p1a | V0.2 bass=1 | -7.5 dB | GTR RR +0dB | -37.8 dB | +| p1a | V0.2 master=0 | -6.6 dB | GTR RR +0dB | -37.5 dB | +| p1a | V0.2 master=1 | 9.2 dB | GTR RR +0dB | -28.5 dB | +| p1a | V0.2 presence=0 | -7.4 dB | GTR RR +0dB | -37.7 dB | +| p1a | V0.2 presence=1 | -8.3 dB | GTR RR +0dB | -37.9 dB | +| p1a | V0.2 all=0 | -6.6 dB | GTR RR +0dB | -37.5 dB | +| p1a | V0.2 all=1 | 10.3 dB | GTR RR +0dB | -28.0 dB | +| p1a | V0.6 treble=0 | -23.9 dB | GTR RR +0dB | -60.6 dB | +| p1a | V0.6 treble=1 | -32.4 dB | GTR L +0dB | -67.2 dB | +| p1a | V0.6 middle=0 | -16.0 dB | GTR RR +0dB | -54.8 dB | +| p1a | V0.6 middle=1 | -14.3 dB | GTR RR +0dB | -53.0 dB | +| p1a | V0.6 bass=0 | -15.7 dB | GTR L +0dB | -49.9 dB | +| p1a | V0.6 bass=1 | -30.9 dB | GTR RR +12dB | -60.3 dB | +| p1a | V0.6 master=0 | -6.3 dB | GTR RR +0dB | -43.1 dB | +| p1a | V0.6 master=1 | 14.2 dB | GTR RR +0dB | -27.5 dB | +| p1a | V0.6 presence=0 | -25.8 dB | GTR RR +0dB | -64.5 dB | +| p1a | V0.6 presence=1 | -12.4 dB | GTR RR +0dB | -52.5 dB | +| p1a | V0.6 all=0 | -6.6 dB | GTR RR +0dB | -43.4 dB | +| p1a | V0.6 all=1 | 14.7 dB | GTR RR +0dB | -27.2 dB | +| p1a | V1 base | -3.9 dB | GTR RR +0dB | -30.8 dB | +| p1a | V1 treble=0 | -2.1 dB | GTR RR +0dB | -30.8 dB | +| p1a | V1 treble=1 | -4.6 dB | GTR RR +0dB | -30.8 dB | +| p1a | V1 middle=0 | -7.9 dB | GTR RR +0dB | -30.8 dB | +| p1a | V1 middle=1 | -0.5 dB | GTR RR +0dB | -30.8 dB | +| p1a | V1 bass=0 | -17.9 dB | GTR RR +0dB | -30.8 dB | +| p1a | V1 bass=1 | -1.7 dB | GTR RR +0dB | -30.7 dB | +| p1a | V1 master=0 | -6.8 dB | GTR L +0dB | -30.7 dB | +| p1a | V1 master=1 | 14.8 dB | GTR RR +0dB | -25.4 dB | +| p1a | V1 presence=0 | -5.0 dB | GTR RR +0dB | -30.8 dB | +| p1a | V1 presence=1 | -1.1 dB | GTR RR +0dB | -30.8 dB | +| p1a | V1 all=0 | -7.7 dB | GTR L +0dB | -30.7 dB | +| p1a | V1 all=1 | 15.0 dB | GTR RR +0dB | -25.4 dB | +| p1b | V0.2 treble=0 | -23.0 dB | GTR RR +12dB | -68.7 dB | +| p1b | V0.2 treble=1 | -29.0 dB | GTR L +0dB | -71.0 dB | +| p1b | V0.2 middle=0 | -14.6 dB | GTR RR +12dB | -57.7 dB | +| p1b | V0.2 middle=1 | -13.2 dB | GTR RR +12dB | -59.2 dB | +| p1b | V0.2 bass=0 | -15.9 dB | GTR L +12dB | -60.6 dB | +| p1b | V0.2 bass=1 | -34.3 dB | GTR RR +0dB | -77.3 dB | +| p1b | V0.2 master=0 | -5.3 dB | GTR L +12dB | -50.4 dB | +| p1b | V0.2 master=1 | 27.9 dB | GTR RR +0dB | -20.5 dB | +| p1b | V0.2 presence=0 | -24.3 dB | GTR RR +12dB | -69.3 dB | +| p1b | V0.2 presence=1 | -10.2 dB | GTR RR +12dB | -55.1 dB | +| p1b | V0.2 all=0 | -5.5 dB | GTR L +12dB | -51.0 dB | +| p1b | V0.2 all=1 | 28.9 dB | GTR RR +0dB | -19.4 dB | +| p1b | V0.6 treble=0 | -24.5 dB | GTR RR +0dB | -57.0 dB | +| p1b | V0.6 treble=1 | -32.9 dB | GTR L +0dB | -62.4 dB | +| p1b | V0.6 middle=0 | -16.6 dB | GTR RR +0dB | -50.5 dB | +| p1b | V0.6 middle=1 | -14.9 dB | GTR RR +0dB | -48.8 dB | +| p1b | V0.6 bass=0 | -16.3 dB | GTR L +0dB | -46.0 dB | +| p1b | V0.6 bass=1 | -30.5 dB | GTR RR +12dB | -56.7 dB | +| p1b | V0.6 master=0 | -6.9 dB | GTR L +0dB | -38.9 dB | +| p1b | V0.6 master=1 | 13.5 dB | GTR RR +0dB | -23.8 dB | +| p1b | V0.6 presence=0 | -26.4 dB | GTR RR +0dB | -60.4 dB | +| p1b | V0.6 presence=1 | -13.0 dB | GTR RR +0dB | -47.8 dB | +| p1b | V0.6 all=0 | -7.1 dB | GTR L +0dB | -39.2 dB | +| p1b | V0.6 all=1 | 14.0 dB | GTR RR +0dB | -23.6 dB | +| p1b | V1 treble=0 | -27.6 dB | GTR L +0dB | -52.1 dB | +| p1b | V1 treble=1 | -35.4 dB | GTR RR +0dB | -59.7 dB | +| p1b | V1 middle=0 | -22.6 dB | GTR RR +0dB | -46.6 dB | +| p1b | V1 middle=1 | -20.8 dB | GTR RR +0dB | -45.0 dB | +| p1b | V1 bass=0 | -16.7 dB | GTR L +0dB | -39.5 dB | +| p1b | V1 bass=1 | -25.8 dB | GTR RR +0dB | -48.0 dB | +| p1b | V1 master=0 | -10.3 dB | GTR RR +0dB | -33.5 dB | +| p1b | V1 master=1 | 2.8 dB | GTR RR +0dB | -22.4 dB | +| p1b | V1 presence=0 | -32.6 dB | GTR RR +0dB | -56.6 dB | +| p1b | V1 presence=1 | -21.8 dB | GTR RR +0dB | -46.2 dB | +| p1b | V1 all=0 | -10.7 dB | GTR RR +0dB | -34.0 dB | +| p1b | V1 all=1 | 3.0 dB | GTR RR +0dB | -22.4 dB | +| p2a | V0.2 treble=0 | -48.6 dB | GTR RR +12dB | -61.0 dB | +| p2a | V0.2 treble=1 | -52.0 dB | GTR L +12dB | -57.7 dB | +| p2a | V0.2 middle=0 | -35.6 dB | GTR L +0dB | -36.2 dB | +| p2a | V0.2 middle=1 | -37.8 dB | GTR RR +12dB | -39.5 dB | +| p2a | V0.2 bass=0 | -39.4 dB | GTR L +12dB | -42.3 dB | +| p2a | V0.2 bass=1 | -48.8 dB | GTR L +12dB | -48.9 dB | +| p2a | V0.2 master=0 | -30.4 dB | GTR L +12dB | -43.3 dB | +| p2a | V0.2 master=1 | -2.1 dB | GTR RR +0dB | -19.6 dB | +| p2a | V0.2 presence=0 | -49.3 dB | GTR L +12dB | -61.7 dB | +| p2a | V0.2 presence=1 | -34.9 dB | GTR L +12dB | -45.3 dB | +| p2a | V0.2 all=0 | -31.3 dB | GTR L +12dB | -35.4 dB | +| p2a | V0.2 all=1 | -1.1 dB | GTR RR +0dB | -19.6 dB | +| p2a | V0.6 treble=0 | -44.6 dB | GTR L +12dB | -52.0 dB | +| p2a | V0.6 treble=1 | -47.3 dB | GTR RR +12dB | -53.5 dB | +| p2a | V0.6 middle=0 | -36.6 dB | GTR RR +12dB | -39.1 dB | +| p2a | V0.6 middle=1 | -35.6 dB | GTR RR +12dB | -42.2 dB | +| p2a | V0.6 bass=0 | -34.2 dB | GTR L +12dB | -39.6 dB | +| p2a | V0.6 bass=1 | -42.2 dB | GTR L +12dB | -43.6 dB | +| p2a | V0.6 master=0 | -25.0 dB | GTR L +12dB | -32.7 dB | +| p2a | V0.6 master=1 | -8.0 dB | GTR RR +0dB | -18.1 dB | +| p2a | V0.6 presence=0 | -45.6 dB | GTR L +12dB | -53.4 dB | +| p2a | V0.6 presence=1 | -31.7 dB | GTR RR +12dB | -39.3 dB | +| p2a | V0.6 all=0 | -27.3 dB | GTR L +12dB | -36.3 dB | +| p2a | V0.6 all=1 | -7.7 dB | GTR RR +0dB | -18.4 dB | +| p2a | V1 treble=0 | -41.0 dB | GTR L +0dB | -47.2 dB | +| p2a | V1 treble=1 | -48.1 dB | GTR RR +12dB | -53.7 dB | +| p2a | V1 middle=0 | -35.8 dB | GTR RR +12dB | -38.0 dB | +| p2a | V1 middle=1 | -35.1 dB | GTR RR +12dB | -39.6 dB | +| p2a | V1 bass=0 | -29.5 dB | GTR L +12dB | -35.0 dB | +| p2a | V1 bass=1 | -36.7 dB | GTR RR +12dB | -40.0 dB | +| p2a | V1 master=0 | -22.4 dB | GTR L +12dB | -28.4 dB | +| p2a | V1 master=1 | -10.6 dB | GTR L +0dB | -17.3 dB | +| p2a | V1 presence=0 | -45.1 dB | GTR L +12dB | -51.0 dB | +| p2a | V1 presence=1 | -34.7 dB | GTR RR +12dB | -40.2 dB | +| p2a | V1 all=0 | -24.3 dB | GTR L +12dB | -30.7 dB | +| p2a | V1 all=1 | -10.6 dB | GTR L +0dB | -17.6 dB | +| k2b | V0.2 treble=0 | -47.2 dB | GTR RR +12dB | -57.4 dB | +| k2b | V0.2 treble=1 | -50.5 dB | GTR L +12dB | -54.3 dB | +| k2b | V0.2 middle=0 | -20.6 dB | GTR L +12dB | -21.5 dB | +| k2b | V0.2 middle=1 | -23.0 dB | GTR L +12dB | -23.9 dB | +| k2b | V0.2 bass=0 | -27.9 dB | GTR L +12dB | -28.4 dB | +| k2b | V0.2 bass=1 | -34.4 dB | GTR L +12dB | -34.3 dB | +| k2b | V0.2 master=0 | -29.4 dB | GTR L +12dB | -42.4 dB | +| k2b | V0.2 master=1 | -1.6 dB | GTR RR +0dB | -19.0 dB | +| k2b | V0.2 presence=0 | -48.3 dB | GTR L +12dB | -60.8 dB | +| k2b | V0.2 presence=1 | -33.9 dB | GTR L +12dB | -44.5 dB | +| k2b | V0.2 all=0 | -18.9 dB | GTR L +12dB | -20.3 dB | +| k2b | V0.2 all=1 | -0.6 dB | GTR RR +0dB | -19.7 dB | +| k2b | V0.6 treble=0 | -42.4 dB | GTR L +12dB | -50.4 dB | +| k2b | V0.6 treble=1 | -45.1 dB | GTR RR +12dB | -50.7 dB | +| k2b | V0.6 middle=0 | -18.1 dB | GTR RR +12dB | -20.5 dB | +| k2b | V0.6 middle=1 | -19.9 dB | GTR RR +12dB | -22.7 dB | +| k2b | V0.6 bass=0 | -22.3 dB | GTR L +12dB | -24.2 dB | +| k2b | V0.6 bass=1 | -29.3 dB | GTR L +12dB | -29.2 dB | +| k2b | V0.6 master=0 | -23.1 dB | GTR L +12dB | -32.5 dB | +| k2b | V0.6 master=1 | -6.6 dB | GTR RR +0dB | -17.6 dB | +| k2b | V0.6 presence=0 | -43.7 dB | GTR L +12dB | -53.2 dB | +| k2b | V0.6 presence=1 | -29.7 dB | GTR RR +12dB | -39.3 dB | +| k2b | V0.6 all=0 | -14.7 dB | GTR L +12dB | -18.6 dB | +| k2b | V0.6 all=1 | -6.0 dB | GTR RR +0dB | -20.3 dB | +| k2b | V1 treble=0 | -38.6 dB | GTR L +12dB | -47.1 dB | +| k2b | V1 treble=1 | -45.6 dB | GTR RR +12dB | -50.9 dB | +| k2b | V1 middle=0 | -17.9 dB | GTR RR +12dB | -20.9 dB | +| k2b | V1 middle=1 | -19.5 dB | GTR RR +12dB | -23.1 dB | +| k2b | V1 bass=0 | -18.9 dB | GTR RR +12dB | -22.5 dB | +| k2b | V1 bass=1 | -25.5 dB | GTR RR +12dB | -26.0 dB | +| k2b | V1 master=0 | -20.2 dB | GTR L +12dB | -28.9 dB | +| k2b | V1 master=1 | -8.9 dB | GTR L +0dB | -17.4 dB | +| k2b | V1 presence=0 | -42.9 dB | GTR L +12dB | -51.6 dB | +| k2b | V1 presence=1 | -32.4 dB | GTR RR +12dB | -40.8 dB | +| k2b | V1 all=0 | -12.8 dB | GTR L +12dB | -18.5 dB | +| k2b | V1 all=1 | -8.2 dB | GTR L +0dB | -20.4 dB | +| y | V0.2 presence=0 | -56.8 dB | GTR L +12dB | -56.6 dB | +| y | V0.2 presence=1 | -38.8 dB | GTR L +12dB | -38.8 dB | +| y | V0.6 presence=0 | -51.6 dB | GTR L +12dB | -51.6 dB | +| y | V0.6 presence=1 | -36.5 dB | GTR RR +12dB | -36.4 dB | +| y | V1 presence=0 | -50.3 dB | GTR L +12dB | -50.3 dB | +| y | V1 presence=1 | -36.7 dB | GTR RR +12dB | -36.7 dB | +| bpa | V0.2 base | -0.9 dB | GTR RR +0dB | -0.9 dB | +| bpa | V0.2 treble=0 | -1.1 dB | GTR L +0dB | -0.9 dB | +| bpa | V0.2 treble=1 | -1.0 dB | GTR RR +0dB | -1.0 dB | +| bpa | V0.2 middle=0 | -0.5 dB | GTR RR +0dB | -0.5 dB | +| bpa | V0.2 middle=1 | -1.3 dB | GTR L +0dB | -1.3 dB | +| bpa | V0.2 bass=0 | -0.7 dB | GTR L +0dB | -0.7 dB | +| bpa | V0.2 bass=1 | -0.9 dB | GTR RR +0dB | -0.8 dB | +| bpa | V0.2 master=0 | 0.0 dB | GTR L +0dB | 0.1 dB | +| bpa | V0.2 master=1 | 15.8 dB | GTR RR +0dB | 16.3 dB | +| bpa | V0.2 presence=0 | -0.8 dB | GTR RR +0dB | -0.8 dB | +| bpa | V0.2 presence=1 | -1.7 dB | GTR L +0dB | -1.7 dB | +| bpa | V0.2 all=0 | 0.1 dB | GTR L +0dB | 0.1 dB | +| bpa | V0.2 all=1 | 16.9 dB | GTR RR +0dB | 17.3 dB | +| bpa | V0.6 treble=0 | -17.3 dB | GTR RR +0dB | -17.2 dB | +| bpa | V0.6 treble=1 | -23.9 dB | GTR RR +12dB | -27.3 dB | +| bpa | V0.6 middle=0 | -9.4 dB | GTR RR +0dB | -10.9 dB | +| bpa | V0.6 middle=1 | -7.7 dB | GTR RR +0dB | -9.2 dB | +| bpa | V0.6 bass=0 | -8.3 dB | GTR L +12dB | -6.4 dB | +| bpa | V0.6 bass=1 | -19.9 dB | GTR RR +12dB | -16.3 dB | +| bpa | V0.6 master=0 | 0.3 dB | GTR RR +0dB | 0.2 dB | +| bpa | V0.6 master=1 | 20.8 dB | GTR RR +0dB | 24.8 dB | +| bpa | V0.6 presence=0 | -19.2 dB | GTR RR +0dB | -19.9 dB | +| bpa | V0.6 presence=1 | -5.8 dB | GTR RR +0dB | -7.4 dB | +| bpa | V0.6 all=0 | 0.0 dB | GTR RR +0dB | -0.0 dB | +| bpa | V0.6 all=1 | 21.2 dB | GTR RR +0dB | 25.2 dB | +| bpa | V1 base | 2.7 dB | GTR RR +0dB | 8.4 dB | +| bpa | V1 treble=0 | 4.5 dB | GTR RR +0dB | 10.1 dB | +| bpa | V1 treble=1 | 2.0 dB | GTR RR +0dB | 7.4 dB | +| bpa | V1 middle=0 | -1.3 dB | GTR RR +0dB | 6.0 dB | +| bpa | V1 middle=1 | 6.1 dB | GTR RR +0dB | 10.7 dB | +| bpa | V1 bass=0 | -11.5 dB | GTR RR +0dB | -3.8 dB | +| bpa | V1 bass=1 | 4.9 dB | GTR RR +0dB | 10.7 dB | +| bpa | V1 master=0 | -0.2 dB | GTR L +0dB | 0.5 dB | +| bpa | V1 master=1 | 21.4 dB | GTR RR +0dB | 25.5 dB | +| bpa | V1 presence=0 | 1.6 dB | GTR RR +0dB | 7.7 dB | +| bpa | V1 presence=1 | 5.5 dB | GTR RR +0dB | 9.6 dB | +| bpa | V1 all=0 | -1.1 dB | GTR L +0dB | -0.6 dB | +| bpa | V1 all=1 | 21.6 dB | GTR RR +0dB | 25.7 dB | +| bpx | V0.2 base | -0.9 dB | GTR RR +0dB | -1.5 dB | +| bpx | V0.2 treble=0 | -1.1 dB | GTR L +0dB | -1.6 dB | +| bpx | V0.2 treble=1 | -1.0 dB | GTR RR +0dB | -1.7 dB | +| bpx | V0.2 middle=0 | -0.5 dB | GTR RR +0dB | -1.0 dB | +| bpx | V0.2 middle=1 | -1.4 dB | GTR L +0dB | -2.1 dB | +| bpx | V0.2 bass=0 | -0.7 dB | GTR L +0dB | -1.4 dB | +| bpx | V0.2 bass=1 | -0.9 dB | GTR RR +0dB | -1.4 dB | +| bpx | V0.2 master=0 | 0.1 dB | GTR RR +12dB | -0.1 dB | +| bpx | V0.2 master=1 | 15.8 dB | GTR RR +0dB | 14.7 dB | +| bpx | V0.2 presence=0 | -0.8 dB | GTR RR +0dB | -1.3 dB | +| bpx | V0.2 presence=1 | -1.7 dB | GTR RR +0dB | -2.8 dB | +| bpx | V0.2 all=0 | 0.1 dB | GTR L +0dB | -0.2 dB | +| bpx | V0.2 all=1 | 16.9 dB | GTR RR +0dB | 15.9 dB | +| bpx | V0.6 treble=0 | -17.0 dB | GTR RR +0dB | -22.9 dB | +| bpx | V0.6 treble=1 | -22.1 dB | GTR RR +12dB | -17.9 dB | +| bpx | V0.6 middle=0 | -9.3 dB | GTR RR +0dB | -9.2 dB | +| bpx | V0.6 middle=1 | -7.6 dB | GTR RR +0dB | -7.9 dB | +| bpx | V0.6 bass=0 | -7.8 dB | GTR L +12dB | -8.2 dB | +| bpx | V0.6 bass=1 | -18.6 dB | GTR RR +12dB | -17.5 dB | +| bpx | V0.6 master=0 | 0.3 dB | GTR RR +0dB | -0.5 dB | +| bpx | V0.6 master=1 | 21.1 dB | GTR RR +0dB | 21.4 dB | +| bpx | V0.6 presence=0 | -19.1 dB | GTR RR +0dB | -19.8 dB | +| bpx | V0.6 presence=1 | -5.4 dB | GTR RR +0dB | -3.9 dB | +| bpx | V0.6 all=0 | 0.0 dB | GTR RR +0dB | -0.7 dB | +| bpx | V0.6 all=1 | 21.5 dB | GTR RR +0dB | 21.6 dB | +| bpx | V1 base | 3.2 dB | GTR RR +0dB | 3.7 dB | +| bpx | V1 treble=0 | 5.0 dB | GTR RR +0dB | 5.1 dB | +| bpx | V1 treble=1 | 2.3 dB | GTR RR +0dB | 3.4 dB | +| bpx | V1 middle=0 | -0.4 dB | GTR RR +0dB | 0.9 dB | +| bpx | V1 middle=1 | 6.4 dB | GTR RR +0dB | 6.6 dB | +| bpx | V1 bass=0 | -11.7 dB | GTR RR +0dB | -1.9 dB | +| bpx | V1 bass=1 | 5.4 dB | GTR RR +0dB | 5.4 dB | +| bpx | V1 master=0 | 0.2 dB | GTR L +0dB | 0.8 dB | +| bpx | V1 master=1 | 21.7 dB | GTR RR +0dB | 21.9 dB | +| bpx | V1 presence=0 | 2.1 dB | GTR RR +0dB | 2.8 dB | +| bpx | V1 presence=1 | 5.7 dB | GTR RR +0dB | 6.3 dB | +| bpx | V1 all=0 | -0.7 dB | GTR L +0dB | 1.4 dB | +| bpx | V1 all=1 | 21.9 dB | GTR RR +0dB | 21.9 dB | From 2fe454accc436e0ac6ad40213f7a985ed64d1595 Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 10:23:29 +0300 Subject: [PATCH 08/18] m6 X2: exact-J4 engine at 2x / 4x / 8x, plugin model check, Newton, aliasing, cost --- docs/PROGRESS.md | 13 ++ harness/reports/M6/20261015_x2_rates.md | 52 ++++++ harness/x2_rates.py | 203 ++++++++++++++++++++++++ 3 files changed, 268 insertions(+) create mode 100644 harness/reports/M6/20261015_x2_rates.md create mode 100644 harness/x2_rates.py diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 3714a44..e181a25 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -681,6 +681,19 @@ Notes: treble and Presence ≤ −35 dB. y: Presence −36.4 dB. Supply nodes bpa / bpx change by +3 … +26 dB with the knobs (they carry the coupling). Implication: a split at y and at p1a / p1b is within −20 dB in the audio band; any preamp block reaching p2a or later needs the supply (power-amp current draw) as an input, or Master as a proxy. +- Owner 2026-10-15: leave the per-block route; run X2, then A1. +- X2 / A1 plan: `harness/x2_rates.py` (A1 = the same script with `--power-rate-divisor 2` and / or `--newton-tol`). + Full amp with exact J4 at 2x / 4x / 8x of 48 kHz (96 / 192 / 384 kHz). Scored as the plugin model check (D-051): + vs SPICE (cached 384 kHz references, 48 kHz band) on the held-out 5 s DI clips at CRUNCH, DRIVE and HOT; vs the + engine at 16x on the 16 X4 validation settings (10 s each); Newton failures (rescued / not) on every run; + aliasing on 5 / 9 kHz sines vs 16x at CRUNCH and HOT; cost from `ampwav --stats` on 10 s DI (default and HOT + knobs), run one at a time, as % of one core at 48 kHz (oversampling filters not included). +- X2 result (`harness/reports/M6/20261015_x2_rates.md`): vs SPICE worst CRUNCH / DRIVE / HOT −46.4 / −49.9 / −26.2 dB at + 2x, −48.5 / −55.6 / −33.2 at 4x, −48.7 / −55.8 / −38.6 at 8x (all PASS ≤ −20 dB); unseen settings vs 16x worst + −20.6 / −26.9 / −39.1 dB. Newton: unrescued failures per 5 s DI clip 6 … 29 at 2x, 0 at 4x and 8x; on loud + 5 / 9 kHz sines thousands to ≈ 140 000 per 2 s at 2x / 4x (and up to 11 496 at 8x, 20 at 16x), which dominate the + aliasing figures there. Cost ≈ 3.4–3.6 µs per engine sample (HOT 4.3–5.5): 34 / 67 / 129 % of one core at 2x / 4x + / 8x (HOT 52 / 92 / 166 %). Limiting factor at lower rates: Newton convergence, not ESR. ## Questions for the owner _(Claude Code: add questions here and also ask them in the session.)_ diff --git a/harness/reports/M6/20261015_x2_rates.md b/harness/reports/M6/20261015_x2_rates.md new file mode 100644 index 0000000..d4f5106 --- /dev/null +++ b/harness/reports/M6/20261015_x2_rates.md @@ -0,0 +1,52 @@ +# M6 X2: full engine (exact J4) at lower internal rates, plugin model check + +- Date 2026-10-15; options: defaults (power amp at full rate, Newton tol 1e-9 V); rates 48 kHz × [2, 4, 8]. +- Plugin model check (D-051): ESR ≤ -20 dB vs SPICE (cached 384000 Hz references, 48 kHz band, first 5 s of every DI track, first 0.1 s excluded). Unseen settings: the 16 X4 validation settings, 10 s, vs the engine at 16x (first 1 s excluded). Cost: ampwav --stats on 10 s of DI GTR L, one run at a time; oversampling filters not included. + +## Summary + +| Rate | vs SPICE worst (CRUNCH / DRIVE / HOT) | Check | Unseen settings vs 16x (worst / median) | Newton failures (not rescued) / runs, max iterations | Cost default / HOT (% of one core) | +|---|---|---|---|---|---| +| 2x (96 kHz) | -46.4 / -49.9 / -26.2 dB | PASS | -20.6 / -39.1 dB | 8723 (1376) / 28, 16 | 34.7 / 52.5 | +| 4x (192 kHz) | -48.5 / -55.6 / -33.2 dB | PASS | -26.9 / -54.2 dB | 2058 (147) / 28, 16 | 67.0 / 93.1 | +| 8x (384 kHz) | -48.7 / -55.8 / -38.6 dB | PASS | -39.1 / -69.7 dB | 128 (3) / 28, 16 | 130.3 / 165.9 | + +## vs SPICE per clip + +| Clip | 2x | 4x | 8x | +|---|---|---|---| +| CRUNCH GTR L | -47.4 dB | -49.0 dB | -49.0 dB | +| CRUNCH GTR LL | -46.4 dB | -48.5 dB | -48.7 dB | +| CRUNCH GTR R | -47.3 dB | -48.9 dB | -48.9 dB | +| CRUNCH GTR RR | -47.2 dB | -49.0 dB | -49.0 dB | +| DRIVE GTR L | -49.9 dB | -55.6 dB | -55.8 dB | +| DRIVE GTR LL | -50.8 dB | -56.1 dB | -56.2 dB | +| DRIVE GTR R | -51.0 dB | -56.0 dB | -56.1 dB | +| DRIVE GTR RR | -50.5 dB | -55.9 dB | -56.0 dB | +| HOT GTR L | -26.3 dB | -33.2 dB | -38.7 dB | +| HOT GTR LL | -26.9 dB | -34.0 dB | -38.6 dB | +| HOT GTR R | -27.2 dB | -34.1 dB | -38.8 dB | +| HOT GTR RR | -26.2 dB | -33.7 dB | -38.9 dB | + +## Aliasing (power outside the harmonics of f0, relative to the total; 16x for reference) + +Each cell: non-harmonic power, then Newton failures (not rescued) in that 2 s run. Non-converged samples add non-harmonic power, so a cell with many failures measures solver error as well as aliasing. + +| Preset | f0 | amplitude (FS) | 16x | 2x | 4x | 8x | +|---|---|---|---|---|---|---| +| CRUNCH | 5000 Hz | 0.1 | -26.0 dB, 0 (0) | -19.5 dB, 4041 (4036) | -25.8 dB, 13 (13) | -26.0 dB, 0 (0) | +| CRUNCH | 5000 Hz | 0.4 | -25.7 dB, 0 (0) | -5.5 dB, 49534 (48102) | -23.0 dB, 11661 (9423) | -25.4 dB, 5 (4) | +| CRUNCH | 9000 Hz | 0.1 | -39.9 dB, 0 (0) | -12.4 dB, 21947 (14870) | -26.1 dB, 54 (54) | -36.6 dB, 0 (0) | +| CRUNCH | 9000 Hz | 0.4 | -38.5 dB, 0 (0) | -2.4 dB, 116713 (114726) | -5.0 dB, 104806 (102557) | -32.8 dB, 20594 (9810) | +| HOT | 5000 Hz | 0.1 | -30.0 dB, 1990 (0) | -19.0 dB, 6016 (4756) | -16.9 dB, 1000 (3) | -26.1 dB, 562 (0) | +| HOT | 5000 Hz | 0.4 | -29.8 dB, 2395 (0) | -7.3 dB, 59038 (55223) | -18.9 dB, 15085 (10854) | -26.2 dB, 1138 (41) | +| HOT | 9000 Hz | 0.1 | -38.2 dB, 1424 (0) | -9.2 dB, 28601 (14100) | -15.0 dB, 9716 (5098) | -24.8 dB, 257 (0) | +| HOT | 9000 Hz | 0.4 | -36.0 dB, 833 (20) | -2.3 dB, 102057 (98116) | -1.2 dB, 142529 (137993) | -27.8 dB, 17697 (11496) | + +## Cost + +| Rate | ns per engine sample (default / HOT) | % of one core at 48 kHz | +|---|---|---| +| 2x | 3612 / 5467 | 34.7 / 52.5 | +| 4x | 3490 / 4847 | 67.0 / 93.1 | +| 8x | 3393 / 4319 | 130.3 / 165.9 | diff --git a/harness/x2_rates.py b/harness/x2_rates.py new file mode 100644 index 0000000..feabc53 --- /dev/null +++ b/harness/x2_rates.py @@ -0,0 +1,203 @@ +"""M6 experiments X2 / A1 (docs/ARCHITECTURE_STUDY.md §6): the full engine (exact J4) at lower internal rates, +scored as the real-time plugin model check (VERIFICATION §7, D-051). + + python -m harness.x2_rates # X2: 2x / 4x / 8x of 48 kHz + python -m harness.x2_rates --tag a1 --power-rate-divisor 2 --newton-tol 1e-5 # A1 variants + +Checks per rate: ESR vs SPICE (cached 384 kHz references, 48 kHz band, held-out 5 s DI at CRUNCH, DRIVE, HOT); +ESR vs the engine at 16x on the 16 X4 validation settings; Newton failures; aliasing vs 16x; cost (ampwav --stats, +one run at a time). Engine runs are cached in harness/cache/x2/. +""" +import argparse +import datetime +import hashlib +import json +import subprocess +import sys +import tempfile +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path + +import numpy as np +from scipy.signal import resample_poly + +from . import config, metrics, tolerances +from . import x3_neural as x3 +from . import x4_conditioned as x4 +from .presets import PRESETS +from .wavio import read_wav, write_wav + +RATE = 48000 +REF_FACTOR = 16 +CACHE = config.HARNESS / 'cache' / 'x2' +WORKERS = 6 +VAL_SECONDS = 10.0 +COST_SECONDS = 10.0 +LIMIT = tolerances.TOLERANCES['plugin_model_esr_db'][0] + + +def _run(x48, knobs, factor, opts, stats_only=False): + """Full amp (exact J4) at 48 kHz × factor on a 48 kHz full-scale input. Returns (out at 48 kHz, stats). + Cached by input, knobs, factor and options (not for cost runs).""" + key = hashlib.sha256(x48.astype(np.float64).tobytes() + json.dumps([knobs, factor, opts], sort_keys=True).encode()) + path = CACHE / f'{key.hexdigest()[:24]}.npz' + if path.exists() and not stats_only: + with np.load(path) as z: + return z['y'], json.loads(str(z['stats'])) + hi = resample_poly(x48, factor, 1) + with tempfile.TemporaryDirectory(prefix='amp2203_x2_') as d: + d = Path(d) + write_wav(d / 'in.wav', hi, RATE * factor) + args = [str(x3.AMPWAV), '--block', 'full', '--in', str(d / 'in.wav'), '--out', str(d / 'out.wav'), + '--units', 'fs', '--rate', str(RATE * factor), '--j4', 'exact', '--stats', str(d / 'stats.json')] + args += [a for k, v in knobs.items() for a in ('--param', f'{k}={v!r}')] + if opts.get('divisor', 1) != 1: + args += ['--power-rate-divisor', str(opts['divisor'])] + if opts.get('tol'): + args += ['--newton-tol', repr(opts['tol'])] + r = subprocess.run(args, capture_output=True, text=True) + if r.returncode != 0: + raise RuntimeError(r.stderr) + stats = json.loads((d / 'stats.json').read_text()) + if stats_only: + return None, stats + y = resample_poly(read_wav(d / 'out.wav')[0], 1, factor)[:len(x48)] + CACHE.mkdir(parents=True, exist_ok=True) + np.savez(path, y=y, stats=json.dumps(stats)) + return y, stats + + +def _jobs(factors, opts): + """Accuracy jobs: (kind, id, x48, knobs, factor, opts).""" + tracks = x3._tracks() + n5 = int(x3.HOLD_HEAD_S * RATE) + jobs = [] + for f in factors: + for pid in x4.EVAL_PRESETS: + for t in sorted(tracks): + jobs.append(('spice', f'{pid}|{t}', tracks[t][:n5], PRESETS[pid].all_knobs(), f, opts)) + vx, _, vk = x4._load('val') + nv = int(VAL_SECONDS * RATE) + for i, (x, k) in enumerate(zip(vx, vk)): + knobs = x4._all_knobs(k) + for f in factors: + jobs.append(('val', str(i), x[:nv].astype(np.float64), knobs, f, opts)) + jobs.append(('val', str(i), x[:nv].astype(np.float64), knobs, REF_FACTOR, {})) + return jobs + + +def run(factors, opts, tag): + refs = dict(np.load(x4.CACHE / 'spice_refs.npz')) + jobs = _jobs(factors, opts) + with ThreadPoolExecutor(max_workers=WORKERS) as ex: + res = list(ex.map(lambda j: _run(j[2], j[3], j[4], j[5]), jobs)) + print(f'x2: {len(jobs)} engine runs done', flush=True) + s = slice(int(0.1 * RATE), int(x3.HOLD_HEAD_S * RATE)) + sv = slice(RATE, int(VAL_SECONDS * RATE)) + spice, val, newton = {}, {}, {} + ref16 = {j[1]: r[0] for j, r in zip(jobs, res) if j[0] == 'val' and j[4] == REF_FACTOR} + for (kind, ident, _, _, f, o), (y, st) in zip(jobs, res): + if f == REF_FACTOR and kind == 'val': + continue + nw = newton.setdefault(f, [0, 0, 0, 0]) + nw[0] += st['newton_failures'] + nw[1] += st.get('newton_unrescued', 0) + nw[2] = max(nw[2], st['max_iterations_used']) + nw[3] += 1 + if kind == 'spice': + ref = refs[ident] + spice.setdefault(f, {})[ident] = metrics.esr_db(ref[s], y[s]) + else: + val.setdefault(f, {})[ident] = metrics.esr_db(ref16[ident][sv], y[sv]) + alias = {} + for f in factors: + for pid in ('CRUNCH', 'HOT'): + knobs = PRESETS[pid].all_knobs() + alias[(f, pid)] = x3._alias_test(lambda x: _run(x, knobs, f, opts)[0], + lambda x: _run(x, knobs, REF_FACTOR, {})[0]) + n = RATE + t = np.arange(2 * n) / RATE + alias_newton = {} # the same sines as x3._alias_test: (failures, not rescued) per rate + for f in factors + [REF_FACTOR]: + for pid in ('CRUNCH', 'HOT'): + knobs = PRESETS[pid].all_knobs() + for f0 in (5000.0, 9000.0): + for amp in (0.1, 0.4): + st = _run(amp * np.sin(2 * np.pi * f0 * t), knobs, f, opts if f != REF_FACTOR else {})[1] + alias_newton[(f, pid, f0, amp)] = (st['newton_failures'], st.get('newton_unrescued', 0)) + print('x2: aliasing done', flush=True) + tracks = x3._tracks() + xc = tracks['GTR L'][RATE:RATE + int(COST_SECONDS * RATE)] + cost = {} + for f in factors: # one at a time: timing + for name, knobs in (('default', config.DEFAULT_KNOBS), ('HOT', PRESETS['HOT'].all_knobs())): + _, st = _run(xc, knobs, f, opts, stats_only=True) + cost[(f, name)] = (st['ns_per_sample'], st['ns_per_sample'] * f * RATE / 1e7) + print(f'x2: cost {f}x {name}: {st["ns_per_sample"]:.0f} ns per engine sample = ' + f'{cost[(f, name)][1]:.1f} % of one core', flush=True) + write_report(factors, opts, tag, spice, val, newton, alias, alias_newton, cost) + + +def write_report(factors, opts, tag, spice, val, newton, alias, alias_newton, cost): + out = config.REPORT_DIR / 'M6' + out.mkdir(parents=True, exist_ok=True) + path = out / f'{datetime.date.today().strftime("%Y%m%d")}_x2_rates{"_" + tag if tag else ""}.md' + o = ', '.join(f'{k} {v}' for k, v in opts.items()) or 'defaults (power amp at full rate, Newton tol 1e-9 V)' + lines = [f'# M6 {"A1" if tag else "X2"}: full engine (exact J4) at lower internal rates, plugin model check', '', + f'- Date {datetime.date.today().isoformat()}; options: {o}; rates 48 kHz × {factors}.', + f'- Plugin model check (D-051): ESR ≤ {LIMIT:g} dB vs SPICE (cached {x4.SPICE_FS} Hz references, 48 kHz ' + 'band, first 5 s of every DI track, first 0.1 s excluded). Unseen settings: the 16 X4 validation ' + f'settings, {VAL_SECONDS:g} s, vs the engine at {REF_FACTOR}x (first 1 s excluded). Cost: ampwav ' + f'--stats on {COST_SECONDS:g} s of DI GTR L, one run at a time; oversampling filters not included.', '', + '## Summary', '', + '| Rate | vs SPICE worst (CRUNCH / DRIVE / HOT) | Check | Unseen settings vs 16x (worst / median) | ' + 'Newton failures (not rescued) / runs, max iterations | Cost default / HOT (% of one core) |', + '|---|---|---|---|---|---|'] + for f in factors: + w = {pid: max(e for k, e in spice[f].items() if k.startswith(pid)) for pid in x4.EVAL_PRESETS} + worst = max(w.values()) + v = list(val[f].values()) + nw = newton[f] + lines.append(f'| {f}x ({RATE * f // 1000} kHz) | {w["CRUNCH"]:.1f} / {w["DRIVE"]:.1f} / {w["HOT"]:.1f} dB | ' + f'{"PASS" if worst <= LIMIT else "FAIL"} | {max(v):.1f} / {np.median(v):.1f} dB | ' + f'{nw[0]} ({nw[1]}) / {nw[3]}, {nw[2]} | {cost[(f, "default")][1]:.1f} / {cost[(f, "HOT")][1]:.1f} |') + lines += ['', '## vs SPICE per clip', '', '| Clip | ' + ' | '.join(f'{f}x' for f in factors) + ' |', + '|---|' + '---|' * len(factors)] + for k in spice[factors[0]]: + lines.append(f'| {k.replace("|", " ")} | ' + ' | '.join(f'{spice[f][k]:.1f} dB' for f in factors) + ' |') + lines += ['', '## Aliasing (power outside the harmonics of f0, relative to the total; 16x for reference)', '', + 'Each cell: non-harmonic power, then Newton failures (not rescued) in that 2 s run. Non-converged ' + 'samples add non-harmonic power, so a cell with many failures measures solver error as well as ' + 'aliasing.', '', + '| Preset | f0 | amplitude (FS) | 16x | ' + ' | '.join(f'{f}x' for f in factors) + ' |', + '|---|---|---|---|' + '---|' * len(factors)] + cell = lambda e, nw: f'{e:.1f} dB, {nw[0]} ({nw[1]})' + for pid in ('CRUNCH', 'HOT'): + for i, (f0, amp, t, _) in enumerate(alias[(factors[0], pid)]): + lines.append(f'| {pid} | {f0:.0f} Hz | {amp:g} | {cell(t, alias_newton[(REF_FACTOR, pid, f0, amp)])} | ' + + ' | '.join(cell(alias[(f, pid)][i][3], alias_newton[(f, pid, f0, amp)]) for f in factors) + ' |') + lines += ['', '## Cost', '', '| Rate | ns per engine sample (default / HOT) | % of one core at 48 kHz |', '|---|---|---|'] + for f in factors: + lines.append(f'| {f}x | {cost[(f, "default")][0]:.0f} / {cost[(f, "HOT")][0]:.0f} | ' + f'{cost[(f, "default")][1]:.1f} / {cost[(f, "HOT")][1]:.1f} |') + path.write_text('\n'.join(lines) + '\n') + print(f'x2: wrote {path.relative_to(config.ROOT)}') + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument('--factors', type=int, nargs='+', default=[2, 4, 8]) + ap.add_argument('--power-rate-divisor', type=int, default=1) + ap.add_argument('--newton-tol', type=float, default=None) + ap.add_argument('--tag', default='') + a = ap.parse_args() + opts = {} + if a.power_rate_divisor != 1: + opts['divisor'] = a.power_rate_divisor + if a.newton_tol: + opts['tol'] = a.newton_tol + run(a.factors, opts, a.tag) + + +if __name__ == '__main__': + sys.exit(main()) From f17a4fc5b8ea4fe57807098425085bfe606d9f29 Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 10:31:35 +0300 Subject: [PATCH 09/18] m6 A1: Newton tol and half-rate power amp at 2x / 4x / 8x; ampwav stats report exact J4 as in effect --- docs/PROGRESS.md | 7 +++ .../M6/20261015_x2_rates_a1_div2_tol1e-5.md | 54 +++++++++++++++++++ .../M6/20261015_x2_rates_a1_tol1e-5.md | 52 ++++++++++++++++++ tools/ampwav/src/Cli.cpp | 3 +- 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 harness/reports/M6/20261015_x2_rates_a1_div2_tol1e-5.md create mode 100644 harness/reports/M6/20261015_x2_rates_a1_tol1e-5.md diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index e181a25..50f39ce 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -694,6 +694,13 @@ Notes: 5 / 9 kHz sines thousands to ≈ 140 000 per 2 s at 2x / 4x (and up to 11 496 at 8x, 20 at 16x), which dominate the aliasing figures there. Cost ≈ 3.4–3.6 µs per engine sample (HOT 4.3–5.5): 34 / 67 / 129 % of one core at 2x / 4x / 8x (HOT 52 / 92 / 166 %). Limiting factor at lower rates: Newton convergence, not ESR. +- A1 result: (a) Newton tolerance 1e-5 V (`…_x2_rates_a1_tol1e-5.md`): accuracy as X2 (worst HOT −26.2 / −33.2 / + −38.7 dB at 2x / 4x / 8x, PASS); cost 36 / 58 / 106 % of one core (HOT 45 / 77 / 157 %); Newton failures fewer + (DI + validation runs: 6568 / 1380 / 22, unrescued 1048 / 138 / 3), loud 5 / 9 kHz sines still fail at 2x / 4x. + (b) plus the power amp at half rate (`…_a1_div2_tol1e-5.md`): not a test of exact J4 — `Amp2203::exactJ4()` is off + when the power amp runs at half rate, so these runs used the Thevenin J4: HOT −13.0 dB at every rate (FAIL), CRUNCH + / DRIVE pass; cost 17 / 31 / 55 % (HOT 27 / 44 / 75 %). ampwav `--stats` now reports `exact_j4` as in effect. + Exact J4 with the half-rate power amp is not implemented (open). ## Questions for the owner _(Claude Code: add questions here and also ask them in the session.)_ diff --git a/harness/reports/M6/20261015_x2_rates_a1_div2_tol1e-5.md b/harness/reports/M6/20261015_x2_rates_a1_div2_tol1e-5.md new file mode 100644 index 0000000..0b21e97 --- /dev/null +++ b/harness/reports/M6/20261015_x2_rates_a1_div2_tol1e-5.md @@ -0,0 +1,54 @@ +# M6 A1: full engine (exact J4) at lower internal rates, plugin model check + +> **Not a valid A1 result for exact J4:** the engine runs the exact J4 coupling only with the power amp at full rate (`Amp2203::exactJ4()`, divisor 1). These runs used the Thevenin J4 (D-015), which explains HOT at −13 dB (the pre-X1 J4 error). The stats field `exact_j4` reported the requested value until ampwav was fixed to report the value in effect. + +- Date 2026-10-15; options: divisor 2, tol 1e-05; rates 48 kHz × [2, 4, 8]. +- Plugin model check (D-051): ESR ≤ -20 dB vs SPICE (cached 384000 Hz references, 48 kHz band, first 5 s of every DI track, first 0.1 s excluded). Unseen settings: the 16 X4 validation settings, 10 s, vs the engine at 16x (first 1 s excluded). Cost: ampwav --stats on 10 s of DI GTR L, one run at a time; oversampling filters not included. + +## Summary + +| Rate | vs SPICE worst (CRUNCH / DRIVE / HOT) | Check | Unseen settings vs 16x (worst / median) | Newton failures (not rescued) / runs, max iterations | Cost default / HOT (% of one core) | +|---|---|---|---|---|---| +| 2x (96 kHz) | -40.1 / -39.2 / -12.9 dB | FAIL | -17.1 / -29.0 dB | 11346 (1145) / 28, 16 | 17.1 / 27.0 | +| 4x (192 kHz) | -47.8 / -51.8 / -13.0 dB | FAIL | -18.2 / -36.5 dB | 5926 (165) / 28, 16 | 31.1 / 43.9 | +| 8x (384 kHz) | -48.7 / -55.8 / -13.0 dB | FAIL | -20.2 / -49.1 dB | 1600 (2) / 28, 16 | 54.8 / 75.0 | + +## vs SPICE per clip + +| Clip | 2x | 4x | 8x | +|---|---|---|---| +| CRUNCH GTR L | -40.4 dB | -48.4 dB | -49.1 dB | +| CRUNCH GTR LL | -40.1 dB | -47.8 dB | -48.7 dB | +| CRUNCH GTR R | -41.3 dB | -48.4 dB | -48.9 dB | +| CRUNCH GTR RR | -40.8 dB | -48.5 dB | -49.0 dB | +| DRIVE GTR L | -39.2 dB | -51.8 dB | -55.8 dB | +| DRIVE GTR LL | -40.3 dB | -52.6 dB | -56.2 dB | +| DRIVE GTR R | -40.5 dB | -52.5 dB | -56.2 dB | +| DRIVE GTR RR | -39.8 dB | -52.0 dB | -56.1 dB | +| HOT GTR L | -13.0 dB | -13.2 dB | -13.2 dB | +| HOT GTR LL | -13.3 dB | -13.4 dB | -13.4 dB | +| HOT GTR R | -13.1 dB | -13.2 dB | -13.2 dB | +| HOT GTR RR | -12.9 dB | -13.0 dB | -13.0 dB | + +## Aliasing (power outside the harmonics of f0, relative to the total; 16x for reference) + +Each cell: non-harmonic power, then Newton failures (not rescued) in that 2 s run. Non-converged samples add non-harmonic power, so a cell with many failures measures solver error as well as aliasing. + +| Preset | f0 | amplitude (FS) | 16x | 2x | 4x | 8x | +|---|---|---|---|---|---|---| +| CRUNCH | 5000 Hz | 0.1 | -26.0 dB, 0 (0) | -18.7 dB, 3956 (3935) | -26.8 dB, 7 (7) | -26.2 dB, 0 (0) | +| CRUNCH | 5000 Hz | 0.4 | -25.7 dB, 0 (0) | -5.5 dB, 43015 (41134) | -24.5 dB, 9705 (7126) | -25.8 dB, 5 (4) | +| CRUNCH | 9000 Hz | 0.1 | -39.9 dB, 0 (0) | -7.8 dB, 38212 (17066) | -25.4 dB, 24 (23) | -37.3 dB, 0 (0) | +| CRUNCH | 9000 Hz | 0.4 | -38.5 dB, 0 (0) | -3.0 dB, 160580 (157904) | -3.8 dB, 77792 (75144) | -31.5 dB, 11637 (4584) | +| HOT | 5000 Hz | 0.1 | -30.0 dB, 1990 (0) | -13.9 dB, 7700 (3494) | -15.3 dB, 1180 (7) | -14.5 dB, 4454 (0) | +| HOT | 5000 Hz | 0.4 | -29.8 dB, 2395 (0) | -2.6 dB, 97620 (93071) | -9.4 dB, 26172 (23590) | -17.3 dB, 2797 (4) | +| HOT | 9000 Hz | 0.1 | -38.2 dB, 1424 (0) | -8.8 dB, 39596 (18160) | -10.4 dB, 8522 (2837) | -13.9 dB, 3770 (0) | +| HOT | 9000 Hz | 0.4 | -36.0 dB, 833 (20) | -1.1 dB, 94772 (79905) | -1.7 dB, 82497 (76306) | -15.0 dB, 17683 (7089) | + +## Cost + +| Rate | ns per engine sample (default / HOT) | % of one core at 48 kHz | +|---|---|---| +| 2x | 1786 / 2809 | 17.1 / 27.0 | +| 4x | 1621 / 2286 | 31.1 / 43.9 | +| 8x | 1427 / 1954 | 54.8 / 75.0 | diff --git a/harness/reports/M6/20261015_x2_rates_a1_tol1e-5.md b/harness/reports/M6/20261015_x2_rates_a1_tol1e-5.md new file mode 100644 index 0000000..84c09b5 --- /dev/null +++ b/harness/reports/M6/20261015_x2_rates_a1_tol1e-5.md @@ -0,0 +1,52 @@ +# M6 A1: full engine (exact J4) at lower internal rates, plugin model check + +- Date 2026-10-15; options: tol 1e-05; rates 48 kHz × [2, 4, 8]. +- Plugin model check (D-051): ESR ≤ -20 dB vs SPICE (cached 384000 Hz references, 48 kHz band, first 5 s of every DI track, first 0.1 s excluded). Unseen settings: the 16 X4 validation settings, 10 s, vs the engine at 16x (first 1 s excluded). Cost: ampwav --stats on 10 s of DI GTR L, one run at a time; oversampling filters not included. + +## Summary + +| Rate | vs SPICE worst (CRUNCH / DRIVE / HOT) | Check | Unseen settings vs 16x (worst / median) | Newton failures (not rescued) / runs, max iterations | Cost default / HOT (% of one core) | +|---|---|---|---|---|---| +| 2x (96 kHz) | -46.5 / -50.0 / -26.2 dB | PASS | -20.6 / -39.1 dB | 6568 (1048) / 28, 16 | 35.8 / 45.3 | +| 4x (192 kHz) | -48.5 / -55.6 / -33.2 dB | PASS | -26.9 / -54.2 dB | 1380 (138) / 28, 16 | 58.4 / 77.4 | +| 8x (384 kHz) | -48.7 / -55.8 / -38.7 dB | PASS | -39.1 / -69.7 dB | 22 (3) / 28, 16 | 105.5 / 156.5 | + +## vs SPICE per clip + +| Clip | 2x | 4x | 8x | +|---|---|---|---| +| CRUNCH GTR L | -47.4 dB | -49.0 dB | -49.0 dB | +| CRUNCH GTR LL | -46.5 dB | -48.5 dB | -48.7 dB | +| CRUNCH GTR R | -47.3 dB | -48.9 dB | -48.9 dB | +| CRUNCH GTR RR | -47.2 dB | -49.0 dB | -49.0 dB | +| DRIVE GTR L | -50.0 dB | -55.6 dB | -55.8 dB | +| DRIVE GTR LL | -50.8 dB | -56.1 dB | -56.2 dB | +| DRIVE GTR R | -51.0 dB | -56.0 dB | -56.1 dB | +| DRIVE GTR RR | -50.5 dB | -55.9 dB | -56.0 dB | +| HOT GTR L | -26.3 dB | -33.2 dB | -38.7 dB | +| HOT GTR LL | -26.9 dB | -34.0 dB | -38.7 dB | +| HOT GTR R | -27.2 dB | -34.1 dB | -38.8 dB | +| HOT GTR RR | -26.2 dB | -33.7 dB | -38.9 dB | + +## Aliasing (power outside the harmonics of f0, relative to the total; 16x for reference) + +Each cell: non-harmonic power, then Newton failures (not rescued) in that 2 s run. Non-converged samples add non-harmonic power, so a cell with many failures measures solver error as well as aliasing. + +| Preset | f0 | amplitude (FS) | 16x | 2x | 4x | 8x | +|---|---|---|---|---|---|---| +| CRUNCH | 5000 Hz | 0.1 | -26.0 dB, 0 (0) | -19.5 dB, 1775 (1681) | -25.8 dB, 12 (12) | -26.0 dB, 0 (0) | +| CRUNCH | 5000 Hz | 0.4 | -25.7 dB, 0 (0) | -5.9 dB, 45420 (43868) | -23.0 dB, 7724 (5347) | -25.4 dB, 5 (4) | +| CRUNCH | 9000 Hz | 0.1 | -39.9 dB, 0 (0) | -12.4 dB, 21424 (12385) | -26.1 dB, 28 (28) | -36.6 dB, 0 (0) | +| CRUNCH | 9000 Hz | 0.4 | -38.5 dB, 0 (0) | -2.0 dB, 125634 (122805) | -5.0 dB, 100403 (98191) | -33.3 dB, 11479 (4727) | +| HOT | 5000 Hz | 0.1 | -30.0 dB, 1990 (0) | -19.0 dB, 3148 (2150) | -17.2 dB, 508 (2) | -26.2 dB, 211 (0) | +| HOT | 5000 Hz | 0.4 | -29.8 dB, 2395 (0) | -6.9 dB, 57909 (54766) | -18.9 dB, 11437 (8072) | -26.2 dB, 571 (4) | +| HOT | 9000 Hz | 0.1 | -38.2 dB, 1424 (0) | -9.2 dB, 20453 (10681) | -14.9 dB, 4278 (1755) | -24.9 dB, 118 (0) | +| HOT | 9000 Hz | 0.4 | -36.0 dB, 833 (20) | -2.3 dB, 98939 (95661) | -1.2 dB, 140659 (137242) | -27.8 dB, 12063 (7862) | + +## Cost + +| Rate | ns per engine sample (default / HOT) | % of one core at 48 kHz | +|---|---|---| +| 2x | 3727 / 4723 | 35.8 / 45.3 | +| 4x | 3044 / 4033 | 58.4 / 77.4 | +| 8x | 2747 / 4075 | 105.5 / 156.5 | diff --git a/tools/ampwav/src/Cli.cpp b/tools/ampwav/src/Cli.cpp index b718abb..c3ed56c 100644 --- a/tools/ampwav/src/Cli.cpp +++ b/tools/ampwav/src/Cli.cpp @@ -59,7 +59,8 @@ void writeStats(const std::string& path, const Options& o, double rate, size_t s << " \"newton_unrescued\": " << st.newtonUnrescued << ",\n" << " \"power_rate_divisor\": " << o.powerRateDivisor << ",\n" << " \"newton_residual_tol\": " << o.newtonTol.value_or(1e-9) << ",\n" - << " \"exact_j4\": " << (o.exactJ4 ? "true" : "false") << "\n" + // in effect: the engine runs the exact J4 coupling only with the power amp at full rate (Amp2203::exactJ4) + << " \"exact_j4\": " << (o.exactJ4 && o.powerRateDivisor == 1 ? "true" : "false") << "\n" << "}\n"; } From 6c3d54e9f159e500300ef144b369d2f28a13582c Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 10:45:05 +0300 Subject: [PATCH 10/18] m6: Newton maxIter 32 in the full amp's roots, per-root Newton stats (D-052) --- docs/DECISIONS.md | 14 ++ docs/PROGRESS.md | 10 + docs/VERIFICATION.md | 2 +- docs/WDF_DESIGN.md | 2 +- engine/src/PowerAmp.cpp | 2 +- engine/src/Preamp.cpp | 2 +- ...261015_full_acceptance_fs384000_j4exact.md | 197 ++++++++++++++++++ ...15_full_acceptance_fs384000_j4exact_hot.md | 52 +++++ harness/tolerances.py | 6 +- harness/x2_rates.py | 14 +- tools/ampwav/src/Cli.cpp | 7 +- 11 files changed, 298 insertions(+), 10 deletions(-) create mode 100644 harness/reports/M6/20261015_full_acceptance_fs384000_j4exact.md create mode 100644 harness/reports/M6/20261015_full_acceptance_fs384000_j4exact_hot.md diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 4eb4365..e8a5768 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -900,3 +900,17 @@ Consequences: the engine's per-block checks flag only errors above −20 dB; an at HOT before X1 (−13 dB) is still caught, smaller ones (e.g. −25 dB) are not. Earlier reports keep their recorded tolerances; reports generated from now on score against −20 dB. Against the new check: X3 (CRUNCH only) −16.5 … −17.1 dB, X4 −3.0 … −7.1 dB, the engine −39.6 … −56.1 dB. + +## D-052 Newton iteration limit 32 in the full amp's roots (preamp B1234, power amp B56) +Date: 2026-10-15 · Status: accepted (owner: "run 1 and 2", Newton convergence at 2x / 4x) +Context: M6 X2 / A1: at 2x / 4x of 48 kHz the preamp root failed to converge in 12 iterations (DI: up to 80 +first-solve failures and 29 unrescued per 5 s clip at 2x; loud 5 / 9 kHz sines up to ≈ 140 000 per 2 s), the power +amp at HOT in 16 (rescued). Diagnosis (preamp, scratch builds): step limits ×4 left the DI failures unchanged; maxIter +64: every case converged, the DI clips in ≤ 21 iterations at 2x and ≤ 17 at 4x (median 2), the sines in ≤ 47–60. +Failing at 12 was also the expensive path: the fallback runs up to 14 more solves. +Decision: `Preamp` and `PowerAmp` use maxIter 32 (`FourTriodes::solverK(32)`, `TriodesPentodes::solverK(32)`); the +standalone sub-blocks keep 12. VERIFICATION §7 "max ≤ configured limit" follows (32 for b1234, b56, full). +Consequences: samples that converged in ≤ 12 / 16 iterations are unchanged bit for bit. Full acceptance at 384 kHz, +exact J4: 107 PASS (`…_20261015_full_acceptance_fs384000_j4exact.md`); HOT 21 PASS, 0 FAIL (was 4 FAIL on +newton_failures), DI ESR within 0.08 dB, max 17–19 iterations. At 2x / 4x the DI clips have 0 failures; only loud +9 kHz sines (and 5 kHz at 2x) still fail. Worst case per sample rises (32 iterations before the fallback). diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 50f39ce..2ac3b17 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -701,6 +701,16 @@ Notes: when the power amp runs at half rate, so these runs used the Thevenin J4: HOT −13.0 dB at every rate (FAIL), CRUNCH / DRIVE pass; cost 17 / 31 / 55 % (HOT 27 / 44 / 75 %). ampwav `--stats` now reports `exact_j4` as in effect. Exact J4 with the half-rate power amp is not implemented (open). +- Owner 2026-10-15: run (1) exact J4 with the half-rate power amp and (2) Newton convergence at 2x / 4x. + Plan (2) first: per-block Newton counts in `ampwav --stats`; diagnose the 2x / 4x failures (which block, which + ports, step-limited or not; diagnostic maxIter / step limits); then a rate-aware fix, e.g. D-043's adaptive + substeps generalised to the preamp and to m > 2, with bounded cost; verify with `harness.x2_rates` and the 16x + acceptance unchanged. Plan (1): the halfband filters put 34 engine samples of delay between the preamp and the + half-rate power amp, so a same-sample (exact) J4 cannot span them; test the load current at y fed back with that + delay (approximation) against the HOT SPICE references, report before adopting. +- (2) done: Newton failures at 2x / 4x come from the preamp's 12-iteration budget (not the step limits); maxIter 32 in + both roots of the full amp (D-052). Acceptance at 384 kHz exact J4: 107 PASS; HOT 21 PASS / 0 FAIL. `ampwav --stats` + reports Newton failures and max iterations per root. ## Questions for the owner _(Claude Code: add questions here and also ask them in the session.)_ diff --git a/docs/VERIFICATION.md b/docs/VERIFICATION.md index b3afb5b..7dc13ef 100644 --- a/docs/VERIFICATION.md +++ b/docs/VERIFICATION.md @@ -143,7 +143,7 @@ See `WDF_DESIGN.md` §5.5. | Real-time plugin model (any architecture, M6), whole amp vs SPICE | ESR ≤ −20 dB on the held-out DI clips at CRUNCH, DRIVE and HOT, 48 kHz band, first 0.1 s excluded (D-051) | | NFB amount | ±0.5 dB at 100 Hz / 1 kHz / 5 kHz | | Stability | 60 s silence at max settings: |out| < 1 mV, no growth | -| Newton | 0 failures on the standard signal set; max iterations ≤ the configured limit (12; 16 for the power amp B56 and the full amp, D-045) | +| Newton | 0 failures on the standard signal set; max iterations ≤ the configured limit (12; 32 for the preamp B1234, the power amp B56 and the full amp, D-052; was 16, D-045) | ESR on heavily clipped signals is sensitive to tiny timing differences at sharp edges. If a check fails mainly because of edge timing, also report ESR after a 5 kHz low-pass on both signals and the harmonic diff --git a/docs/WDF_DESIGN.md b/docs/WDF_DESIGN.md index a84a5fc..684e3b2 100644 --- a/docs/WDF_DESIGN.md +++ b/docs/WDF_DESIGN.md @@ -134,7 +134,7 @@ Algorithm (per sample): 1. Initial guess: linear extrapolation from the two previous solutions, v = v[n−1] + (v[n−1] − v[n−2]), with each port's extrapolated step clamped to its step limit; after a sample whose solve failed, the trend restarts at zero (v[n−2] := v[n−1]) (DECISIONS D-029, D-034). -2. Up to `maxIter` (default 12) iterations; per-port step limits, upward and downward separately +2. Up to `maxIter` (default 12; 32 in the full amp's two roots, DECISIONS D-052) iterations; per-port step limits, upward and downward separately (triode blocks: grid ports +50 V / −1000 V, plate ports ±50 V, D-034, D-037). Steps into cutoff are harmless and the device equations cannot overflow (closed-form ω, stable softplus), so the limits only shape convergence. The step is scaled uniformly so that no port exceeds its limit (this keeps the Newton diff --git a/engine/src/PowerAmp.cpp b/engine/src/PowerAmp.cpp index 22a3ef0..c8bb1a5 100644 --- a/engine/src/PowerAmp.cpp +++ b/engine/src/PowerAmp.cpp @@ -16,7 +16,7 @@ using devices::TriodesPentodesLoad; constexpr stage::RootNetwork kNet{kElements, kRoot}; // D-045 (owner decision Q21) -const solver::MultiportNewton kRootSolver = TriodesPentodes::solverK(16); +const solver::MultiportNewton kRootSolver = TriodesPentodes::solverK(32); const solver::MultiportNewton kDcSolver = TriodesPentodes::solverK(500); // reset() only constexpr std::array kNoRootOffsets{}; constexpr double kWindingDcConductance = 1e6; // S: the windings are shorts at DC (as PowerStage) diff --git a/engine/src/Preamp.cpp b/engine/src/Preamp.cpp index c18cd57..e086dfa 100644 --- a/engine/src/Preamp.cpp +++ b/engine/src/Preamp.cpp @@ -13,7 +13,7 @@ using devices::FourTriodes; using devices::FourTriodesLoad; constexpr stage::RootNetwork kNet{kElements, kRoot}; -const solver::MultiportNewton kRootSolver = FourTriodes::solverK(12); +const solver::MultiportNewton kRootSolver = FourTriodes::solverK(32); const solver::MultiportNewton kDcSolver = FourTriodes::solverK(500); // reset() only } // namespace diff --git a/harness/reports/M6/20261015_full_acceptance_fs384000_j4exact.md b/harness/reports/M6/20261015_full_acceptance_fs384000_j4exact.md new file mode 100644 index 0000000..bf1d358 --- /dev/null +++ b/harness/reports/M6/20261015_full_acceptance_fs384000_j4exact.md @@ -0,0 +1,197 @@ +# M4 acceptance: full amplifier vs the full netlist + +- Date: 2026-10-15, engine commit `f17a4fc (dirty)`, ngspice 47, fs = 384000 Hz, J4 exact, SPICE reltol 1e-08 +- Netlist sha256 `7bea8385e4adb04e`, tubes.lib sha256 `9ec33154be8127d7` +- Engine: `ampwav --block full` (volts at the jack and across the 16 Ω load), the checks of SPEC §7 M4. + +## Summary + +PASS 107 · FAIL 0 + +| Section | Check | Item | Measured | Tolerance | Status | +|---|---|---|---|---|---| +| dc | dc_rel_error | v(ct) | 4.141e-07 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(nscr) | 4.454e-07 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(bpx) | 3.65e-06 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(bpa) | 5.682e-06 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(nbias) | 4.636e-08 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(p1a) | 1.996e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(k1a) | 3.983e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(p1b) | 1.688e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(k1b) | 0.0001012 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(p2a) | 2.198e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(k2a) | 2.11e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(k2b) | 2.143e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(p3a) | 1.578e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(p3b) | 1.709e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(k3) | 2.143e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(nf) | 2.143e-05 | ≤ 0.005 | PASS | +| dc | dc_rel_error | v(ng) | 2.143e-05 | ≤ 0.005 | PASS | +| dc | el34_plate_ma | ip_tube_a | 1.194e-05 mA | ≤ 1 | PASS | +| dc | el34_plate_ma | ip_tube_b | 1.195e-05 mA | ≤ 1 | PASS | +| dc | el34_screen_ma | is_tube_a | 1.046e-06 mA | ≤ 0.3 | PASS | +| dc | el34_screen_ma | is_tube_b | 1.044e-06 mA | ≤ 0.3 | PASS | +| dc | ct_rel | v(ct) | 4.141e-07 | ≤ 0.01 | PASS | +| dc | nbias_v | v(nbias) | 2.15e-06 V | ≤ 0.2 | PASS | +| nfb | nfb_gain_db | closed 100 Hz | 0.001954 dB | ≤ 0.5 | PASS | +| nfb | newton_failures | closed 100 Hz | 0 | ≤ 0 | PASS | +| nfb | newton_unrescued | closed 100 Hz | 0 | — | INFO | +| nfb | newton_median_iterations | closed 100 Hz | 1 | ≤ 3 | PASS | +| nfb | newton_max_iterations | closed 100 Hz | 2 | ≤ 32 | PASS | +| nfb | nfb_gain_db | closed 1000 Hz | 0.004996 dB | ≤ 0.5 | PASS | +| nfb | newton_failures | closed 1000 Hz | 0 | ≤ 0 | PASS | +| nfb | newton_unrescued | closed 1000 Hz | 0 | — | INFO | +| nfb | newton_median_iterations | closed 1000 Hz | 1 | ≤ 3 | PASS | +| nfb | newton_max_iterations | closed 1000 Hz | 2 | ≤ 32 | PASS | +| nfb | nfb_gain_db | closed 5000 Hz | 0.01361 dB | ≤ 0.5 | PASS | +| nfb | newton_failures | closed 5000 Hz | 0 | ≤ 0 | PASS | +| nfb | newton_unrescued | closed 5000 Hz | 0 | — | INFO | +| nfb | newton_median_iterations | closed 5000 Hz | 2 | ≤ 3 | PASS | +| nfb | newton_max_iterations | closed 5000 Hz | 2 | ≤ 32 | PASS | +| nfb | nfb_gain_db | open 100 Hz | 0.002342 dB | ≤ 0.5 | PASS | +| nfb | newton_failures | open 100 Hz | 0 | ≤ 0 | PASS | +| nfb | newton_unrescued | open 100 Hz | 0 | — | INFO | +| nfb | newton_median_iterations | open 100 Hz | 1 | ≤ 3 | PASS | +| nfb | newton_max_iterations | open 100 Hz | 2 | ≤ 32 | PASS | +| nfb | nfb_gain_db | open 1000 Hz | 0.0057 dB | ≤ 0.5 | PASS | +| nfb | newton_failures | open 1000 Hz | 0 | ≤ 0 | PASS | +| nfb | newton_unrescued | open 1000 Hz | 0 | — | INFO | +| nfb | newton_median_iterations | open 1000 Hz | 1 | ≤ 3 | PASS | +| nfb | newton_max_iterations | open 1000 Hz | 2 | ≤ 32 | PASS | +| nfb | nfb_gain_db | open 5000 Hz | 0.02095 dB | ≤ 0.5 | PASS | +| nfb | newton_failures | open 5000 Hz | 0 | ≤ 0 | PASS | +| nfb | newton_unrescued | open 5000 Hz | 0 | — | INFO | +| nfb | newton_median_iterations | open 5000 Hz | 2 | ≤ 3 | PASS | +| nfb | newton_max_iterations | open 5000 Hz | 2 | ≤ 32 | PASS | +| clean | total_esr_db | CLEAN SIN-1000Hz-0.025V | -60.06 dB | ≤ -20 | PASS | +| clean | total_harmonics_2_9_db | CLEAN SIN-1000Hz-0.025V | 0.02042 dB | ≤ 1.5 | PASS | +| clean | newton_failures | CLEAN SIN-1000Hz-0.025V | 0 | ≤ 0 | PASS | +| clean | newton_unrescued | CLEAN SIN-1000Hz-0.025V | 0 | — | INFO | +| clean | newton_median_iterations | CLEAN SIN-1000Hz-0.025V | 2 | ≤ 3 | PASS | +| clean | newton_max_iterations | CLEAN SIN-1000Hz-0.025V | 6 | ≤ 32 | PASS | +| clean | total_power_rel | CLEAN SIN-1000Hz-0.025V | 0.001105 | ≤ 0.05 | PASS | +| clean | total_thd_pp | CLEAN SIN-1000Hz-0.025V | 0.0006695 pp | ≤ 0.5 | PASS | +| stability | stability_out_v | max |out| | 4.409e-05 V | ≤ 0.001 | PASS | +| stability | stability_growth | RMS last 10 s − first 10 s | 1.83e-12 V | ≤ 1e-06 | PASS | +| stability | newton_failures | 60 s silence | 0 | ≤ 0 | PASS | +| stability | newton_unrescued | 60 s silence | 0 | — | INFO | +| stability | newton_median_iterations | 60 s silence | 1 | ≤ 3 | PASS | +| stability | newton_max_iterations | 60 s silence | 1 | ≤ 32 | PASS | +| overdrive | total_esr_db | OVERDRIVE SIN-220Hz-0.3V | -67.66 dB | ≤ -20 | PASS | +| overdrive | total_harmonics_2_9_db | OVERDRIVE SIN-220Hz-0.3V | 0.008332 dB | ≤ 1.5 | PASS | +| overdrive | sag_min_v | OVERDRIVE SIN-220Hz-0.3V | 0.004485 V | ≤ 2 | PASS | +| overdrive | newton_failures | OVERDRIVE SIN-220Hz-0.3V | 0 | ≤ 0 | PASS | +| overdrive | newton_unrescued | OVERDRIVE SIN-220Hz-0.3V | 0 | — | INFO | +| overdrive | newton_median_iterations | OVERDRIVE SIN-220Hz-0.3V | 2 | ≤ 3 | PASS | +| overdrive | newton_max_iterations | OVERDRIVE SIN-220Hz-0.3V | 6 | ≤ 32 | PASS | +| crunch_drive | total_esr_db | CRUNCH SIN-220Hz-0.3V | -69.99 dB | ≤ -20 | PASS | +| crunch_drive | total_harmonics_2_9_db | CRUNCH SIN-220Hz-0.3V | 0.01118 dB | ≤ 1.5 | PASS | +| crunch_drive | newton_failures | CRUNCH SIN-220Hz-0.3V | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | CRUNCH SIN-220Hz-0.3V | 0 | — | INFO | +| crunch_drive | newton_median_iterations | CRUNCH SIN-220Hz-0.3V | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | CRUNCH SIN-220Hz-0.3V | 5 | ≤ 32 | PASS | +| crunch_drive | total_esr_db | CRUNCH DI-GTR L | -61.26 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | CRUNCH DI-GTR L | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | CRUNCH DI-GTR L | 0 | — | INFO | +| crunch_drive | newton_median_iterations | CRUNCH DI-GTR L | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | CRUNCH DI-GTR L | 8 | ≤ 32 | PASS | +| crunch_drive | total_esr_db | CRUNCH DI-GTR LL | -61.57 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | CRUNCH DI-GTR LL | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | CRUNCH DI-GTR LL | 0 | — | INFO | +| crunch_drive | newton_median_iterations | CRUNCH DI-GTR LL | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | CRUNCH DI-GTR LL | 8 | ≤ 32 | PASS | +| crunch_drive | total_esr_db | CRUNCH DI-GTR R | -62.34 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | CRUNCH DI-GTR R | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | CRUNCH DI-GTR R | 0 | — | INFO | +| crunch_drive | newton_median_iterations | CRUNCH DI-GTR R | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | CRUNCH DI-GTR R | 8 | ≤ 32 | PASS | +| crunch_drive | total_esr_db | CRUNCH DI-GTR RR | -61.11 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | CRUNCH DI-GTR RR | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | CRUNCH DI-GTR RR | 0 | — | INFO | +| crunch_drive | newton_median_iterations | CRUNCH DI-GTR RR | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | CRUNCH DI-GTR RR | 11 | ≤ 32 | PASS | +| crunch_drive | total_esr_db | DRIVE SIN-220Hz-0.3V | -63.6 dB | ≤ -20 | PASS | +| crunch_drive | total_harmonics_2_9_db | DRIVE SIN-220Hz-0.3V | 0.03526 dB | ≤ 1.5 | PASS | +| crunch_drive | newton_failures | DRIVE SIN-220Hz-0.3V | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | DRIVE SIN-220Hz-0.3V | 0 | — | INFO | +| crunch_drive | newton_median_iterations | DRIVE SIN-220Hz-0.3V | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | DRIVE SIN-220Hz-0.3V | 6 | ≤ 32 | PASS | +| crunch_drive | total_esr_db | DRIVE DI-GTR L | -60.62 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | DRIVE DI-GTR L | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | DRIVE DI-GTR L | 0 | — | INFO | +| crunch_drive | newton_median_iterations | DRIVE DI-GTR L | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | DRIVE DI-GTR L | 9 | ≤ 32 | PASS | +| crunch_drive | total_esr_db | DRIVE DI-GTR LL | -61.56 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | DRIVE DI-GTR LL | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | DRIVE DI-GTR LL | 0 | — | INFO | +| crunch_drive | newton_median_iterations | DRIVE DI-GTR LL | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | DRIVE DI-GTR LL | 9 | ≤ 32 | PASS | +| crunch_drive | total_esr_db | DRIVE DI-GTR R | -61.32 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | DRIVE DI-GTR R | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | DRIVE DI-GTR R | 0 | — | INFO | +| crunch_drive | newton_median_iterations | DRIVE DI-GTR R | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | DRIVE DI-GTR R | 9 | ≤ 32 | PASS | +| crunch_drive | total_esr_db | DRIVE DI-GTR RR | -60.57 dB | ≤ -20 | PASS | +| crunch_drive | newton_failures | DRIVE DI-GTR RR | 0 | ≤ 0 | PASS | +| crunch_drive | newton_unrescued | DRIVE DI-GTR RR | 0 | — | INFO | +| crunch_drive | newton_median_iterations | DRIVE DI-GTR RR | 2 | ≤ 3 | PASS | +| crunch_drive | newton_max_iterations | DRIVE DI-GTR RR | 9 | ≤ 32 | PASS | + +## DC operating point + +| Node / current | SPICE | Engine | +|---|---|---| +| ct | 488.7084 V | 488.7086 V | +| nscr | 485.6493 V | 485.6495 V | +| bpx | 402.3360 V | 402.3375 V | +| bpa | 389.3496 V | 389.3518 V | +| nbias | -46.3636 V | -46.3636 V | +| p1a | 296.4025 V | 296.4084 V | +| k1a | 2.5096 V | 2.5095 V | +| p1b | 352.4328 V | 352.4387 V | +| k1b | 3.6917 V | 3.6913 V | +| p2a | 231.1295 V | 231.1346 V | +| k2a | 1.4039 V | 1.4039 V | +| k2b | 231.6858 V | 231.6908 V | +| p3a | 270.8809 V | 270.8852 V | +| p3b | 262.2706 V | 262.2751 V | +| k3 | 44.9334 V | 44.9324 V | +| nf | 43.5216 V | 43.5207 V | +| ng | 13.4840 V | 13.4837 V | +| ip_tube_a | 34.0783 mA | 34.0783 mA | +| ip_tube_b | 34.0780 mA | 34.0780 mA | +| is_tube_a | 4.5674 mA | 4.5674 mA | +| is_tube_b | 4.5675 mA | 4.5675 mA | + +## NFB (gain in → out, dB) + +| f | SPICE closed | engine closed | SPICE open | engine open | NFB SPICE | NFB engine | +|---|---|---|---|---|---|---| +| 100 Hz | 43.44 | 43.45 | 50.07 | 50.07 | 6.63 | 6.63 | +| 1000 Hz | 55.58 | 55.59 | 61.74 | 61.74 | 6.16 | 6.16 | +| 5000 Hz | 68.07 | 68.09 | 74.12 | 74.14 | 6.04 | 6.05 | + +## Signals + +| Preset | Signal | ESR | best lag | harm. 2–9 | THD F / E | Power F / E | min v(ct) F / E | +|---|---|---|---|---|---|---|---| +| CLEAN | SIN-1000Hz-0.025V | -60.1 dB | 0 | 0.02 dB | 3.47 / 3.47 % | 100.2 / 100.3 W | — | +| OVERDRIVE | SIN-220Hz-0.3V | -67.7 dB | 0 | 0.01 dB | 23.35 / 23.35 % | 33.8 / 33.8 W | 469.82 / 469.81 V | +| CRUNCH | SIN-220Hz-0.3V | -70.0 dB | 0 | 0.01 dB | 15.04 / 15.04 % | 32.9 / 32.9 W | — | +| CRUNCH | DI-GTR L | -61.3 dB | 0 | — | — | — | — | +| CRUNCH | DI-GTR LL | -61.6 dB | 0 | — | — | — | — | +| CRUNCH | DI-GTR R | -62.3 dB | 0 | — | — | — | — | +| CRUNCH | DI-GTR RR | -61.1 dB | 0 | — | — | — | — | +| DRIVE | SIN-220Hz-0.3V | -63.6 dB | 0 | 0.04 dB | 25.98 / 25.99 % | 36.2 / 36.2 W | — | +| DRIVE | DI-GTR L | -60.6 dB | 0 | — | — | — | — | +| DRIVE | DI-GTR LL | -61.6 dB | 0 | — | — | — | — | +| DRIVE | DI-GTR R | -61.3 dB | 0 | — | — | — | — | +| DRIVE | DI-GTR RR | -60.6 dB | 0 | — | — | — | — | + +## Stability + +60 s silence at Preamp / Master / Presence 1.0: max |out| 4.41e-05 V, RMS first / last 10 s 4.23e-05 / 4.23e-05 V. + +## Delayed-NFB break + +Not applicable: since D-041 the global NFB (R22) is part of the B56 tree and has no delay (SPEC §7 M4 asked for a 1- vs 2-sample comparison of the delayed design). diff --git a/harness/reports/M6/20261015_full_acceptance_fs384000_j4exact_hot.md b/harness/reports/M6/20261015_full_acceptance_fs384000_j4exact_hot.md new file mode 100644 index 0000000..8efb0e3 --- /dev/null +++ b/harness/reports/M6/20261015_full_acceptance_fs384000_j4exact_hot.md @@ -0,0 +1,52 @@ +# M4 acceptance: full amplifier vs the full netlist + +- Date: 2026-10-15, engine commit `f17a4fc (dirty)`, ngspice 47, fs = 384000 Hz, J4 exact, SPICE reltol 1e-08 +- Netlist sha256 `7bea8385e4adb04e`, tubes.lib sha256 `9ec33154be8127d7` +- Engine: `ampwav --block full` (volts at the jack and across the 16 Ω load), the checks of SPEC §7 M4. + +## Summary + +PASS 21 · FAIL 0 + +| Section | Check | Item | Measured | Tolerance | Status | +|---|---|---|---|---|---| +| hot | total_esr_db | HOT SIN-220Hz-0.3V | -58.9 dB | ≤ -20 | PASS | +| hot | total_harmonics_2_9_db | HOT SIN-220Hz-0.3V | 0.01274 dB | ≤ 1.5 | PASS | +| hot | newton_failures | HOT SIN-220Hz-0.3V | 0 | ≤ 0 | PASS | +| hot | newton_unrescued | HOT SIN-220Hz-0.3V | 0 | — | INFO | +| hot | newton_median_iterations | HOT SIN-220Hz-0.3V | 2 | ≤ 3 | PASS | +| hot | newton_max_iterations | HOT SIN-220Hz-0.3V | 7 | ≤ 32 | PASS | +| hot | total_esr_db | HOT DI-GTR L | -41.4 dB | ≤ -20 | PASS | +| hot | newton_failures | HOT DI-GTR L | 0 | ≤ 0 | PASS | +| hot | newton_unrescued | HOT DI-GTR L | 0 | — | INFO | +| hot | newton_median_iterations | HOT DI-GTR L | 2 | ≤ 3 | PASS | +| hot | newton_max_iterations | HOT DI-GTR L | 17 | ≤ 32 | PASS | +| hot | total_esr_db | HOT DI-GTR LL | -42.05 dB | ≤ -20 | PASS | +| hot | newton_failures | HOT DI-GTR LL | 0 | ≤ 0 | PASS | +| hot | newton_unrescued | HOT DI-GTR LL | 0 | — | INFO | +| hot | newton_median_iterations | HOT DI-GTR LL | 2 | ≤ 3 | PASS | +| hot | newton_max_iterations | HOT DI-GTR LL | 18 | ≤ 32 | PASS | +| hot | total_esr_db | HOT DI-GTR R | -42.65 dB | ≤ -20 | PASS | +| hot | newton_failures | HOT DI-GTR R | 0 | ≤ 0 | PASS | +| hot | newton_unrescued | HOT DI-GTR R | 0 | — | INFO | +| hot | newton_median_iterations | HOT DI-GTR R | 2 | ≤ 3 | PASS | +| hot | newton_max_iterations | HOT DI-GTR R | 19 | ≤ 32 | PASS | +| hot | total_esr_db | HOT DI-GTR RR | -41.83 dB | ≤ -20 | PASS | +| hot | newton_failures | HOT DI-GTR RR | 0 | ≤ 0 | PASS | +| hot | newton_unrescued | HOT DI-GTR RR | 0 | — | INFO | +| hot | newton_median_iterations | HOT DI-GTR RR | 2 | ≤ 3 | PASS | +| hot | newton_max_iterations | HOT DI-GTR RR | 17 | ≤ 32 | PASS | + +## Signals + +| Preset | Signal | ESR | best lag | harm. 2–9 | THD F / E | Power F / E | min v(ct) F / E | +|---|---|---|---|---|---|---|---| +| HOT | SIN-220Hz-0.3V | -58.9 dB | 0 | 0.01 dB | 46.17 / 46.17 % | 183.4 / 183.4 W | — | +| HOT | DI-GTR L | -41.4 dB | 0 | — | — | — | — | +| HOT | DI-GTR LL | -42.0 dB | 0 | — | — | — | — | +| HOT | DI-GTR R | -42.7 dB | 0 | — | — | — | — | +| HOT | DI-GTR RR | -41.8 dB | 0 | — | — | — | — | + +## Delayed-NFB break + +Not applicable: since D-041 the global NFB (R22) is part of the B56 tree and has no delay (SPEC §7 M4 asked for a 1- vs 2-sample comparison of the delayed design). diff --git a/harness/tolerances.py b/harness/tolerances.py index 3f75c7a..ba3fa25 100644 --- a/harness/tolerances.py +++ b/harness/tolerances.py @@ -71,9 +71,9 @@ } -# per-block overrides of a tolerance: the Newton maximum is "the configured limit", which is 16 for the power -# amp B56 and therefore for the full amp (owner decision Q21, DECISIONS D-045); every other root has 12 -OVERRIDES = {('b56', 'newton_max_iterations'): (16, 'max'), ('full', 'newton_max_iterations'): (16, 'max')} +# per-block overrides of a tolerance: the Newton maximum is "the configured limit", which is 32 for the two roots +# of the full amp, the preamp B1234 and the power amp B56 (D-052; was 12 / 16, D-045); every other root has 12 +OVERRIDES = {(b, 'newton_max_iterations'): (32, 'max') for b in ('b56', 'full', 'b1234', 'preamp')} def status(milestone, block_id, check, value): diff --git a/harness/x2_rates.py b/harness/x2_rates.py index feabc53..3a540fe 100644 --- a/harness/x2_rates.py +++ b/harness/x2_rates.py @@ -36,10 +36,22 @@ LIMIT = tolerances.TOLERANCES['plugin_model_esr_db'][0] +_ENGINE_ID = None + + +def _engine_id(): + """Hash of the ampwav binary: cached runs belong to the engine build that made them.""" + global _ENGINE_ID + if _ENGINE_ID is None: + _ENGINE_ID = hashlib.sha256(x3.AMPWAV.read_bytes()).hexdigest()[:16] + return _ENGINE_ID + + def _run(x48, knobs, factor, opts, stats_only=False): """Full amp (exact J4) at 48 kHz × factor on a 48 kHz full-scale input. Returns (out at 48 kHz, stats). Cached by input, knobs, factor and options (not for cost runs).""" - key = hashlib.sha256(x48.astype(np.float64).tobytes() + json.dumps([knobs, factor, opts], sort_keys=True).encode()) + key = hashlib.sha256(x48.astype(np.float64).tobytes() + json.dumps([knobs, factor, opts], sort_keys=True).encode() + + _engine_id().encode()) path = CACHE / f'{key.hexdigest()[:24]}.npz' if path.exists() and not stats_only: with np.load(path) as z: diff --git a/tools/ampwav/src/Cli.cpp b/tools/ampwav/src/Cli.cpp index c3ed56c..d0badf9 100644 --- a/tools/ampwav/src/Cli.cpp +++ b/tools/ampwav/src/Cli.cpp @@ -57,10 +57,13 @@ void writeStats(const std::string& path, const Options& o, double rate, size_t s << " \"median_iterations\": " << st.medianIterations << ",\n" << " \"newton_rescued\": " << st.newtonRescued << ",\n" << " \"newton_unrescued\": " << st.newtonUnrescued << ",\n" + << " \"newton_failures_preamp\": " << st.preamp.failures << ",\n" + << " \"newton_failures_power\": " << st.powerAmp.failures << ",\n" + << " \"max_iterations_preamp\": " << st.preamp.maxIterations << ",\n" + << " \"max_iterations_power\": " << st.powerAmp.maxIterations << ",\n" << " \"power_rate_divisor\": " << o.powerRateDivisor << ",\n" << " \"newton_residual_tol\": " << o.newtonTol.value_or(1e-9) << ",\n" - // in effect: the engine runs the exact J4 coupling only with the power amp at full rate (Amp2203::exactJ4) - << " \"exact_j4\": " << (o.exactJ4 && o.powerRateDivisor == 1 ? "true" : "false") << "\n" + << " \"exact_j4\": " << (o.exactJ4 ? "true" : "false") << "\n" << "}\n"; } From c304ee2a11e19d05bdd204d5735de46dc5aaaab3 Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 10:56:07 +0300 Subject: [PATCH 11/18] m6: exact J4 with the half-rate power amp, coupled at the power-amp samples (D-053); X2 / A1 rerun --- docs/DECISIONS.md | 15 ++++++ docs/PROGRESS.md | 8 +++ engine/include/amp2203/Amp2203.h | 9 ++-- engine/include/amp2203/Params.h | 3 +- engine/src/Amp2203.cpp | 27 +++++++--- .../M6/20261015_x2_rates_a1_div2_exact.md | 52 +++++++++++++++++++ .../reports/M6/20261015_x2_rates_maxiter32.md | 52 +++++++++++++++++++ 7 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 harness/reports/M6/20261015_x2_rates_a1_div2_exact.md create mode 100644 harness/reports/M6/20261015_x2_rates_maxiter32.md diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index e8a5768..d83e59f 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -914,3 +914,18 @@ Consequences: samples that converged in ≤ 12 / 16 iterations are unchanged bit exact J4: 107 PASS (`…_20261015_full_acceptance_fs384000_j4exact.md`); HOT 21 PASS, 0 FAIL (was 4 FAIL on newton_failures), DI ESR within 0.08 dB, max 17–19 iterations. At 2x / 4x the DI clips have 0 failures; only loud 9 kHz sines (and 5 kHz at 2x) still fail. Worst case per sample rises (32 iterations before the fallback). + +## D-053 Exact J4 with the power amp at half rate: coupling at the power-amp samples, no decimation filter +Date: 2026-10-15 · Status: implemented as an engine option (owner: "run 1 and 2"); not used by the plugin +Context: with `powerAmpRateDivisor = 2` the exact J4 coupling (X1) was switched off (`Amp2203::exactJ4()`), so A1's +half-rate runs used the Thevenin J4 (HOT −13 dB). A same-sample coupling cannot pass through the halfband +decimator (17 engine samples of delay in the loop). +Decision: with exact J4 and divisor 2 the junction is solved exactly at the samples where the power amp runs (the +preamp's linearised Thevenin source at y taken directly, no decimation filter); in between the preamp solves with +the held load current; the power amp output goes through the halfband interpolator as before. Latency 17 engine +samples (was 34 with the decimator). Divisor 1 is unchanged bit for bit. +Consequences (`harness/reports/M6/20261015_x2_rates_a1_div2_exact.md`, maxIter 32): vs SPICE worst HOT −22.5 / −26.3 / +−33.2 dB at 2x / 4x / 8x (PASS; was −13.0), CRUNCH / DRIVE ≤ −38.0 dB; unseen settings vs 16x worst −18.9 / −20.6 / +−26.8 dB; cost 24 / 46 / 89 % of one core (HOT 36 / 63 / 115 %), full-rate power amp 34 / 67 / 129 % (HOT 52 / 92 / +166 %). Without the decimation filter the preamp content above a quarter of the engine rate folds into the power amp: +non-harmonic power on 5 / 9 kHz sines −4.6 … −29.1 dB (full rate at the same engine rate: −6.4 … −36.6 dB). diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 2ac3b17..d351dda 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -711,6 +711,14 @@ Notes: - (2) done: Newton failures at 2x / 4x come from the preamp's 12-iteration budget (not the step limits); maxIter 32 in both roots of the full amp (D-052). Acceptance at 384 kHz exact J4: 107 PASS; HOT 21 PASS / 0 FAIL. `ampwav --stats` reports Newton failures and max iterations per root. +- X2 with maxIter 32 (`…_x2_rates_maxiter32.md`): accuracy as before; Newton failures over 28 runs 15 (7 unrescued) / + 4 (0) / 3 (0) at 2x / 4x / 8x (was 8723 (1376) / 2058 (147) / 128 (3)); DI cost unchanged (34 / 67 / 129 %). + Aliasing now measurable without solver noise: non-harmonic power on 5 / 9 kHz sines −6.4 … −19.6 dB at 2x, + −14.9 … −26.1 at 4x, −24.8 … −36.6 at 8x (16x −25.7 … −39.9). +- (1) done (D-053): exact J4 with the half-rate power amp, coupled at the power-amp samples without a decimation + filter (`…_x2_rates_a1_div2_exact.md`): HOT −22.5 / −26.3 / −33.2 dB vs SPICE at 2x / 4x / 8x (PASS), unseen settings + worst −18.9 / −20.6 / −26.8 dB vs 16x, cost 24 / 46 / 89 % (HOT 36 / 63 / 115 %), latency 17 engine samples. The + plugin still runs the Thevenin J4 (exact J4 not enabled there). ## Questions for the owner _(Claude Code: add questions here and also ask them in the session.)_ diff --git a/engine/include/amp2203/Amp2203.h b/engine/include/amp2203/Amp2203.h index 6fccaa7..a40e5e0 100644 --- a/engine/include/amp2203/Amp2203.h +++ b/engine/include/amp2203/Amp2203.h @@ -61,8 +61,11 @@ class Amp2203 { /// Newton statistics of both roots together (preamp, power amp). Stats getStats() const; - /// Output delay in samples at the engine rate added by the power amp's rate change (0 at divisor 1; M6). - int latencySamples() const noexcept { return cfg_.powerAmpRateDivisor == 2 ? 2 * kHalfbandM : 0; } + /// Output delay in samples at the engine rate added by the power amp's rate change (0 at divisor 1; M6): + /// decimator and interpolator, or the interpolator alone with the exact J4 coupling. + int latencySamples() const noexcept { + return cfg_.powerAmpRateDivisor == 2 ? (cfg_.exactJ4 ? kHalfbandM : 2 * kHalfbandM) : 0; + } /// Node probes for the harness (`ampwav --dump-node`), netlist names. Values are valid after each /// processed sample. @@ -87,7 +90,7 @@ class Amp2203 { detail::FirWindow decim_, interp_; bool powerPhase_ = false; // true: the power amp runs at this sample double iY_ = 0.0; // exact J4: the load current at y of the last sample (A) - bool exactJ4() const noexcept { return cfg_.exactJ4 && cfg_.powerAmpRateDivisor == 1; } + bool exactJ4() const noexcept { return cfg_.exactJ4; } double vIn_ = 0.0; // V at the input jack double vOut_ = 0.0; // V across the 16 Ω load diff --git a/engine/include/amp2203/Params.h b/engine/include/amp2203/Params.h index 73123d5..bcb00cc 100644 --- a/engine/include/amp2203/Params.h +++ b/engine/include/amp2203/Params.h @@ -23,7 +23,8 @@ struct EngineConfig { int powerAmpRateDivisor = 1; /// M6 X1: the preamp → power amp junction at the master wiper solved exactly (a load port on both roots, /// the preamp linearised as a Thevenin source each sample) instead of v_oc(y) behind R_th4 (D-015). - /// Needs powerAmpRateDivisor = 1. + /// With powerAmpRateDivisor = 2 the junction is solved exactly at the samples where the power amp runs + /// (no decimation filter in the loop) and the preamp holds the last load current in between (M6). bool exactJ4 = false; }; diff --git a/engine/src/Amp2203.cpp b/engine/src/Amp2203.cpp index 1012081..ac50d9e 100644 --- a/engine/src/Amp2203.cpp +++ b/engine/src/Amp2203.cpp @@ -135,16 +135,29 @@ double Amp2203::processSample(double x) noexcept { applySupplies(); if (exactJ4()) { // preamp with the last load current, its linearised Thevenin source at y, the power amp against it, - // then the preamp again with the power amp's actual load current + // then the preamp again with the power amp's actual load current. Power amp at half rate: the same at + // the samples where it runs (the Thevenin source taken directly, no decimation filter in the loop), + // the preamp alone with the held load current in between; the output through the interpolator. preamp_->beginSample(vIn_); preamp_->solveLoaded(iY_); - double vTh = 0.0, rTh = 0.0; - preamp_->theveninAtY(iY_, vTh, rTh); - power_->setPreampPort(vTh, 1.0 / rTh); - vOut_ = power_->process(0.0); - iY_ = power_->preampPortCurrent(); - preamp_->solveLoaded(iY_); + const bool runPower = cfg_.powerAmpRateDivisor == 1 || powerPhase_; + double u = 0.0; + if (runPower) { + double vTh = 0.0, rTh = 0.0; + preamp_->theveninAtY(iY_, vTh, rTh); + power_->setPreampPort(vTh, 1.0 / rTh); + u = power_->process(0.0); + iY_ = power_->preampPortCurrent(); + preamp_->solveLoaded(iY_); + } preamp_->finishSample(); + if (cfg_.powerAmpRateDivisor == 1) { + vOut_ = u; + } else { + interp_.push(runPower ? 2.0 * u : 0.0); + vOut_ = interp_.dot(halfband_.h); + powerPhase_ = !powerPhase_; + } supply_->process({power_->centreTapCurrent(), power_->screenCurrent(), preamp_->supplyCurrentX() + power_->supplyCurrentX(), preamp_->supplyCurrentA()}); const double o = vOut_ / cfg_.outputVoltsPerFullScale; diff --git a/harness/reports/M6/20261015_x2_rates_a1_div2_exact.md b/harness/reports/M6/20261015_x2_rates_a1_div2_exact.md new file mode 100644 index 0000000..e8b4c4a --- /dev/null +++ b/harness/reports/M6/20261015_x2_rates_a1_div2_exact.md @@ -0,0 +1,52 @@ +# M6 A1: full engine (exact J4) at lower internal rates, plugin model check + +- Date 2026-10-15; options: divisor 2; rates 48 kHz × [2, 4, 8]. +- Plugin model check (D-051): ESR ≤ -20 dB vs SPICE (cached 384000 Hz references, 48 kHz band, first 5 s of every DI track, first 0.1 s excluded). Unseen settings: the 16 X4 validation settings, 10 s, vs the engine at 16x (first 1 s excluded). Cost: ampwav --stats on 10 s of DI GTR L, one run at a time; oversampling filters not included. + +## Summary + +| Rate | vs SPICE worst (CRUNCH / DRIVE / HOT) | Check | Unseen settings vs 16x (worst / median) | Newton failures (not rescued) / runs, max iterations | Cost default / HOT (% of one core) | +|---|---|---|---|---|---| +| 2x (96 kHz) | -38.8 / -38.0 / -22.5 dB | PASS | -18.9 / -30.1 dB | 40 (31) / 28, 32 | 24.1 / 35.8 | +| 4x (192 kHz) | -47.6 / -51.0 / -26.3 dB | PASS | -20.6 / -40.1 dB | 4 (0) / 28, 32 | 45.5 / 63.4 | +| 8x (384 kHz) | -48.7 / -55.7 / -33.2 dB | PASS | -26.8 / -54.5 dB | 3 (0) / 28, 32 | 88.7 / 114.8 | + +## vs SPICE per clip + +| Clip | 2x | 4x | 8x | +|---|---|---|---| +| CRUNCH GTR L | -39.3 dB | -48.2 dB | -49.0 dB | +| CRUNCH GTR LL | -38.8 dB | -47.6 dB | -48.7 dB | +| CRUNCH GTR R | -40.2 dB | -48.2 dB | -48.9 dB | +| CRUNCH GTR RR | -39.6 dB | -48.1 dB | -49.0 dB | +| DRIVE GTR L | -38.0 dB | -51.0 dB | -55.7 dB | +| DRIVE GTR LL | -39.0 dB | -51.8 dB | -56.2 dB | +| DRIVE GTR R | -39.2 dB | -51.9 dB | -56.0 dB | +| DRIVE GTR RR | -38.1 dB | -51.2 dB | -55.9 dB | +| HOT GTR L | -22.7 dB | -26.3 dB | -33.2 dB | +| HOT GTR LL | -23.7 dB | -27.0 dB | -33.8 dB | +| HOT GTR R | -23.5 dB | -27.3 dB | -34.1 dB | +| HOT GTR RR | -22.5 dB | -26.4 dB | -33.5 dB | + +## Aliasing (power outside the harmonics of f0, relative to the total; 16x for reference) + +Each cell: non-harmonic power, then Newton failures (not rescued) in that 2 s run. Non-converged samples add non-harmonic power, so a cell with many failures measures solver error as well as aliasing. + +| Preset | f0 | amplitude (FS) | 16x | 2x | 4x | 8x | +|---|---|---|---|---|---|---| +| CRUNCH | 5000 Hz | 0.1 | -26.0 dB, 0 (0) | -14.4 dB, 0 (0) | -19.8 dB, 0 (0) | -25.5 dB, 0 (0) | +| CRUNCH | 5000 Hz | 0.4 | -25.7 dB, 0 (0) | -12.1 dB, 2037 (1993) | -17.4 dB, 0 (0) | -23.5 dB, 0 (0) | +| CRUNCH | 9000 Hz | 0.1 | -39.9 dB, 0 (0) | -8.9 dB, 0 (0) | -16.8 dB, 1 (0) | -29.1 dB, 0 (0) | +| CRUNCH | 9000 Hz | 0.4 | -38.5 dB, 0 (0) | -4.6 dB, 26930 (24584) | -17.0 dB, 2368 (977) | -21.8 dB, 0 (0) | +| HOT | 5000 Hz | 0.1 | -30.0 dB, 387 (0) | -11.2 dB, 0 (0) | -16.1 dB, 0 (0) | -17.3 dB, 0 (0) | +| HOT | 5000 Hz | 0.4 | -29.8 dB, 408 (0) | -10.2 dB, 662 (653) | -16.0 dB, 0 (0) | -16.6 dB, 0 (0) | +| HOT | 9000 Hz | 0.1 | -38.2 dB, 60 (0) | -10.3 dB, 0 (0) | -13.7 dB, 1 (0) | -17.8 dB, 0 (0) | +| HOT | 9000 Hz | 0.4 | -36.0 dB, 25 (0) | -7.3 dB, 19100 (15938) | -15.5 dB, 6191 (2259) | -14.9 dB, 0 (0) | + +## Cost + +| Rate | ns per engine sample (default / HOT) | % of one core at 48 kHz | +|---|---|---| +| 2x | 2505 / 3729 | 24.1 / 35.8 | +| 4x | 2367 / 3305 | 45.5 / 63.4 | +| 8x | 2309 / 2989 | 88.7 / 114.8 | diff --git a/harness/reports/M6/20261015_x2_rates_maxiter32.md b/harness/reports/M6/20261015_x2_rates_maxiter32.md new file mode 100644 index 0000000..8bdf6d2 --- /dev/null +++ b/harness/reports/M6/20261015_x2_rates_maxiter32.md @@ -0,0 +1,52 @@ +# M6 A1: full engine (exact J4) at lower internal rates, plugin model check + +- Date 2026-10-15; options: defaults (power amp at full rate, Newton tol 1e-9 V); rates 48 kHz × [2, 4, 8]. +- Plugin model check (D-051): ESR ≤ -20 dB vs SPICE (cached 384000 Hz references, 48 kHz band, first 5 s of every DI track, first 0.1 s excluded). Unseen settings: the 16 X4 validation settings, 10 s, vs the engine at 16x (first 1 s excluded). Cost: ampwav --stats on 10 s of DI GTR L, one run at a time; oversampling filters not included. + +## Summary + +| Rate | vs SPICE worst (CRUNCH / DRIVE / HOT) | Check | Unseen settings vs 16x (worst / median) | Newton failures (not rescued) / runs, max iterations | Cost default / HOT (% of one core) | +|---|---|---|---|---|---| +| 2x (96 kHz) | -46.5 / -50.0 / -26.2 dB | PASS | -20.6 / -39.1 dB | 15 (7) / 28, 32 | 34.1 / 51.8 | +| 4x (192 kHz) | -48.5 / -55.6 / -33.2 dB | PASS | -26.9 / -54.2 dB | 4 (0) / 28, 32 | 67.3 / 91.6 | +| 8x (384 kHz) | -48.7 / -55.8 / -38.6 dB | PASS | -39.1 / -69.7 dB | 3 (0) / 28, 32 | 129.0 / 165.7 | + +## vs SPICE per clip + +| Clip | 2x | 4x | 8x | +|---|---|---|---| +| CRUNCH GTR L | -47.5 dB | -49.0 dB | -49.0 dB | +| CRUNCH GTR LL | -46.5 dB | -48.5 dB | -48.7 dB | +| CRUNCH GTR R | -47.6 dB | -48.9 dB | -48.9 dB | +| CRUNCH GTR RR | -47.5 dB | -49.0 dB | -49.0 dB | +| DRIVE GTR L | -50.0 dB | -55.6 dB | -55.8 dB | +| DRIVE GTR LL | -51.0 dB | -56.1 dB | -56.2 dB | +| DRIVE GTR R | -51.1 dB | -56.0 dB | -56.1 dB | +| DRIVE GTR RR | -50.5 dB | -55.9 dB | -56.0 dB | +| HOT GTR L | -26.2 dB | -33.2 dB | -38.7 dB | +| HOT GTR LL | -27.0 dB | -34.0 dB | -38.6 dB | +| HOT GTR R | -27.5 dB | -34.1 dB | -38.8 dB | +| HOT GTR RR | -26.2 dB | -33.7 dB | -38.9 dB | + +## Aliasing (power outside the harmonics of f0, relative to the total; 16x for reference) + +Each cell: non-harmonic power, then Newton failures (not rescued) in that 2 s run. Non-converged samples add non-harmonic power, so a cell with many failures measures solver error as well as aliasing. + +| Preset | f0 | amplitude (FS) | 16x | 2x | 4x | 8x | +|---|---|---|---|---|---|---| +| CRUNCH | 5000 Hz | 0.1 | -26.0 dB, 0 (0) | -19.6 dB, 0 (0) | -25.8 dB, 0 (0) | -26.0 dB, 0 (0) | +| CRUNCH | 5000 Hz | 0.4 | -25.7 dB, 0 (0) | -18.5 dB, 2021 (1993) | -24.3 dB, 0 (0) | -25.4 dB, 0 (0) | +| CRUNCH | 9000 Hz | 0.1 | -39.9 dB, 0 (0) | -15.6 dB, 0 (0) | -26.1 dB, 1 (0) | -36.6 dB, 0 (0) | +| CRUNCH | 9000 Hz | 0.4 | -38.5 dB, 0 (0) | -6.4 dB, 31818 (29769) | -22.0 dB, 2345 (1001) | -33.3 dB, 0 (0) | +| HOT | 5000 Hz | 0.1 | -30.0 dB, 387 (0) | -16.9 dB, 0 (0) | -17.2 dB, 0 (0) | -26.1 dB, 11 (0) | +| HOT | 5000 Hz | 0.4 | -29.8 dB, 408 (0) | -16.3 dB, 686 (680) | -18.7 dB, 0 (0) | -26.2 dB, 59 (0) | +| HOT | 9000 Hz | 0.1 | -38.2 dB, 60 (0) | -14.3 dB, 0 (0) | -14.9 dB, 1 (0) | -24.8 dB, 29 (0) | +| HOT | 9000 Hz | 0.4 | -36.0 dB, 25 (0) | -7.4 dB, 25634 (22778) | -15.4 dB, 4663 (1119) | -27.9 dB, 13 (0) | + +## Cost + +| Rate | ns per engine sample (default / HOT) | % of one core at 48 kHz | +|---|---|---| +| 2x | 3553 / 5392 | 34.1 / 51.8 | +| 4x | 3503 / 4773 | 67.3 / 91.6 | +| 8x | 3360 / 4316 | 129.0 / 165.7 | From e4ed0302ca516e677953da425f6bf048b5b63f91 Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 12:18:35 +0300 Subject: [PATCH 12/18] m6: listening comparison renders (engine rates, half-rate power amp, X3 / X4) --- docs/PROGRESS.md | 5 +++ harness/listening.py | 82 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 harness/listening.py diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index d351dda..d7dbe88 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -719,6 +719,11 @@ Notes: filter (`…_x2_rates_a1_div2_exact.md`): HOT −22.5 / −26.3 / −33.2 dB vs SPICE at 2x / 4x / 8x (PASS), unseen settings worst −18.9 / −20.6 / −26.8 dB vs 16x, cost 24 / 46 / 89 % (HOT 36 / 63 / 115 %), latency 17 engine samples. The plugin still runs the Thevenin J4 (exact J4 not enabled there). +- Listening set (owner 2026-10-15): `python -m harness.listening` → `harness/cache/listening/20261015/` (not committed): + DI GTR L / GTR RR 10–30 s at CRUNCH, DRIVE, HOT; engine 16x reference, full-rate power amp 2x / 4x / 8x, half rate + 2x / 4x, X4 network, X3 (CRUNCH); named / blind (KEY.txt) / difference; same gain per excerpt and preset, 32-bit + float. Difference RMS vs the reference: CRUNCH / DRIVE −52 … −94 dBFS (engine variants), HOT −27 … −58 dBFS; + networks −13 … −29 dBFS. No cabinet in the files. ## Questions for the owner _(Claude Code: add questions here and also ask them in the session.)_ diff --git a/harness/listening.py b/harness/listening.py new file mode 100644 index 0000000..de362c1 --- /dev/null +++ b/harness/listening.py @@ -0,0 +1,82 @@ +"""M6 listening comparison: the same DI excerpts through the engine at several rates / power-amp rates and through +the X3 / X4 networks, as 48 kHz WAVs for a DAW (no cabinet: raw amp output across the 16 Ω load). + + python -m harness.listening # writes harness/cache/listening// (not committed) + +Per excerpt and preset every file gets the same gain (the reference's peak at −1 dBFS), so level does not differ. +Folders: named/ (configuration in the file name), blind/ (letters, key in blind/KEY.txt), difference/ (each +configuration minus the reference, same gain). Exact J4, Newton maxIter 32 (D-052, D-053). +""" +import datetime +import random +import sys + +import numpy as np +import soundfile as sf + +from . import config +from . import x2_rates as x2 +from . import x3_neural as x3 +from . import x4_conditioned as x4 +from .presets import PRESETS + +RATE = 48000 +EXCERPTS = {'GTR L': 10.0, 'GTR RR': 10.0} # track: start (s) +SECONDS = 20.0 +PRESET_IDS = ('CRUNCH', 'DRIVE', 'HOT') +REFERENCE = 'ref_16x' +ENGINE = { # name: (factor, options) + REFERENCE: (16, {}), + 'full_2x': (2, {}), + 'full_4x': (4, {}), + 'full_8x': (8, {}), + 'half_2x': (2, {'divisor': 2}), + 'half_4x': (4, {'divisor': 2}), +} + + +def write_wav(path, x, rate): + sf.write(str(path), np.asarray(x, dtype=np.float32), int(rate), subtype='FLOAT') # 32-bit float: every DAW + + +OUT = config.HARNESS / 'cache' / 'listening' / datetime.date.today().strftime('%Y%m%d') + + +def _renders(x, pid): + knobs = PRESETS[pid].all_knobs() + out = {name: x2._run(x, knobs, f, o)[0] for name, (f, o) in ENGINE.items()} + out['net_x4'] = x4._student(x, [knobs[n] for n in x4.KNOB_NAMES]) + if pid == 'CRUNCH': + out['net_x3'] = x3._student(x) + return out + + +def main(): + tracks = x3._tracks() + rng = random.Random(15) + for sub in ('named', 'blind', 'difference'): + (OUT / sub).mkdir(parents=True, exist_ok=True) + key = [] + for track, start in EXCERPTS.items(): + x = tracks[track][int(start * RATE):int((start + SECONDS) * RATE)] + for pid in PRESET_IDS: + r = _renders(x, pid) + gain = 10 ** (-1 / 20) / np.max(np.abs(r[REFERENCE])) + tag = f'{track.replace(" ", "")}_{pid}' + names = sorted(r) + letters = list('ABCDEFGH'[:len(names)]) + rng.shuffle(letters) + for name, letter in zip(names, letters): + y = r[name] * gain + write_wav(OUT / 'named' / f'{tag}_{name}.wav', y, RATE) + write_wav(OUT / 'blind' / f'{tag}_{letter}.wav', y, RATE) + key.append(f'{tag}_{letter}: {name}') + if name != REFERENCE: + write_wav(OUT / 'difference' / f'{tag}_{name}_minus_ref.wav', (r[name] - r[REFERENCE]) * gain, RATE) + print(f'listening: {tag} ({len(names)} versions, gain {20 * np.log10(gain):+.1f} dB)', flush=True) + (OUT / 'blind' / 'KEY.txt').write_text('\n'.join(sorted(key)) + '\n') + print(f'listening: wrote {OUT}') + + +if __name__ == '__main__': + sys.exit(main()) From e4521b3e0dafb353caf26d45441125b52f901318 Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 12:37:09 +0300 Subject: [PATCH 13/18] m6: owner listening result --- docs/PROGRESS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index d7dbe88..e0d2c63 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -724,6 +724,8 @@ Notes: 2x / 4x, X4 network, X3 (CRUNCH); named / blind (KEY.txt) / difference; same gain per excerpt and preset, 32-bit float. Difference RMS vs the reference: CRUNCH / DRIVE −52 … −94 dBFS (engine variants), HOT −27 … −58 dBFS; networks −13 … −29 dBFS. No cabinet in the files. +- Owner listening result 2026-10-15: full-rate power amp at 4x and half-rate power amp at 2x sound identical to the + 16x reference; the X3 / X4 networks sound weaker, with clearly less gain, most of all at HOT. ## Questions for the owner _(Claude Code: add questions here and also ask them in the session.)_ From 9f018250d8636b96efa322fd7c42f925f7cc5e70 Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 12:51:57 +0300 Subject: [PATCH 14/18] m6: plugin defaults 2x / half-rate power amp / exact J4 (D-054); plugin CPU timing; tolerance and half-step studies at half 2x --- docs/DECISIONS.md | 13 ++++ docs/PROGRESS.md | 12 ++++ docs/SPEC.md | 10 +-- harness/automation.py | 11 +++- harness/plugin_null.py | 43 ++++++++---- ...261015_automation_fs96000_pdiv2_j4exact.md | 66 +++++++++++++++++++ .../M6/20261015_plugin_null_2x_full.md | 62 +++++++++++++++++ .../M6/20261015_plugin_null_2x_half.md | 62 +++++++++++++++++ .../20261015_x2_rates_half2x_off_tol1e-4.md | 48 ++++++++++++++ .../20261015_x2_rates_half2x_off_tol1e-5.md | 48 ++++++++++++++ .../20261015_x2_rates_half2x_off_tol1e-6.md | 48 ++++++++++++++ .../M6/20261015_x2_rates_half_trigger_off.md | 50 ++++++++++++++ harness/x2_rates.py | 5 ++ plugin/render/main.cpp | 7 ++ plugin/src/PluginProcessor.cpp | 7 +- 15 files changed, 471 insertions(+), 21 deletions(-) create mode 100644 harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md create mode 100644 harness/reports/M6/20261015_plugin_null_2x_full.md create mode 100644 harness/reports/M6/20261015_plugin_null_2x_half.md create mode 100644 harness/reports/M6/20261015_x2_rates_half2x_off_tol1e-4.md create mode 100644 harness/reports/M6/20261015_x2_rates_half2x_off_tol1e-5.md create mode 100644 harness/reports/M6/20261015_x2_rates_half2x_off_tol1e-6.md create mode 100644 harness/reports/M6/20261015_x2_rates_half_trigger_off.md diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index d83e59f..147c27e 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -929,3 +929,16 @@ Consequences (`harness/reports/M6/20261015_x2_rates_a1_div2_exact.md`, maxIter 3 −26.8 dB; cost 24 / 46 / 89 % of one core (HOT 36 / 63 / 115 %), full-rate power amp 34 / 67 / 129 % (HOT 52 / 92 / 166 %). Without the decimation filter the preamp content above a quarter of the engine rate folds into the power amp: non-harmonic power on 5 / 9 kHz sines −4.6 … −29.1 dB (full rate at the same engine rate: −6.4 … −36.6 dB). + +## D-054 Plugin defaults: 2x oversampling, power amp at half rate, exact J4 +Date: 2026-10-15 · Status: accepted (owner: "put half-rate power amp at 2x into the plugin"; listening: half rate 2x +sounds identical to the 16x reference) +Decision: Oversampling default 2x (was 8x, D-048), Power Amp Rate default Half (was Full, D-049); the plugin's +engine always runs the exact J4 coupling (D-053). The other factors and Full stay selectable. +Consequences: plugin CPU (`amp2203_render` now reports processBlock time) on 60 s DI GTR L at 48 kHz: 25.1 % / 24.8 % +of one core (FIR / IIR), HOT 39.2 % / 39.1 %; latency 58 (FIR) / 13 (IIR) samples. Null test at 2x Half vs ampwav +(same options, the reference re-delayed by the 17-sample interpolator delay and fed the plugin's exact +12 dBu scale): +25 PASS, 1 FAIL (5 kHz −6 dBFS IIR −53.5 dB): with the power amp at half rate the D-043 half-step decision (100 V +trigger) flips on last-bit input differences and the two discretisations differ locally by up to −27 dB; with the +trigger off the same perturbation stays at −105 dB. Full rate: 26 PASS. Automation (96 kHz, Half, exact J4): 56 runs, +0 non-finite, 0 Newton failures; 5.5–6.2 µs per engine sample while knobs move (static ≈ 2.5). diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index e0d2c63..13b06f9 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -726,6 +726,18 @@ Notes: networks −13 … −29 dBFS. No cabinet in the files. - Owner listening result 2026-10-15: full-rate power amp at 4x and half-rate power amp at 2x sound identical to the 16x reference; the X3 / X4 networks sound weaker, with clearly less gain, most of all at HOT. +- Owner 2026-10-15: (1) put half-rate power amp at 2x into the plugin and measure its CPU; (2) close the CPU gap. + Plan (1): plugin engine with exact J4; defaults Oversampling 2x, Power Amp Rate Half (SPEC §6.3, DECISIONS); + `amp2203_render` reports processing time; plugin CPU on 60 s DI at default and HOT knobs, FIR and IIR filters; + re-run plugin_null and automation. Plan (2): Newton tolerance 1e-6 / 1e-5 / 1e-4 V at half rate 2x (accuracy vs + SPICE, unseen settings, cost; adoption is the owner's decision, D-049); profile the engine at half rate 2x. +- (1) done (D-054): plugin defaults 2x / Half / exact J4; plugin CPU 25 % (HOT 39 %) incl. oversampling; null test + 25 / 26 (half-step decision sensitivity at half rate); automation 0 failures. +- (2) measured, not adopted (owner decisions): half rate 2x with the half-step trigger off + (`…_x2_rates_half_trigger_off.md`): accuracy as with it (HOT −22.6 dB, unseen −19.7 dB), engine 24 / 32 %; plus + Newton tolerance 1e-6 / 1e-5 / 1e-4 V (`…_half2x_off_tol*.md`): accuracy identical, failures 12 (2 unrescued), + engine 22.3 / 21.5 / 19.4 % (HOT 29.4 / 28.3 / 27.1 %). Profile (half 2x, HOT): LU solves 29 %, libm pow / exp / log + 26 %, rest of the Newton loops 34 %, WDF 6 %. ## Questions for the owner _(Claude Code: add questions here and also ask them in the session.)_ diff --git a/docs/SPEC.md b/docs/SPEC.md index 25240dc..5dc97e3 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -152,9 +152,9 @@ DECISIONS D-033), `PhaseInverter`, `PowerStage` (verified sub-blocks of `PowerAm ### 3.4 Sample rate and oversampling - The engine runs at whatever rate `prepare()` receives. The JUCE plugin layer wraps it with `juce::dsp::Oversampling` and passes the oversampled rate. -- Plugin oversampling: a parameter with 2x / 4x / 8x / 16x, default 8x, the same factor at every host rate - (DECISIONS D-048, owner decision after the D-047 rate study; was 16x / 8x / 4x by host rate, D-035). - At 48 kHz 8x is 384 kHz inside, verified against SPICE like 768 kHz (D-047). +- Plugin oversampling: a parameter with 2x / 4x / 8x / 16x, default 2x with the power amp at half rate and the + exact J4 coupling (DECISIONS D-054, owner listening result; was 8x, D-048), the same factor at every host rate. + 2x is 96 kHz inside at 48 kHz, verified against SPICE with the D-051 tolerances (M6 X2 / A1). - Verification runs the engine directly at 768 kHz (16 x 48 kHz) on signals generated at that rate, so no resampler sits between SPICE and the engine. See `VERIFICATION.md`. (At 384 kHz the preamp's fastest cutoff transitions were not resolved: D-034, D-035.) @@ -319,8 +319,8 @@ Must reproduce `reference/spice/tubes.lib` exactly (same equations and parameter ### 6.3 Plugin parameters (M5) Preamp Volume, Treble, Middle, Bass, Master, Presence (0–10 display, rotation internally), Interface Level (dBu at 0 dBFS, 0..+24 dBu, default +12.0 dBu; sets `inputVoltsPerFullScale`), -Input Trim (−24..+24 dB, default 0 dB), Output Level (−48..+12 dB), Oversampling (2x/4x/8x/16x, default 8x), Power Amp Rate (Full / Half, default -Full; D-049), +Input Trim (−24..+24 dB, default 0 dB), Output Level (−48..+12 dB), Oversampling (2x/4x/8x/16x, default 2x), Power Amp Rate (Full / Half, default +Half; D-049, D-054), optional Cabinet IR on/off with file chooser (only if implemented in this repo; see 1.3). --- diff --git a/harness/automation.py b/harness/automation.py index 15ebf19..76752c5 100644 --- a/harness/automation.py +++ b/harness/automation.py @@ -56,6 +56,12 @@ def one(base, knobset, kind, name, x): write_wav(d / 'in.wav', x) args = [str(config.AMPWAV), '--block', 'full', '--in', str(d / 'in.wav'), '--out', str(d / 'out.wav'), '--rate', str(config.FS), '--stats', str(d / 's.json')] + if config.POWER_DIVISOR != 1: # M6 configurations (AMP2203_POWER_DIVISOR, AMP2203_J4, AMP2203_NEWTON_TOL) + args += ['--power-rate-divisor', str(config.POWER_DIVISOR)] + if config.J4 != 'thevenin': + args += ['--j4', config.J4] + if config.NEWTON_TOL: + args += ['--newton-tol', config.NEWTON_TOL] args += [a for k, v in BASES[base].items() if k not in knobset for a in ('--param', f'{k}={v!r}')] for k in knobset: args += ramps(k, kind, len(x) / config.FS) @@ -92,9 +98,10 @@ def main(): lines.append(f'| {r["base"]} | {r["knobs"]} | {r["kind"]} | {r["signal"]} | {"yes" if r["finite"] else "NO"} | ' f'{r["peak"]:.1f} | {r["newton_solves"]} | {r["newton_failures"]} | {r["newton_rescued"]} | ' f'{r["newton_unrescued"]} | {r["median_iterations"]} / {r["max_iterations_used"]} | {r["ns_per_sample"]:.0f} |') - out = config.REPORT_DIR / 'M5' + m6 = config.POWER_DIVISOR != 1 or config.J4 != 'thevenin' or config.NEWTON_TOL + out = config.REPORT_DIR / ('M6' if m6 else 'M5') out.mkdir(parents=True, exist_ok=True) - path = out / f'{datetime.date.today().strftime("%Y%m%d")}_automation.md' + path = out / (f'{datetime.date.today().strftime("%Y%m%d")}_automation' + (f'_fs{config.FS}_pdiv{config.POWER_DIVISOR}_j4{config.J4}' if m6 else '') + '.md') path.write_text('\n'.join(lines) + '\n') print(f'automation: wrote {path.relative_to(config.ROOT)} (non-finite {nonfinite}, failures {fails}, unrescued {unresc})') diff --git a/harness/plugin_null.py b/harness/plugin_null.py index 028773a..db8b88f 100644 --- a/harness/plugin_null.py +++ b/harness/plugin_null.py @@ -1,12 +1,17 @@ -"""M5 wrapper null test (SPEC §7 M5): an offline render through the plugin's processor at 16x -(`amp2203_render plugin`) against `ampwav --block full` at 768 kHz on the processor's own oversampled input, +"""M5 wrapper null test (SPEC §7 M5): an offline render through the plugin's processor (`amp2203_render plugin`, +default 2x, Power Amp Rate Half, exact J4: the plugin defaults, D-054) against `ampwav --block full` at the same +engine rate and options on the processor's own oversampled input, followed by the same resampling: down(ampwav(up(x))), with the oversampler's up and down stages run alone (`amp2203_render up / down`). Default levels (Interface Level +12 dBu, trim and output 0 dB), default knobs. ESR ≤ −60 dB proves the wrapper adds nothing unintended. Both oversampling filters (linear-phase FIR, polyphase IIR). Signals at 48 kHz: sines (100 Hz, 1 kHz, 5 kHz at −40 / −20 / −6 dBFS, 0.2 s) and the first 5 s of each DI clip (resampled 44.1 → 48 kHz). Writes harness/reports/M5/_plugin_null.md. - python -m harness.plugin_null [--jobs N] + python -m harness.plugin_null [--factor 2|4|8|16] [--power-rate half|full] [--jobs N] + +With the half-rate power amp the processor's output carries the engine's rate-change delay +(Amp2203::latencySamples(), at the engine rate), which ampwav removes; the reference gets it back before the down +stage (the first engine output sample repeated). """ import argparse import datetime @@ -25,9 +30,12 @@ from .wavio import read_wav, write_wav HOST_RATE = 48000 -FACTOR = 16 +FACTOR = 2 +POWER_RATE = 'half' +HALF_RATE_DELAY = 17 # engine samples: Amp2203::latencySamples() with exact J4 at divisor 2 RENDER = config.ROOT / 'build' / 'plugin' / 'amp2203_render_artefacts' / 'Release' / 'amp2203_render' LIMIT_DB = -60.0 +PLUGIN_VOLTS_PER_FS = 0.7746 * np.sqrt(2.0) * 10 ** (12 / 20) # Interface Level +12 dBu (PluginProcessor) def signals(): @@ -55,10 +63,17 @@ def one(name, x, filt): with tempfile.TemporaryDirectory(prefix='amp2203_null_') as d: d = Path(d) write_wav(d / 'x.wav', x, HOST_RATE) - log = _run([RENDER, 'plugin', '--in', d / 'x.wav', '--out', d / 'plug.wav', '--factor', FACTOR, '--filter', filt]) + half = POWER_RATE == 'half' + log = _run([RENDER, 'plugin', '--in', d / 'x.wav', '--out', d / 'plug.wav', '--factor', FACTOR, '--filter', filt, + '--param', f'powerAmpRate={1 if half else 0}']) _run([RENDER, 'up', '--in', d / 'x.wav', '--out', d / 'up.wav', '--factor', FACTOR, '--filter', filt]) + u, rate = read_wav(d / 'up.wav') # the plugin's exact +12 dBu scale vs ampwav's 4.361 V per full scale + write_wav(d / 'up.wav', u * PLUGIN_VOLTS_PER_FS / config.INPUT_VOLTS_PER_FS, rate) _run([config.AMPWAV, '--block', 'full', '--units', 'fs', '--in', d / 'up.wav', '--out', d / 'eng.wav', - '--rate', HOST_RATE * FACTOR]) + '--rate', HOST_RATE * FACTOR, '--j4', 'exact', '--power-rate-divisor', 2 if half else 1]) + if half: + e, rate = read_wav(d / 'eng.wav') + write_wav(d / 'eng.wav', np.concatenate([np.full(HALF_RATE_DELAY, e[0]), e[:len(e) - HALF_RATE_DELAY]]), rate) _run([RENDER, 'down', '--in', d / 'eng.wav', '--out', d / 'ref.wav', '--factor', FACTOR, '--filter', filt]) a, b = read_wav(d / 'plug.wav')[0], read_wav(d / 'ref.wav')[0] n = min(len(a), len(b)) @@ -67,26 +82,32 @@ def one(name, x, filt): def main(): + global FACTOR, POWER_RATE ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument('--jobs', type=int) + ap.add_argument('--factor', type=int, default=FACTOR, choices=(2, 4, 8, 16)) + ap.add_argument('--power-rate', default=POWER_RATE, choices=('half', 'full')) a = ap.parse_args() + FACTOR, POWER_RATE = a.factor, a.power_rate sigs = signals() with ThreadPoolExecutor(max_workers=a.jobs or os.cpu_count()) as ex: rows = list(ex.map(lambda item: one(*item), [(n, x, f) for f in ('fir', 'iir') for n, x in sigs])) nfail = sum(r['esr'] > LIMIT_DB for r in rows) - lines = ['# M5 wrapper null test: plugin vs ampwav at 768 kHz with the same resampling', '', - f'- Date: {datetime.date.today().isoformat()}, commit `{_git()}`; host rate {HOST_RATE} Hz, {FACTOR}x', + lines = [f'# M5 wrapper null test: plugin vs ampwav at {HOST_RATE * FACTOR // 1000} kHz with the same resampling', '', + f'- Date: {datetime.date.today().isoformat()}, commit `{_git()}`; host rate {HOST_RATE} Hz, {FACTOR}x, ' + f'Power Amp Rate {POWER_RATE}, exact J4', '- Plugin: `amp2203_render plugin` (the Amp2203Processor offline). Reference: the processor\'s oversampler ' - 'up stage, `ampwav --block full --units fs` at 768 kHz, the down stage. Default knobs and levels.', + f'up stage, `ampwav --block full --units fs --j4 exact` at {HOST_RATE * FACTOR} Hz, the down stage. ' + 'Default knobs and levels.', f'- Tolerance (SPEC §7 M5): ESR ≤ {LIMIT_DB:g} dB.', '', f'PASS {len(rows) - nfail} · FAIL {nfail}', '', '| Filter | Signal | Peak out [FS] | ESR | Status | Render log |', '|---|---|---|---|---|---|'] for r in rows: lines.append(f'| {r["filter"]} | {r["signal"]} | {r["peak"]:.3f} | {r["esr"]:.1f} dB | ' f'{"PASS" if r["esr"] <= LIMIT_DB else "FAIL"} | {r["log"].replace("amp2203_render: ", "")} |') - out = config.REPORT_DIR / 'M5' + out = config.REPORT_DIR / 'M6' out.mkdir(parents=True, exist_ok=True) - path = out / f'{datetime.date.today().strftime("%Y%m%d")}_plugin_null.md' + path = out / f'{datetime.date.today().strftime("%Y%m%d")}_plugin_null_{FACTOR}x_{POWER_RATE}.md' path.write_text('\n'.join(lines) + '\n') print(f'plugin_null: wrote {path.relative_to(config.ROOT)} ({nfail} FAIL)') diff --git a/harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md b/harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md new file mode 100644 index 0000000..03623b7 --- /dev/null +++ b/harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md @@ -0,0 +1,66 @@ +# M5 automation: every knob over its full range at audio rate + +- Date: 2026-10-15, commit `e4521b3 (dirty)`, engine `ampwav --block full` at 96000 Hz +- Modulation: 10 Hz triangle 0 → 1 → 0 (`lfo`) or 0 ↔ 1 jumps every 50 ms (`jump`) on the knob targets (smoothed by the engine, 20 ms); bases: default knobs, every knob at 1.0. +- SPEC §7 M5: no NaN / Inf, no Newton failures. Newton failures count first solves that did not converge in 12 iterations; the fallback (D-042) rescued or did not rescue them. + +Runs: 56 · non-finite outputs: 0 · Newton failures (first solve): 0 · unrescued: 0 + +| Base | Knobs | Modulation | Signal | finite | peak [V] | solves | failures | rescued | unrescued | median / max it | ns/sample | +|---|---|---|---|---|---|---|---|---|---|---|---| +| default | preampVolume | lfo | SIN-220Hz-0.3V | yes | 33.2 | 192033 | 0 | 0 | 0 | 2 / 20 | 5668 | +| default | preampVolume | lfo | DI-GTR L | yes | 47.2 | 960095 | 0 | 0 | 0 | 2 / 19 | 5820 | +| default | preampVolume | jump | SIN-220Hz-0.3V | yes | 38.4 | 192033 | 0 | 0 | 0 | 2 / 26 | 5536 | +| default | preampVolume | jump | DI-GTR L | yes | 45.9 | 960165 | 0 | 0 | 0 | 2 / 20 | 5817 | +| default | treble | lfo | SIN-220Hz-0.3V | yes | 40.3 | 192033 | 0 | 0 | 0 | 2 / 23 | 5736 | +| default | treble | lfo | DI-GTR L | yes | 50.7 | 960114 | 0 | 0 | 0 | 2 / 20 | 6169 | +| default | treble | jump | SIN-220Hz-0.3V | yes | 32.5 | 192034 | 0 | 0 | 0 | 2 / 23 | 5597 | +| default | treble | jump | DI-GTR L | yes | 65.6 | 960208 | 0 | 0 | 0 | 2 / 20 | 6097 | +| default | middle | lfo | SIN-220Hz-0.3V | yes | 34.1 | 192033 | 0 | 0 | 0 | 2 / 26 | 5770 | +| default | middle | lfo | DI-GTR L | yes | 45.9 | 960112 | 0 | 0 | 0 | 2 / 19 | 6149 | +| default | middle | jump | SIN-220Hz-0.3V | yes | 38.7 | 192033 | 0 | 0 | 0 | 2 / 23 | 5579 | +| default | middle | jump | DI-GTR L | yes | 52.5 | 960143 | 0 | 0 | 0 | 2 / 20 | 6100 | +| default | bass | lfo | SIN-220Hz-0.3V | yes | 37.1 | 192033 | 0 | 0 | 0 | 2 / 23 | 5655 | +| default | bass | lfo | DI-GTR L | yes | 45.5 | 960126 | 0 | 0 | 0 | 2 / 20 | 5763 | +| default | bass | jump | SIN-220Hz-0.3V | yes | 42.2 | 192033 | 0 | 0 | 0 | 2 / 23 | 5498 | +| default | bass | jump | DI-GTR L | yes | 48.3 | 960123 | 0 | 0 | 0 | 2 / 18 | 5731 | +| default | master | lfo | SIN-220Hz-0.3V | yes | 69.3 | 192040 | 0 | 0 | 0 | 2 / 23 | 6067 | +| default | master | lfo | DI-GTR L | yes | 75.1 | 966556 | 0 | 0 | 0 | 2 / 26 | 6430 | +| default | master | jump | SIN-220Hz-0.3V | yes | 75.9 | 192466 | 0 | 0 | 0 | 2 / 23 | 6023 | +| default | master | jump | DI-GTR L | yes | 81.2 | 969786 | 0 | 0 | 0 | 2 / 23 | 6508 | +| default | presence | lfo | SIN-220Hz-0.3V | yes | 34.2 | 192033 | 0 | 0 | 0 | 2 / 23 | 6457 | +| default | presence | lfo | DI-GTR L | yes | 45.6 | 960120 | 0 | 0 | 0 | 2 / 19 | 6608 | +| default | presence | jump | SIN-220Hz-0.3V | yes | 49.3 | 192034 | 0 | 0 | 0 | 2 / 23 | 6255 | +| default | presence | jump | DI-GTR L | yes | 65.6 | 960191 | 0 | 0 | 0 | 2 / 19 | 6590 | +| default | all | lfo | SIN-220Hz-0.3V | yes | 71.1 | 192398 | 0 | 0 | 0 | 2 / 12 | 10594 | +| default | all | lfo | DI-GTR L | yes | 78.7 | 970090 | 0 | 0 | 0 | 2 / 23 | 13166 | +| default | all | jump | SIN-220Hz-0.3V | yes | 77.5 | 195640 | 0 | 0 | 0 | 2 / 26 | 10531 | +| default | all | jump | DI-GTR L | yes | 82.2 | 976641 | 0 | 0 | 0 | 2 / 25 | 13172 | +| max | preampVolume | lfo | SIN-220Hz-0.3V | yes | 75.4 | 199440 | 0 | 0 | 0 | 3 / 14 | 7118 | +| max | preampVolume | lfo | DI-GTR L | yes | 94.4 | 1030617 | 0 | 0 | 0 | 3 / 19 | 7639 | +| max | preampVolume | jump | SIN-220Hz-0.3V | yes | 84.3 | 204266 | 0 | 0 | 0 | 3 / 26 | 7115 | +| max | preampVolume | jump | DI-GTR L | yes | 89.5 | 1030611 | 0 | 0 | 0 | 3 / 21 | 7537 | +| max | treble | lfo | SIN-220Hz-0.3V | yes | 79.4 | 210094 | 0 | 0 | 0 | 2 / 26 | 7584 | +| max | treble | lfo | DI-GTR L | yes | 88.0 | 1023639 | 0 | 0 | 0 | 2 / 28 | 7505 | +| max | treble | jump | SIN-220Hz-0.3V | yes | 84.6 | 209196 | 0 | 0 | 0 | 2 / 26 | 7371 | +| max | treble | jump | DI-GTR L | yes | 90.0 | 1021417 | 0 | 0 | 0 | 2 / 26 | 7431 | +| max | middle | lfo | SIN-220Hz-0.3V | yes | 85.5 | 211603 | 0 | 0 | 0 | 2 / 26 | 7621 | +| max | middle | lfo | DI-GTR L | yes | 91.7 | 1029512 | 0 | 0 | 0 | 3 / 22 | 7867 | +| max | middle | jump | SIN-220Hz-0.3V | yes | 87.2 | 212094 | 0 | 0 | 0 | 2 / 26 | 7507 | +| max | middle | jump | DI-GTR L | yes | 90.2 | 1030194 | 0 | 0 | 0 | 3 / 20 | 7830 | +| max | bass | lfo | SIN-220Hz-0.3V | yes | 79.9 | 216154 | 0 | 0 | 0 | 2 / 26 | 7975 | +| max | bass | lfo | DI-GTR L | yes | 92.0 | 1040450 | 0 | 0 | 0 | 3 / 22 | 7684 | +| max | bass | jump | SIN-220Hz-0.3V | yes | 80.1 | 214554 | 0 | 0 | 0 | 2 / 26 | 7663 | +| max | bass | jump | DI-GTR L | yes | 91.0 | 1036902 | 0 | 0 | 0 | 3 / 22 | 7622 | +| max | master | lfo | SIN-220Hz-0.3V | yes | 73.3 | 199385 | 0 | 0 | 0 | 2 / 26 | 6884 | +| max | master | lfo | DI-GTR L | yes | 81.6 | 984999 | 0 | 0 | 0 | 2 / 28 | 7004 | +| max | master | jump | SIN-220Hz-0.3V | yes | 77.5 | 201669 | 0 | 0 | 0 | 2 / 26 | 6776 | +| max | master | jump | DI-GTR L | yes | 86.4 | 985722 | 0 | 0 | 0 | 2 / 26 | 6909 | +| max | presence | lfo | SIN-220Hz-0.3V | yes | 82.6 | 211727 | 0 | 0 | 0 | 2 / 26 | 8444 | +| max | presence | lfo | DI-GTR L | yes | 92.3 | 1022025 | 0 | 0 | 0 | 3 / 22 | 8293 | +| max | presence | jump | SIN-220Hz-0.3V | yes | 79.9 | 212150 | 0 | 0 | 0 | 2 / 26 | 8175 | +| max | presence | jump | DI-GTR L | yes | 92.3 | 1024014 | 0 | 0 | 0 | 3 / 25 | 8307 | +| max | all | lfo | SIN-220Hz-0.3V | yes | 71.1 | 192398 | 0 | 0 | 0 | 2 / 12 | 10612 | +| max | all | lfo | DI-GTR L | yes | 78.7 | 970090 | 0 | 0 | 0 | 2 / 23 | 13037 | +| max | all | jump | SIN-220Hz-0.3V | yes | 77.5 | 195640 | 0 | 0 | 0 | 2 / 26 | 10572 | +| max | all | jump | DI-GTR L | yes | 82.2 | 976641 | 0 | 0 | 0 | 2 / 25 | 13051 | diff --git a/harness/reports/M6/20261015_plugin_null_2x_full.md b/harness/reports/M6/20261015_plugin_null_2x_full.md new file mode 100644 index 0000000..6f62e35 --- /dev/null +++ b/harness/reports/M6/20261015_plugin_null_2x_full.md @@ -0,0 +1,62 @@ +# M5 wrapper null test: plugin vs ampwav at 96 kHz with the same resampling + +- Date: 2026-10-15, commit `e4521b3 (dirty)`; host rate 48000 Hz, 2x, Power Amp Rate full, exact J4 +- Plugin: `amp2203_render plugin` (the Amp2203Processor offline). Reference: the processor's oversampler up stage, `ampwav --block full --units fs --j4 exact` at 96000 Hz, the down stage. Default knobs and levels. +- Tolerance (SPEC §7 M5): ESR ≤ -60 dB. + +PASS 26 · FAIL 0 + +| Filter | Signal | Peak out [FS] | ESR | Status | Render log | +|---|---|---|---|---|---| +| fir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.061 s for 0.200 s of audio: 30.4 % of one core, real-time factor 3.29 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 3 | +| fir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.066 s for 0.200 s of audio: 32.9 % of one core, real-time factor 3.04 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 5 | +| fir | SIN-100Hz--6dBFS | 0.825 | -138.5 dB | PASS | processBlock 0.065 s for 0.200 s of audio: 32.4 % of one core, real-time factor 3.08 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 10 | +| fir | SIN-1000Hz--40dBFS | 0.341 | -138.5 dB | PASS | processBlock 0.075 s for 0.200 s of audio: 37.7 % of one core, real-time factor 2.65 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 7 | +| fir | SIN-1000Hz--20dBFS | 0.708 | -138.5 dB | PASS | processBlock 0.084 s for 0.200 s of audio: 42.2 % of one core, real-time factor 2.37 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 10 | +| fir | SIN-1000Hz--6dBFS | 0.704 | -138.5 dB | PASS | processBlock 0.092 s for 0.200 s of audio: 46.2 % of one core, real-time factor 2.17 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 13 | +| fir | SIN-5000Hz--40dBFS | 0.677 | -138.5 dB | PASS | processBlock 0.110 s for 0.200 s of audio: 55.1 % of one core, real-time factor 1.82 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 19 | +| fir | SIN-5000Hz--20dBFS | 0.706 | -138.5 dB | PASS | processBlock 0.127 s for 0.200 s of audio: 63.6 % of one core, real-time factor 1.57 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 20 | +| fir | SIN-5000Hz--6dBFS | 0.738 | -138.5 dB | PASS | processBlock 0.196 s for 0.200 s of audio: 98.0 % of one core, real-time factor 1.02 +latency 49 samples, Newton failures 443 (rescued 195), max iterations 32 | +| fir | DI-GTR L | 0.767 | -138.5 dB | PASS | processBlock 1.586 s for 5.000 s of audio: 31.7 % of one core, real-time factor 3.15 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 18 | +| fir | DI-GTR LL | 0.711 | -138.5 dB | PASS | processBlock 1.568 s for 5.000 s of audio: 31.4 % of one core, real-time factor 3.19 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 18 | +| fir | DI-GTR R | 0.782 | -138.5 dB | PASS | processBlock 1.569 s for 5.000 s of audio: 31.4 % of one core, real-time factor 3.19 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 21 | +| fir | DI-GTR RR | 0.798 | -138.5 dB | PASS | processBlock 1.581 s for 5.000 s of audio: 31.6 % of one core, real-time factor 3.16 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 19 | +| iir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.061 s for 0.200 s of audio: 30.4 % of one core, real-time factor 3.29 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 3 | +| iir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.066 s for 0.200 s of audio: 32.8 % of one core, real-time factor 3.05 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 6 | +| iir | SIN-100Hz--6dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.064 s for 0.200 s of audio: 32.1 % of one core, real-time factor 3.11 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 7 | +| iir | SIN-1000Hz--40dBFS | 0.362 | -138.5 dB | PASS | processBlock 0.075 s for 0.200 s of audio: 37.4 % of one core, real-time factor 2.68 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 8 | +| iir | SIN-1000Hz--20dBFS | 0.774 | -138.5 dB | PASS | processBlock 0.082 s for 0.200 s of audio: 40.9 % of one core, real-time factor 2.45 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 11 | +| iir | SIN-1000Hz--6dBFS | 0.881 | -138.5 dB | PASS | processBlock 0.094 s for 0.200 s of audio: 46.8 % of one core, real-time factor 2.14 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 22 | +| iir | SIN-5000Hz--40dBFS | 0.816 | -138.5 dB | PASS | processBlock 0.110 s for 0.200 s of audio: 55.1 % of one core, real-time factor 1.81 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 17 | +| iir | SIN-5000Hz--20dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.128 s for 0.200 s of audio: 63.8 % of one core, real-time factor 1.57 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 22 | +| iir | SIN-5000Hz--6dBFS | 0.841 | -138.5 dB | PASS | processBlock 0.174 s for 0.200 s of audio: 86.9 % of one core, real-time factor 1.15 +latency 4 samples, Newton failures 305 (rescued 96), max iterations 32 | +| iir | DI-GTR L | 0.814 | -138.5 dB | PASS | processBlock 1.576 s for 5.000 s of audio: 31.5 % of one core, real-time factor 3.17 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 19 | +| iir | DI-GTR LL | 0.781 | -138.5 dB | PASS | processBlock 1.563 s for 5.000 s of audio: 31.3 % of one core, real-time factor 3.20 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 24 | +| iir | DI-GTR R | 0.891 | -138.5 dB | PASS | processBlock 1.553 s for 5.000 s of audio: 31.1 % of one core, real-time factor 3.22 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 18 | +| iir | DI-GTR RR | 0.866 | -138.5 dB | PASS | processBlock 1.566 s for 5.000 s of audio: 31.3 % of one core, real-time factor 3.19 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 20 | diff --git a/harness/reports/M6/20261015_plugin_null_2x_half.md b/harness/reports/M6/20261015_plugin_null_2x_half.md new file mode 100644 index 0000000..bf6cf96 --- /dev/null +++ b/harness/reports/M6/20261015_plugin_null_2x_half.md @@ -0,0 +1,62 @@ +# M5 wrapper null test: plugin vs ampwav at 96 kHz with the same resampling + +- Date: 2026-10-15, commit `e4521b3 (dirty)`; host rate 48000 Hz, 2x, Power Amp Rate half, exact J4 +- Plugin: `amp2203_render plugin` (the Amp2203Processor offline). Reference: the processor's oversampler up stage, `ampwav --block full --units fs --j4 exact` at 96000 Hz, the down stage. Default knobs and levels. +- Tolerance (SPEC §7 M5): ESR ≤ -60 dB. + +PASS 25 · FAIL 1 + +| Filter | Signal | Peak out [FS] | ESR | Status | Render log | +|---|---|---|---|---|---| +| fir | SIN-100Hz--40dBFS | 0.111 | -138.3 dB | PASS | processBlock 0.041 s for 0.200 s of audio: 20.5 % of one core, real-time factor 4.88 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 3 | +| fir | SIN-100Hz--20dBFS | 0.719 | -138.1 dB | PASS | processBlock 0.045 s for 0.200 s of audio: 22.4 % of one core, real-time factor 4.46 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 5 | +| fir | SIN-100Hz--6dBFS | 0.825 | -133.8 dB | PASS | processBlock 0.045 s for 0.200 s of audio: 22.5 % of one core, real-time factor 4.45 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 10 | +| fir | SIN-1000Hz--40dBFS | 0.335 | -137.1 dB | PASS | processBlock 0.055 s for 0.200 s of audio: 27.7 % of one core, real-time factor 3.60 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 7 | +| fir | SIN-1000Hz--20dBFS | 0.655 | -124.6 dB | PASS | processBlock 0.059 s for 0.200 s of audio: 29.5 % of one core, real-time factor 3.39 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 10 | +| fir | SIN-1000Hz--6dBFS | 0.708 | -111.3 dB | PASS | processBlock 0.066 s for 0.200 s of audio: 33.1 % of one core, real-time factor 3.02 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 13 | +| fir | SIN-5000Hz--40dBFS | 0.703 | -131.6 dB | PASS | processBlock 0.080 s for 0.200 s of audio: 40.2 % of one core, real-time factor 2.49 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 19 | +| fir | SIN-5000Hz--20dBFS | 0.706 | -113.0 dB | PASS | processBlock 0.089 s for 0.200 s of audio: 44.3 % of one core, real-time factor 2.26 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 20 | +| fir | SIN-5000Hz--6dBFS | 0.744 | -99.0 dB | PASS | processBlock 0.157 s for 0.200 s of audio: 78.3 % of one core, real-time factor 1.28 +latency 58 samples, Newton failures 450 (rescued 190), max iterations 32 | +| fir | DI-GTR L | 0.741 | -138.5 dB | PASS | processBlock 1.096 s for 5.000 s of audio: 21.9 % of one core, real-time factor 4.56 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 18 | +| fir | DI-GTR LL | 0.756 | -138.5 dB | PASS | processBlock 1.078 s for 5.000 s of audio: 21.6 % of one core, real-time factor 4.64 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 18 | +| fir | DI-GTR R | 0.783 | -138.5 dB | PASS | processBlock 1.083 s for 5.000 s of audio: 21.7 % of one core, real-time factor 4.62 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 21 | +| fir | DI-GTR RR | 0.758 | -138.5 dB | PASS | processBlock 1.097 s for 5.000 s of audio: 21.9 % of one core, real-time factor 4.56 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 19 | +| iir | SIN-100Hz--40dBFS | 0.111 | -103.8 dB | PASS | processBlock 0.041 s for 0.200 s of audio: 20.5 % of one core, real-time factor 4.88 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 3 | +| iir | SIN-100Hz--20dBFS | 0.719 | -87.2 dB | PASS | processBlock 0.045 s for 0.200 s of audio: 22.4 % of one core, real-time factor 4.47 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 6 | +| iir | SIN-100Hz--6dBFS | 0.826 | -68.0 dB | PASS | processBlock 0.044 s for 0.200 s of audio: 22.2 % of one core, real-time factor 4.51 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 7 | +| iir | SIN-1000Hz--40dBFS | 0.358 | -81.4 dB | PASS | processBlock 0.055 s for 0.200 s of audio: 27.5 % of one core, real-time factor 3.64 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 8 | +| iir | SIN-1000Hz--20dBFS | 0.729 | -62.5 dB | PASS | processBlock 0.057 s for 0.200 s of audio: 28.6 % of one core, real-time factor 3.50 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 11 | +| iir | SIN-1000Hz--6dBFS | 0.766 | -60.3 dB | PASS | processBlock 0.066 s for 0.200 s of audio: 32.9 % of one core, real-time factor 3.04 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 22 | +| iir | SIN-5000Hz--40dBFS | 0.682 | -68.4 dB | PASS | processBlock 0.080 s for 0.200 s of audio: 40.1 % of one core, real-time factor 2.49 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 17 | +| iir | SIN-5000Hz--20dBFS | 0.788 | -62.6 dB | PASS | processBlock 0.088 s for 0.200 s of audio: 43.8 % of one core, real-time factor 2.28 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 22 | +| iir | SIN-5000Hz--6dBFS | 0.803 | -53.5 dB | FAIL | processBlock 0.122 s for 0.200 s of audio: 60.8 % of one core, real-time factor 1.64 +latency 13 samples, Newton failures 176 (rescued 98), max iterations 32 | +| iir | DI-GTR L | 0.796 | -138.5 dB | PASS | processBlock 1.091 s for 5.000 s of audio: 21.8 % of one core, real-time factor 4.58 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 19 | +| iir | DI-GTR LL | 0.750 | -138.5 dB | PASS | processBlock 1.072 s for 5.000 s of audio: 21.4 % of one core, real-time factor 4.67 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 24 | +| iir | DI-GTR R | 0.837 | -138.5 dB | PASS | processBlock 1.072 s for 5.000 s of audio: 21.4 % of one core, real-time factor 4.67 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 18 | +| iir | DI-GTR RR | 0.843 | -138.5 dB | PASS | processBlock 1.084 s for 5.000 s of audio: 21.7 % of one core, real-time factor 4.61 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 20 | diff --git a/harness/reports/M6/20261015_x2_rates_half2x_off_tol1e-4.md b/harness/reports/M6/20261015_x2_rates_half2x_off_tol1e-4.md new file mode 100644 index 0000000..4eea8fc --- /dev/null +++ b/harness/reports/M6/20261015_x2_rates_half2x_off_tol1e-4.md @@ -0,0 +1,48 @@ +# M6 A1: full engine (exact J4) at lower internal rates, plugin model check + +- Date 2026-10-15; options: divisor 2, tol 0.0001, trigger 0.0; rates 48 kHz × [2]. +- Plugin model check (D-051): ESR ≤ -20 dB vs SPICE (cached 384000 Hz references, 48 kHz band, first 5 s of every DI track, first 0.1 s excluded). Unseen settings: the 16 X4 validation settings, 10 s, vs the engine at 16x (first 1 s excluded). Cost: ampwav --stats on 10 s of DI GTR L, one run at a time; oversampling filters not included. + +## Summary + +| Rate | vs SPICE worst (CRUNCH / DRIVE / HOT) | Check | Unseen settings vs 16x (worst / median) | Newton failures (not rescued) / runs, max iterations | Cost default / HOT (% of one core) | +|---|---|---|---|---|---| +| 2x (96 kHz) | -38.2 / -36.8 / -22.6 dB | PASS | -19.7 / -29.8 dB | 12 (2) / 28, 32 | 19.4 / 27.1 | + +## vs SPICE per clip + +| Clip | 2x | +|---|---| +| CRUNCH GTR L | -38.8 dB | +| CRUNCH GTR LL | -38.2 dB | +| CRUNCH GTR R | -39.5 dB | +| CRUNCH GTR RR | -38.9 dB | +| DRIVE GTR L | -36.8 dB | +| DRIVE GTR LL | -38.0 dB | +| DRIVE GTR R | -38.0 dB | +| DRIVE GTR RR | -36.9 dB | +| HOT GTR L | -23.1 dB | +| HOT GTR LL | -23.7 dB | +| HOT GTR R | -23.9 dB | +| HOT GTR RR | -22.6 dB | + +## Aliasing (power outside the harmonics of f0, relative to the total; 16x for reference) + +Each cell: non-harmonic power, then Newton failures (not rescued) in that 2 s run. Non-converged samples add non-harmonic power, so a cell with many failures measures solver error as well as aliasing. + +| Preset | f0 | amplitude (FS) | 16x | 2x | +|---|---|---|---|---| +| CRUNCH | 5000 Hz | 0.1 | -26.0 dB, 0 (0) | -14.1 dB, 0 (0) | +| CRUNCH | 5000 Hz | 0.4 | -25.7 dB, 0 (0) | -11.8 dB, 2065 (1992) | +| CRUNCH | 9000 Hz | 0.1 | -39.9 dB, 0 (0) | -9.4 dB, 0 (0) | +| CRUNCH | 9000 Hz | 0.4 | -38.5 dB, 0 (0) | -4.8 dB, 23335 (20619) | +| HOT | 5000 Hz | 0.1 | -30.0 dB, 387 (0) | -14.4 dB, 0 (0) | +| HOT | 5000 Hz | 0.4 | -29.8 dB, 408 (0) | -13.5 dB, 699 (697) | +| HOT | 9000 Hz | 0.1 | -38.2 dB, 60 (0) | -4.9 dB, 0 (0) | +| HOT | 9000 Hz | 0.4 | -36.0 dB, 25 (0) | -7.0 dB, 18905 (16364) | + +## Cost + +| Rate | ns per engine sample (default / HOT) | % of one core at 48 kHz | +|---|---|---| +| 2x | 2025 / 2827 | 19.4 / 27.1 | diff --git a/harness/reports/M6/20261015_x2_rates_half2x_off_tol1e-5.md b/harness/reports/M6/20261015_x2_rates_half2x_off_tol1e-5.md new file mode 100644 index 0000000..6df8803 --- /dev/null +++ b/harness/reports/M6/20261015_x2_rates_half2x_off_tol1e-5.md @@ -0,0 +1,48 @@ +# M6 A1: full engine (exact J4) at lower internal rates, plugin model check + +- Date 2026-10-15; options: divisor 2, tol 1e-05, trigger 0.0; rates 48 kHz × [2]. +- Plugin model check (D-051): ESR ≤ -20 dB vs SPICE (cached 384000 Hz references, 48 kHz band, first 5 s of every DI track, first 0.1 s excluded). Unseen settings: the 16 X4 validation settings, 10 s, vs the engine at 16x (first 1 s excluded). Cost: ampwav --stats on 10 s of DI GTR L, one run at a time; oversampling filters not included. + +## Summary + +| Rate | vs SPICE worst (CRUNCH / DRIVE / HOT) | Check | Unseen settings vs 16x (worst / median) | Newton failures (not rescued) / runs, max iterations | Cost default / HOT (% of one core) | +|---|---|---|---|---|---| +| 2x (96 kHz) | -38.2 / -36.8 / -22.6 dB | PASS | -19.7 / -29.8 dB | 12 (2) / 28, 32 | 21.5 / 28.3 | + +## vs SPICE per clip + +| Clip | 2x | +|---|---| +| CRUNCH GTR L | -38.8 dB | +| CRUNCH GTR LL | -38.2 dB | +| CRUNCH GTR R | -39.5 dB | +| CRUNCH GTR RR | -38.9 dB | +| DRIVE GTR L | -36.8 dB | +| DRIVE GTR LL | -38.0 dB | +| DRIVE GTR R | -38.0 dB | +| DRIVE GTR RR | -36.9 dB | +| HOT GTR L | -23.1 dB | +| HOT GTR LL | -23.7 dB | +| HOT GTR R | -23.9 dB | +| HOT GTR RR | -22.6 dB | + +## Aliasing (power outside the harmonics of f0, relative to the total; 16x for reference) + +Each cell: non-harmonic power, then Newton failures (not rescued) in that 2 s run. Non-converged samples add non-harmonic power, so a cell with many failures measures solver error as well as aliasing. + +| Preset | f0 | amplitude (FS) | 16x | 2x | +|---|---|---|---|---| +| CRUNCH | 5000 Hz | 0.1 | -26.0 dB, 0 (0) | -14.1 dB, 0 (0) | +| CRUNCH | 5000 Hz | 0.4 | -25.7 dB, 0 (0) | -11.8 dB, 2065 (1992) | +| CRUNCH | 9000 Hz | 0.1 | -39.9 dB, 0 (0) | -9.4 dB, 0 (0) | +| CRUNCH | 9000 Hz | 0.4 | -38.5 dB, 0 (0) | -4.3 dB, 26687 (24457) | +| HOT | 5000 Hz | 0.1 | -30.0 dB, 387 (0) | -14.4 dB, 0 (0) | +| HOT | 5000 Hz | 0.4 | -29.8 dB, 408 (0) | -13.5 dB, 700 (697) | +| HOT | 9000 Hz | 0.1 | -38.2 dB, 60 (0) | -4.9 dB, 0 (0) | +| HOT | 9000 Hz | 0.4 | -36.0 dB, 25 (0) | -7.1 dB, 19041 (16294) | + +## Cost + +| Rate | ns per engine sample (default / HOT) | % of one core at 48 kHz | +|---|---|---| +| 2x | 2237 / 2948 | 21.5 / 28.3 | diff --git a/harness/reports/M6/20261015_x2_rates_half2x_off_tol1e-6.md b/harness/reports/M6/20261015_x2_rates_half2x_off_tol1e-6.md new file mode 100644 index 0000000..954fcb2 --- /dev/null +++ b/harness/reports/M6/20261015_x2_rates_half2x_off_tol1e-6.md @@ -0,0 +1,48 @@ +# M6 A1: full engine (exact J4) at lower internal rates, plugin model check + +- Date 2026-10-15; options: divisor 2, tol 1e-06, trigger 0.0; rates 48 kHz × [2]. +- Plugin model check (D-051): ESR ≤ -20 dB vs SPICE (cached 384000 Hz references, 48 kHz band, first 5 s of every DI track, first 0.1 s excluded). Unseen settings: the 16 X4 validation settings, 10 s, vs the engine at 16x (first 1 s excluded). Cost: ampwav --stats on 10 s of DI GTR L, one run at a time; oversampling filters not included. + +## Summary + +| Rate | vs SPICE worst (CRUNCH / DRIVE / HOT) | Check | Unseen settings vs 16x (worst / median) | Newton failures (not rescued) / runs, max iterations | Cost default / HOT (% of one core) | +|---|---|---|---|---|---| +| 2x (96 kHz) | -38.2 / -36.8 / -22.6 dB | PASS | -19.7 / -29.8 dB | 12 (2) / 28, 32 | 22.3 / 29.4 | + +## vs SPICE per clip + +| Clip | 2x | +|---|---| +| CRUNCH GTR L | -38.8 dB | +| CRUNCH GTR LL | -38.2 dB | +| CRUNCH GTR R | -39.5 dB | +| CRUNCH GTR RR | -38.9 dB | +| DRIVE GTR L | -36.8 dB | +| DRIVE GTR LL | -38.0 dB | +| DRIVE GTR R | -38.0 dB | +| DRIVE GTR RR | -36.9 dB | +| HOT GTR L | -23.1 dB | +| HOT GTR LL | -23.7 dB | +| HOT GTR R | -23.9 dB | +| HOT GTR RR | -22.6 dB | + +## Aliasing (power outside the harmonics of f0, relative to the total; 16x for reference) + +Each cell: non-harmonic power, then Newton failures (not rescued) in that 2 s run. Non-converged samples add non-harmonic power, so a cell with many failures measures solver error as well as aliasing. + +| Preset | f0 | amplitude (FS) | 16x | 2x | +|---|---|---|---|---| +| CRUNCH | 5000 Hz | 0.1 | -26.0 dB, 0 (0) | -14.1 dB, 0 (0) | +| CRUNCH | 5000 Hz | 0.4 | -25.7 dB, 0 (0) | -11.8 dB, 2065 (1992) | +| CRUNCH | 9000 Hz | 0.1 | -39.9 dB, 0 (0) | -9.4 dB, 0 (0) | +| CRUNCH | 9000 Hz | 0.4 | -38.5 dB, 0 (0) | -6.1 dB, 24907 (23101) | +| HOT | 5000 Hz | 0.1 | -30.0 dB, 387 (0) | -14.4 dB, 0 (0) | +| HOT | 5000 Hz | 0.4 | -29.8 dB, 408 (0) | -13.5 dB, 702 (697) | +| HOT | 9000 Hz | 0.1 | -38.2 dB, 60 (0) | -4.9 dB, 0 (0) | +| HOT | 9000 Hz | 0.4 | -36.0 dB, 25 (0) | -7.1 dB, 20042 (17761) | + +## Cost + +| Rate | ns per engine sample (default / HOT) | % of one core at 48 kHz | +|---|---|---| +| 2x | 2328 / 3068 | 22.3 / 29.4 | diff --git a/harness/reports/M6/20261015_x2_rates_half_trigger_off.md b/harness/reports/M6/20261015_x2_rates_half_trigger_off.md new file mode 100644 index 0000000..e259f62 --- /dev/null +++ b/harness/reports/M6/20261015_x2_rates_half_trigger_off.md @@ -0,0 +1,50 @@ +# M6 A1: full engine (exact J4) at lower internal rates, plugin model check + +- Date 2026-10-15; options: divisor 2, trigger 0.0; rates 48 kHz × [2, 4]. +- Plugin model check (D-051): ESR ≤ -20 dB vs SPICE (cached 384000 Hz references, 48 kHz band, first 5 s of every DI track, first 0.1 s excluded). Unseen settings: the 16 X4 validation settings, 10 s, vs the engine at 16x (first 1 s excluded). Cost: ampwav --stats on 10 s of DI GTR L, one run at a time; oversampling filters not included. + +## Summary + +| Rate | vs SPICE worst (CRUNCH / DRIVE / HOT) | Check | Unseen settings vs 16x (worst / median) | Newton failures (not rescued) / runs, max iterations | Cost default / HOT (% of one core) | +|---|---|---|---|---|---| +| 2x (96 kHz) | -38.2 / -36.8 / -22.6 dB | PASS | -19.7 / -29.8 dB | 40 (31) / 28, 32 | 23.9 / 32.3 | +| 4x (192 kHz) | -48.1 / -52.8 / -24.3 dB | PASS | -18.8 / -33.2 dB | 4 (0) / 28, 32 | 45.1 / 60.1 | + +## vs SPICE per clip + +| Clip | 2x | 4x | +|---|---|---| +| CRUNCH GTR L | -38.8 dB | -48.5 dB | +| CRUNCH GTR LL | -38.2 dB | -48.1 dB | +| CRUNCH GTR R | -39.5 dB | -48.4 dB | +| CRUNCH GTR RR | -38.9 dB | -48.4 dB | +| DRIVE GTR L | -36.8 dB | -52.8 dB | +| DRIVE GTR LL | -38.0 dB | -53.6 dB | +| DRIVE GTR R | -38.0 dB | -53.6 dB | +| DRIVE GTR RR | -36.9 dB | -52.9 dB | +| HOT GTR L | -23.1 dB | -24.3 dB | +| HOT GTR LL | -23.7 dB | -25.0 dB | +| HOT GTR R | -23.9 dB | -25.3 dB | +| HOT GTR RR | -22.6 dB | -24.3 dB | + +## Aliasing (power outside the harmonics of f0, relative to the total; 16x for reference) + +Each cell: non-harmonic power, then Newton failures (not rescued) in that 2 s run. Non-converged samples add non-harmonic power, so a cell with many failures measures solver error as well as aliasing. + +| Preset | f0 | amplitude (FS) | 16x | 2x | 4x | +|---|---|---|---|---|---| +| CRUNCH | 5000 Hz | 0.1 | -26.0 dB, 0 (0) | -14.1 dB, 0 (0) | -19.7 dB, 0 (0) | +| CRUNCH | 5000 Hz | 0.4 | -25.7 dB, 0 (0) | -11.8 dB, 2102 (1993) | -17.5 dB, 0 (0) | +| CRUNCH | 9000 Hz | 0.1 | -39.9 dB, 0 (0) | -9.4 dB, 0 (0) | -17.1 dB, 1 (0) | +| CRUNCH | 9000 Hz | 0.4 | -38.5 dB, 0 (0) | -4.2 dB, 28226 (26103) | -17.2 dB, 2239 (961) | +| HOT | 5000 Hz | 0.1 | -30.0 dB, 387 (0) | -14.4 dB, 0 (0) | -13.4 dB, 0 (0) | +| HOT | 5000 Hz | 0.4 | -29.8 dB, 408 (0) | -13.5 dB, 728 (697) | -13.3 dB, 0 (0) | +| HOT | 9000 Hz | 0.1 | -38.2 dB, 60 (0) | -4.9 dB, 0 (0) | -13.8 dB, 1 (0) | +| HOT | 9000 Hz | 0.4 | -36.0 dB, 25 (0) | -7.0 dB, 20490 (18206) | -17.7 dB, 5699 (1851) | + +## Cost + +| Rate | ns per engine sample (default / HOT) | % of one core at 48 kHz | +|---|---|---| +| 2x | 2489 / 3360 | 23.9 / 32.3 | +| 4x | 2350 / 3132 | 45.1 / 60.1 | diff --git a/harness/x2_rates.py b/harness/x2_rates.py index 3a540fe..4b6b470 100644 --- a/harness/x2_rates.py +++ b/harness/x2_rates.py @@ -67,6 +67,8 @@ def _run(x48, knobs, factor, opts, stats_only=False): args += ['--power-rate-divisor', str(opts['divisor'])] if opts.get('tol'): args += ['--newton-tol', repr(opts['tol'])] + if 'trigger' in opts: + args += ['--half-step-trigger', repr(opts['trigger'])] r = subprocess.run(args, capture_output=True, text=True) if r.returncode != 0: raise RuntimeError(r.stderr) @@ -201,6 +203,7 @@ def main(): ap.add_argument('--factors', type=int, nargs='+', default=[2, 4, 8]) ap.add_argument('--power-rate-divisor', type=int, default=1) ap.add_argument('--newton-tol', type=float, default=None) + ap.add_argument('--half-step-trigger', type=float, default=None, help='V; 0 = off (D-043 default 100)') ap.add_argument('--tag', default='') a = ap.parse_args() opts = {} @@ -208,6 +211,8 @@ def main(): opts['divisor'] = a.power_rate_divisor if a.newton_tol: opts['tol'] = a.newton_tol + if a.half_step_trigger is not None: + opts['trigger'] = a.half_step_trigger run(a.factors, opts, a.tag) diff --git a/plugin/render/main.cpp b/plugin/render/main.cpp index aaa7663..192c2da 100644 --- a/plugin/render/main.cpp +++ b/plugin/render/main.cpp @@ -4,6 +4,7 @@ // displayed values (knobs 0–10, levels in dB / dBu) // amp2203_render up --in x.wav --out x_os.wav [--factor 8] [--filter fir|iir] oversampler up stage only // amp2203_render down --in y_os.wav --out y.wav [--factor 8] [--filter fir|iir] oversampler down stage only +#include #include #include #include @@ -89,16 +90,22 @@ int main(int argc, char** argv) { return fail("oversampling factor not applied"); juce::AudioBuffer buf(2, kBlock); juce::MidiBuffer midi; + double busy = 0.0; // seconds inside processBlock (the plugin's cost: oversampler, engine, levels) for (size_t pos = 0; pos < in.samples.size(); pos += kBlock) { const int n = static_cast(std::min(kBlock, in.samples.size() - pos)); buf.setSize(2, n, false, false, true); for (int c = 0; c < 2; ++c) for (int i = 0; i < n; ++i) buf.setSample(c, i, c == 0 ? in.samples[pos + static_cast(i)] : 0.0); + const auto t0 = std::chrono::steady_clock::now(); proc.processBlock(buf, midi); + busy += std::chrono::duration(std::chrono::steady_clock::now() - t0).count(); for (int i = 0; i < n; ++i) out.push_back(buf.getSample(0, i)); } + const double audio = static_cast(in.samples.size()) / in.sampleRate; + std::fprintf(stderr, "amp2203_render: processBlock %.3f s for %.3f s of audio: %.1f %% of one core, real-time factor %.2f\n", + busy, audio, 100.0 * busy / audio, audio / busy); const auto st = proc.engineStats(); std::fprintf(stderr, "amp2203_render: latency %d samples, Newton failures %llu (rescued %llu), max iterations %.0f\n", proc.getLatencySamples(), static_cast(st.newtonFailures), diff --git a/plugin/src/PluginProcessor.cpp b/plugin/src/PluginProcessor.cpp index a7acb19..2c9e8d3 100644 --- a/plugin/src/PluginProcessor.cpp +++ b/plugin/src/PluginProcessor.cpp @@ -42,12 +42,12 @@ juce::AudioProcessorValueTreeState::ParameterLayout Amp2203Processor::layout() { juce::NormalisableRange(-48.0f, 12.0f, 0.1f), 0.0f, juce::AudioParameterFloatAttributes().withLabel("dB"))); l.add(std::make_unique(juce::ParameterID{"oversampling", 1}, "Oversampling", - juce::StringArray{"2x", "4x", "8x", "16x"}, 2)); + juce::StringArray{"2x", "4x", "8x", "16x"}, 0)); // 2x (D-054) l.add(std::make_unique(juce::ParameterID{"oversamplingFilter", 1}, "Oversampling Filter", juce::StringArray{"Linear phase (FIR)", "Low latency (IIR)"}, 0)); - // power amp at the oversampled rate or at half of it (M6, D-049; faster, less accurate) + // power amp at the oversampled rate or at half of it (M6, D-049, D-053; default Half, D-054) l.add(std::make_unique(juce::ParameterID{"powerAmpRate", 1}, "Power Amp Rate", - juce::StringArray{"Full", "Half"}, 0)); + juce::StringArray{"Full", "Half"}, 1)); return l; } @@ -77,6 +77,7 @@ void Amp2203Processor::rebuild(double hostRate, int maxBlock) { cfg.sampleRate = hostRate * activeFactor_; cfg.maxBlockSize = maxBlock * activeFactor_; cfg.powerAmpRateDivisor = activePowerRate_ == 1 ? 2 : 1; + cfg.exactJ4 = true; // preamp → power amp junction solved exactly (M6 X1, D-053, D-054) engine_.prepare(cfg); pushParams(); engine_.reset(); // DC operating point at the current knobs (SPEC §3.6) From 9778d0deee952ade151778695202a09bb0aef5dd Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 12:58:18 +0300 Subject: [PATCH 15/18] m6: plugin Newton tolerance 1e-4 V, no half-steps at half rate (D-055); ampwav --volts-per-fs, --keep-delay; null test 26/26 --- docs/DECISIONS.md | 28 ++++- docs/PROGRESS.md | 4 + engine/include/amp2203/Amp2203.h | 2 + engine/src/Amp2203.cpp | 2 + harness/automation.py | 2 + harness/plugin_null.py | 19 ++- ...261015_automation_fs96000_pdiv2_j4exact.md | 116 +++++++++--------- .../M6/20261015_plugin_null_2x_full.md | 86 ++++++------- .../M6/20261015_plugin_null_2x_half.md | 88 ++++++------- plugin/src/PluginProcessor.cpp | 6 + tools/ampwav/src/Cli.cpp | 39 ++++-- tools/ampwav/src/Cli.h | 2 + tools/ampwav/src/Runner.cpp | 12 +- tools/ampwav/src/Runner.h | 9 ++ 14 files changed, 245 insertions(+), 170 deletions(-) diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 147c27e..7136e50 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -936,9 +936,27 @@ sounds identical to the 16x reference) Decision: Oversampling default 2x (was 8x, D-048), Power Amp Rate default Half (was Full, D-049); the plugin's engine always runs the exact J4 coupling (D-053). The other factors and Full stay selectable. Consequences: plugin CPU (`amp2203_render` now reports processBlock time) on 60 s DI GTR L at 48 kHz: 25.1 % / 24.8 % -of one core (FIR / IIR), HOT 39.2 % / 39.1 %; latency 58 (FIR) / 13 (IIR) samples. Null test at 2x Half vs ampwav -(same options, the reference re-delayed by the 17-sample interpolator delay and fed the plugin's exact +12 dBu scale): -25 PASS, 1 FAIL (5 kHz −6 dBFS IIR −53.5 dB): with the power amp at half rate the D-043 half-step decision (100 V -trigger) flips on last-bit input differences and the two discretisations differ locally by up to −27 dB; with the -trigger off the same perturbation stays at −105 dB. Full rate: 26 PASS. Automation (96 kHz, Half, exact J4): 56 runs, +of one core (FIR / IIR), HOT 39.2 % / 39.1 %; latency 58 (FIR) / 13 (IIR) samples. Null test at 2x Half: first 25 PASS, +1 FAIL; corrected in D-055 (the FAIL came from the null test's own reference, not from the plugin). Separately measured: +with the power amp at half rate a 1.5e-5 change of the input level (4.361 vs the plugin's exact 4.361064 V per full +scale) flips some D-043 half-step decisions, and the two discretisations then differ locally by up to −27 dB (5 kHz +−40 dBFS); with the trigger off the same change stays at −105 dB. Full rate: 26 PASS. Automation (96 kHz, Half, exact J4): 56 runs, 0 non-finite, 0 Newton failures; 5.5–6.2 µs per engine sample while knobs move (static ≈ 2.5). + +## D-055 Plugin: Newton tolerance 1e-4 V; no adaptive half-steps with the power amp at half rate +Date: 2026-10-15 · Status: accepted (owner: "adopt both, use 1e-4"; revises D-049's tolerance for the plugin and D-043 +for the half-rate power amp) +Context: half rate 2x (`harness/reports/M6/20261015_x2_rates_half*.md`): Newton tolerance 1e-9 → 1e-4 V leaves every +accuracy figure unchanged (HOT −22.6 dB vs SPICE, unseen settings −19.7 dB) and cuts first-solve failures 40 → 12 +(unrescued 31 → 2) and engine cost 24 → 19.4 % (HOT 32 → 27.1 %); half-steps off at 2x: accuracy unchanged, HOT +36 → 32 %, no switch sensitivity (D-054). +Decision: the plugin sets the Newton residual tolerance to 1e-4 V (`kNewtonTolerance`) and turns the adaptive +half-steps off when the power amp runs at half rate (`Amp2203::setAdaptiveHalfSteps`); full rate keeps the D-043 +trigger. The engine defaults (1e-9 V, 100 V) stay for verification. ampwav: `--volts-per-fs` (the plugin's exact input +scale, full-scale samples reach the engine unscaled) and `--keep-delay` (keep the half-rate output delay). +Consequences: null test at 2x Half / Full with the plugin's settings, input scale and delay: 26 / 26 PASS, worst +−138.5 dB (the earlier FAIL was the reference's padding of the removed delay). Measured after the decision (static, +every knob 1.0, 5 s DI, half 2x): the power amp's first solves fail 656 times without half-steps (0 with them), all +rescued, 5371 vs 3501 ns per engine sample (tolerance 1e-4); automation at every knob 1.0: up to 1732 rescued +failures per run and up to 10.4 µs per engine sample (was 0 and ≈ 6 µs). Open: owner review of the half-step choice +at extreme settings. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 13b06f9..303dd03 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -738,6 +738,10 @@ Notes: Newton tolerance 1e-6 / 1e-5 / 1e-4 V (`…_half2x_off_tol*.md`): accuracy identical, failures 12 (2 unrescued), engine 22.3 / 21.5 / 19.4 % (HOT 29.4 / 28.3 / 27.1 %). Profile (half 2x, HOT): LU solves 29 %, libm pow / exp / log 26 %, rest of the Newton loops 34 %, WDF 6 %. +- Owner 2026-10-15: adopt both (tolerance 1e-4 V, no half-steps at half rate), D-055. Null test 26 / 26 at Half and + Full (ampwav `--volts-per-fs`, `--keep-delay`). Found after the decision: at every knob 1.0 the half-rate power amp + without half-steps has 656 rescued first-solve failures per 5 s and costs 5371 vs 3501 ns per engine sample; + automation at every knob 1.0 up to 10.4 µs. Raised with the owner. ## Questions for the owner _(Claude Code: add questions here and also ask them in the session.)_ diff --git a/engine/include/amp2203/Amp2203.h b/engine/include/amp2203/Amp2203.h index a40e5e0..fbc0225 100644 --- a/engine/include/amp2203/Amp2203.h +++ b/engine/include/amp2203/Amp2203.h @@ -44,6 +44,8 @@ class Amp2203 { void setFeedbackClosed(bool closed); /// Power-amp adaptive half-step trigger in volts (D-043; 0 = off). Not real-time. void setHalfStepTrigger(double volts); + /// Adaptive half-steps in the power amp on (the D-043 trigger) or off (D-055: off with the half-rate power amp). + void setAdaptiveHalfSteps(bool on); /// Newton residual tolerance of both roots in volts (default 1e-9, WDF_DESIGN §4.3; M6 study). Not real-time. void setNewtonResidualTolerance(double volts); diff --git a/engine/src/Amp2203.cpp b/engine/src/Amp2203.cpp index ac50d9e..47977e3 100644 --- a/engine/src/Amp2203.cpp +++ b/engine/src/Amp2203.cpp @@ -76,6 +76,8 @@ void Amp2203::setFeedbackClosed(bool closed) { power_->setFeedbackResistance(clo void Amp2203::setHalfStepTrigger(double volts) { power_->setHalfStepTrigger(volts); } +void Amp2203::setAdaptiveHalfSteps(bool on) { power_->setHalfStepTrigger(on ? PowerAmp::kDefaultHalfStepTrigger : 0.0); } + void Amp2203::setNewtonResidualTolerance(double volts) { preamp_->setNewtonResidualTolerance(volts); power_->setNewtonResidualTolerance(volts); diff --git a/harness/automation.py b/harness/automation.py index 76752c5..c537c57 100644 --- a/harness/automation.py +++ b/harness/automation.py @@ -62,6 +62,8 @@ def one(base, knobset, kind, name, x): args += ['--j4', config.J4] if config.NEWTON_TOL: args += ['--newton-tol', config.NEWTON_TOL] + if config.POWER_DIVISOR == 2: + args += ['--half-step-trigger', '0'] # no adaptive half-steps at half rate, as the plugin (D-055) args += [a for k, v in BASES[base].items() if k not in knobset for a in ('--param', f'{k}={v!r}')] for k in knobset: args += ramps(k, kind, len(x) / config.FS) diff --git a/harness/plugin_null.py b/harness/plugin_null.py index db8b88f..4ed1179 100644 --- a/harness/plugin_null.py +++ b/harness/plugin_null.py @@ -10,11 +10,12 @@ python -m harness.plugin_null [--factor 2|4|8|16] [--power-rate half|full] [--jobs N] With the half-rate power amp the processor's output carries the engine's rate-change delay -(Amp2203::latencySamples(), at the engine rate), which ampwav removes; the reference gets it back before the down -stage (the first engine output sample repeated). +(Amp2203::latencySamples()); ampwav keeps it too (`--keep-delay`). ampwav gets the plugin's exact input scale +(`--volts-per-fs`) and engine settings, so both runs see the same samples. """ import argparse import datetime +import math import os import subprocess import tempfile @@ -32,10 +33,11 @@ HOST_RATE = 48000 FACTOR = 2 POWER_RATE = 'half' -HALF_RATE_DELAY = 17 # engine samples: Amp2203::latencySamples() with exact J4 at divisor 2 RENDER = config.ROOT / 'build' / 'plugin' / 'amp2203_render_artefacts' / 'Release' / 'amp2203_render' LIMIT_DB = -60.0 -PLUGIN_VOLTS_PER_FS = 0.7746 * np.sqrt(2.0) * 10 ** (12 / 20) # Interface Level +12 dBu (PluginProcessor) +NEWTON_TOL = 1e-4 # V, the plugin's (D-055) +# Interface Level +12 dBu, computed as PluginProcessor::interfaceVoltsPerFullScale (same libm calls) +PLUGIN_VOLTS_PER_FS = 0.7746 * math.sqrt(2.0) * math.pow(10.0, 12.0 / 20.0) def signals(): @@ -67,13 +69,10 @@ def one(name, x, filt): log = _run([RENDER, 'plugin', '--in', d / 'x.wav', '--out', d / 'plug.wav', '--factor', FACTOR, '--filter', filt, '--param', f'powerAmpRate={1 if half else 0}']) _run([RENDER, 'up', '--in', d / 'x.wav', '--out', d / 'up.wav', '--factor', FACTOR, '--filter', filt]) - u, rate = read_wav(d / 'up.wav') # the plugin's exact +12 dBu scale vs ampwav's 4.361 V per full scale - write_wav(d / 'up.wav', u * PLUGIN_VOLTS_PER_FS / config.INPUT_VOLTS_PER_FS, rate) _run([config.AMPWAV, '--block', 'full', '--units', 'fs', '--in', d / 'up.wav', '--out', d / 'eng.wav', - '--rate', HOST_RATE * FACTOR, '--j4', 'exact', '--power-rate-divisor', 2 if half else 1]) - if half: - e, rate = read_wav(d / 'eng.wav') - write_wav(d / 'eng.wav', np.concatenate([np.full(HALF_RATE_DELAY, e[0]), e[:len(e) - HALF_RATE_DELAY]]), rate) + '--rate', HOST_RATE * FACTOR, '--j4', 'exact', '--power-rate-divisor', 2 if half else 1, + '--newton-tol', NEWTON_TOL, '--half-step-trigger', 0 if half else 100, # the plugin's settings (D-055) + '--volts-per-fs', repr(PLUGIN_VOLTS_PER_FS), '--keep-delay']) _run([RENDER, 'down', '--in', d / 'eng.wav', '--out', d / 'ref.wav', '--factor', FACTOR, '--filter', filt]) a, b = read_wav(d / 'plug.wav')[0], read_wav(d / 'ref.wav')[0] n = min(len(a), len(b)) diff --git a/harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md b/harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md index 03623b7..a8ca7d8 100644 --- a/harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md +++ b/harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md @@ -1,66 +1,66 @@ # M5 automation: every knob over its full range at audio rate -- Date: 2026-10-15, commit `e4521b3 (dirty)`, engine `ampwav --block full` at 96000 Hz +- Date: 2026-10-15, commit `9f01825 (dirty)`, engine `ampwav --block full` at 96000 Hz - Modulation: 10 Hz triangle 0 → 1 → 0 (`lfo`) or 0 ↔ 1 jumps every 50 ms (`jump`) on the knob targets (smoothed by the engine, 20 ms); bases: default knobs, every knob at 1.0. - SPEC §7 M5: no NaN / Inf, no Newton failures. Newton failures count first solves that did not converge in 12 iterations; the fallback (D-042) rescued or did not rescue them. -Runs: 56 · non-finite outputs: 0 · Newton failures (first solve): 0 · unrescued: 0 +Runs: 56 · non-finite outputs: 0 · Newton failures (first solve): 7652 · unrescued: 0 | Base | Knobs | Modulation | Signal | finite | peak [V] | solves | failures | rescued | unrescued | median / max it | ns/sample | |---|---|---|---|---|---|---|---|---|---|---|---| -| default | preampVolume | lfo | SIN-220Hz-0.3V | yes | 33.2 | 192033 | 0 | 0 | 0 | 2 / 20 | 5668 | -| default | preampVolume | lfo | DI-GTR L | yes | 47.2 | 960095 | 0 | 0 | 0 | 2 / 19 | 5820 | -| default | preampVolume | jump | SIN-220Hz-0.3V | yes | 38.4 | 192033 | 0 | 0 | 0 | 2 / 26 | 5536 | -| default | preampVolume | jump | DI-GTR L | yes | 45.9 | 960165 | 0 | 0 | 0 | 2 / 20 | 5817 | -| default | treble | lfo | SIN-220Hz-0.3V | yes | 40.3 | 192033 | 0 | 0 | 0 | 2 / 23 | 5736 | -| default | treble | lfo | DI-GTR L | yes | 50.7 | 960114 | 0 | 0 | 0 | 2 / 20 | 6169 | -| default | treble | jump | SIN-220Hz-0.3V | yes | 32.5 | 192034 | 0 | 0 | 0 | 2 / 23 | 5597 | -| default | treble | jump | DI-GTR L | yes | 65.6 | 960208 | 0 | 0 | 0 | 2 / 20 | 6097 | -| default | middle | lfo | SIN-220Hz-0.3V | yes | 34.1 | 192033 | 0 | 0 | 0 | 2 / 26 | 5770 | -| default | middle | lfo | DI-GTR L | yes | 45.9 | 960112 | 0 | 0 | 0 | 2 / 19 | 6149 | -| default | middle | jump | SIN-220Hz-0.3V | yes | 38.7 | 192033 | 0 | 0 | 0 | 2 / 23 | 5579 | -| default | middle | jump | DI-GTR L | yes | 52.5 | 960143 | 0 | 0 | 0 | 2 / 20 | 6100 | -| default | bass | lfo | SIN-220Hz-0.3V | yes | 37.1 | 192033 | 0 | 0 | 0 | 2 / 23 | 5655 | -| default | bass | lfo | DI-GTR L | yes | 45.5 | 960126 | 0 | 0 | 0 | 2 / 20 | 5763 | -| default | bass | jump | SIN-220Hz-0.3V | yes | 42.2 | 192033 | 0 | 0 | 0 | 2 / 23 | 5498 | -| default | bass | jump | DI-GTR L | yes | 48.3 | 960123 | 0 | 0 | 0 | 2 / 18 | 5731 | -| default | master | lfo | SIN-220Hz-0.3V | yes | 69.3 | 192040 | 0 | 0 | 0 | 2 / 23 | 6067 | -| default | master | lfo | DI-GTR L | yes | 75.1 | 966556 | 0 | 0 | 0 | 2 / 26 | 6430 | -| default | master | jump | SIN-220Hz-0.3V | yes | 75.9 | 192466 | 0 | 0 | 0 | 2 / 23 | 6023 | -| default | master | jump | DI-GTR L | yes | 81.2 | 969786 | 0 | 0 | 0 | 2 / 23 | 6508 | -| default | presence | lfo | SIN-220Hz-0.3V | yes | 34.2 | 192033 | 0 | 0 | 0 | 2 / 23 | 6457 | -| default | presence | lfo | DI-GTR L | yes | 45.6 | 960120 | 0 | 0 | 0 | 2 / 19 | 6608 | -| default | presence | jump | SIN-220Hz-0.3V | yes | 49.3 | 192034 | 0 | 0 | 0 | 2 / 23 | 6255 | -| default | presence | jump | DI-GTR L | yes | 65.6 | 960191 | 0 | 0 | 0 | 2 / 19 | 6590 | -| default | all | lfo | SIN-220Hz-0.3V | yes | 71.1 | 192398 | 0 | 0 | 0 | 2 / 12 | 10594 | -| default | all | lfo | DI-GTR L | yes | 78.7 | 970090 | 0 | 0 | 0 | 2 / 23 | 13166 | -| default | all | jump | SIN-220Hz-0.3V | yes | 77.5 | 195640 | 0 | 0 | 0 | 2 / 26 | 10531 | -| default | all | jump | DI-GTR L | yes | 82.2 | 976641 | 0 | 0 | 0 | 2 / 25 | 13172 | -| max | preampVolume | lfo | SIN-220Hz-0.3V | yes | 75.4 | 199440 | 0 | 0 | 0 | 3 / 14 | 7118 | -| max | preampVolume | lfo | DI-GTR L | yes | 94.4 | 1030617 | 0 | 0 | 0 | 3 / 19 | 7639 | -| max | preampVolume | jump | SIN-220Hz-0.3V | yes | 84.3 | 204266 | 0 | 0 | 0 | 3 / 26 | 7115 | -| max | preampVolume | jump | DI-GTR L | yes | 89.5 | 1030611 | 0 | 0 | 0 | 3 / 21 | 7537 | -| max | treble | lfo | SIN-220Hz-0.3V | yes | 79.4 | 210094 | 0 | 0 | 0 | 2 / 26 | 7584 | -| max | treble | lfo | DI-GTR L | yes | 88.0 | 1023639 | 0 | 0 | 0 | 2 / 28 | 7505 | -| max | treble | jump | SIN-220Hz-0.3V | yes | 84.6 | 209196 | 0 | 0 | 0 | 2 / 26 | 7371 | -| max | treble | jump | DI-GTR L | yes | 90.0 | 1021417 | 0 | 0 | 0 | 2 / 26 | 7431 | -| max | middle | lfo | SIN-220Hz-0.3V | yes | 85.5 | 211603 | 0 | 0 | 0 | 2 / 26 | 7621 | -| max | middle | lfo | DI-GTR L | yes | 91.7 | 1029512 | 0 | 0 | 0 | 3 / 22 | 7867 | -| max | middle | jump | SIN-220Hz-0.3V | yes | 87.2 | 212094 | 0 | 0 | 0 | 2 / 26 | 7507 | -| max | middle | jump | DI-GTR L | yes | 90.2 | 1030194 | 0 | 0 | 0 | 3 / 20 | 7830 | -| max | bass | lfo | SIN-220Hz-0.3V | yes | 79.9 | 216154 | 0 | 0 | 0 | 2 / 26 | 7975 | -| max | bass | lfo | DI-GTR L | yes | 92.0 | 1040450 | 0 | 0 | 0 | 3 / 22 | 7684 | -| max | bass | jump | SIN-220Hz-0.3V | yes | 80.1 | 214554 | 0 | 0 | 0 | 2 / 26 | 7663 | -| max | bass | jump | DI-GTR L | yes | 91.0 | 1036902 | 0 | 0 | 0 | 3 / 22 | 7622 | -| max | master | lfo | SIN-220Hz-0.3V | yes | 73.3 | 199385 | 0 | 0 | 0 | 2 / 26 | 6884 | -| max | master | lfo | DI-GTR L | yes | 81.6 | 984999 | 0 | 0 | 0 | 2 / 28 | 7004 | -| max | master | jump | SIN-220Hz-0.3V | yes | 77.5 | 201669 | 0 | 0 | 0 | 2 / 26 | 6776 | -| max | master | jump | DI-GTR L | yes | 86.4 | 985722 | 0 | 0 | 0 | 2 / 26 | 6909 | -| max | presence | lfo | SIN-220Hz-0.3V | yes | 82.6 | 211727 | 0 | 0 | 0 | 2 / 26 | 8444 | -| max | presence | lfo | DI-GTR L | yes | 92.3 | 1022025 | 0 | 0 | 0 | 3 / 22 | 8293 | -| max | presence | jump | SIN-220Hz-0.3V | yes | 79.9 | 212150 | 0 | 0 | 0 | 2 / 26 | 8175 | -| max | presence | jump | DI-GTR L | yes | 92.3 | 1024014 | 0 | 0 | 0 | 3 / 25 | 8307 | -| max | all | lfo | SIN-220Hz-0.3V | yes | 71.1 | 192398 | 0 | 0 | 0 | 2 / 12 | 10612 | -| max | all | lfo | DI-GTR L | yes | 78.7 | 970090 | 0 | 0 | 0 | 2 / 23 | 13037 | -| max | all | jump | SIN-220Hz-0.3V | yes | 77.5 | 195640 | 0 | 0 | 0 | 2 / 26 | 10572 | -| max | all | jump | DI-GTR L | yes | 82.2 | 976641 | 0 | 0 | 0 | 2 / 25 | 13051 | +| default | preampVolume | lfo | SIN-220Hz-0.3V | yes | 33.2 | 192033 | 0 | 0 | 0 | 1 / 19 | 5048 | +| default | preampVolume | lfo | DI-GTR L | yes | 50.7 | 960033 | 0 | 0 | 0 | 1 / 17 | 5333 | +| default | preampVolume | jump | SIN-220Hz-0.3V | yes | 38.4 | 192033 | 0 | 0 | 0 | 1 / 25 | 4922 | +| default | preampVolume | jump | DI-GTR L | yes | 51.6 | 960033 | 0 | 0 | 0 | 1 / 19 | 5287 | +| default | treble | lfo | SIN-220Hz-0.3V | yes | 40.3 | 192033 | 0 | 0 | 0 | 1 / 22 | 5139 | +| default | treble | lfo | DI-GTR L | yes | 61.2 | 960033 | 0 | 0 | 0 | 1 / 19 | 5736 | +| default | treble | jump | SIN-220Hz-0.3V | yes | 34.3 | 192033 | 0 | 0 | 0 | 1 / 22 | 5021 | +| default | treble | jump | DI-GTR L | yes | 73.0 | 960033 | 0 | 0 | 0 | 1 / 19 | 5675 | +| default | middle | lfo | SIN-220Hz-0.3V | yes | 34.1 | 192033 | 0 | 0 | 0 | 1 / 25 | 5182 | +| default | middle | lfo | DI-GTR L | yes | 52.2 | 960033 | 0 | 0 | 0 | 1 / 18 | 5751 | +| default | middle | jump | SIN-220Hz-0.3V | yes | 38.7 | 192033 | 0 | 0 | 0 | 1 / 22 | 5041 | +| default | middle | jump | DI-GTR L | yes | 60.2 | 960033 | 0 | 0 | 0 | 1 / 19 | 5681 | +| default | bass | lfo | SIN-220Hz-0.3V | yes | 37.1 | 192033 | 0 | 0 | 0 | 1 / 22 | 5099 | +| default | bass | lfo | DI-GTR L | yes | 54.3 | 960033 | 0 | 0 | 0 | 1 / 19 | 5318 | +| default | bass | jump | SIN-220Hz-0.3V | yes | 42.2 | 192033 | 0 | 0 | 0 | 1 / 22 | 4886 | +| default | bass | jump | DI-GTR L | yes | 55.0 | 960033 | 0 | 0 | 0 | 1 / 17 | 5312 | +| default | master | lfo | SIN-220Hz-0.3V | yes | 69.3 | 192033 | 0 | 0 | 0 | 1 / 22 | 5434 | +| default | master | lfo | DI-GTR L | yes | 82.3 | 960033 | 0 | 0 | 0 | 1 / 24 | 5916 | +| default | master | jump | SIN-220Hz-0.3V | yes | 76.6 | 192033 | 0 | 0 | 0 | 1 / 22 | 5387 | +| default | master | jump | DI-GTR L | yes | 84.8 | 960033 | 0 | 0 | 0 | 1 / 25 | 5870 | +| default | presence | lfo | SIN-220Hz-0.3V | yes | 34.2 | 192033 | 0 | 0 | 0 | 1 / 22 | 5836 | +| default | presence | lfo | DI-GTR L | yes | 54.0 | 960033 | 0 | 0 | 0 | 1 / 18 | 6145 | +| default | presence | jump | SIN-220Hz-0.3V | yes | 53.2 | 192033 | 0 | 0 | 0 | 1 / 22 | 5656 | +| default | presence | jump | DI-GTR L | yes | 73.2 | 960033 | 0 | 0 | 0 | 1 / 18 | 6105 | +| default | all | lfo | SIN-220Hz-0.3V | yes | 71.0 | 192033 | 0 | 0 | 0 | 1 / 11 | 9975 | +| default | all | lfo | DI-GTR L | yes | 79.3 | 960033 | 0 | 0 | 0 | 1 / 25 | 12490 | +| default | all | jump | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 1 / 25 | 9774 | +| default | all | jump | DI-GTR L | yes | 87.0 | 960033 | 0 | 0 | 0 | 1 / 23 | 12559 | +| max | preampVolume | lfo | SIN-220Hz-0.3V | yes | 87.7 | 192033 | 1191 | 1191 | 0 | 5 / 32 | 10447 | +| max | preampVolume | lfo | DI-GTR L | yes | 96.3 | 960033 | 1670 | 1670 | 0 | 5 / 32 | 10396 | +| max | preampVolume | jump | SIN-220Hz-0.3V | yes | 91.5 | 192033 | 768 | 768 | 0 | 5 / 32 | 9979 | +| max | preampVolume | jump | DI-GTR L | yes | 92.6 | 960033 | 1732 | 1732 | 0 | 5 / 32 | 10305 | +| max | treble | lfo | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 1 / 25 | 6212 | +| max | treble | lfo | DI-GTR L | yes | 90.3 | 960033 | 0 | 0 | 0 | 2 / 27 | 6449 | +| max | treble | jump | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 2 / 25 | 6114 | +| max | treble | jump | DI-GTR L | yes | 91.6 | 960033 | 0 | 0 | 0 | 2 / 26 | 6354 | +| max | middle | lfo | SIN-220Hz-0.3V | yes | 80.9 | 192033 | 0 | 0 | 0 | 2 / 25 | 6105 | +| max | middle | lfo | DI-GTR L | yes | 94.9 | 960033 | 513 | 513 | 0 | 2 / 32 | 7530 | +| max | middle | jump | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 2 / 25 | 6001 | +| max | middle | jump | DI-GTR L | yes | 97.3 | 960033 | 714 | 714 | 0 | 2 / 32 | 7775 | +| max | bass | lfo | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 2 / 25 | 6193 | +| max | bass | lfo | DI-GTR L | yes | 98.3 | 960033 | 329 | 329 | 0 | 2 / 32 | 7525 | +| max | bass | jump | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 2 / 25 | 6060 | +| max | bass | jump | DI-GTR L | yes | 99.0 | 960033 | 701 | 701 | 0 | 3 / 32 | 8289 | +| max | master | lfo | SIN-220Hz-0.3V | yes | 74.4 | 192033 | 0 | 0 | 0 | 1 / 25 | 5935 | +| max | master | lfo | DI-GTR L | yes | 79.1 | 960033 | 0 | 0 | 0 | 1 / 25 | 6205 | +| max | master | jump | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 1 / 25 | 5744 | +| max | master | jump | DI-GTR L | yes | 83.9 | 960033 | 0 | 0 | 0 | 1 / 26 | 6134 | +| max | presence | lfo | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 2 / 25 | 6861 | +| max | presence | lfo | DI-GTR L | yes | 89.7 | 960033 | 14 | 14 | 0 | 2 / 32 | 7447 | +| max | presence | jump | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 2 / 25 | 6691 | +| max | presence | jump | DI-GTR L | yes | 93.9 | 960033 | 20 | 20 | 0 | 2 / 32 | 7425 | +| max | all | lfo | SIN-220Hz-0.3V | yes | 71.0 | 192033 | 0 | 0 | 0 | 1 / 11 | 9993 | +| max | all | lfo | DI-GTR L | yes | 79.3 | 960033 | 0 | 0 | 0 | 1 / 25 | 12467 | +| max | all | jump | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 1 / 25 | 9738 | +| max | all | jump | DI-GTR L | yes | 87.0 | 960033 | 0 | 0 | 0 | 1 / 23 | 12507 | diff --git a/harness/reports/M6/20261015_plugin_null_2x_full.md b/harness/reports/M6/20261015_plugin_null_2x_full.md index 6f62e35..2b1277b 100644 --- a/harness/reports/M6/20261015_plugin_null_2x_full.md +++ b/harness/reports/M6/20261015_plugin_null_2x_full.md @@ -1,6 +1,6 @@ # M5 wrapper null test: plugin vs ampwav at 96 kHz with the same resampling -- Date: 2026-10-15, commit `e4521b3 (dirty)`; host rate 48000 Hz, 2x, Power Amp Rate full, exact J4 +- Date: 2026-10-15, commit `9f01825 (dirty)`; host rate 48000 Hz, 2x, Power Amp Rate full, exact J4 - Plugin: `amp2203_render plugin` (the Amp2203Processor offline). Reference: the processor's oversampler up stage, `ampwav --block full --units fs --j4 exact` at 96000 Hz, the down stage. Default knobs and levels. - Tolerance (SPEC §7 M5): ESR ≤ -60 dB. @@ -8,55 +8,55 @@ PASS 26 · FAIL 0 | Filter | Signal | Peak out [FS] | ESR | Status | Render log | |---|---|---|---|---|---| -| fir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.061 s for 0.200 s of audio: 30.4 % of one core, real-time factor 3.29 +| fir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.043 s for 0.200 s of audio: 21.4 % of one core, real-time factor 4.67 latency 49 samples, Newton failures 0 (rescued 0), max iterations 3 | -| fir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.066 s for 0.200 s of audio: 32.9 % of one core, real-time factor 3.04 -latency 49 samples, Newton failures 0 (rescued 0), max iterations 5 | -| fir | SIN-100Hz--6dBFS | 0.825 | -138.5 dB | PASS | processBlock 0.065 s for 0.200 s of audio: 32.4 % of one core, real-time factor 3.08 -latency 49 samples, Newton failures 0 (rescued 0), max iterations 10 | -| fir | SIN-1000Hz--40dBFS | 0.341 | -138.5 dB | PASS | processBlock 0.075 s for 0.200 s of audio: 37.7 % of one core, real-time factor 2.65 -latency 49 samples, Newton failures 0 (rescued 0), max iterations 7 | -| fir | SIN-1000Hz--20dBFS | 0.708 | -138.5 dB | PASS | processBlock 0.084 s for 0.200 s of audio: 42.2 % of one core, real-time factor 2.37 -latency 49 samples, Newton failures 0 (rescued 0), max iterations 10 | -| fir | SIN-1000Hz--6dBFS | 0.704 | -138.5 dB | PASS | processBlock 0.092 s for 0.200 s of audio: 46.2 % of one core, real-time factor 2.17 -latency 49 samples, Newton failures 0 (rescued 0), max iterations 13 | -| fir | SIN-5000Hz--40dBFS | 0.677 | -138.5 dB | PASS | processBlock 0.110 s for 0.200 s of audio: 55.1 % of one core, real-time factor 1.82 +| fir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.049 s for 0.200 s of audio: 24.4 % of one core, real-time factor 4.10 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 4 | +| fir | SIN-100Hz--6dBFS | 0.825 | -138.5 dB | PASS | processBlock 0.049 s for 0.200 s of audio: 24.4 % of one core, real-time factor 4.10 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 9 | +| fir | SIN-1000Hz--40dBFS | 0.341 | -138.5 dB | PASS | processBlock 0.069 s for 0.200 s of audio: 34.7 % of one core, real-time factor 2.88 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 6 | +| fir | SIN-1000Hz--20dBFS | 0.708 | -138.5 dB | PASS | processBlock 0.073 s for 0.200 s of audio: 36.3 % of one core, real-time factor 2.76 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 9 | +| fir | SIN-1000Hz--6dBFS | 0.704 | -138.5 dB | PASS | processBlock 0.080 s for 0.200 s of audio: 40.0 % of one core, real-time factor 2.50 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 12 | +| fir | SIN-5000Hz--40dBFS | 0.677 | -138.5 dB | PASS | processBlock 0.091 s for 0.200 s of audio: 45.7 % of one core, real-time factor 2.19 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 18 | +| fir | SIN-5000Hz--20dBFS | 0.706 | -138.5 dB | PASS | processBlock 0.110 s for 0.200 s of audio: 55.1 % of one core, real-time factor 1.82 latency 49 samples, Newton failures 0 (rescued 0), max iterations 19 | -| fir | SIN-5000Hz--20dBFS | 0.706 | -138.5 dB | PASS | processBlock 0.127 s for 0.200 s of audio: 63.6 % of one core, real-time factor 1.57 +| fir | SIN-5000Hz--6dBFS | 0.738 | -138.5 dB | PASS | processBlock 0.177 s for 0.200 s of audio: 88.7 % of one core, real-time factor 1.13 +latency 49 samples, Newton failures 441 (rescued 194), max iterations 32 | +| fir | DI-GTR L | 0.767 | -138.5 dB | PASS | processBlock 1.242 s for 5.000 s of audio: 24.8 % of one core, real-time factor 4.03 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 17 | +| fir | DI-GTR LL | 0.711 | -138.5 dB | PASS | processBlock 1.201 s for 5.000 s of audio: 24.0 % of one core, real-time factor 4.16 +latency 49 samples, Newton failures 0 (rescued 0), max iterations 17 | +| fir | DI-GTR R | 0.782 | -138.5 dB | PASS | processBlock 1.210 s for 5.000 s of audio: 24.2 % of one core, real-time factor 4.13 latency 49 samples, Newton failures 0 (rescued 0), max iterations 20 | -| fir | SIN-5000Hz--6dBFS | 0.738 | -138.5 dB | PASS | processBlock 0.196 s for 0.200 s of audio: 98.0 % of one core, real-time factor 1.02 -latency 49 samples, Newton failures 443 (rescued 195), max iterations 32 | -| fir | DI-GTR L | 0.767 | -138.5 dB | PASS | processBlock 1.586 s for 5.000 s of audio: 31.7 % of one core, real-time factor 3.15 -latency 49 samples, Newton failures 0 (rescued 0), max iterations 18 | -| fir | DI-GTR LL | 0.711 | -138.5 dB | PASS | processBlock 1.568 s for 5.000 s of audio: 31.4 % of one core, real-time factor 3.19 +| fir | DI-GTR RR | 0.798 | -138.5 dB | PASS | processBlock 1.246 s for 5.000 s of audio: 24.9 % of one core, real-time factor 4.01 latency 49 samples, Newton failures 0 (rescued 0), max iterations 18 | -| fir | DI-GTR R | 0.782 | -138.5 dB | PASS | processBlock 1.569 s for 5.000 s of audio: 31.4 % of one core, real-time factor 3.19 -latency 49 samples, Newton failures 0 (rescued 0), max iterations 21 | -| fir | DI-GTR RR | 0.798 | -138.5 dB | PASS | processBlock 1.581 s for 5.000 s of audio: 31.6 % of one core, real-time factor 3.16 -latency 49 samples, Newton failures 0 (rescued 0), max iterations 19 | -| iir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.061 s for 0.200 s of audio: 30.4 % of one core, real-time factor 3.29 -latency 4 samples, Newton failures 0 (rescued 0), max iterations 3 | -| iir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.066 s for 0.200 s of audio: 32.8 % of one core, real-time factor 3.05 +| iir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.041 s for 0.200 s of audio: 20.4 % of one core, real-time factor 4.91 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 2 | +| iir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.045 s for 0.200 s of audio: 22.3 % of one core, real-time factor 4.49 latency 4 samples, Newton failures 0 (rescued 0), max iterations 6 | -| iir | SIN-100Hz--6dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.064 s for 0.200 s of audio: 32.1 % of one core, real-time factor 3.11 +| iir | SIN-100Hz--6dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.044 s for 0.200 s of audio: 22.0 % of one core, real-time factor 4.55 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 6 | +| iir | SIN-1000Hz--40dBFS | 0.362 | -138.5 dB | PASS | processBlock 0.064 s for 0.200 s of audio: 32.0 % of one core, real-time factor 3.12 latency 4 samples, Newton failures 0 (rescued 0), max iterations 7 | -| iir | SIN-1000Hz--40dBFS | 0.362 | -138.5 dB | PASS | processBlock 0.075 s for 0.200 s of audio: 37.4 % of one core, real-time factor 2.68 -latency 4 samples, Newton failures 0 (rescued 0), max iterations 8 | -| iir | SIN-1000Hz--20dBFS | 0.774 | -138.5 dB | PASS | processBlock 0.082 s for 0.200 s of audio: 40.9 % of one core, real-time factor 2.45 -latency 4 samples, Newton failures 0 (rescued 0), max iterations 11 | -| iir | SIN-1000Hz--6dBFS | 0.881 | -138.5 dB | PASS | processBlock 0.094 s for 0.200 s of audio: 46.8 % of one core, real-time factor 2.14 +| iir | SIN-1000Hz--20dBFS | 0.774 | -138.5 dB | PASS | processBlock 0.069 s for 0.200 s of audio: 34.4 % of one core, real-time factor 2.91 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 9 | +| iir | SIN-1000Hz--6dBFS | 0.881 | -138.5 dB | PASS | processBlock 0.075 s for 0.200 s of audio: 37.6 % of one core, real-time factor 2.66 latency 4 samples, Newton failures 0 (rescued 0), max iterations 22 | -| iir | SIN-5000Hz--40dBFS | 0.816 | -138.5 dB | PASS | processBlock 0.110 s for 0.200 s of audio: 55.1 % of one core, real-time factor 1.81 -latency 4 samples, Newton failures 0 (rescued 0), max iterations 17 | -| iir | SIN-5000Hz--20dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.128 s for 0.200 s of audio: 63.8 % of one core, real-time factor 1.57 +| iir | SIN-5000Hz--40dBFS | 0.816 | -138.5 dB | PASS | processBlock 0.091 s for 0.200 s of audio: 45.7 % of one core, real-time factor 2.19 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 16 | +| iir | SIN-5000Hz--20dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.110 s for 0.200 s of audio: 55.2 % of one core, real-time factor 1.81 latency 4 samples, Newton failures 0 (rescued 0), max iterations 22 | -| iir | SIN-5000Hz--6dBFS | 0.841 | -138.5 dB | PASS | processBlock 0.174 s for 0.200 s of audio: 86.9 % of one core, real-time factor 1.15 -latency 4 samples, Newton failures 305 (rescued 96), max iterations 32 | -| iir | DI-GTR L | 0.814 | -138.5 dB | PASS | processBlock 1.576 s for 5.000 s of audio: 31.5 % of one core, real-time factor 3.17 -latency 4 samples, Newton failures 0 (rescued 0), max iterations 19 | -| iir | DI-GTR LL | 0.781 | -138.5 dB | PASS | processBlock 1.563 s for 5.000 s of audio: 31.3 % of one core, real-time factor 3.20 -latency 4 samples, Newton failures 0 (rescued 0), max iterations 24 | -| iir | DI-GTR R | 0.891 | -138.5 dB | PASS | processBlock 1.553 s for 5.000 s of audio: 31.1 % of one core, real-time factor 3.22 +| iir | SIN-5000Hz--6dBFS | 0.841 | -138.5 dB | PASS | processBlock 0.145 s for 0.200 s of audio: 72.4 % of one core, real-time factor 1.38 +latency 4 samples, Newton failures 157 (rescued 80), max iterations 32 | +| iir | DI-GTR L | 0.814 | -138.5 dB | PASS | processBlock 1.229 s for 5.000 s of audio: 24.6 % of one core, real-time factor 4.07 latency 4 samples, Newton failures 0 (rescued 0), max iterations 18 | -| iir | DI-GTR RR | 0.866 | -138.5 dB | PASS | processBlock 1.566 s for 5.000 s of audio: 31.3 % of one core, real-time factor 3.19 +| iir | DI-GTR LL | 0.781 | -138.5 dB | PASS | processBlock 1.192 s for 5.000 s of audio: 23.8 % of one core, real-time factor 4.20 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 23 | +| iir | DI-GTR R | 0.891 | -138.5 dB | PASS | processBlock 1.196 s for 5.000 s of audio: 23.9 % of one core, real-time factor 4.18 +latency 4 samples, Newton failures 0 (rescued 0), max iterations 17 | +| iir | DI-GTR RR | 0.866 | -138.5 dB | PASS | processBlock 1.229 s for 5.000 s of audio: 24.6 % of one core, real-time factor 4.07 latency 4 samples, Newton failures 0 (rescued 0), max iterations 20 | diff --git a/harness/reports/M6/20261015_plugin_null_2x_half.md b/harness/reports/M6/20261015_plugin_null_2x_half.md index bf6cf96..fe31ed9 100644 --- a/harness/reports/M6/20261015_plugin_null_2x_half.md +++ b/harness/reports/M6/20261015_plugin_null_2x_half.md @@ -1,62 +1,62 @@ # M5 wrapper null test: plugin vs ampwav at 96 kHz with the same resampling -- Date: 2026-10-15, commit `e4521b3 (dirty)`; host rate 48000 Hz, 2x, Power Amp Rate half, exact J4 +- Date: 2026-10-15, commit `9f01825 (dirty)`; host rate 48000 Hz, 2x, Power Amp Rate half, exact J4 - Plugin: `amp2203_render plugin` (the Amp2203Processor offline). Reference: the processor's oversampler up stage, `ampwav --block full --units fs --j4 exact` at 96000 Hz, the down stage. Default knobs and levels. - Tolerance (SPEC §7 M5): ESR ≤ -60 dB. -PASS 25 · FAIL 1 +PASS 26 · FAIL 0 | Filter | Signal | Peak out [FS] | ESR | Status | Render log | |---|---|---|---|---|---| -| fir | SIN-100Hz--40dBFS | 0.111 | -138.3 dB | PASS | processBlock 0.041 s for 0.200 s of audio: 20.5 % of one core, real-time factor 4.88 +| fir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.029 s for 0.200 s of audio: 14.7 % of one core, real-time factor 6.79 latency 58 samples, Newton failures 0 (rescued 0), max iterations 3 | -| fir | SIN-100Hz--20dBFS | 0.719 | -138.1 dB | PASS | processBlock 0.045 s for 0.200 s of audio: 22.4 % of one core, real-time factor 4.46 -latency 58 samples, Newton failures 0 (rescued 0), max iterations 5 | -| fir | SIN-100Hz--6dBFS | 0.825 | -133.8 dB | PASS | processBlock 0.045 s for 0.200 s of audio: 22.5 % of one core, real-time factor 4.45 -latency 58 samples, Newton failures 0 (rescued 0), max iterations 10 | -| fir | SIN-1000Hz--40dBFS | 0.335 | -137.1 dB | PASS | processBlock 0.055 s for 0.200 s of audio: 27.7 % of one core, real-time factor 3.60 -latency 58 samples, Newton failures 0 (rescued 0), max iterations 7 | -| fir | SIN-1000Hz--20dBFS | 0.655 | -124.6 dB | PASS | processBlock 0.059 s for 0.200 s of audio: 29.5 % of one core, real-time factor 3.39 -latency 58 samples, Newton failures 0 (rescued 0), max iterations 10 | -| fir | SIN-1000Hz--6dBFS | 0.708 | -111.3 dB | PASS | processBlock 0.066 s for 0.200 s of audio: 33.1 % of one core, real-time factor 3.02 -latency 58 samples, Newton failures 0 (rescued 0), max iterations 13 | -| fir | SIN-5000Hz--40dBFS | 0.703 | -131.6 dB | PASS | processBlock 0.080 s for 0.200 s of audio: 40.2 % of one core, real-time factor 2.49 +| fir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.032 s for 0.200 s of audio: 16.1 % of one core, real-time factor 6.22 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 4 | +| fir | SIN-100Hz--6dBFS | 0.825 | -138.5 dB | PASS | processBlock 0.033 s for 0.200 s of audio: 16.7 % of one core, real-time factor 5.98 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 9 | +| fir | SIN-1000Hz--40dBFS | 0.335 | -138.5 dB | PASS | processBlock 0.048 s for 0.200 s of audio: 23.8 % of one core, real-time factor 4.21 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 6 | +| fir | SIN-1000Hz--20dBFS | 0.696 | -138.5 dB | PASS | processBlock 0.052 s for 0.200 s of audio: 26.2 % of one core, real-time factor 3.82 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 9 | +| fir | SIN-1000Hz--6dBFS | 0.886 | -138.5 dB | PASS | processBlock 0.054 s for 0.200 s of audio: 26.8 % of one core, real-time factor 3.73 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 12 | +| fir | SIN-5000Hz--40dBFS | 0.775 | -138.5 dB | PASS | processBlock 0.066 s for 0.200 s of audio: 33.1 % of one core, real-time factor 3.02 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 18 | +| fir | SIN-5000Hz--20dBFS | 0.866 | -138.5 dB | PASS | processBlock 0.074 s for 0.200 s of audio: 37.0 % of one core, real-time factor 2.70 latency 58 samples, Newton failures 0 (rescued 0), max iterations 19 | -| fir | SIN-5000Hz--20dBFS | 0.706 | -113.0 dB | PASS | processBlock 0.089 s for 0.200 s of audio: 44.3 % of one core, real-time factor 2.26 +| fir | SIN-5000Hz--6dBFS | 0.914 | -138.5 dB | PASS | processBlock 0.144 s for 0.200 s of audio: 72.2 % of one core, real-time factor 1.39 +latency 58 samples, Newton failures 449 (rescued 193), max iterations 32 | +| fir | DI-GTR L | 0.786 | -138.5 dB | PASS | processBlock 0.892 s for 5.000 s of audio: 17.8 % of one core, real-time factor 5.60 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 17 | +| fir | DI-GTR LL | 0.775 | -138.5 dB | PASS | processBlock 0.866 s for 5.000 s of audio: 17.3 % of one core, real-time factor 5.77 +latency 58 samples, Newton failures 0 (rescued 0), max iterations 17 | +| fir | DI-GTR R | 0.869 | -138.5 dB | PASS | processBlock 0.875 s for 5.000 s of audio: 17.5 % of one core, real-time factor 5.71 latency 58 samples, Newton failures 0 (rescued 0), max iterations 20 | -| fir | SIN-5000Hz--6dBFS | 0.744 | -99.0 dB | PASS | processBlock 0.157 s for 0.200 s of audio: 78.3 % of one core, real-time factor 1.28 -latency 58 samples, Newton failures 450 (rescued 190), max iterations 32 | -| fir | DI-GTR L | 0.741 | -138.5 dB | PASS | processBlock 1.096 s for 5.000 s of audio: 21.9 % of one core, real-time factor 4.56 -latency 58 samples, Newton failures 0 (rescued 0), max iterations 18 | -| fir | DI-GTR LL | 0.756 | -138.5 dB | PASS | processBlock 1.078 s for 5.000 s of audio: 21.6 % of one core, real-time factor 4.64 +| fir | DI-GTR RR | 0.815 | -138.5 dB | PASS | processBlock 0.887 s for 5.000 s of audio: 17.7 % of one core, real-time factor 5.64 latency 58 samples, Newton failures 0 (rescued 0), max iterations 18 | -| fir | DI-GTR R | 0.783 | -138.5 dB | PASS | processBlock 1.083 s for 5.000 s of audio: 21.7 % of one core, real-time factor 4.62 -latency 58 samples, Newton failures 0 (rescued 0), max iterations 21 | -| fir | DI-GTR RR | 0.758 | -138.5 dB | PASS | processBlock 1.097 s for 5.000 s of audio: 21.9 % of one core, real-time factor 4.56 -latency 58 samples, Newton failures 0 (rescued 0), max iterations 19 | -| iir | SIN-100Hz--40dBFS | 0.111 | -103.8 dB | PASS | processBlock 0.041 s for 0.200 s of audio: 20.5 % of one core, real-time factor 4.88 -latency 13 samples, Newton failures 0 (rescued 0), max iterations 3 | -| iir | SIN-100Hz--20dBFS | 0.719 | -87.2 dB | PASS | processBlock 0.045 s for 0.200 s of audio: 22.4 % of one core, real-time factor 4.47 +| iir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.028 s for 0.200 s of audio: 14.1 % of one core, real-time factor 7.11 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 2 | +| iir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.031 s for 0.200 s of audio: 15.6 % of one core, real-time factor 6.42 latency 13 samples, Newton failures 0 (rescued 0), max iterations 6 | -| iir | SIN-100Hz--6dBFS | 0.826 | -68.0 dB | PASS | processBlock 0.044 s for 0.200 s of audio: 22.2 % of one core, real-time factor 4.51 +| iir | SIN-100Hz--6dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.032 s for 0.200 s of audio: 15.8 % of one core, real-time factor 6.31 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 6 | +| iir | SIN-1000Hz--40dBFS | 0.358 | -138.5 dB | PASS | processBlock 0.046 s for 0.200 s of audio: 23.1 % of one core, real-time factor 4.33 latency 13 samples, Newton failures 0 (rescued 0), max iterations 7 | -| iir | SIN-1000Hz--40dBFS | 0.358 | -81.4 dB | PASS | processBlock 0.055 s for 0.200 s of audio: 27.5 % of one core, real-time factor 3.64 -latency 13 samples, Newton failures 0 (rescued 0), max iterations 8 | -| iir | SIN-1000Hz--20dBFS | 0.729 | -62.5 dB | PASS | processBlock 0.057 s for 0.200 s of audio: 28.6 % of one core, real-time factor 3.50 -latency 13 samples, Newton failures 0 (rescued 0), max iterations 11 | -| iir | SIN-1000Hz--6dBFS | 0.766 | -60.3 dB | PASS | processBlock 0.066 s for 0.200 s of audio: 32.9 % of one core, real-time factor 3.04 +| iir | SIN-1000Hz--20dBFS | 0.774 | -138.5 dB | PASS | processBlock 0.049 s for 0.200 s of audio: 24.3 % of one core, real-time factor 4.11 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 9 | +| iir | SIN-1000Hz--6dBFS | 0.854 | -138.5 dB | PASS | processBlock 0.051 s for 0.200 s of audio: 25.4 % of one core, real-time factor 3.93 latency 13 samples, Newton failures 0 (rescued 0), max iterations 22 | -| iir | SIN-5000Hz--40dBFS | 0.682 | -68.4 dB | PASS | processBlock 0.080 s for 0.200 s of audio: 40.1 % of one core, real-time factor 2.49 -latency 13 samples, Newton failures 0 (rescued 0), max iterations 17 | -| iir | SIN-5000Hz--20dBFS | 0.788 | -62.6 dB | PASS | processBlock 0.088 s for 0.200 s of audio: 43.8 % of one core, real-time factor 2.28 +| iir | SIN-5000Hz--40dBFS | 0.790 | -138.5 dB | PASS | processBlock 0.064 s for 0.200 s of audio: 31.8 % of one core, real-time factor 3.14 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 16 | +| iir | SIN-5000Hz--20dBFS | 0.835 | -138.5 dB | PASS | processBlock 0.071 s for 0.200 s of audio: 35.4 % of one core, real-time factor 2.82 latency 13 samples, Newton failures 0 (rescued 0), max iterations 22 | -| iir | SIN-5000Hz--6dBFS | 0.803 | -53.5 dB | FAIL | processBlock 0.122 s for 0.200 s of audio: 60.8 % of one core, real-time factor 1.64 -latency 13 samples, Newton failures 176 (rescued 98), max iterations 32 | -| iir | DI-GTR L | 0.796 | -138.5 dB | PASS | processBlock 1.091 s for 5.000 s of audio: 21.8 % of one core, real-time factor 4.58 -latency 13 samples, Newton failures 0 (rescued 0), max iterations 19 | -| iir | DI-GTR LL | 0.750 | -138.5 dB | PASS | processBlock 1.072 s for 5.000 s of audio: 21.4 % of one core, real-time factor 4.67 -latency 13 samples, Newton failures 0 (rescued 0), max iterations 24 | -| iir | DI-GTR R | 0.837 | -138.5 dB | PASS | processBlock 1.072 s for 5.000 s of audio: 21.4 % of one core, real-time factor 4.67 +| iir | SIN-5000Hz--6dBFS | 0.843 | -138.5 dB | PASS | processBlock 0.101 s for 0.200 s of audio: 50.4 % of one core, real-time factor 1.99 +latency 13 samples, Newton failures 161 (rescued 84), max iterations 32 | +| iir | DI-GTR L | 0.836 | -138.5 dB | PASS | processBlock 0.899 s for 5.000 s of audio: 18.0 % of one core, real-time factor 5.56 latency 13 samples, Newton failures 0 (rescued 0), max iterations 18 | -| iir | DI-GTR RR | 0.843 | -138.5 dB | PASS | processBlock 1.084 s for 5.000 s of audio: 21.7 % of one core, real-time factor 4.61 +| iir | DI-GTR LL | 0.806 | -138.5 dB | PASS | processBlock 0.870 s for 5.000 s of audio: 17.4 % of one core, real-time factor 5.75 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 23 | +| iir | DI-GTR R | 0.887 | -138.5 dB | PASS | processBlock 0.860 s for 5.000 s of audio: 17.2 % of one core, real-time factor 5.81 +latency 13 samples, Newton failures 0 (rescued 0), max iterations 17 | +| iir | DI-GTR RR | 0.848 | -138.5 dB | PASS | processBlock 0.881 s for 5.000 s of audio: 17.6 % of one core, real-time factor 5.68 latency 13 samples, Newton failures 0 (rescued 0), max iterations 20 | diff --git a/plugin/src/PluginProcessor.cpp b/plugin/src/PluginProcessor.cpp index 2c9e8d3..0410885 100644 --- a/plugin/src/PluginProcessor.cpp +++ b/plugin/src/PluginProcessor.cpp @@ -13,6 +13,8 @@ constexpr const char* kKnobNames[] = {"Preamp Volume", "Treble", "Middle", "Bass juce::String tenths(float v, int) { return juce::String(v, 1); } +constexpr double kNewtonTolerance = 1e-4; // V, D-055 (engine default 1e-9 for verification) + } // namespace int Amp2203Processor::factorFor(int choice) noexcept { @@ -79,6 +81,10 @@ void Amp2203Processor::rebuild(double hostRate, int maxBlock) { cfg.powerAmpRateDivisor = activePowerRate_ == 1 ? 2 : 1; cfg.exactJ4 = true; // preamp → power amp junction solved exactly (M6 X1, D-053, D-054) engine_.prepare(cfg); + // D-055: Newton residual tolerance 1e-4 V; no adaptive half-steps when the power amp runs at half rate (the + // step decision there flips on last-bit differences, M6); the D-043 trigger at full rate + engine_.setNewtonResidualTolerance(kNewtonTolerance); + engine_.setAdaptiveHalfSteps(cfg.powerAmpRateDivisor == 1); pushParams(); engine_.reset(); // DC operating point at the current knobs (SPEC §3.6) trim_.reset(hostRate, 0.02); diff --git a/tools/ampwav/src/Cli.cpp b/tools/ampwav/src/Cli.cpp index d0badf9..66cac6b 100644 --- a/tools/ampwav/src/Cli.cpp +++ b/tools/ampwav/src/Cli.cpp @@ -92,6 +92,9 @@ void printUsage(std::ostream& os) { " Params are then set every sample (knob-jump test, VERIFICATION §7)\n" " --half-step-trigger power amp (b56, full): compute a sample as two half-steps when a root\n" " Thevenin voltage moves more than this many volts (D-043; 0 = off)\n" + " --keep-delay keep the block's output delay (full amp at power-rate divisor 2) instead of removing it\n" + " --volts-per-fs full amp, fs units: input volts per full-scale sample (default 4.361, D-010); the\n" + " samples reach the engine unscaled, as in the plugin\n" " --nfb full amp NFB diagnostic (SPEC §7 M4): closed (default) or open (R22 -> 1 TOhm)\n" " --power-rate-divisor full amp: 2 runs the power amp at half the rate behind halfband filters (M6);\n" " the filters' delay is removed from the output and the dumps\n" @@ -179,6 +182,18 @@ std::optional parseArgs(const std::vector& args, std::stri return std::nullopt; } o.halfStepTrigger = x; + } else if (a == "--keep-delay") { + o.keepDelay = true; + } else if (a == "--volts-per-fs") { + std::string v; + if (!value(v)) return std::nullopt; + char* end = nullptr; + const double x = std::strtod(v.c_str(), &end); + if (end == v.c_str() || *end != '\0' || !(x > 0.0)) { + error = "--volts-per-fs expects volts > 0, got '" + v + "'"; + return std::nullopt; + } + o.voltsPerFs = x; } else if (a == "--j4") { std::string v; if (!value(v)) return std::nullopt; @@ -312,6 +327,10 @@ int run(const Options& o, std::ostream& log) { } runner->prepare(rate, o.blockSize); runner->setParams(o.params); + if (o.voltsPerFs && !runner->setInputVoltsPerFullScale(*o.voltsPerFs)) { + log << "ampwav: block '" << o.block << "' has no input scale option\n"; + return 1; + } if (o.halfStepTrigger && !runner->setHalfStepTrigger(*o.halfStepTrigger)) { log << "ampwav: block '" << o.block << "' has no half-step option\n"; return 1; @@ -360,14 +379,18 @@ int run(const Options& o, std::ostream& log) { supply.push_back(std::move(w.samples)); } - std::vector y = in.samples; - if (o.units == Units::FullScale) - for (double& s : y) - s *= runner->inputVoltsPerFullScale(); + std::vector y = in.samples; // full-scale samples stay unscaled: processFullScale (bit-exact as the plugin) + const bool fullScale = o.units == Units::FullScale; + auto process = [&](double* v, int n) { + if (fullScale) + runner->processFullScale(v, n); + else + runner->processVolts(v, n); + }; // a block with an output delay (full amp at power-rate divisor 2) runs `latency` samples longer on the // last input value (supplies likewise); the first `latency` output and dump samples are then dropped - const size_t latency = static_cast(runner->latency()); + const size_t latency = o.keepDelay ? 0 : static_cast(runner->latency()); const size_t outLength = y.size(); if (!y.empty()) y.resize(outLength + latency, y.back()); @@ -406,18 +429,18 @@ int run(const Options& o, std::ostream& log) { runner->setParams(paramsAt(n)); for (size_t k = 0; k < supply.size(); ++k) runner->setSupply(o.supplies[k].node, supply[k][n]); - runner->processVolts(y.data() + n, 1); + process(y.data() + n, 1); for (size_t k = 0; k < dumps.size(); ++k) dumps[k][n] = *dumpSrc[k]; } } else if (dumps.empty()) { for (size_t pos = 0; pos < total; pos += static_cast(o.blockSize)) { const auto n = static_cast(std::min(static_cast(o.blockSize), total - pos)); - runner->processVolts(y.data() + pos, n); + process(y.data() + pos, n); } } else { for (size_t n = 0; n < total; ++n) { - runner->processVolts(y.data() + n, 1); + process(y.data() + n, 1); for (size_t k = 0; k < dumps.size(); ++k) dumps[k][n] = *dumpSrc[k]; } diff --git a/tools/ampwav/src/Cli.h b/tools/ampwav/src/Cli.h index c1ac014..976de22 100644 --- a/tools/ampwav/src/Cli.h +++ b/tools/ampwav/src/Cli.h @@ -39,6 +39,8 @@ struct Options { std::vector supplies; // --supply node=file.wav: recorded supply voltages, set every sample std::string statsPath; // optional JSON with timing and Newton statistics std::optional halfStepTrigger; // --half-step-trigger V (power amp, D-043) + bool keepDelay = false; // --keep-delay: keep the block's output delay (plugin null test) + std::optional voltsPerFs; // --volts-per-fs V (full amp, fs units: the plugin's input scale) std::string nfb; // --nfb closed | open (full amp only; empty = closed) int powerRateDivisor = 1; // --power-rate-divisor 1 | 2 (full amp, M6) std::optional newtonTol; // --newton-tol V (full amp, M6 study) diff --git a/tools/ampwav/src/Runner.cpp b/tools/ampwav/src/Runner.cpp index 9f0675c..3dd4983 100644 --- a/tools/ampwav/src/Runner.cpp +++ b/tools/ampwav/src/Runner.cpp @@ -47,16 +47,24 @@ class FullRunner final : public Runner { int latency() const override { return amp_.latencySamples(); } void processVolts(double* v, int numSamples) noexcept override { + for (int n = 0; n < numSamples; ++n) + v[n] /= cfg_.inputVoltsPerFullScale; + processFullScale(v, numSamples); + } + void processFullScale(double* v, int numSamples) noexcept override { if (numSamples > 0 && !started_) { amp_.reset(); // DC operating point at the knobs given (SPICE starts there too) started_ = true; } - for (int n = 0; n < numSamples; ++n) - v[n] /= cfg_.inputVoltsPerFullScale; amp_.process(v, numSamples); for (int n = 0; n < numSamples; ++n) v[n] *= cfg_.outputVoltsPerFullScale; } + bool setInputVoltsPerFullScale(double v) override { + cfg_.inputVoltsPerFullScale = v; + amp_.setInputVoltsPerFullScale(v); + return true; + } std::span probes() const noexcept override { return amp_.probes(); } amp2203::Amp2203::Stats stats() const override { return amp_.getStats(); } diff --git a/tools/ampwav/src/Runner.h b/tools/ampwav/src/Runner.h index 5362f44..7c708cb 100644 --- a/tools/ampwav/src/Runner.h +++ b/tools/ampwav/src/Runner.h @@ -36,6 +36,15 @@ class Runner { virtual int latency() const { return 0; } /// Volts per full-scale sample at the input and output (only meaningful for the full amp). virtual double inputVoltsPerFullScale() const { return 1.0; } + /// Input volts per full-scale sample (full amp, `--volts-per-fs`). False if the block has none. + virtual bool setInputVoltsPerFullScale(double) { return false; } + /// Full-scale input samples in, volts out. The full amp passes them to the engine unchanged (bit-exact as the + /// plugin); other blocks scale to volts first. + virtual void processFullScale(double* v, int numSamples) noexcept { + for (int n = 0; n < numSamples; ++n) + v[n] *= inputVoltsPerFullScale(); + processVolts(v, numSamples); + } virtual double outputVoltsPerFullScale() const { return 1.0; } }; From e274e3d8e0f248f783828ee8a258c0f59cf431ff Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 13:06:41 +0300 Subject: [PATCH 16/18] m6: plugin half-steps back on at half rate (D-056); null test and automation rerun --- docs/DECISIONS.md | 13 ++ docs/PROGRESS.md | 2 + harness/automation.py | 2 - harness/plugin_null.py | 2 +- ...261015_automation_fs96000_pdiv2_j4exact.md | 116 +++++++++--------- .../M6/20261015_plugin_null_2x_full.md | 54 ++++---- .../M6/20261015_plugin_null_2x_half.md | 58 ++++----- plugin/src/PluginProcessor.cpp | 5 +- 8 files changed, 132 insertions(+), 120 deletions(-) diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 7136e50..fa21411 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -960,3 +960,16 @@ every knob 1.0, 5 s DI, half 2x): the power amp's first solves fail 656 times wi rescued, 5371 vs 3501 ns per engine sample (tolerance 1e-4); automation at every knob 1.0: up to 1732 rescued failures per run and up to 10.4 µs per engine sample (was 0 and ≈ 6 µs). Open: owner review of the half-step choice at extreme settings. + +## D-056 Plugin: adaptive half-steps back on with the power amp at half rate +Date: 2026-10-15 · Status: accepted (owner: "turn half-steps back on at half rate"; revises the half-step part of D-055) +Context: after D-055, every knob at 1.0 showed that without half-steps the half-rate power amp's first solves fail +(656 per 5 s, all rescued) and the sample costs more (5371 vs 3501 ns, engine). +Decision: the plugin keeps the D-043 half-steps at both power-amp rates (`setAdaptiveHalfSteps(true)`); the Newton +tolerance stays 1e-4 V (D-055). +Consequences: plugin CPU at 2x Half, 60 s DI GTR L, FIR: default 20.6 %, HOT 32.9 % (28.5 % without half-steps), every +knob 10: 35.6 %, 0 Newton failures (48.8 %, 5777 rescued without). Null test 26 / 26 at Half and Full (worst +−138.5 dB). Automation (96 kHz, Half, exact J4, 1e-4 V, one run at a time): 56 runs, 0 non-finite, 0 failures; ampwav +cost 4.9 / 6.3 / 9.6 / 13.0 µs per engine sample (min / median / 90 % / max, all six knobs jumping 0 ↔ 1 every 50 ms), +which includes ampwav setting every knob each sample (the plugin sets them once per block); the plugin's cost under +automation is not measured yet. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 303dd03..91d9963 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -742,6 +742,8 @@ Notes: Full (ampwav `--volts-per-fs`, `--keep-delay`). Found after the decision: at every knob 1.0 the half-rate power amp without half-steps has 656 rescued first-solve failures per 5 s and costs 5371 vs 3501 ns per engine sample; automation at every knob 1.0 up to 10.4 µs. Raised with the owner. +- Owner 2026-10-15: half-steps back on at half rate (D-056). Plugin CPU 2x Half FIR: default 20.6 %, HOT 32.9 %, every + knob 10 35.6 % (0 failures). Null test 26 / 26; automation 0 failures. Open: plugin CPU under knob automation. ## Questions for the owner _(Claude Code: add questions here and also ask them in the session.)_ diff --git a/harness/automation.py b/harness/automation.py index c537c57..76752c5 100644 --- a/harness/automation.py +++ b/harness/automation.py @@ -62,8 +62,6 @@ def one(base, knobset, kind, name, x): args += ['--j4', config.J4] if config.NEWTON_TOL: args += ['--newton-tol', config.NEWTON_TOL] - if config.POWER_DIVISOR == 2: - args += ['--half-step-trigger', '0'] # no adaptive half-steps at half rate, as the plugin (D-055) args += [a for k, v in BASES[base].items() if k not in knobset for a in ('--param', f'{k}={v!r}')] for k in knobset: args += ramps(k, kind, len(x) / config.FS) diff --git a/harness/plugin_null.py b/harness/plugin_null.py index 4ed1179..be418b1 100644 --- a/harness/plugin_null.py +++ b/harness/plugin_null.py @@ -71,7 +71,7 @@ def one(name, x, filt): _run([RENDER, 'up', '--in', d / 'x.wav', '--out', d / 'up.wav', '--factor', FACTOR, '--filter', filt]) _run([config.AMPWAV, '--block', 'full', '--units', 'fs', '--in', d / 'up.wav', '--out', d / 'eng.wav', '--rate', HOST_RATE * FACTOR, '--j4', 'exact', '--power-rate-divisor', 2 if half else 1, - '--newton-tol', NEWTON_TOL, '--half-step-trigger', 0 if half else 100, # the plugin's settings (D-055) + '--newton-tol', NEWTON_TOL, '--half-step-trigger', 100, # the plugin's settings (D-055, D-056) '--volts-per-fs', repr(PLUGIN_VOLTS_PER_FS), '--keep-delay']) _run([RENDER, 'down', '--in', d / 'eng.wav', '--out', d / 'ref.wav', '--factor', FACTOR, '--filter', filt]) a, b = read_wav(d / 'plug.wav')[0], read_wav(d / 'ref.wav')[0] diff --git a/harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md b/harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md index a8ca7d8..b3cdd1d 100644 --- a/harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md +++ b/harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md @@ -1,66 +1,66 @@ # M5 automation: every knob over its full range at audio rate -- Date: 2026-10-15, commit `9f01825 (dirty)`, engine `ampwav --block full` at 96000 Hz +- Date: 2026-10-15, commit `9778d0d (dirty)`, engine `ampwav --block full` at 96000 Hz - Modulation: 10 Hz triangle 0 → 1 → 0 (`lfo`) or 0 ↔ 1 jumps every 50 ms (`jump`) on the knob targets (smoothed by the engine, 20 ms); bases: default knobs, every knob at 1.0. - SPEC §7 M5: no NaN / Inf, no Newton failures. Newton failures count first solves that did not converge in 12 iterations; the fallback (D-042) rescued or did not rescue them. -Runs: 56 · non-finite outputs: 0 · Newton failures (first solve): 7652 · unrescued: 0 +Runs: 56 · non-finite outputs: 0 · Newton failures (first solve): 0 · unrescued: 0 | Base | Knobs | Modulation | Signal | finite | peak [V] | solves | failures | rescued | unrescued | median / max it | ns/sample | |---|---|---|---|---|---|---|---|---|---|---|---| -| default | preampVolume | lfo | SIN-220Hz-0.3V | yes | 33.2 | 192033 | 0 | 0 | 0 | 1 / 19 | 5048 | -| default | preampVolume | lfo | DI-GTR L | yes | 50.7 | 960033 | 0 | 0 | 0 | 1 / 17 | 5333 | -| default | preampVolume | jump | SIN-220Hz-0.3V | yes | 38.4 | 192033 | 0 | 0 | 0 | 1 / 25 | 4922 | -| default | preampVolume | jump | DI-GTR L | yes | 51.6 | 960033 | 0 | 0 | 0 | 1 / 19 | 5287 | -| default | treble | lfo | SIN-220Hz-0.3V | yes | 40.3 | 192033 | 0 | 0 | 0 | 1 / 22 | 5139 | -| default | treble | lfo | DI-GTR L | yes | 61.2 | 960033 | 0 | 0 | 0 | 1 / 19 | 5736 | -| default | treble | jump | SIN-220Hz-0.3V | yes | 34.3 | 192033 | 0 | 0 | 0 | 1 / 22 | 5021 | -| default | treble | jump | DI-GTR L | yes | 73.0 | 960033 | 0 | 0 | 0 | 1 / 19 | 5675 | -| default | middle | lfo | SIN-220Hz-0.3V | yes | 34.1 | 192033 | 0 | 0 | 0 | 1 / 25 | 5182 | -| default | middle | lfo | DI-GTR L | yes | 52.2 | 960033 | 0 | 0 | 0 | 1 / 18 | 5751 | -| default | middle | jump | SIN-220Hz-0.3V | yes | 38.7 | 192033 | 0 | 0 | 0 | 1 / 22 | 5041 | -| default | middle | jump | DI-GTR L | yes | 60.2 | 960033 | 0 | 0 | 0 | 1 / 19 | 5681 | -| default | bass | lfo | SIN-220Hz-0.3V | yes | 37.1 | 192033 | 0 | 0 | 0 | 1 / 22 | 5099 | -| default | bass | lfo | DI-GTR L | yes | 54.3 | 960033 | 0 | 0 | 0 | 1 / 19 | 5318 | -| default | bass | jump | SIN-220Hz-0.3V | yes | 42.2 | 192033 | 0 | 0 | 0 | 1 / 22 | 4886 | -| default | bass | jump | DI-GTR L | yes | 55.0 | 960033 | 0 | 0 | 0 | 1 / 17 | 5312 | -| default | master | lfo | SIN-220Hz-0.3V | yes | 69.3 | 192033 | 0 | 0 | 0 | 1 / 22 | 5434 | -| default | master | lfo | DI-GTR L | yes | 82.3 | 960033 | 0 | 0 | 0 | 1 / 24 | 5916 | -| default | master | jump | SIN-220Hz-0.3V | yes | 76.6 | 192033 | 0 | 0 | 0 | 1 / 22 | 5387 | -| default | master | jump | DI-GTR L | yes | 84.8 | 960033 | 0 | 0 | 0 | 1 / 25 | 5870 | -| default | presence | lfo | SIN-220Hz-0.3V | yes | 34.2 | 192033 | 0 | 0 | 0 | 1 / 22 | 5836 | -| default | presence | lfo | DI-GTR L | yes | 54.0 | 960033 | 0 | 0 | 0 | 1 / 18 | 6145 | -| default | presence | jump | SIN-220Hz-0.3V | yes | 53.2 | 192033 | 0 | 0 | 0 | 1 / 22 | 5656 | -| default | presence | jump | DI-GTR L | yes | 73.2 | 960033 | 0 | 0 | 0 | 1 / 18 | 6105 | -| default | all | lfo | SIN-220Hz-0.3V | yes | 71.0 | 192033 | 0 | 0 | 0 | 1 / 11 | 9975 | -| default | all | lfo | DI-GTR L | yes | 79.3 | 960033 | 0 | 0 | 0 | 1 / 25 | 12490 | -| default | all | jump | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 1 / 25 | 9774 | -| default | all | jump | DI-GTR L | yes | 87.0 | 960033 | 0 | 0 | 0 | 1 / 23 | 12559 | -| max | preampVolume | lfo | SIN-220Hz-0.3V | yes | 87.7 | 192033 | 1191 | 1191 | 0 | 5 / 32 | 10447 | -| max | preampVolume | lfo | DI-GTR L | yes | 96.3 | 960033 | 1670 | 1670 | 0 | 5 / 32 | 10396 | -| max | preampVolume | jump | SIN-220Hz-0.3V | yes | 91.5 | 192033 | 768 | 768 | 0 | 5 / 32 | 9979 | -| max | preampVolume | jump | DI-GTR L | yes | 92.6 | 960033 | 1732 | 1732 | 0 | 5 / 32 | 10305 | -| max | treble | lfo | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 1 / 25 | 6212 | -| max | treble | lfo | DI-GTR L | yes | 90.3 | 960033 | 0 | 0 | 0 | 2 / 27 | 6449 | -| max | treble | jump | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 2 / 25 | 6114 | -| max | treble | jump | DI-GTR L | yes | 91.6 | 960033 | 0 | 0 | 0 | 2 / 26 | 6354 | -| max | middle | lfo | SIN-220Hz-0.3V | yes | 80.9 | 192033 | 0 | 0 | 0 | 2 / 25 | 6105 | -| max | middle | lfo | DI-GTR L | yes | 94.9 | 960033 | 513 | 513 | 0 | 2 / 32 | 7530 | -| max | middle | jump | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 2 / 25 | 6001 | -| max | middle | jump | DI-GTR L | yes | 97.3 | 960033 | 714 | 714 | 0 | 2 / 32 | 7775 | -| max | bass | lfo | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 2 / 25 | 6193 | -| max | bass | lfo | DI-GTR L | yes | 98.3 | 960033 | 329 | 329 | 0 | 2 / 32 | 7525 | -| max | bass | jump | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 2 / 25 | 6060 | -| max | bass | jump | DI-GTR L | yes | 99.0 | 960033 | 701 | 701 | 0 | 3 / 32 | 8289 | -| max | master | lfo | SIN-220Hz-0.3V | yes | 74.4 | 192033 | 0 | 0 | 0 | 1 / 25 | 5935 | -| max | master | lfo | DI-GTR L | yes | 79.1 | 960033 | 0 | 0 | 0 | 1 / 25 | 6205 | -| max | master | jump | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 1 / 25 | 5744 | -| max | master | jump | DI-GTR L | yes | 83.9 | 960033 | 0 | 0 | 0 | 1 / 26 | 6134 | -| max | presence | lfo | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 2 / 25 | 6861 | -| max | presence | lfo | DI-GTR L | yes | 89.7 | 960033 | 14 | 14 | 0 | 2 / 32 | 7447 | -| max | presence | jump | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 2 / 25 | 6691 | -| max | presence | jump | DI-GTR L | yes | 93.9 | 960033 | 20 | 20 | 0 | 2 / 32 | 7425 | -| max | all | lfo | SIN-220Hz-0.3V | yes | 71.0 | 192033 | 0 | 0 | 0 | 1 / 11 | 9993 | -| max | all | lfo | DI-GTR L | yes | 79.3 | 960033 | 0 | 0 | 0 | 1 / 25 | 12467 | -| max | all | jump | SIN-220Hz-0.3V | yes | 80.3 | 192033 | 0 | 0 | 0 | 1 / 25 | 9738 | -| max | all | jump | DI-GTR L | yes | 87.0 | 960033 | 0 | 0 | 0 | 1 / 23 | 12507 | +| default | preampVolume | lfo | SIN-220Hz-0.3V | yes | 33.2 | 192033 | 0 | 0 | 0 | 1 / 19 | 4949 | +| default | preampVolume | lfo | DI-GTR L | yes | 47.2 | 960095 | 0 | 0 | 0 | 1 / 17 | 5208 | +| default | preampVolume | jump | SIN-220Hz-0.3V | yes | 38.4 | 192033 | 0 | 0 | 0 | 1 / 25 | 4931 | +| default | preampVolume | jump | DI-GTR L | yes | 45.9 | 960165 | 0 | 0 | 0 | 1 / 19 | 5209 | +| default | treble | lfo | SIN-220Hz-0.3V | yes | 40.3 | 192033 | 0 | 0 | 0 | 1 / 22 | 5034 | +| default | treble | lfo | DI-GTR L | yes | 50.7 | 960114 | 0 | 0 | 0 | 1 / 19 | 5662 | +| default | treble | jump | SIN-220Hz-0.3V | yes | 32.5 | 192034 | 0 | 0 | 0 | 1 / 22 | 4916 | +| default | treble | jump | DI-GTR L | yes | 65.6 | 960208 | 0 | 0 | 0 | 1 / 19 | 5538 | +| default | middle | lfo | SIN-220Hz-0.3V | yes | 34.1 | 192033 | 0 | 0 | 0 | 1 / 25 | 5234 | +| default | middle | lfo | DI-GTR L | yes | 45.9 | 960112 | 0 | 0 | 0 | 1 / 18 | 5641 | +| default | middle | jump | SIN-220Hz-0.3V | yes | 38.7 | 192033 | 0 | 0 | 0 | 1 / 22 | 4961 | +| default | middle | jump | DI-GTR L | yes | 52.5 | 960143 | 0 | 0 | 0 | 1 / 19 | 5613 | +| default | bass | lfo | SIN-220Hz-0.3V | yes | 37.1 | 192033 | 0 | 0 | 0 | 1 / 22 | 5019 | +| default | bass | lfo | DI-GTR L | yes | 45.5 | 960126 | 0 | 0 | 0 | 1 / 19 | 5335 | +| default | bass | jump | SIN-220Hz-0.3V | yes | 42.2 | 192033 | 0 | 0 | 0 | 1 / 22 | 4866 | +| default | bass | jump | DI-GTR L | yes | 48.3 | 960123 | 0 | 0 | 0 | 1 / 17 | 5219 | +| default | master | lfo | SIN-220Hz-0.3V | yes | 69.3 | 192040 | 0 | 0 | 0 | 1 / 22 | 5343 | +| default | master | lfo | DI-GTR L | yes | 75.1 | 966556 | 0 | 0 | 0 | 1 / 25 | 5877 | +| default | master | jump | SIN-220Hz-0.3V | yes | 75.9 | 192466 | 0 | 0 | 0 | 1 / 22 | 5298 | +| default | master | jump | DI-GTR L | yes | 81.2 | 969786 | 0 | 0 | 0 | 1 / 22 | 5834 | +| default | presence | lfo | SIN-220Hz-0.3V | yes | 34.2 | 192033 | 0 | 0 | 0 | 1 / 22 | 5685 | +| default | presence | lfo | DI-GTR L | yes | 45.6 | 960120 | 0 | 0 | 0 | 1 / 18 | 5973 | +| default | presence | jump | SIN-220Hz-0.3V | yes | 49.3 | 192034 | 0 | 0 | 0 | 1 / 22 | 5520 | +| default | presence | jump | DI-GTR L | yes | 65.6 | 960191 | 0 | 0 | 0 | 1 / 18 | 5924 | +| default | all | lfo | SIN-220Hz-0.3V | yes | 71.1 | 192398 | 0 | 0 | 0 | 1 / 11 | 9666 | +| default | all | lfo | DI-GTR L | yes | 78.7 | 970077 | 0 | 0 | 0 | 1 / 23 | 12244 | +| default | all | jump | SIN-220Hz-0.3V | yes | 77.5 | 195640 | 0 | 0 | 0 | 1 / 25 | 9566 | +| default | all | jump | DI-GTR L | yes | 82.2 | 976641 | 0 | 0 | 0 | 1 / 24 | 13015 | +| max | preampVolume | lfo | SIN-220Hz-0.3V | yes | 74.4 | 200376 | 0 | 0 | 0 | 2 / 13 | 6401 | +| max | preampVolume | lfo | DI-GTR L | yes | 92.5 | 1031436 | 0 | 0 | 0 | 2 / 22 | 6701 | +| max | preampVolume | jump | SIN-220Hz-0.3V | yes | 84.1 | 204462 | 0 | 0 | 0 | 2 / 25 | 6299 | +| max | preampVolume | jump | DI-GTR L | yes | 96.3 | 1030859 | 0 | 0 | 0 | 2 / 19 | 6615 | +| max | treble | lfo | SIN-220Hz-0.3V | yes | 76.6 | 211133 | 0 | 0 | 0 | 1 / 25 | 6722 | +| max | treble | lfo | DI-GTR L | yes | 88.7 | 1024109 | 0 | 0 | 0 | 2 / 26 | 6767 | +| max | treble | jump | SIN-220Hz-0.3V | yes | 81.2 | 209657 | 0 | 0 | 0 | 1 / 25 | 6481 | +| max | treble | jump | DI-GTR L | yes | 90.8 | 1020679 | 0 | 0 | 0 | 2 / 28 | 6618 | +| max | middle | lfo | SIN-220Hz-0.3V | yes | 85.5 | 211603 | 0 | 0 | 0 | 2 / 25 | 6806 | +| max | middle | lfo | DI-GTR L | yes | 95.5 | 1020606 | 0 | 0 | 0 | 2 / 20 | 6662 | +| max | middle | jump | SIN-220Hz-0.3V | yes | 87.2 | 212094 | 0 | 0 | 0 | 2 / 25 | 6548 | +| max | middle | jump | DI-GTR L | yes | 92.6 | 1020912 | 0 | 0 | 0 | 2 / 21 | 6572 | +| max | bass | lfo | SIN-220Hz-0.3V | yes | 79.7 | 215682 | 0 | 0 | 0 | 2 / 25 | 6933 | +| max | bass | lfo | DI-GTR L | yes | 90.1 | 1031382 | 0 | 0 | 0 | 2 / 20 | 6540 | +| max | bass | jump | SIN-220Hz-0.3V | yes | 81.5 | 214476 | 0 | 0 | 0 | 2 / 25 | 7001 | +| max | bass | jump | DI-GTR L | yes | 92.9 | 1028087 | 0 | 0 | 0 | 2 / 20 | 6419 | +| max | master | lfo | SIN-220Hz-0.3V | yes | 73.3 | 199385 | 0 | 0 | 0 | 1 / 25 | 6042 | +| max | master | lfo | DI-GTR L | yes | 81.6 | 984915 | 0 | 0 | 0 | 1 / 27 | 6291 | +| max | master | jump | SIN-220Hz-0.3V | yes | 77.5 | 201669 | 0 | 0 | 0 | 1 / 25 | 5960 | +| max | master | jump | DI-GTR L | yes | 86.4 | 985722 | 0 | 0 | 0 | 1 / 25 | 6136 | +| max | presence | lfo | SIN-220Hz-0.3V | yes | 82.6 | 211567 | 0 | 0 | 0 | 2 / 25 | 7378 | +| max | presence | lfo | DI-GTR L | yes | 89.3 | 1016866 | 0 | 0 | 0 | 2 / 19 | 6999 | +| max | presence | jump | SIN-220Hz-0.3V | yes | 79.4 | 212158 | 0 | 0 | 0 | 2 / 25 | 7131 | +| max | presence | jump | DI-GTR L | yes | 90.6 | 1017189 | 0 | 0 | 0 | 2 / 21 | 6987 | +| max | all | lfo | SIN-220Hz-0.3V | yes | 71.1 | 192398 | 0 | 0 | 0 | 1 / 11 | 9643 | +| max | all | lfo | DI-GTR L | yes | 78.7 | 970077 | 0 | 0 | 0 | 1 / 23 | 12208 | +| max | all | jump | SIN-220Hz-0.3V | yes | 77.5 | 195640 | 0 | 0 | 0 | 1 / 25 | 9560 | +| max | all | jump | DI-GTR L | yes | 82.2 | 976641 | 0 | 0 | 0 | 1 / 24 | 12879 | diff --git a/harness/reports/M6/20261015_plugin_null_2x_full.md b/harness/reports/M6/20261015_plugin_null_2x_full.md index 2b1277b..7ed1f0d 100644 --- a/harness/reports/M6/20261015_plugin_null_2x_full.md +++ b/harness/reports/M6/20261015_plugin_null_2x_full.md @@ -1,6 +1,6 @@ # M5 wrapper null test: plugin vs ampwav at 96 kHz with the same resampling -- Date: 2026-10-15, commit `9f01825 (dirty)`; host rate 48000 Hz, 2x, Power Amp Rate full, exact J4 +- Date: 2026-10-15, commit `9778d0d (dirty)`; host rate 48000 Hz, 2x, Power Amp Rate full, exact J4 - Plugin: `amp2203_render plugin` (the Amp2203Processor offline). Reference: the processor's oversampler up stage, `ampwav --block full --units fs --j4 exact` at 96000 Hz, the down stage. Default knobs and levels. - Tolerance (SPEC §7 M5): ESR ≤ -60 dB. @@ -8,55 +8,55 @@ PASS 26 · FAIL 0 | Filter | Signal | Peak out [FS] | ESR | Status | Render log | |---|---|---|---|---|---| -| fir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.043 s for 0.200 s of audio: 21.4 % of one core, real-time factor 4.67 +| fir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.041 s for 0.200 s of audio: 20.4 % of one core, real-time factor 4.91 latency 49 samples, Newton failures 0 (rescued 0), max iterations 3 | -| fir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.049 s for 0.200 s of audio: 24.4 % of one core, real-time factor 4.10 +| fir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.044 s for 0.200 s of audio: 22.0 % of one core, real-time factor 4.55 latency 49 samples, Newton failures 0 (rescued 0), max iterations 4 | -| fir | SIN-100Hz--6dBFS | 0.825 | -138.5 dB | PASS | processBlock 0.049 s for 0.200 s of audio: 24.4 % of one core, real-time factor 4.10 +| fir | SIN-100Hz--6dBFS | 0.825 | -138.5 dB | PASS | processBlock 0.045 s for 0.200 s of audio: 22.3 % of one core, real-time factor 4.48 latency 49 samples, Newton failures 0 (rescued 0), max iterations 9 | -| fir | SIN-1000Hz--40dBFS | 0.341 | -138.5 dB | PASS | processBlock 0.069 s for 0.200 s of audio: 34.7 % of one core, real-time factor 2.88 +| fir | SIN-1000Hz--40dBFS | 0.341 | -138.5 dB | PASS | processBlock 0.065 s for 0.200 s of audio: 32.4 % of one core, real-time factor 3.09 latency 49 samples, Newton failures 0 (rescued 0), max iterations 6 | -| fir | SIN-1000Hz--20dBFS | 0.708 | -138.5 dB | PASS | processBlock 0.073 s for 0.200 s of audio: 36.3 % of one core, real-time factor 2.76 +| fir | SIN-1000Hz--20dBFS | 0.708 | -138.5 dB | PASS | processBlock 0.069 s for 0.200 s of audio: 34.5 % of one core, real-time factor 2.90 latency 49 samples, Newton failures 0 (rescued 0), max iterations 9 | -| fir | SIN-1000Hz--6dBFS | 0.704 | -138.5 dB | PASS | processBlock 0.080 s for 0.200 s of audio: 40.0 % of one core, real-time factor 2.50 +| fir | SIN-1000Hz--6dBFS | 0.704 | -138.5 dB | PASS | processBlock 0.075 s for 0.200 s of audio: 37.4 % of one core, real-time factor 2.67 latency 49 samples, Newton failures 0 (rescued 0), max iterations 12 | -| fir | SIN-5000Hz--40dBFS | 0.677 | -138.5 dB | PASS | processBlock 0.091 s for 0.200 s of audio: 45.7 % of one core, real-time factor 2.19 +| fir | SIN-5000Hz--40dBFS | 0.677 | -138.5 dB | PASS | processBlock 0.091 s for 0.200 s of audio: 45.4 % of one core, real-time factor 2.20 latency 49 samples, Newton failures 0 (rescued 0), max iterations 18 | -| fir | SIN-5000Hz--20dBFS | 0.706 | -138.5 dB | PASS | processBlock 0.110 s for 0.200 s of audio: 55.1 % of one core, real-time factor 1.82 +| fir | SIN-5000Hz--20dBFS | 0.706 | -138.5 dB | PASS | processBlock 0.110 s for 0.200 s of audio: 54.8 % of one core, real-time factor 1.82 latency 49 samples, Newton failures 0 (rescued 0), max iterations 19 | -| fir | SIN-5000Hz--6dBFS | 0.738 | -138.5 dB | PASS | processBlock 0.177 s for 0.200 s of audio: 88.7 % of one core, real-time factor 1.13 +| fir | SIN-5000Hz--6dBFS | 0.738 | -138.5 dB | PASS | processBlock 0.176 s for 0.200 s of audio: 88.2 % of one core, real-time factor 1.13 latency 49 samples, Newton failures 441 (rescued 194), max iterations 32 | -| fir | DI-GTR L | 0.767 | -138.5 dB | PASS | processBlock 1.242 s for 5.000 s of audio: 24.8 % of one core, real-time factor 4.03 +| fir | DI-GTR L | 0.767 | -138.5 dB | PASS | processBlock 1.232 s for 5.000 s of audio: 24.6 % of one core, real-time factor 4.06 latency 49 samples, Newton failures 0 (rescued 0), max iterations 17 | -| fir | DI-GTR LL | 0.711 | -138.5 dB | PASS | processBlock 1.201 s for 5.000 s of audio: 24.0 % of one core, real-time factor 4.16 +| fir | DI-GTR LL | 0.711 | -138.5 dB | PASS | processBlock 1.196 s for 5.000 s of audio: 23.9 % of one core, real-time factor 4.18 latency 49 samples, Newton failures 0 (rescued 0), max iterations 17 | -| fir | DI-GTR R | 0.782 | -138.5 dB | PASS | processBlock 1.210 s for 5.000 s of audio: 24.2 % of one core, real-time factor 4.13 +| fir | DI-GTR R | 0.782 | -138.5 dB | PASS | processBlock 1.209 s for 5.000 s of audio: 24.2 % of one core, real-time factor 4.14 latency 49 samples, Newton failures 0 (rescued 0), max iterations 20 | -| fir | DI-GTR RR | 0.798 | -138.5 dB | PASS | processBlock 1.246 s for 5.000 s of audio: 24.9 % of one core, real-time factor 4.01 +| fir | DI-GTR RR | 0.798 | -138.5 dB | PASS | processBlock 1.241 s for 5.000 s of audio: 24.8 % of one core, real-time factor 4.03 latency 49 samples, Newton failures 0 (rescued 0), max iterations 18 | -| iir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.041 s for 0.200 s of audio: 20.4 % of one core, real-time factor 4.91 +| iir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.041 s for 0.200 s of audio: 20.3 % of one core, real-time factor 4.92 latency 4 samples, Newton failures 0 (rescued 0), max iterations 2 | -| iir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.045 s for 0.200 s of audio: 22.3 % of one core, real-time factor 4.49 +| iir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.044 s for 0.200 s of audio: 22.1 % of one core, real-time factor 4.54 latency 4 samples, Newton failures 0 (rescued 0), max iterations 6 | -| iir | SIN-100Hz--6dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.044 s for 0.200 s of audio: 22.0 % of one core, real-time factor 4.55 +| iir | SIN-100Hz--6dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.044 s for 0.200 s of audio: 21.9 % of one core, real-time factor 4.56 latency 4 samples, Newton failures 0 (rescued 0), max iterations 6 | -| iir | SIN-1000Hz--40dBFS | 0.362 | -138.5 dB | PASS | processBlock 0.064 s for 0.200 s of audio: 32.0 % of one core, real-time factor 3.12 +| iir | SIN-1000Hz--40dBFS | 0.362 | -138.5 dB | PASS | processBlock 0.064 s for 0.200 s of audio: 32.2 % of one core, real-time factor 3.10 latency 4 samples, Newton failures 0 (rescued 0), max iterations 7 | -| iir | SIN-1000Hz--20dBFS | 0.774 | -138.5 dB | PASS | processBlock 0.069 s for 0.200 s of audio: 34.4 % of one core, real-time factor 2.91 +| iir | SIN-1000Hz--20dBFS | 0.774 | -138.5 dB | PASS | processBlock 0.068 s for 0.200 s of audio: 34.0 % of one core, real-time factor 2.94 latency 4 samples, Newton failures 0 (rescued 0), max iterations 9 | -| iir | SIN-1000Hz--6dBFS | 0.881 | -138.5 dB | PASS | processBlock 0.075 s for 0.200 s of audio: 37.6 % of one core, real-time factor 2.66 +| iir | SIN-1000Hz--6dBFS | 0.881 | -138.5 dB | PASS | processBlock 0.077 s for 0.200 s of audio: 38.5 % of one core, real-time factor 2.60 latency 4 samples, Newton failures 0 (rescued 0), max iterations 22 | -| iir | SIN-5000Hz--40dBFS | 0.816 | -138.5 dB | PASS | processBlock 0.091 s for 0.200 s of audio: 45.7 % of one core, real-time factor 2.19 +| iir | SIN-5000Hz--40dBFS | 0.816 | -138.5 dB | PASS | processBlock 0.092 s for 0.200 s of audio: 46.0 % of one core, real-time factor 2.17 latency 4 samples, Newton failures 0 (rescued 0), max iterations 16 | -| iir | SIN-5000Hz--20dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.110 s for 0.200 s of audio: 55.2 % of one core, real-time factor 1.81 +| iir | SIN-5000Hz--20dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.111 s for 0.200 s of audio: 55.6 % of one core, real-time factor 1.80 latency 4 samples, Newton failures 0 (rescued 0), max iterations 22 | -| iir | SIN-5000Hz--6dBFS | 0.841 | -138.5 dB | PASS | processBlock 0.145 s for 0.200 s of audio: 72.4 % of one core, real-time factor 1.38 +| iir | SIN-5000Hz--6dBFS | 0.841 | -138.5 dB | PASS | processBlock 0.140 s for 0.200 s of audio: 69.8 % of one core, real-time factor 1.43 latency 4 samples, Newton failures 157 (rescued 80), max iterations 32 | -| iir | DI-GTR L | 0.814 | -138.5 dB | PASS | processBlock 1.229 s for 5.000 s of audio: 24.6 % of one core, real-time factor 4.07 +| iir | DI-GTR L | 0.814 | -138.5 dB | PASS | processBlock 1.222 s for 5.000 s of audio: 24.4 % of one core, real-time factor 4.09 latency 4 samples, Newton failures 0 (rescued 0), max iterations 18 | -| iir | DI-GTR LL | 0.781 | -138.5 dB | PASS | processBlock 1.192 s for 5.000 s of audio: 23.8 % of one core, real-time factor 4.20 +| iir | DI-GTR LL | 0.781 | -138.5 dB | PASS | processBlock 1.180 s for 5.000 s of audio: 23.6 % of one core, real-time factor 4.24 latency 4 samples, Newton failures 0 (rescued 0), max iterations 23 | -| iir | DI-GTR R | 0.891 | -138.5 dB | PASS | processBlock 1.196 s for 5.000 s of audio: 23.9 % of one core, real-time factor 4.18 +| iir | DI-GTR R | 0.891 | -138.5 dB | PASS | processBlock 1.188 s for 5.000 s of audio: 23.8 % of one core, real-time factor 4.21 latency 4 samples, Newton failures 0 (rescued 0), max iterations 17 | -| iir | DI-GTR RR | 0.866 | -138.5 dB | PASS | processBlock 1.229 s for 5.000 s of audio: 24.6 % of one core, real-time factor 4.07 +| iir | DI-GTR RR | 0.866 | -138.5 dB | PASS | processBlock 1.227 s for 5.000 s of audio: 24.5 % of one core, real-time factor 4.08 latency 4 samples, Newton failures 0 (rescued 0), max iterations 20 | diff --git a/harness/reports/M6/20261015_plugin_null_2x_half.md b/harness/reports/M6/20261015_plugin_null_2x_half.md index fe31ed9..796287f 100644 --- a/harness/reports/M6/20261015_plugin_null_2x_half.md +++ b/harness/reports/M6/20261015_plugin_null_2x_half.md @@ -1,6 +1,6 @@ # M5 wrapper null test: plugin vs ampwav at 96 kHz with the same resampling -- Date: 2026-10-15, commit `9f01825 (dirty)`; host rate 48000 Hz, 2x, Power Amp Rate half, exact J4 +- Date: 2026-10-15, commit `9778d0d (dirty)`; host rate 48000 Hz, 2x, Power Amp Rate half, exact J4 - Plugin: `amp2203_render plugin` (the Amp2203Processor offline). Reference: the processor's oversampler up stage, `ampwav --block full --units fs --j4 exact` at 96000 Hz, the down stage. Default knobs and levels. - Tolerance (SPEC §7 M5): ESR ≤ -60 dB. @@ -8,55 +8,55 @@ PASS 26 · FAIL 0 | Filter | Signal | Peak out [FS] | ESR | Status | Render log | |---|---|---|---|---|---| -| fir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.029 s for 0.200 s of audio: 14.7 % of one core, real-time factor 6.79 +| fir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.029 s for 0.200 s of audio: 14.4 % of one core, real-time factor 6.95 latency 58 samples, Newton failures 0 (rescued 0), max iterations 3 | -| fir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.032 s for 0.200 s of audio: 16.1 % of one core, real-time factor 6.22 +| fir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.031 s for 0.200 s of audio: 15.7 % of one core, real-time factor 6.38 latency 58 samples, Newton failures 0 (rescued 0), max iterations 4 | -| fir | SIN-100Hz--6dBFS | 0.825 | -138.5 dB | PASS | processBlock 0.033 s for 0.200 s of audio: 16.7 % of one core, real-time factor 5.98 +| fir | SIN-100Hz--6dBFS | 0.825 | -138.5 dB | PASS | processBlock 0.033 s for 0.200 s of audio: 16.3 % of one core, real-time factor 6.14 latency 58 samples, Newton failures 0 (rescued 0), max iterations 9 | -| fir | SIN-1000Hz--40dBFS | 0.335 | -138.5 dB | PASS | processBlock 0.048 s for 0.200 s of audio: 23.8 % of one core, real-time factor 4.21 +| fir | SIN-1000Hz--40dBFS | 0.335 | -138.5 dB | PASS | processBlock 0.046 s for 0.200 s of audio: 23.2 % of one core, real-time factor 4.31 latency 58 samples, Newton failures 0 (rescued 0), max iterations 6 | -| fir | SIN-1000Hz--20dBFS | 0.696 | -138.5 dB | PASS | processBlock 0.052 s for 0.200 s of audio: 26.2 % of one core, real-time factor 3.82 +| fir | SIN-1000Hz--20dBFS | 0.655 | -138.5 dB | PASS | processBlock 0.050 s for 0.200 s of audio: 25.1 % of one core, real-time factor 3.98 latency 58 samples, Newton failures 0 (rescued 0), max iterations 9 | -| fir | SIN-1000Hz--6dBFS | 0.886 | -138.5 dB | PASS | processBlock 0.054 s for 0.200 s of audio: 26.8 % of one core, real-time factor 3.73 +| fir | SIN-1000Hz--6dBFS | 0.708 | -138.5 dB | PASS | processBlock 0.055 s for 0.200 s of audio: 27.4 % of one core, real-time factor 3.64 latency 58 samples, Newton failures 0 (rescued 0), max iterations 12 | -| fir | SIN-5000Hz--40dBFS | 0.775 | -138.5 dB | PASS | processBlock 0.066 s for 0.200 s of audio: 33.1 % of one core, real-time factor 3.02 +| fir | SIN-5000Hz--40dBFS | 0.703 | -138.5 dB | PASS | processBlock 0.067 s for 0.200 s of audio: 33.5 % of one core, real-time factor 2.98 latency 58 samples, Newton failures 0 (rescued 0), max iterations 18 | -| fir | SIN-5000Hz--20dBFS | 0.866 | -138.5 dB | PASS | processBlock 0.074 s for 0.200 s of audio: 37.0 % of one core, real-time factor 2.70 +| fir | SIN-5000Hz--20dBFS | 0.706 | -138.5 dB | PASS | processBlock 0.078 s for 0.200 s of audio: 38.9 % of one core, real-time factor 2.57 latency 58 samples, Newton failures 0 (rescued 0), max iterations 19 | -| fir | SIN-5000Hz--6dBFS | 0.914 | -138.5 dB | PASS | processBlock 0.144 s for 0.200 s of audio: 72.2 % of one core, real-time factor 1.39 -latency 58 samples, Newton failures 449 (rescued 193), max iterations 32 | -| fir | DI-GTR L | 0.786 | -138.5 dB | PASS | processBlock 0.892 s for 5.000 s of audio: 17.8 % of one core, real-time factor 5.60 +| fir | SIN-5000Hz--6dBFS | 0.744 | -138.5 dB | PASS | processBlock 0.145 s for 0.200 s of audio: 72.5 % of one core, real-time factor 1.38 +latency 58 samples, Newton failures 449 (rescued 191), max iterations 32 | +| fir | DI-GTR L | 0.741 | -138.5 dB | PASS | processBlock 0.889 s for 5.000 s of audio: 17.8 % of one core, real-time factor 5.63 latency 58 samples, Newton failures 0 (rescued 0), max iterations 17 | -| fir | DI-GTR LL | 0.775 | -138.5 dB | PASS | processBlock 0.866 s for 5.000 s of audio: 17.3 % of one core, real-time factor 5.77 +| fir | DI-GTR LL | 0.756 | -138.5 dB | PASS | processBlock 0.870 s for 5.000 s of audio: 17.4 % of one core, real-time factor 5.74 latency 58 samples, Newton failures 0 (rescued 0), max iterations 17 | -| fir | DI-GTR R | 0.869 | -138.5 dB | PASS | processBlock 0.875 s for 5.000 s of audio: 17.5 % of one core, real-time factor 5.71 +| fir | DI-GTR R | 0.783 | -138.5 dB | PASS | processBlock 0.873 s for 5.000 s of audio: 17.5 % of one core, real-time factor 5.73 latency 58 samples, Newton failures 0 (rescued 0), max iterations 20 | -| fir | DI-GTR RR | 0.815 | -138.5 dB | PASS | processBlock 0.887 s for 5.000 s of audio: 17.7 % of one core, real-time factor 5.64 +| fir | DI-GTR RR | 0.758 | -138.5 dB | PASS | processBlock 0.890 s for 5.000 s of audio: 17.8 % of one core, real-time factor 5.62 latency 58 samples, Newton failures 0 (rescued 0), max iterations 18 | -| iir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.028 s for 0.200 s of audio: 14.1 % of one core, real-time factor 7.11 +| iir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.028 s for 0.200 s of audio: 14.0 % of one core, real-time factor 7.12 latency 13 samples, Newton failures 0 (rescued 0), max iterations 2 | -| iir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.031 s for 0.200 s of audio: 15.6 % of one core, real-time factor 6.42 +| iir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.031 s for 0.200 s of audio: 15.5 % of one core, real-time factor 6.45 latency 13 samples, Newton failures 0 (rescued 0), max iterations 6 | -| iir | SIN-100Hz--6dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.032 s for 0.200 s of audio: 15.8 % of one core, real-time factor 6.31 +| iir | SIN-100Hz--6dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.033 s for 0.200 s of audio: 16.3 % of one core, real-time factor 6.14 latency 13 samples, Newton failures 0 (rescued 0), max iterations 6 | -| iir | SIN-1000Hz--40dBFS | 0.358 | -138.5 dB | PASS | processBlock 0.046 s for 0.200 s of audio: 23.1 % of one core, real-time factor 4.33 +| iir | SIN-1000Hz--40dBFS | 0.358 | -138.5 dB | PASS | processBlock 0.046 s for 0.200 s of audio: 22.9 % of one core, real-time factor 4.36 latency 13 samples, Newton failures 0 (rescued 0), max iterations 7 | -| iir | SIN-1000Hz--20dBFS | 0.774 | -138.5 dB | PASS | processBlock 0.049 s for 0.200 s of audio: 24.3 % of one core, real-time factor 4.11 +| iir | SIN-1000Hz--20dBFS | 0.729 | -138.5 dB | PASS | processBlock 0.049 s for 0.200 s of audio: 24.6 % of one core, real-time factor 4.06 latency 13 samples, Newton failures 0 (rescued 0), max iterations 9 | -| iir | SIN-1000Hz--6dBFS | 0.854 | -138.5 dB | PASS | processBlock 0.051 s for 0.200 s of audio: 25.4 % of one core, real-time factor 3.93 +| iir | SIN-1000Hz--6dBFS | 0.766 | -138.5 dB | PASS | processBlock 0.054 s for 0.200 s of audio: 27.1 % of one core, real-time factor 3.69 latency 13 samples, Newton failures 0 (rescued 0), max iterations 22 | -| iir | SIN-5000Hz--40dBFS | 0.790 | -138.5 dB | PASS | processBlock 0.064 s for 0.200 s of audio: 31.8 % of one core, real-time factor 3.14 +| iir | SIN-5000Hz--40dBFS | 0.682 | -138.5 dB | PASS | processBlock 0.067 s for 0.200 s of audio: 33.5 % of one core, real-time factor 2.99 latency 13 samples, Newton failures 0 (rescued 0), max iterations 16 | -| iir | SIN-5000Hz--20dBFS | 0.835 | -138.5 dB | PASS | processBlock 0.071 s for 0.200 s of audio: 35.4 % of one core, real-time factor 2.82 +| iir | SIN-5000Hz--20dBFS | 0.788 | -138.5 dB | PASS | processBlock 0.076 s for 0.200 s of audio: 38.1 % of one core, real-time factor 2.63 latency 13 samples, Newton failures 0 (rescued 0), max iterations 22 | -| iir | SIN-5000Hz--6dBFS | 0.843 | -138.5 dB | PASS | processBlock 0.101 s for 0.200 s of audio: 50.4 % of one core, real-time factor 1.99 -latency 13 samples, Newton failures 161 (rescued 84), max iterations 32 | -| iir | DI-GTR L | 0.836 | -138.5 dB | PASS | processBlock 0.899 s for 5.000 s of audio: 18.0 % of one core, real-time factor 5.56 +| iir | SIN-5000Hz--6dBFS | 0.803 | -138.5 dB | PASS | processBlock 0.107 s for 0.200 s of audio: 53.7 % of one core, real-time factor 1.86 +latency 13 samples, Newton failures 163 (rescued 86), max iterations 32 | +| iir | DI-GTR L | 0.796 | -138.5 dB | PASS | processBlock 0.871 s for 5.000 s of audio: 17.4 % of one core, real-time factor 5.74 latency 13 samples, Newton failures 0 (rescued 0), max iterations 18 | -| iir | DI-GTR LL | 0.806 | -138.5 dB | PASS | processBlock 0.870 s for 5.000 s of audio: 17.4 % of one core, real-time factor 5.75 +| iir | DI-GTR LL | 0.750 | -138.5 dB | PASS | processBlock 0.844 s for 5.000 s of audio: 16.9 % of one core, real-time factor 5.93 latency 13 samples, Newton failures 0 (rescued 0), max iterations 23 | -| iir | DI-GTR R | 0.887 | -138.5 dB | PASS | processBlock 0.860 s for 5.000 s of audio: 17.2 % of one core, real-time factor 5.81 +| iir | DI-GTR R | 0.837 | -138.5 dB | PASS | processBlock 0.849 s for 5.000 s of audio: 17.0 % of one core, real-time factor 5.89 latency 13 samples, Newton failures 0 (rescued 0), max iterations 17 | -| iir | DI-GTR RR | 0.848 | -138.5 dB | PASS | processBlock 0.881 s for 5.000 s of audio: 17.6 % of one core, real-time factor 5.68 +| iir | DI-GTR RR | 0.843 | -138.5 dB | PASS | processBlock 0.872 s for 5.000 s of audio: 17.4 % of one core, real-time factor 5.73 latency 13 samples, Newton failures 0 (rescued 0), max iterations 20 | diff --git a/plugin/src/PluginProcessor.cpp b/plugin/src/PluginProcessor.cpp index 0410885..ebc7598 100644 --- a/plugin/src/PluginProcessor.cpp +++ b/plugin/src/PluginProcessor.cpp @@ -81,10 +81,9 @@ void Amp2203Processor::rebuild(double hostRate, int maxBlock) { cfg.powerAmpRateDivisor = activePowerRate_ == 1 ? 2 : 1; cfg.exactJ4 = true; // preamp → power amp junction solved exactly (M6 X1, D-053, D-054) engine_.prepare(cfg); - // D-055: Newton residual tolerance 1e-4 V; no adaptive half-steps when the power amp runs at half rate (the - // step decision there flips on last-bit differences, M6); the D-043 trigger at full rate + // D-055: Newton residual tolerance 1e-4 V; adaptive half-steps (D-043) at both power-amp rates (D-056) engine_.setNewtonResidualTolerance(kNewtonTolerance); - engine_.setAdaptiveHalfSteps(cfg.powerAmpRateDivisor == 1); + engine_.setAdaptiveHalfSteps(true); pushParams(); engine_.reset(); // DC operating point at the current knobs (SPEC §3.6) trim_.reset(hostRate, 0.02); From 831e39375c1f5e47632625ad0f7c9c3969ce06ab Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 13:24:17 +0300 Subject: [PATCH 17/18] m6: plugin CPU under knob automation (amp2203_render --automate, --block; harness.plugin_cpu) --- docs/PROGRESS.md | 5 ++ harness/plugin_cpu.py | 75 +++++++++++++++++++++++ harness/reports/M6/20261015_plugin_cpu.md | 39 ++++++++++++ plugin/render/main.cpp | 37 +++++++++-- 4 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 harness/plugin_cpu.py create mode 100644 harness/reports/M6/20261015_plugin_cpu.md diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 91d9963..af21cb0 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -744,6 +744,11 @@ Notes: automation at every knob 1.0 up to 10.4 µs. Raised with the owner. - Owner 2026-10-15: half-steps back on at half rate (D-056). Plugin CPU 2x Half FIR: default 20.6 %, HOT 32.9 %, every knob 10 35.6 % (0 failures). Null test 26 / 26; automation 0 failures. Open: plugin CPU under knob automation. +- Plugin CPU under knob automation (`python -m harness.plugin_cpu`, `amp2203_render --automate`, report + `harness/reports/M6/20261015_plugin_cpu.md`): static 18.9 % (every knob 10: 32.1 %); one knob moving 45.8 … 55.0 % + (from every knob 10: 54.4 … 67.7 %); all six moving 84.6 … 87.1 %; host block 64 as 512; 1 rescued failure in 31 + runs. Profile with all knobs moving: 76 % in the double-double MNA solve of the matrix updates (D-026; every 32 + samples while a knob is smoothed), Newton + LU 14 %, libm 5 %. ## Questions for the owner _(Claude Code: add questions here and also ask them in the session.)_ diff --git a/harness/plugin_cpu.py b/harness/plugin_cpu.py new file mode 100644 index 0000000..20bad66 --- /dev/null +++ b/harness/plugin_cpu.py @@ -0,0 +1,75 @@ +"""M6: plugin CPU (`amp2203_render plugin`, the Amp2203Processor offline, oversampling included) at its defaults, +static and under knob automation between host blocks (lfo: 10 Hz triangle 0 → 10 → 0; jump: 0 ↔ 10 every 50 ms; +each knob alone and all six), from the default knobs and from every knob at 10. One render at a time (timing). + + python -m harness.plugin_cpu [--seconds 20] [--factor 2] + +Writes harness/reports/M6/_plugin_cpu.md. +""" +import argparse +import datetime +import re +import subprocess +import tempfile +from pathlib import Path + +from . import config +from . import x3_neural as x3 +from .compare import _git +from .plugin_null import RENDER +from .wavio import write_wav + +RATE = 48000 +KNOBS = ('preampVolume', 'treble', 'middle', 'bass', 'master', 'presence') +BASES = {'default': [], 'every knob 10': [a for k in KNOBS for a in ('--param', f'{k}=10')]} + + +def _render(x_path, out_path, factor, extra): + r = subprocess.run([str(RENDER), 'plugin', '--in', x_path, '--out', out_path, '--factor', str(factor)] + extra, + capture_output=True, text=True) + if r.returncode != 0: + raise RuntimeError(r.stderr) + m = re.search(r'([\d.]+) % of one core', r.stderr) + f = re.search(r'Newton failures (\d+) \(rescued (\d+)\)', r.stderr) + return float(m.group(1)), int(f.group(1)), int(f.group(2)) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument('--seconds', type=float, default=20.0) + ap.add_argument('--factor', type=int, default=2) + a = ap.parse_args() + x = x3._tracks()['GTR L'][RATE:RATE + int(a.seconds * RATE)] + rows = [] + with tempfile.TemporaryDirectory(prefix='amp2203_cpu_') as d: + d = Path(d) + write_wav(d / 'x.wav', x, RATE) + cases = [(base, 'static', '—', 512) for base in BASES] + cases += [(base, kind, knob, 512) for base in BASES for knob in KNOBS + ('all',) for kind in ('lfo', 'jump')] + cases += [(base, 'jump', 'all', 64) for base in BASES] + for base, kind, knob, block in cases: + extra = BASES[base] + ['--block', str(block)] + if kind != 'static': + extra += ['--automate', f'{knob}={kind}'] + cpu, fail, resc = _render(d / 'x.wav', d / 'y.wav', a.factor, extra) + rows.append((base, knob, kind, block, cpu, fail, resc)) + print(f'plugin_cpu: {base:14s} {knob:12s} {kind:6s} block {block:3d}: {cpu:5.1f} % (failures {fail}, ' + f'rescued {resc})', flush=True) + lines = ['# M6 plugin CPU under knob automation', '', + f'- Date {datetime.date.today().isoformat()}, commit `{_git()}`; `amp2203_render plugin` at the plugin defaults ' + f'({a.factor}x, Power Amp Rate Half, exact J4, Newton tol 1e-4 V, half-steps on; D-054 – D-056), FIR ' + f'oversampling, {a.seconds:g} s of DI GTR L at {RATE} Hz; processBlock time only.', + '- Automation between host blocks: lfo = 10 Hz triangle 0 → 10 → 0, jump = 0 ↔ 10 every 50 ms; "all" moves ' + 'the six knobs together. One render at a time.', '', + '| Base | Knob(s) | Modulation | Host block | CPU (% of one core) | Newton failures (rescued) |', + '|---|---|---|---|---|---|'] + lines += [f'| {b} | {k} | {m} | {bl} | {c:.1f} | {f} ({r}) |' for b, k, m, bl, c, f, r in rows] + out = config.REPORT_DIR / 'M6' + out.mkdir(parents=True, exist_ok=True) + path = out / f'{datetime.date.today().strftime("%Y%m%d")}_plugin_cpu.md' + path.write_text('\n'.join(lines) + '\n') + print(f'plugin_cpu: wrote {path.relative_to(config.ROOT)}') + + +if __name__ == '__main__': + main() diff --git a/harness/reports/M6/20261015_plugin_cpu.md b/harness/reports/M6/20261015_plugin_cpu.md new file mode 100644 index 0000000..ab95926 --- /dev/null +++ b/harness/reports/M6/20261015_plugin_cpu.md @@ -0,0 +1,39 @@ +# M6 plugin CPU under knob automation + +- Date 2026-10-15, commit `e274e3d (dirty)`; `amp2203_render plugin` at the plugin defaults (2x, Power Amp Rate Half, exact J4, Newton tol 1e-4 V, half-steps on; D-054 – D-056), FIR oversampling, 20 s of DI GTR L at 48000 Hz; processBlock time only. +- Automation between host blocks: lfo = 10 Hz triangle 0 → 10 → 0, jump = 0 ↔ 10 every 50 ms; "all" moves the six knobs together. One render at a time. + +| Base | Knob(s) | Modulation | Host block | CPU (% of one core) | Newton failures (rescued) | +|---|---|---|---|---|---| +| default | — | static | 512 | 18.9 | 0 (0) | +| every knob 10 | — | static | 512 | 32.1 | 0 (0) | +| default | preampVolume | lfo | 512 | 45.9 | 0 (0) | +| default | preampVolume | jump | 512 | 45.8 | 0 (0) | +| default | treble | lfo | 512 | 46.6 | 0 (0) | +| default | treble | jump | 512 | 46.4 | 0 (0) | +| default | middle | lfo | 512 | 47.8 | 0 (0) | +| default | middle | jump | 512 | 46.7 | 0 (0) | +| default | bass | lfo | 512 | 46.5 | 0 (0) | +| default | bass | jump | 512 | 47.6 | 0 (0) | +| default | master | lfo | 512 | 48.7 | 0 (0) | +| default | master | jump | 512 | 49.6 | 0 (0) | +| default | presence | lfo | 512 | 55.0 | 0 (0) | +| default | presence | jump | 512 | 54.1 | 0 (0) | +| default | all | lfo | 512 | 84.6 | 0 (0) | +| default | all | jump | 512 | 85.4 | 0 (0) | +| every knob 10 | preampVolume | lfo | 512 | 58.9 | 0 (0) | +| every knob 10 | preampVolume | jump | 512 | 58.6 | 0 (0) | +| every knob 10 | treble | lfo | 512 | 59.3 | 0 (0) | +| every knob 10 | treble | jump | 512 | 59.6 | 0 (0) | +| every knob 10 | middle | lfo | 512 | 59.2 | 0 (0) | +| every knob 10 | middle | jump | 512 | 60.2 | 0 (0) | +| every knob 10 | bass | lfo | 512 | 61.5 | 0 (0) | +| every knob 10 | bass | jump | 512 | 60.8 | 0 (0) | +| every knob 10 | master | lfo | 512 | 55.1 | 0 (0) | +| every knob 10 | master | jump | 512 | 54.4 | 0 (0) | +| every knob 10 | presence | lfo | 512 | 67.7 | 0 (0) | +| every knob 10 | presence | jump | 512 | 66.7 | 0 (0) | +| every knob 10 | all | lfo | 512 | 86.4 | 0 (0) | +| every knob 10 | all | jump | 512 | 86.6 | 0 (0) | +| default | all | jump | 64 | 87.1 | 1 (1) | +| every knob 10 | all | jump | 64 | 86.1 | 1 (1) | diff --git a/plugin/render/main.cpp b/plugin/render/main.cpp index 192c2da..e775319 100644 --- a/plugin/render/main.cpp +++ b/plugin/render/main.cpp @@ -1,5 +1,7 @@ // amp2203_render — offline renders through the plugin's processor (SPEC §7 M5 null test, DECISIONS D-044). // amp2203_render plugin --in x.wav --out y.wav [--factor 8] [--filter fir|iir] [--param id=value ...] +// [--automate id=lfo|jump ...] [--block N]: knob automation between blocks (lfo: 10 Hz triangle 0 → 10 → 0; +// jump: 0 ↔ 10 every 50 ms; id "all" = the six knobs), as a host delivers it; host block size N (default 512) // the whole Amp2203Processor (oversampling, engine, levels); --param uses the plugin's parameter ids and // displayed values (knobs 0–10, levels in dB / dBu) // amp2203_render up --in x.wav --out x_os.wav [--factor 8] [--filter fir|iir] oversampler up stage only @@ -45,6 +47,8 @@ int main(int argc, char** argv) { std::string inPath, outPath, filter = "fir"; int factor = 8; std::map params; + std::map automate; // knob id → lfo | jump + int hostBlock = kBlock; for (int i = 2; i + 1 < argc; i += 2) { const std::string a = argv[i], v = argv[i + 1]; if (a == "--in") @@ -57,6 +61,17 @@ int main(int argc, char** argv) { filter = v; else if (a == "--param" && v.find('=') != std::string::npos) params[v.substr(0, v.find('='))] = std::strtof(v.substr(v.find('=') + 1).c_str(), nullptr); + else if (a == "--automate" && v.find('=') != std::string::npos) { + const std::string id = v.substr(0, v.find('=')), kind = v.substr(v.find('=') + 1); + if (kind != "lfo" && kind != "jump") + return fail("--automate expects id=lfo or id=jump"); + if (id == "all") + for (const char* k : {"preampVolume", "treble", "middle", "bass", "master", "presence"}) + automate[k] = kind; + else + automate[id] = kind; + } else if (a == "--block") + hostBlock = std::atoi(v.c_str()); else return fail("unknown option " + a); } @@ -84,15 +99,27 @@ int main(int argc, char** argv) { for (const auto& [id, v] : params) if (!set(id, v)) return fail("unknown parameter " + id); - proc.setPlayConfigDetails(2, 2, in.sampleRate, kBlock); - proc.prepareToPlay(in.sampleRate, kBlock); + if (hostBlock < 1 || hostBlock > 8192) + return fail("--block must be 1..8192"); + for (const auto& [id, kind] : automate) + if (apvts.getParameter(id) == nullptr) + return fail("unknown parameter " + id); + proc.setPlayConfigDetails(2, 2, in.sampleRate, hostBlock); + proc.prepareToPlay(in.sampleRate, hostBlock); if (proc.activeFactor() != factor) return fail("oversampling factor not applied"); - juce::AudioBuffer buf(2, kBlock); + juce::AudioBuffer buf(2, hostBlock); juce::MidiBuffer midi; double busy = 0.0; // seconds inside processBlock (the plugin's cost: oversampler, engine, levels) - for (size_t pos = 0; pos < in.samples.size(); pos += kBlock) { - const int n = static_cast(std::min(kBlock, in.samples.size() - pos)); + for (size_t pos = 0; pos < in.samples.size(); pos += static_cast(hostBlock)) { + const int n = static_cast(std::min(static_cast(hostBlock), in.samples.size() - pos)); + const double t = static_cast(pos) / in.sampleRate; // block start (s) + for (const auto& [id, kind] : automate) { // outside the timed region, as a host's automation + const double ph = std::fmod(t * 10.0, 1.0); + const double u = kind == "lfo" ? 1.0 - std::fabs(2.0 * ph - 1.0) + : (static_cast(t / 0.05) % 2 == 0 ? 1.0 : 0.0); + set(id, static_cast(10.0 * u)); + } buf.setSize(2, n, false, false, true); for (int c = 0; c < 2; ++c) for (int i = 0; i < n; ++i) From 59a79f97958c427cb1813eca70ac3f5ecec06075 Mon Sep 17 00:00:00 2001 From: CatastrophicCoder Date: Thu, 15 Oct 2026 13:36:49 +0300 Subject: [PATCH 18/18] m6: full amp matrix updates in double, every 128 samples (D-057); verification rerun --- docs/DECISIONS.md | 16 +++ docs/PROGRESS.md | 7 ++ engine/include/amp2203/PowerAmp.h | 2 +- engine/include/amp2203/Preamp.h | 2 +- engine/src/PowerAmp.cpp | 2 +- engine/src/Preamp.cpp | 2 +- engine/src/mna/PortNetwork.h | 68 ++++++----- engine/src/stage/RootNetwork.h | 7 +- .../reports/M3/20261015_b1234_knob_jump.md | 14 +++ harness/reports/M5/20261015_b56_knob_jump.md | 9 ++ ...261015_automation_fs96000_pdiv2_j4exact.md | 114 +++++++++--------- ...261015_full_acceptance_fs384000_j4exact.md | 8 +- ...15_full_acceptance_fs384000_j4exact_hot.md | 14 +-- harness/reports/M6/20261015_plugin_cpu.md | 66 +++++----- .../M6/20261015_plugin_null_2x_full.md | 54 ++++----- .../M6/20261015_plugin_null_2x_half.md | 54 ++++----- .../M6/20261015_x2_rates_d057_half2x.md | 48 ++++++++ 17 files changed, 297 insertions(+), 190 deletions(-) create mode 100644 harness/reports/M3/20261015_b1234_knob_jump.md create mode 100644 harness/reports/M5/20261015_b56_knob_jump.md create mode 100644 harness/reports/M6/20261015_x2_rates_d057_half2x.md diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index fa21411..741da26 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -973,3 +973,19 @@ knob 10: 35.6 %, 0 Newton failures (48.8 %, 5777 rescued without). Null test 26 cost 4.9 / 6.3 / 9.6 / 13.0 µs per engine sample (min / median / 90 % / max, all six knobs jumping 0 ↔ 1 every 50 ms), which includes ampwav setting every knob each sample (the plugin sets them once per block); the plugin's cost under automation is not measured yet. + +## D-057 Full amp roots: matrix updates in double precision, every 128 samples while a knob moves +Date: 2026-10-15 · Status: accepted (owner: "run 3", both changes; refines D-026 for `Preamp` / `PowerAmp`) +Context: plugin CPU under knob automation (`harness/reports/M6/20261015_plugin_cpu.md` before this change): one knob +moving 45.8 … 67.7 %, all six 84.6 … 87.1 % (static 18.9 %); 76 % of the time in the double-double MNA solve of the +matrix updates (every 32 samples while a knob is smoothed). +Decision: `mna::rootTransfer` and its helpers are templated on the arithmetic (DD default, or double); +`RootNetwork::updateMatrices` takes it as a flag. `Preamp` and `PowerAmp` (the full amp's roots) update in double and +every 128 samples (`kParamUpdateInterval`, was 32). DC solves, the standalone sub-blocks and the R-type fixtures keep +double-double (D-026 unchanged there). +Consequences: plugin CPU (2x Half, plugin_cpu): automation of any knob or all six 18.7 … 24.6 % from the default knobs, +22.8 … 33.5 % from every knob 10 (static 32.5 %), 0 Newton failures. Static accuracy unchanged: full acceptance at +384 kHz exact J4 107 PASS (ESR identical), HOT 21 PASS (DI ESR within 0.14 dB); half 2x vs SPICE HOT −22.4 dB, unseen +settings −18.9 dB; null test 26 / 26 at Half and Full (−138.5 dB); automation 0 failures. Knob jump vs SPICE (768 kHz) +still PASS with less margin: B1234 −39.2 … −55.1 dB (was −50.0 … −59.4), B56 Presence −63.2 dB (was −77.0); which of +the two changes accounts for it is not separated. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index af21cb0..4779c3f 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -749,6 +749,13 @@ Notes: (from every knob 10: 54.4 … 67.7 %); all six moving 84.6 … 87.1 %; host block 64 as 512; 1 rescued failure in 31 runs. Profile with all knobs moving: 76 % in the double-double MNA solve of the matrix updates (D-026; every 32 samples while a knob is smoothed), Newton + LU 14 %, libm 5 %. +- Owner 2026-10-15: run both: (a) plain double instead of double-double for the matrix updates and (b) a longer + update interval. Plan: `mna` routines templated on the scalar (DD / double); `Preamp` and `PowerAmp` (the full amp's + roots) update in double and every 128 samples (was 32); sub-blocks, DC solves and the fixtures keep DD. Verify: + ctest, knob-jump vs SPICE (b1234, b56), full acceptance at 384 kHz exact J4 + HOT, X2 at half 2x, null test, + automation, plugin CPU. +- Done (D-057): plugin CPU under automation 18.7 … 24.6 % (default base), 22.8 … 33.5 % (every knob 10); static + ≈ 19 % / 32.5 %. Accuracy: acceptance and HOT unchanged; knob jump PASS (B1234 −39.2 … −55.1 dB, B56 −63.2 dB). ## Questions for the owner _(Claude Code: add questions here and also ask them in the session.)_ diff --git a/engine/include/amp2203/PowerAmp.h b/engine/include/amp2203/PowerAmp.h index db1c357..4063801 100644 --- a/engine/include/amp2203/PowerAmp.h +++ b/engine/include/amp2203/PowerAmp.h @@ -29,7 +29,7 @@ class PowerAmp { public: using PI = PhaseInverter; using PS = PowerStage; - static constexpr int kParamUpdateInterval = 32; + static constexpr int kParamUpdateInterval = 128; // samples between matrix updates while a knob moves (D-057; was 32) static constexpr int kNodes = 21, kElements = 42, kRootPorts = 11; // 10 tube ports + the preamp at y static constexpr double kDefaultHalfStepTrigger = 100.0; // V; D-043 (0 = off) diff --git a/engine/include/amp2203/Preamp.h b/engine/include/amp2203/Preamp.h index a018916..f6be57e 100644 --- a/engine/include/amp2203/Preamp.h +++ b/engine/include/amp2203/Preamp.h @@ -27,7 +27,7 @@ class Preamp { public: static constexpr double kSupplyBpA = 389.349603; // CIRCUIT.md §5; B7 sets them in M4 static constexpr double kSupplyBpX = 402.335999; - static constexpr int kParamUpdateInterval = 32; + static constexpr int kParamUpdateInterval = 128; // samples between matrix updates while a knob moves (D-057; was 32) static constexpr int kNodes = 20, kElements = 38, kRootPorts = 9; // 8 triode ports + the load at y // Component values, CIRCUIT.md §1 and tubes.lib (SI units). diff --git a/engine/src/PowerAmp.cpp b/engine/src/PowerAmp.cpp index c8bb1a5..cffd422 100644 --- a/engine/src/PowerAmp.cpp +++ b/engine/src/PowerAmp.cpp @@ -137,7 +137,7 @@ void PowerAmp::applyKnobs() noexcept { // plain resistors reflect exactly 0 and have no offset (only the windings do): left out of the // open-circuit product (M6) constexpr auto kZeroWave = stage::plainResistors(); - kNet.updateMatrices(step == 1.0 ? root_ : half_, G, mutual, &kZeroWave); + kNet.updateMatrices(step == 1.0 ? root_ : half_, G, mutual, &kZeroWave, false); // plain double (D-057) } ++updates_; } diff --git a/engine/src/Preamp.cpp b/engine/src/Preamp.cpp index e086dfa..1af997d 100644 --- a/engine/src/Preamp.cpp +++ b/engine/src/Preamp.cpp @@ -118,7 +118,7 @@ void Preamp::applyKnobs() noexcept { auto zeroWave = stage::plainResistors(); for (int k = 0; k < kNumElements; ++k) zeroWave[static_cast(k)] = zeroWave[static_cast(k)] && kElementSupplySign[k] == 0; - kNet.updateMatrices(root_, conductances(false), {}, &zeroWave); + kNet.updateMatrices(root_, conductances(false), {}, &zeroWave, false); // plain double (D-057) ++updates_; // R_th4 as ToneStackMaster (D-015): VR2b ‖ (VR2a + VR3b + VR5 + VR4), capacitors open, k2b shorted diff --git a/engine/src/mna/PortNetwork.h b/engine/src/mna/PortNetwork.h index b755567..16b1b75 100644 --- a/engine/src/mna/PortNetwork.h +++ b/engine/src/mna/PortNetwork.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -30,17 +31,25 @@ struct Mutual { namespace detail { +// scalar helpers for the two precisions: DD (D-026, default) and double (M6 D-057: the full amp's run-time updates) +inline double lead(DD x) noexcept { return x.hi; } +inline double lead(double x) noexcept { return x; } +inline double toDouble(DD x) noexcept { return x.toDouble(); } +inline double toDouble(double x) noexcept { return x; } +inline double magnitude(DD x) noexcept { return abs(x); } +inline double magnitude(double x) noexcept { return std::fabs(x); } + /// Gaussian elimination with partial pivoting on an N×N system with M right-hand sides, in place. /// Returns false if a pivot is exactly zero (floating node). -template -bool solve(std::type_identity_t, N>>& Y, - std::type_identity_t, M>>& rhs) noexcept { +template +bool solve(std::type_identity_t, N>>& Y, + std::type_identity_t, M>>& rhs) noexcept { for (int k = 0; k < N; ++k) { int p = k; for (int i = k + 1; i < N; ++i) - if (abs(Y[i][k]) > abs(Y[p][k])) + if (magnitude(Y[i][k]) > magnitude(Y[p][k])) p = i; - if (Y[p][k].hi == 0.0) + if (lead(Y[p][k]) == 0.0) return false; if (p != k) { std::swap(Y[p], Y[k]); @@ -48,8 +57,8 @@ bool solve(std::type_identity_t, N>>& Y, std::swap(r[p], r[k]); } for (int i = k + 1; i < N; ++i) { - const DD f = Y[i][k] / Y[k][k]; - if (f.hi == 0.0) + const T f = Y[i][k] / Y[k][k]; + if (lead(f) == 0.0) continue; for (int j = k; j < N; ++j) Y[i][j] -= f * Y[k][j]; @@ -59,7 +68,7 @@ bool solve(std::type_identity_t, N>>& Y, } for (auto& r : rhs) for (int i = N - 1; i >= 0; --i) { - DD s = r[i]; + T s = r[i]; for (int j = i + 1; j < N; ++j) s -= Y[i][j] * r[j]; r[i] = s / Y[i][i]; @@ -67,10 +76,10 @@ bool solve(std::type_identity_t, N>>& Y, return true; } -template -std::array, N> nodal(const PortNetwork& net, - const std::type_identity_t>& G) noexcept { - std::array, N> Y{}; +template +std::array, N> nodal(const PortNetwork& net, + const std::type_identity_t>& G) noexcept { + std::array, N> Y{}; for (int k = 0; k < P; ++k) { const int a = net.ports[k][0]; const int b = net.ports[k][1]; @@ -87,11 +96,11 @@ std::array, N> nodal(const PortNetwork& net, } /// Adds the mutual stamps to the nodal matrix: the port-conductance block A_kᵀ·g·A_l + A_lᵀ·g·A_k. -template +template void addMutual(const PortNetwork& net, std::span mutual, - std::type_identity_t, N>>& Y) noexcept { + std::type_identity_t, N>>& Y) noexcept { for (const auto& m : mutual) { - const DD g(m.g); + const T g(m.g); for (const auto& [x, y] : {std::array{m.k, m.l}, std::array{m.l, m.k}}) { const int t[2] = {net.ports[x][0], net.ports[x][1]}; const int u[2] = {net.ports[y][0], net.ports[y][1]}; @@ -169,17 +178,18 @@ bool scattering(const PortNetwork& net, const std::type_identity_t +/// T: the arithmetic of the solve, DD (default, D-026) or double (D-057). +template bool rootTransfer(const PortNetwork& elements, const PortNetwork& root, const std::type_identity_t>& G, std::type_identity_t& Xe, std::type_identity_t& Xr, std::span mutual = {}) noexcept { - std::array g{}; + std::array g{}; for (int k = 0; k < P; ++k) - g[k] = DD(G[k]); - auto Y = detail::nodal(elements, g); - detail::addMutual(elements, mutual, Y); - std::array, P + K> rhs{}; - auto inject = [&](int column, int port, const DD& value) { + g[k] = T(G[k]); + auto Y = detail::nodal(elements, g); + detail::addMutual(elements, mutual, Y); + std::array, P + K> rhs{}; + auto inject = [&](int column, int port, const T& value) { if (elements.ports[port][0] != kGround) rhs[column][elements.ports[port][0]] += value; if (elements.ports[port][1] != kGround) @@ -188,23 +198,23 @@ bool rootTransfer(const PortNetwork& elements, const PortNetwork& ro for (int j = 0; j < P; ++j) // Norton injection G_j·b_j for b_j = 1 inject(j, j, g[j]); for (const auto& m : mutual) { - inject(m.l, m.k, DD(m.g)); - inject(m.k, m.l, DD(m.g)); + inject(m.l, m.k, T(m.g)); + inject(m.k, m.l, T(m.g)); } for (int r = 0; r < K; ++r) { // 1 A drawn out of node+ and returned at node− is a −1 / +1 injection; // Xr is stored with the opposite sign (v = Xe·b − Xr·i) if (root.ports[r][0] != kGround) - rhs[P + r][root.ports[r][0]] += DD(1.0); + rhs[P + r][root.ports[r][0]] += T(1.0); if (root.ports[r][1] != kGround) - rhs[P + r][root.ports[r][1]] -= DD(1.0); + rhs[P + r][root.ports[r][1]] -= T(1.0); } - if (!detail::solve(Y, rhs)) + if (!detail::solve(Y, rhs)) return false; for (int n = 0; n < N; ++n) { for (int j = 0; j < P; ++j) - Xe[n][j] = rhs[j][n].toDouble(); + Xe[n][j] = detail::toDouble(rhs[j][n]); for (int r = 0; r < K; ++r) - Xr[n][r] = rhs[P + r][n].toDouble(); + Xr[n][r] = detail::toDouble(rhs[P + r][n]); } return true; } diff --git a/engine/src/stage/RootNetwork.h b/engine/src/stage/RootNetwork.h index 70f8573..dc74705 100644 --- a/engine/src/stage/RootNetwork.h +++ b/engine/src/stage/RootNetwork.h @@ -82,10 +82,13 @@ struct RootNetwork { /// resistors without a supply offset): their Xe columns are left out of the open-circuit product (M6; the /// sum then skips exact zeros and is grouped differently, so results change at the rounding level). /// Returns false (state unchanged) if singular. + /// `doubleDouble`: the solve in double-double (D-026, default) or plain double (D-057, the full amp's roots). bool updateMatrices(State& s, const EVec& G, std::span mutual = {}, - const std::array* zeroWave = nullptr) const noexcept { + const std::array* zeroWave = nullptr, bool doubleDouble = true) const noexcept { double Xe[N][P], Xr[N][K]; - if (!mna::rootTransfer(elements, root, G, Xe, Xr, mutual)) + const bool ok = doubleDouble ? mna::rootTransfer(elements, root, G, Xe, Xr, mutual) + : mna::rootTransfer(elements, root, G, Xe, Xr, mutual); + if (!ok) return false; s.activeCount = 0; for (int j = 0; j < P; ++j) diff --git a/harness/reports/M3/20261015_b1234_knob_jump.md b/harness/reports/M3/20261015_b1234_knob_jump.md new file mode 100644 index 0000000..9843480 --- /dev/null +++ b/harness/reports/M3/20261015_b1234_knob_jump.md @@ -0,0 +1,14 @@ +# M3 report: B1234 knob-jump test + +- Date: 2026-10-15, engine commit `831e393 (dirty)`, ngspice 47, fs = 768000 Hz, SPICE reltol 1e-08 +- Knob targets 0 → 1 in 10 ms from t = 20 ms during a 1000 Hz, 0.1 V tone at the guitar input (DC 0.0000 V); other knobs at their defaults. Engine: smoothing 20 ms, matrix update every 32 samples. SPICE: pot resistances follow the same smoothed trajectory continuously. +- Window: the ramp and the 20 ms after it. Pass (D-025): no NaN/Inf, ESR ≤ -20 dB. + +| Case | Finite | ESR | Status | Peak error / ref peak | Error energy > 5 kHz vs tone | +|---|---|---|---|---|---| +| preampVolume | yes | -41.0 dB | PASS | 2.54e-02 | -44.3 dB | +| treble | yes | -54.2 dB | PASS | 5.63e-03 | -56.6 dB | +| middle | yes | -50.4 dB | PASS | 5.15e-03 | -57.9 dB | +| bass | yes | -55.1 dB | PASS | 3.56e-03 | -64.9 dB | +| master | yes | -41.7 dB | PASS | 8.01e-03 | -45.9 dB | +| preampVolume+treble+middle+bass+master | yes | -39.2 dB | PASS | 1.18e-02 | -43.3 dB | diff --git a/harness/reports/M5/20261015_b56_knob_jump.md b/harness/reports/M5/20261015_b56_knob_jump.md new file mode 100644 index 0000000..6e01933 --- /dev/null +++ b/harness/reports/M5/20261015_b56_knob_jump.md @@ -0,0 +1,9 @@ +# M5 report: B56 knob-jump test + +- Date: 2026-10-15, engine commit `831e393 (dirty)`, ngspice 47, fs = 768000 Hz, SPICE reltol 1e-08 +- Knob targets 0 → 1 in 10 ms from t = 20 ms during a 1000 Hz, 1 V tone on the J4 DC level (DC 0.0000 V); other knobs at their defaults. Engine: smoothing 20 ms, matrix update every 32 samples. SPICE: pot resistances follow the same smoothed trajectory continuously. +- Window: the ramp and the 20 ms after it. Pass (D-025): no NaN/Inf, ESR ≤ -20 dB. + +| Case | Finite | ESR | Status | Peak error / ref peak | Error energy > 5 kHz vs tone | +|---|---|---|---|---|---| +| presence | yes | -63.2 dB | PASS | 1.64e-03 | -70.3 dB | diff --git a/harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md b/harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md index b3cdd1d..a9591f1 100644 --- a/harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md +++ b/harness/reports/M6/20261015_automation_fs96000_pdiv2_j4exact.md @@ -1,6 +1,6 @@ # M5 automation: every knob over its full range at audio rate -- Date: 2026-10-15, commit `9778d0d (dirty)`, engine `ampwav --block full` at 96000 Hz +- Date: 2026-10-15, commit `831e393 (dirty)`, engine `ampwav --block full` at 96000 Hz - Modulation: 10 Hz triangle 0 → 1 → 0 (`lfo`) or 0 ↔ 1 jumps every 50 ms (`jump`) on the knob targets (smoothed by the engine, 20 ms); bases: default knobs, every knob at 1.0. - SPEC §7 M5: no NaN / Inf, no Newton failures. Newton failures count first solves that did not converge in 12 iterations; the fallback (D-042) rescued or did not rescue them. @@ -8,59 +8,59 @@ Runs: 56 · non-finite outputs: 0 · Newton failures (first solve): 0 · unrescu | Base | Knobs | Modulation | Signal | finite | peak [V] | solves | failures | rescued | unrescued | median / max it | ns/sample | |---|---|---|---|---|---|---|---|---|---|---|---| -| default | preampVolume | lfo | SIN-220Hz-0.3V | yes | 33.2 | 192033 | 0 | 0 | 0 | 1 / 19 | 4949 | -| default | preampVolume | lfo | DI-GTR L | yes | 47.2 | 960095 | 0 | 0 | 0 | 1 / 17 | 5208 | -| default | preampVolume | jump | SIN-220Hz-0.3V | yes | 38.4 | 192033 | 0 | 0 | 0 | 1 / 25 | 4931 | -| default | preampVolume | jump | DI-GTR L | yes | 45.9 | 960165 | 0 | 0 | 0 | 1 / 19 | 5209 | -| default | treble | lfo | SIN-220Hz-0.3V | yes | 40.3 | 192033 | 0 | 0 | 0 | 1 / 22 | 5034 | -| default | treble | lfo | DI-GTR L | yes | 50.7 | 960114 | 0 | 0 | 0 | 1 / 19 | 5662 | -| default | treble | jump | SIN-220Hz-0.3V | yes | 32.5 | 192034 | 0 | 0 | 0 | 1 / 22 | 4916 | -| default | treble | jump | DI-GTR L | yes | 65.6 | 960208 | 0 | 0 | 0 | 1 / 19 | 5538 | -| default | middle | lfo | SIN-220Hz-0.3V | yes | 34.1 | 192033 | 0 | 0 | 0 | 1 / 25 | 5234 | -| default | middle | lfo | DI-GTR L | yes | 45.9 | 960112 | 0 | 0 | 0 | 1 / 18 | 5641 | -| default | middle | jump | SIN-220Hz-0.3V | yes | 38.7 | 192033 | 0 | 0 | 0 | 1 / 22 | 4961 | -| default | middle | jump | DI-GTR L | yes | 52.5 | 960143 | 0 | 0 | 0 | 1 / 19 | 5613 | -| default | bass | lfo | SIN-220Hz-0.3V | yes | 37.1 | 192033 | 0 | 0 | 0 | 1 / 22 | 5019 | -| default | bass | lfo | DI-GTR L | yes | 45.5 | 960126 | 0 | 0 | 0 | 1 / 19 | 5335 | -| default | bass | jump | SIN-220Hz-0.3V | yes | 42.2 | 192033 | 0 | 0 | 0 | 1 / 22 | 4866 | -| default | bass | jump | DI-GTR L | yes | 48.3 | 960123 | 0 | 0 | 0 | 1 / 17 | 5219 | -| default | master | lfo | SIN-220Hz-0.3V | yes | 69.3 | 192040 | 0 | 0 | 0 | 1 / 22 | 5343 | -| default | master | lfo | DI-GTR L | yes | 75.1 | 966556 | 0 | 0 | 0 | 1 / 25 | 5877 | -| default | master | jump | SIN-220Hz-0.3V | yes | 75.9 | 192466 | 0 | 0 | 0 | 1 / 22 | 5298 | -| default | master | jump | DI-GTR L | yes | 81.2 | 969786 | 0 | 0 | 0 | 1 / 22 | 5834 | -| default | presence | lfo | SIN-220Hz-0.3V | yes | 34.2 | 192033 | 0 | 0 | 0 | 1 / 22 | 5685 | -| default | presence | lfo | DI-GTR L | yes | 45.6 | 960120 | 0 | 0 | 0 | 1 / 18 | 5973 | -| default | presence | jump | SIN-220Hz-0.3V | yes | 49.3 | 192034 | 0 | 0 | 0 | 1 / 22 | 5520 | -| default | presence | jump | DI-GTR L | yes | 65.6 | 960191 | 0 | 0 | 0 | 1 / 18 | 5924 | -| default | all | lfo | SIN-220Hz-0.3V | yes | 71.1 | 192398 | 0 | 0 | 0 | 1 / 11 | 9666 | -| default | all | lfo | DI-GTR L | yes | 78.7 | 970077 | 0 | 0 | 0 | 1 / 23 | 12244 | -| default | all | jump | SIN-220Hz-0.3V | yes | 77.5 | 195640 | 0 | 0 | 0 | 1 / 25 | 9566 | -| default | all | jump | DI-GTR L | yes | 82.2 | 976641 | 0 | 0 | 0 | 1 / 24 | 13015 | -| max | preampVolume | lfo | SIN-220Hz-0.3V | yes | 74.4 | 200376 | 0 | 0 | 0 | 2 / 13 | 6401 | -| max | preampVolume | lfo | DI-GTR L | yes | 92.5 | 1031436 | 0 | 0 | 0 | 2 / 22 | 6701 | -| max | preampVolume | jump | SIN-220Hz-0.3V | yes | 84.1 | 204462 | 0 | 0 | 0 | 2 / 25 | 6299 | -| max | preampVolume | jump | DI-GTR L | yes | 96.3 | 1030859 | 0 | 0 | 0 | 2 / 19 | 6615 | -| max | treble | lfo | SIN-220Hz-0.3V | yes | 76.6 | 211133 | 0 | 0 | 0 | 1 / 25 | 6722 | -| max | treble | lfo | DI-GTR L | yes | 88.7 | 1024109 | 0 | 0 | 0 | 2 / 26 | 6767 | -| max | treble | jump | SIN-220Hz-0.3V | yes | 81.2 | 209657 | 0 | 0 | 0 | 1 / 25 | 6481 | -| max | treble | jump | DI-GTR L | yes | 90.8 | 1020679 | 0 | 0 | 0 | 2 / 28 | 6618 | -| max | middle | lfo | SIN-220Hz-0.3V | yes | 85.5 | 211603 | 0 | 0 | 0 | 2 / 25 | 6806 | -| max | middle | lfo | DI-GTR L | yes | 95.5 | 1020606 | 0 | 0 | 0 | 2 / 20 | 6662 | -| max | middle | jump | SIN-220Hz-0.3V | yes | 87.2 | 212094 | 0 | 0 | 0 | 2 / 25 | 6548 | -| max | middle | jump | DI-GTR L | yes | 92.6 | 1020912 | 0 | 0 | 0 | 2 / 21 | 6572 | -| max | bass | lfo | SIN-220Hz-0.3V | yes | 79.7 | 215682 | 0 | 0 | 0 | 2 / 25 | 6933 | -| max | bass | lfo | DI-GTR L | yes | 90.1 | 1031382 | 0 | 0 | 0 | 2 / 20 | 6540 | -| max | bass | jump | SIN-220Hz-0.3V | yes | 81.5 | 214476 | 0 | 0 | 0 | 2 / 25 | 7001 | -| max | bass | jump | DI-GTR L | yes | 92.9 | 1028087 | 0 | 0 | 0 | 2 / 20 | 6419 | -| max | master | lfo | SIN-220Hz-0.3V | yes | 73.3 | 199385 | 0 | 0 | 0 | 1 / 25 | 6042 | -| max | master | lfo | DI-GTR L | yes | 81.6 | 984915 | 0 | 0 | 0 | 1 / 27 | 6291 | -| max | master | jump | SIN-220Hz-0.3V | yes | 77.5 | 201669 | 0 | 0 | 0 | 1 / 25 | 5960 | -| max | master | jump | DI-GTR L | yes | 86.4 | 985722 | 0 | 0 | 0 | 1 / 25 | 6136 | -| max | presence | lfo | SIN-220Hz-0.3V | yes | 82.6 | 211567 | 0 | 0 | 0 | 2 / 25 | 7378 | -| max | presence | lfo | DI-GTR L | yes | 89.3 | 1016866 | 0 | 0 | 0 | 2 / 19 | 6999 | -| max | presence | jump | SIN-220Hz-0.3V | yes | 79.4 | 212158 | 0 | 0 | 0 | 2 / 25 | 7131 | -| max | presence | jump | DI-GTR L | yes | 90.6 | 1017189 | 0 | 0 | 0 | 2 / 21 | 6987 | -| max | all | lfo | SIN-220Hz-0.3V | yes | 71.1 | 192398 | 0 | 0 | 0 | 1 / 11 | 9643 | -| max | all | lfo | DI-GTR L | yes | 78.7 | 970077 | 0 | 0 | 0 | 1 / 23 | 12208 | -| max | all | jump | SIN-220Hz-0.3V | yes | 77.5 | 195640 | 0 | 0 | 0 | 1 / 25 | 9560 | -| max | all | jump | DI-GTR L | yes | 82.2 | 976641 | 0 | 0 | 0 | 1 / 24 | 12879 | +| default | preampVolume | lfo | SIN-220Hz-0.3V | yes | 33.0 | 192033 | 0 | 0 | 0 | 1 / 12 | 1989 | +| default | preampVolume | lfo | DI-GTR L | yes | 46.9 | 960098 | 0 | 0 | 0 | 1 / 17 | 2290 | +| default | preampVolume | jump | SIN-220Hz-0.3V | yes | 38.4 | 192033 | 0 | 0 | 0 | 1 / 25 | 1972 | +| default | preampVolume | jump | DI-GTR L | yes | 47.1 | 960162 | 0 | 0 | 0 | 1 / 20 | 2245 | +| default | treble | lfo | SIN-220Hz-0.3V | yes | 40.4 | 192033 | 0 | 0 | 0 | 1 / 22 | 2104 | +| default | treble | lfo | DI-GTR L | yes | 50.8 | 960112 | 0 | 0 | 0 | 1 / 19 | 2773 | +| default | treble | jump | SIN-220Hz-0.3V | yes | 32.5 | 192034 | 0 | 0 | 0 | 1 / 22 | 2115 | +| default | treble | jump | DI-GTR L | yes | 65.2 | 960214 | 0 | 0 | 0 | 1 / 19 | 2678 | +| default | middle | lfo | SIN-220Hz-0.3V | yes | 34.1 | 192033 | 0 | 0 | 0 | 1 / 25 | 2122 | +| default | middle | lfo | DI-GTR L | yes | 45.7 | 960112 | 0 | 0 | 0 | 1 / 18 | 2765 | +| default | middle | jump | SIN-220Hz-0.3V | yes | 38.7 | 192033 | 0 | 0 | 0 | 1 / 22 | 2135 | +| default | middle | jump | DI-GTR L | yes | 52.5 | 960141 | 0 | 0 | 0 | 1 / 19 | 2700 | +| default | bass | lfo | SIN-220Hz-0.3V | yes | 37.1 | 192033 | 0 | 0 | 0 | 1 / 22 | 1971 | +| default | bass | lfo | DI-GTR L | yes | 45.6 | 960126 | 0 | 0 | 0 | 1 / 19 | 2289 | +| default | bass | jump | SIN-220Hz-0.3V | yes | 42.2 | 192033 | 0 | 0 | 0 | 1 / 22 | 1971 | +| default | bass | jump | DI-GTR L | yes | 48.3 | 960122 | 0 | 0 | 0 | 1 / 17 | 2333 | +| default | master | lfo | SIN-220Hz-0.3V | yes | 69.3 | 192043 | 0 | 0 | 0 | 1 / 22 | 2387 | +| default | master | lfo | DI-GTR L | yes | 74.8 | 966574 | 0 | 0 | 0 | 1 / 23 | 2980 | +| default | master | jump | SIN-220Hz-0.3V | yes | 75.9 | 192428 | 0 | 0 | 0 | 1 / 22 | 2501 | +| default | master | jump | DI-GTR L | yes | 79.6 | 969939 | 0 | 0 | 0 | 1 / 24 | 2964 | +| default | presence | lfo | SIN-220Hz-0.3V | yes | 34.2 | 192033 | 0 | 0 | 0 | 1 / 22 | 1996 | +| default | presence | lfo | DI-GTR L | yes | 45.6 | 960120 | 0 | 0 | 0 | 1 / 18 | 2311 | +| default | presence | jump | SIN-220Hz-0.3V | yes | 49.3 | 192034 | 0 | 0 | 0 | 1 / 22 | 2026 | +| default | presence | jump | DI-GTR L | yes | 65.6 | 960187 | 0 | 0 | 0 | 1 / 18 | 2318 | +| default | all | lfo | SIN-220Hz-0.3V | yes | 70.9 | 192486 | 0 | 0 | 0 | 1 / 10 | 3135 | +| default | all | lfo | DI-GTR L | yes | 75.4 | 969884 | 0 | 0 | 0 | 1 / 25 | 5848 | +| default | all | jump | SIN-220Hz-0.3V | yes | 77.5 | 196473 | 0 | 0 | 0 | 1 / 25 | 3366 | +| default | all | jump | DI-GTR L | yes | 79.7 | 976884 | 0 | 0 | 0 | 1 / 23 | 6042 | +| max | preampVolume | lfo | SIN-220Hz-0.3V | yes | 75.8 | 199130 | 0 | 0 | 0 | 2 / 12 | 3276 | +| max | preampVolume | lfo | DI-GTR L | yes | 91.6 | 1028822 | 0 | 0 | 0 | 2 / 20 | 3792 | +| max | preampVolume | jump | SIN-220Hz-0.3V | yes | 82.9 | 204823 | 0 | 0 | 0 | 2 / 25 | 3528 | +| max | preampVolume | jump | DI-GTR L | yes | 91.6 | 1029192 | 0 | 0 | 0 | 2 / 20 | 3749 | +| max | treble | lfo | SIN-220Hz-0.3V | yes | 77.2 | 209907 | 0 | 0 | 0 | 1 / 25 | 3741 | +| max | treble | lfo | DI-GTR L | yes | 90.4 | 1023943 | 0 | 0 | 0 | 2 / 26 | 3922 | +| max | treble | jump | SIN-220Hz-0.3V | yes | 79.7 | 210856 | 0 | 0 | 0 | 1 / 25 | 3795 | +| max | treble | jump | DI-GTR L | yes | 89.9 | 1023805 | 0 | 0 | 0 | 2 / 25 | 3778 | +| max | middle | lfo | SIN-220Hz-0.3V | yes | 84.9 | 211625 | 0 | 0 | 0 | 2 / 25 | 3785 | +| max | middle | lfo | DI-GTR L | yes | 93.1 | 1021602 | 0 | 0 | 0 | 2 / 19 | 3806 | +| max | middle | jump | SIN-220Hz-0.3V | yes | 83.8 | 210931 | 0 | 0 | 0 | 2 / 25 | 3756 | +| max | middle | jump | DI-GTR L | yes | 92.6 | 1021405 | 0 | 0 | 0 | 2 / 19 | 3723 | +| max | bass | lfo | SIN-220Hz-0.3V | yes | 82.6 | 216639 | 0 | 0 | 0 | 2 / 25 | 4078 | +| max | bass | lfo | DI-GTR L | yes | 92.2 | 1031162 | 0 | 0 | 0 | 2 / 25 | 3538 | +| max | bass | jump | SIN-220Hz-0.3V | yes | 83.0 | 214264 | 0 | 0 | 0 | 2 / 25 | 3877 | +| max | bass | jump | DI-GTR L | yes | 92.6 | 1030219 | 0 | 0 | 0 | 2 / 20 | 3549 | +| max | master | lfo | SIN-220Hz-0.3V | yes | 73.5 | 199476 | 0 | 0 | 0 | 1 / 25 | 3164 | +| max | master | lfo | DI-GTR L | yes | 83.6 | 984506 | 0 | 0 | 0 | 1 / 26 | 3423 | +| max | master | jump | SIN-220Hz-0.3V | yes | 77.5 | 202077 | 0 | 0 | 0 | 1 / 25 | 3233 | +| max | master | jump | DI-GTR L | yes | 86.5 | 986627 | 0 | 0 | 0 | 1 / 25 | 3330 | +| max | presence | lfo | SIN-220Hz-0.3V | yes | 78.6 | 212027 | 0 | 0 | 0 | 2 / 25 | 3796 | +| max | presence | lfo | DI-GTR L | yes | 88.7 | 1015948 | 0 | 0 | 0 | 2 / 19 | 3380 | +| max | presence | jump | SIN-220Hz-0.3V | yes | 81.2 | 212607 | 0 | 0 | 0 | 2 / 25 | 3710 | +| max | presence | jump | DI-GTR L | yes | 91.9 | 1018271 | 0 | 0 | 0 | 2 / 19 | 3400 | +| max | all | lfo | SIN-220Hz-0.3V | yes | 70.9 | 192486 | 0 | 0 | 0 | 1 / 10 | 3163 | +| max | all | lfo | DI-GTR L | yes | 75.4 | 969884 | 0 | 0 | 0 | 1 / 25 | 5741 | +| max | all | jump | SIN-220Hz-0.3V | yes | 77.5 | 196473 | 0 | 0 | 0 | 1 / 25 | 3377 | +| max | all | jump | DI-GTR L | yes | 79.7 | 976884 | 0 | 0 | 0 | 1 / 23 | 5931 | diff --git a/harness/reports/M6/20261015_full_acceptance_fs384000_j4exact.md b/harness/reports/M6/20261015_full_acceptance_fs384000_j4exact.md index bf1d358..fe090e5 100644 --- a/harness/reports/M6/20261015_full_acceptance_fs384000_j4exact.md +++ b/harness/reports/M6/20261015_full_acceptance_fs384000_j4exact.md @@ -1,6 +1,6 @@ # M4 acceptance: full amplifier vs the full netlist -- Date: 2026-10-15, engine commit `f17a4fc (dirty)`, ngspice 47, fs = 384000 Hz, J4 exact, SPICE reltol 1e-08 +- Date: 2026-10-15, engine commit `831e393 (dirty)`, ngspice 47, fs = 384000 Hz, J4 exact, SPICE reltol 1e-08 - Netlist sha256 `7bea8385e4adb04e`, tubes.lib sha256 `9ec33154be8127d7` - Engine: `ampwav --block full` (volts at the jack and across the 16 Ω load), the checks of SPEC §7 M4. @@ -71,8 +71,8 @@ PASS 107 · FAIL 0 | clean | newton_max_iterations | CLEAN SIN-1000Hz-0.025V | 6 | ≤ 32 | PASS | | clean | total_power_rel | CLEAN SIN-1000Hz-0.025V | 0.001105 | ≤ 0.05 | PASS | | clean | total_thd_pp | CLEAN SIN-1000Hz-0.025V | 0.0006695 pp | ≤ 0.5 | PASS | -| stability | stability_out_v | max |out| | 4.409e-05 V | ≤ 0.001 | PASS | -| stability | stability_growth | RMS last 10 s − first 10 s | 1.83e-12 V | ≤ 1e-06 | PASS | +| stability | stability_out_v | max |out| | 4.389e-05 V | ≤ 0.001 | PASS | +| stability | stability_growth | RMS last 10 s − first 10 s | 5.497e-12 V | ≤ 1e-06 | PASS | | stability | newton_failures | 60 s silence | 0 | ≤ 0 | PASS | | stability | newton_unrescued | 60 s silence | 0 | — | INFO | | stability | newton_median_iterations | 60 s silence | 1 | ≤ 3 | PASS | @@ -190,7 +190,7 @@ PASS 107 · FAIL 0 ## Stability -60 s silence at Preamp / Master / Presence 1.0: max |out| 4.41e-05 V, RMS first / last 10 s 4.23e-05 / 4.23e-05 V. +60 s silence at Preamp / Master / Presence 1.0: max |out| 4.39e-05 V, RMS first / last 10 s 4.23e-05 / 4.23e-05 V. ## Delayed-NFB break diff --git a/harness/reports/M6/20261015_full_acceptance_fs384000_j4exact_hot.md b/harness/reports/M6/20261015_full_acceptance_fs384000_j4exact_hot.md index 8efb0e3..7fb0450 100644 --- a/harness/reports/M6/20261015_full_acceptance_fs384000_j4exact_hot.md +++ b/harness/reports/M6/20261015_full_acceptance_fs384000_j4exact_hot.md @@ -1,6 +1,6 @@ # M4 acceptance: full amplifier vs the full netlist -- Date: 2026-10-15, engine commit `f17a4fc (dirty)`, ngspice 47, fs = 384000 Hz, J4 exact, SPICE reltol 1e-08 +- Date: 2026-10-15, engine commit `831e393 (dirty)`, ngspice 47, fs = 384000 Hz, J4 exact, SPICE reltol 1e-08 - Netlist sha256 `7bea8385e4adb04e`, tubes.lib sha256 `9ec33154be8127d7` - Engine: `ampwav --block full` (volts at the jack and across the 16 Ω load), the checks of SPEC §7 M4. @@ -16,17 +16,17 @@ PASS 21 · FAIL 0 | hot | newton_unrescued | HOT SIN-220Hz-0.3V | 0 | — | INFO | | hot | newton_median_iterations | HOT SIN-220Hz-0.3V | 2 | ≤ 3 | PASS | | hot | newton_max_iterations | HOT SIN-220Hz-0.3V | 7 | ≤ 32 | PASS | -| hot | total_esr_db | HOT DI-GTR L | -41.4 dB | ≤ -20 | PASS | +| hot | total_esr_db | HOT DI-GTR L | -41.54 dB | ≤ -20 | PASS | | hot | newton_failures | HOT DI-GTR L | 0 | ≤ 0 | PASS | | hot | newton_unrescued | HOT DI-GTR L | 0 | — | INFO | | hot | newton_median_iterations | HOT DI-GTR L | 2 | ≤ 3 | PASS | | hot | newton_max_iterations | HOT DI-GTR L | 17 | ≤ 32 | PASS | -| hot | total_esr_db | HOT DI-GTR LL | -42.05 dB | ≤ -20 | PASS | +| hot | total_esr_db | HOT DI-GTR LL | -42.12 dB | ≤ -20 | PASS | | hot | newton_failures | HOT DI-GTR LL | 0 | ≤ 0 | PASS | | hot | newton_unrescued | HOT DI-GTR LL | 0 | — | INFO | | hot | newton_median_iterations | HOT DI-GTR LL | 2 | ≤ 3 | PASS | | hot | newton_max_iterations | HOT DI-GTR LL | 18 | ≤ 32 | PASS | -| hot | total_esr_db | HOT DI-GTR R | -42.65 dB | ≤ -20 | PASS | +| hot | total_esr_db | HOT DI-GTR R | -42.59 dB | ≤ -20 | PASS | | hot | newton_failures | HOT DI-GTR R | 0 | ≤ 0 | PASS | | hot | newton_unrescued | HOT DI-GTR R | 0 | — | INFO | | hot | newton_median_iterations | HOT DI-GTR R | 2 | ≤ 3 | PASS | @@ -42,9 +42,9 @@ PASS 21 · FAIL 0 | Preset | Signal | ESR | best lag | harm. 2–9 | THD F / E | Power F / E | min v(ct) F / E | |---|---|---|---|---|---|---|---| | HOT | SIN-220Hz-0.3V | -58.9 dB | 0 | 0.01 dB | 46.17 / 46.17 % | 183.4 / 183.4 W | — | -| HOT | DI-GTR L | -41.4 dB | 0 | — | — | — | — | -| HOT | DI-GTR LL | -42.0 dB | 0 | — | — | — | — | -| HOT | DI-GTR R | -42.7 dB | 0 | — | — | — | — | +| HOT | DI-GTR L | -41.5 dB | 0 | — | — | — | — | +| HOT | DI-GTR LL | -42.1 dB | 0 | — | — | — | — | +| HOT | DI-GTR R | -42.6 dB | 0 | — | — | — | — | | HOT | DI-GTR RR | -41.8 dB | 0 | — | — | — | — | ## Delayed-NFB break diff --git a/harness/reports/M6/20261015_plugin_cpu.md b/harness/reports/M6/20261015_plugin_cpu.md index ab95926..7009e67 100644 --- a/harness/reports/M6/20261015_plugin_cpu.md +++ b/harness/reports/M6/20261015_plugin_cpu.md @@ -1,39 +1,39 @@ # M6 plugin CPU under knob automation -- Date 2026-10-15, commit `e274e3d (dirty)`; `amp2203_render plugin` at the plugin defaults (2x, Power Amp Rate Half, exact J4, Newton tol 1e-4 V, half-steps on; D-054 – D-056), FIR oversampling, 20 s of DI GTR L at 48000 Hz; processBlock time only. +- Date 2026-10-15, commit `831e393 (dirty)`; `amp2203_render plugin` at the plugin defaults (2x, Power Amp Rate Half, exact J4, Newton tol 1e-4 V, half-steps on; D-054 – D-056), FIR oversampling, 20 s of DI GTR L at 48000 Hz; processBlock time only. - Automation between host blocks: lfo = 10 Hz triangle 0 → 10 → 0, jump = 0 ↔ 10 every 50 ms; "all" moves the six knobs together. One render at a time. | Base | Knob(s) | Modulation | Host block | CPU (% of one core) | Newton failures (rescued) | |---|---|---|---|---|---| -| default | — | static | 512 | 18.9 | 0 (0) | -| every knob 10 | — | static | 512 | 32.1 | 0 (0) | -| default | preampVolume | lfo | 512 | 45.9 | 0 (0) | -| default | preampVolume | jump | 512 | 45.8 | 0 (0) | -| default | treble | lfo | 512 | 46.6 | 0 (0) | -| default | treble | jump | 512 | 46.4 | 0 (0) | -| default | middle | lfo | 512 | 47.8 | 0 (0) | -| default | middle | jump | 512 | 46.7 | 0 (0) | -| default | bass | lfo | 512 | 46.5 | 0 (0) | -| default | bass | jump | 512 | 47.6 | 0 (0) | -| default | master | lfo | 512 | 48.7 | 0 (0) | -| default | master | jump | 512 | 49.6 | 0 (0) | -| default | presence | lfo | 512 | 55.0 | 0 (0) | -| default | presence | jump | 512 | 54.1 | 0 (0) | -| default | all | lfo | 512 | 84.6 | 0 (0) | -| default | all | jump | 512 | 85.4 | 0 (0) | -| every knob 10 | preampVolume | lfo | 512 | 58.9 | 0 (0) | -| every knob 10 | preampVolume | jump | 512 | 58.6 | 0 (0) | -| every knob 10 | treble | lfo | 512 | 59.3 | 0 (0) | -| every knob 10 | treble | jump | 512 | 59.6 | 0 (0) | -| every knob 10 | middle | lfo | 512 | 59.2 | 0 (0) | -| every knob 10 | middle | jump | 512 | 60.2 | 0 (0) | -| every knob 10 | bass | lfo | 512 | 61.5 | 0 (0) | -| every knob 10 | bass | jump | 512 | 60.8 | 0 (0) | -| every knob 10 | master | lfo | 512 | 55.1 | 0 (0) | -| every knob 10 | master | jump | 512 | 54.4 | 0 (0) | -| every knob 10 | presence | lfo | 512 | 67.7 | 0 (0) | -| every knob 10 | presence | jump | 512 | 66.7 | 0 (0) | -| every knob 10 | all | lfo | 512 | 86.4 | 0 (0) | -| every knob 10 | all | jump | 512 | 86.6 | 0 (0) | -| default | all | jump | 64 | 87.1 | 1 (1) | -| every knob 10 | all | jump | 64 | 86.1 | 1 (1) | +| default | — | static | 512 | 19.6 | 0 (0) | +| every knob 10 | — | static | 512 | 32.5 | 0 (0) | +| default | preampVolume | lfo | 512 | 19.1 | 0 (0) | +| default | preampVolume | jump | 512 | 18.7 | 0 (0) | +| default | treble | lfo | 512 | 19.6 | 0 (0) | +| default | treble | jump | 512 | 19.7 | 0 (0) | +| default | middle | lfo | 512 | 19.5 | 0 (0) | +| default | middle | jump | 512 | 20.2 | 0 (0) | +| default | bass | lfo | 512 | 19.8 | 0 (0) | +| default | bass | jump | 512 | 19.6 | 0 (0) | +| default | master | lfo | 512 | 21.6 | 0 (0) | +| default | master | jump | 512 | 22.6 | 0 (0) | +| default | presence | lfo | 512 | 19.9 | 0 (0) | +| default | presence | jump | 512 | 19.9 | 0 (0) | +| default | all | lfo | 512 | 23.0 | 0 (0) | +| default | all | jump | 512 | 24.6 | 0 (0) | +| every knob 10 | preampVolume | lfo | 512 | 32.0 | 0 (0) | +| every knob 10 | preampVolume | jump | 512 | 31.8 | 0 (0) | +| every knob 10 | treble | lfo | 512 | 32.1 | 0 (0) | +| every knob 10 | treble | jump | 512 | 32.2 | 0 (0) | +| every knob 10 | middle | lfo | 512 | 32.3 | 0 (0) | +| every knob 10 | middle | jump | 512 | 32.1 | 0 (0) | +| every knob 10 | bass | lfo | 512 | 33.4 | 0 (0) | +| every knob 10 | bass | jump | 512 | 33.5 | 0 (0) | +| every knob 10 | master | lfo | 512 | 27.5 | 0 (0) | +| every knob 10 | master | jump | 512 | 27.2 | 0 (0) | +| every knob 10 | presence | lfo | 512 | 32.3 | 0 (0) | +| every knob 10 | presence | jump | 512 | 32.6 | 0 (0) | +| every knob 10 | all | lfo | 512 | 22.8 | 0 (0) | +| every knob 10 | all | jump | 512 | 25.3 | 0 (0) | +| default | all | jump | 64 | 24.7 | 0 (0) | +| every knob 10 | all | jump | 64 | 24.6 | 0 (0) | diff --git a/harness/reports/M6/20261015_plugin_null_2x_full.md b/harness/reports/M6/20261015_plugin_null_2x_full.md index 7ed1f0d..b0f15f2 100644 --- a/harness/reports/M6/20261015_plugin_null_2x_full.md +++ b/harness/reports/M6/20261015_plugin_null_2x_full.md @@ -1,6 +1,6 @@ # M5 wrapper null test: plugin vs ampwav at 96 kHz with the same resampling -- Date: 2026-10-15, commit `9778d0d (dirty)`; host rate 48000 Hz, 2x, Power Amp Rate full, exact J4 +- Date: 2026-10-15, commit `831e393 (dirty)`; host rate 48000 Hz, 2x, Power Amp Rate full, exact J4 - Plugin: `amp2203_render plugin` (the Amp2203Processor offline). Reference: the processor's oversampler up stage, `ampwav --block full --units fs --j4 exact` at 96000 Hz, the down stage. Default knobs and levels. - Tolerance (SPEC §7 M5): ESR ≤ -60 dB. @@ -8,55 +8,55 @@ PASS 26 · FAIL 0 | Filter | Signal | Peak out [FS] | ESR | Status | Render log | |---|---|---|---|---|---| -| fir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.041 s for 0.200 s of audio: 20.4 % of one core, real-time factor 4.91 +| fir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.041 s for 0.200 s of audio: 20.3 % of one core, real-time factor 4.93 latency 49 samples, Newton failures 0 (rescued 0), max iterations 3 | -| fir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.044 s for 0.200 s of audio: 22.0 % of one core, real-time factor 4.55 +| fir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.043 s for 0.200 s of audio: 21.7 % of one core, real-time factor 4.60 latency 49 samples, Newton failures 0 (rescued 0), max iterations 4 | -| fir | SIN-100Hz--6dBFS | 0.825 | -138.5 dB | PASS | processBlock 0.045 s for 0.200 s of audio: 22.3 % of one core, real-time factor 4.48 +| fir | SIN-100Hz--6dBFS | 0.825 | -138.5 dB | PASS | processBlock 0.044 s for 0.200 s of audio: 21.9 % of one core, real-time factor 4.57 latency 49 samples, Newton failures 0 (rescued 0), max iterations 9 | -| fir | SIN-1000Hz--40dBFS | 0.341 | -138.5 dB | PASS | processBlock 0.065 s for 0.200 s of audio: 32.4 % of one core, real-time factor 3.09 +| fir | SIN-1000Hz--40dBFS | 0.341 | -138.5 dB | PASS | processBlock 0.064 s for 0.200 s of audio: 32.1 % of one core, real-time factor 3.12 latency 49 samples, Newton failures 0 (rescued 0), max iterations 6 | -| fir | SIN-1000Hz--20dBFS | 0.708 | -138.5 dB | PASS | processBlock 0.069 s for 0.200 s of audio: 34.5 % of one core, real-time factor 2.90 +| fir | SIN-1000Hz--20dBFS | 0.708 | -138.5 dB | PASS | processBlock 0.068 s for 0.200 s of audio: 34.1 % of one core, real-time factor 2.93 latency 49 samples, Newton failures 0 (rescued 0), max iterations 9 | -| fir | SIN-1000Hz--6dBFS | 0.704 | -138.5 dB | PASS | processBlock 0.075 s for 0.200 s of audio: 37.4 % of one core, real-time factor 2.67 +| fir | SIN-1000Hz--6dBFS | 0.704 | -138.5 dB | PASS | processBlock 0.074 s for 0.200 s of audio: 37.1 % of one core, real-time factor 2.69 latency 49 samples, Newton failures 0 (rescued 0), max iterations 12 | -| fir | SIN-5000Hz--40dBFS | 0.677 | -138.5 dB | PASS | processBlock 0.091 s for 0.200 s of audio: 45.4 % of one core, real-time factor 2.20 +| fir | SIN-5000Hz--40dBFS | 0.677 | -138.5 dB | PASS | processBlock 0.090 s for 0.200 s of audio: 45.2 % of one core, real-time factor 2.21 latency 49 samples, Newton failures 0 (rescued 0), max iterations 18 | -| fir | SIN-5000Hz--20dBFS | 0.706 | -138.5 dB | PASS | processBlock 0.110 s for 0.200 s of audio: 54.8 % of one core, real-time factor 1.82 +| fir | SIN-5000Hz--20dBFS | 0.706 | -138.5 dB | PASS | processBlock 0.109 s for 0.200 s of audio: 54.5 % of one core, real-time factor 1.83 latency 49 samples, Newton failures 0 (rescued 0), max iterations 19 | -| fir | SIN-5000Hz--6dBFS | 0.738 | -138.5 dB | PASS | processBlock 0.176 s for 0.200 s of audio: 88.2 % of one core, real-time factor 1.13 +| fir | SIN-5000Hz--6dBFS | 0.738 | -138.5 dB | PASS | processBlock 0.176 s for 0.200 s of audio: 87.8 % of one core, real-time factor 1.14 latency 49 samples, Newton failures 441 (rescued 194), max iterations 32 | -| fir | DI-GTR L | 0.767 | -138.5 dB | PASS | processBlock 1.232 s for 5.000 s of audio: 24.6 % of one core, real-time factor 4.06 +| fir | DI-GTR L | 0.767 | -138.5 dB | PASS | processBlock 1.237 s for 5.000 s of audio: 24.7 % of one core, real-time factor 4.04 latency 49 samples, Newton failures 0 (rescued 0), max iterations 17 | -| fir | DI-GTR LL | 0.711 | -138.5 dB | PASS | processBlock 1.196 s for 5.000 s of audio: 23.9 % of one core, real-time factor 4.18 +| fir | DI-GTR LL | 0.711 | -138.5 dB | PASS | processBlock 1.199 s for 5.000 s of audio: 24.0 % of one core, real-time factor 4.17 latency 49 samples, Newton failures 0 (rescued 0), max iterations 17 | -| fir | DI-GTR R | 0.782 | -138.5 dB | PASS | processBlock 1.209 s for 5.000 s of audio: 24.2 % of one core, real-time factor 4.14 +| fir | DI-GTR R | 0.782 | -138.5 dB | PASS | processBlock 1.218 s for 5.000 s of audio: 24.4 % of one core, real-time factor 4.10 latency 49 samples, Newton failures 0 (rescued 0), max iterations 20 | -| fir | DI-GTR RR | 0.798 | -138.5 dB | PASS | processBlock 1.241 s for 5.000 s of audio: 24.8 % of one core, real-time factor 4.03 +| fir | DI-GTR RR | 0.798 | -138.5 dB | PASS | processBlock 1.240 s for 5.000 s of audio: 24.8 % of one core, real-time factor 4.03 latency 49 samples, Newton failures 0 (rescued 0), max iterations 18 | -| iir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.041 s for 0.200 s of audio: 20.3 % of one core, real-time factor 4.92 +| iir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.040 s for 0.200 s of audio: 20.1 % of one core, real-time factor 4.96 latency 4 samples, Newton failures 0 (rescued 0), max iterations 2 | -| iir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.044 s for 0.200 s of audio: 22.1 % of one core, real-time factor 4.54 +| iir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.043 s for 0.200 s of audio: 21.7 % of one core, real-time factor 4.62 latency 4 samples, Newton failures 0 (rescued 0), max iterations 6 | -| iir | SIN-100Hz--6dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.044 s for 0.200 s of audio: 21.9 % of one core, real-time factor 4.56 +| iir | SIN-100Hz--6dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.043 s for 0.200 s of audio: 21.6 % of one core, real-time factor 4.62 latency 4 samples, Newton failures 0 (rescued 0), max iterations 6 | -| iir | SIN-1000Hz--40dBFS | 0.362 | -138.5 dB | PASS | processBlock 0.064 s for 0.200 s of audio: 32.2 % of one core, real-time factor 3.10 +| iir | SIN-1000Hz--40dBFS | 0.362 | -138.5 dB | PASS | processBlock 0.063 s for 0.200 s of audio: 31.7 % of one core, real-time factor 3.16 latency 4 samples, Newton failures 0 (rescued 0), max iterations 7 | -| iir | SIN-1000Hz--20dBFS | 0.774 | -138.5 dB | PASS | processBlock 0.068 s for 0.200 s of audio: 34.0 % of one core, real-time factor 2.94 +| iir | SIN-1000Hz--20dBFS | 0.774 | -138.5 dB | PASS | processBlock 0.067 s for 0.200 s of audio: 33.7 % of one core, real-time factor 2.97 latency 4 samples, Newton failures 0 (rescued 0), max iterations 9 | -| iir | SIN-1000Hz--6dBFS | 0.881 | -138.5 dB | PASS | processBlock 0.077 s for 0.200 s of audio: 38.5 % of one core, real-time factor 2.60 +| iir | SIN-1000Hz--6dBFS | 0.881 | -138.5 dB | PASS | processBlock 0.075 s for 0.200 s of audio: 37.3 % of one core, real-time factor 2.68 latency 4 samples, Newton failures 0 (rescued 0), max iterations 22 | -| iir | SIN-5000Hz--40dBFS | 0.816 | -138.5 dB | PASS | processBlock 0.092 s for 0.200 s of audio: 46.0 % of one core, real-time factor 2.17 +| iir | SIN-5000Hz--40dBFS | 0.816 | -138.5 dB | PASS | processBlock 0.090 s for 0.200 s of audio: 45.2 % of one core, real-time factor 2.21 latency 4 samples, Newton failures 0 (rescued 0), max iterations 16 | -| iir | SIN-5000Hz--20dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.111 s for 0.200 s of audio: 55.6 % of one core, real-time factor 1.80 +| iir | SIN-5000Hz--20dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.111 s for 0.200 s of audio: 55.5 % of one core, real-time factor 1.80 latency 4 samples, Newton failures 0 (rescued 0), max iterations 22 | -| iir | SIN-5000Hz--6dBFS | 0.841 | -138.5 dB | PASS | processBlock 0.140 s for 0.200 s of audio: 69.8 % of one core, real-time factor 1.43 +| iir | SIN-5000Hz--6dBFS | 0.841 | -138.5 dB | PASS | processBlock 0.139 s for 0.200 s of audio: 69.7 % of one core, real-time factor 1.44 latency 4 samples, Newton failures 157 (rescued 80), max iterations 32 | -| iir | DI-GTR L | 0.814 | -138.5 dB | PASS | processBlock 1.222 s for 5.000 s of audio: 24.4 % of one core, real-time factor 4.09 +| iir | DI-GTR L | 0.814 | -138.5 dB | PASS | processBlock 1.247 s for 5.000 s of audio: 24.9 % of one core, real-time factor 4.01 latency 4 samples, Newton failures 0 (rescued 0), max iterations 18 | -| iir | DI-GTR LL | 0.781 | -138.5 dB | PASS | processBlock 1.180 s for 5.000 s of audio: 23.6 % of one core, real-time factor 4.24 +| iir | DI-GTR LL | 0.781 | -138.5 dB | PASS | processBlock 1.196 s for 5.000 s of audio: 23.9 % of one core, real-time factor 4.18 latency 4 samples, Newton failures 0 (rescued 0), max iterations 23 | -| iir | DI-GTR R | 0.891 | -138.5 dB | PASS | processBlock 1.188 s for 5.000 s of audio: 23.8 % of one core, real-time factor 4.21 +| iir | DI-GTR R | 0.891 | -138.5 dB | PASS | processBlock 1.180 s for 5.000 s of audio: 23.6 % of one core, real-time factor 4.24 latency 4 samples, Newton failures 0 (rescued 0), max iterations 17 | -| iir | DI-GTR RR | 0.866 | -138.5 dB | PASS | processBlock 1.227 s for 5.000 s of audio: 24.5 % of one core, real-time factor 4.08 +| iir | DI-GTR RR | 0.866 | -138.5 dB | PASS | processBlock 1.213 s for 5.000 s of audio: 24.3 % of one core, real-time factor 4.12 latency 4 samples, Newton failures 0 (rescued 0), max iterations 20 | diff --git a/harness/reports/M6/20261015_plugin_null_2x_half.md b/harness/reports/M6/20261015_plugin_null_2x_half.md index 796287f..17cbb90 100644 --- a/harness/reports/M6/20261015_plugin_null_2x_half.md +++ b/harness/reports/M6/20261015_plugin_null_2x_half.md @@ -1,6 +1,6 @@ # M5 wrapper null test: plugin vs ampwav at 96 kHz with the same resampling -- Date: 2026-10-15, commit `9778d0d (dirty)`; host rate 48000 Hz, 2x, Power Amp Rate half, exact J4 +- Date: 2026-10-15, commit `831e393 (dirty)`; host rate 48000 Hz, 2x, Power Amp Rate half, exact J4 - Plugin: `amp2203_render plugin` (the Amp2203Processor offline). Reference: the processor's oversampler up stage, `ampwav --block full --units fs --j4 exact` at 96000 Hz, the down stage. Default knobs and levels. - Tolerance (SPEC §7 M5): ESR ≤ -60 dB. @@ -8,55 +8,55 @@ PASS 26 · FAIL 0 | Filter | Signal | Peak out [FS] | ESR | Status | Render log | |---|---|---|---|---|---| -| fir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.029 s for 0.200 s of audio: 14.4 % of one core, real-time factor 6.95 +| fir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.028 s for 0.200 s of audio: 14.0 % of one core, real-time factor 7.16 latency 58 samples, Newton failures 0 (rescued 0), max iterations 3 | -| fir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.031 s for 0.200 s of audio: 15.7 % of one core, real-time factor 6.38 +| fir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.031 s for 0.200 s of audio: 15.3 % of one core, real-time factor 6.53 latency 58 samples, Newton failures 0 (rescued 0), max iterations 4 | -| fir | SIN-100Hz--6dBFS | 0.825 | -138.5 dB | PASS | processBlock 0.033 s for 0.200 s of audio: 16.3 % of one core, real-time factor 6.14 +| fir | SIN-100Hz--6dBFS | 0.825 | -138.5 dB | PASS | processBlock 0.032 s for 0.200 s of audio: 15.8 % of one core, real-time factor 6.33 latency 58 samples, Newton failures 0 (rescued 0), max iterations 9 | -| fir | SIN-1000Hz--40dBFS | 0.335 | -138.5 dB | PASS | processBlock 0.046 s for 0.200 s of audio: 23.2 % of one core, real-time factor 4.31 +| fir | SIN-1000Hz--40dBFS | 0.335 | -138.5 dB | PASS | processBlock 0.045 s for 0.200 s of audio: 22.6 % of one core, real-time factor 4.43 latency 58 samples, Newton failures 0 (rescued 0), max iterations 6 | -| fir | SIN-1000Hz--20dBFS | 0.655 | -138.5 dB | PASS | processBlock 0.050 s for 0.200 s of audio: 25.1 % of one core, real-time factor 3.98 +| fir | SIN-1000Hz--20dBFS | 0.655 | -138.5 dB | PASS | processBlock 0.048 s for 0.200 s of audio: 24.2 % of one core, real-time factor 4.14 latency 58 samples, Newton failures 0 (rescued 0), max iterations 9 | -| fir | SIN-1000Hz--6dBFS | 0.708 | -138.5 dB | PASS | processBlock 0.055 s for 0.200 s of audio: 27.4 % of one core, real-time factor 3.64 +| fir | SIN-1000Hz--6dBFS | 0.708 | -138.5 dB | PASS | processBlock 0.055 s for 0.200 s of audio: 27.3 % of one core, real-time factor 3.67 latency 58 samples, Newton failures 0 (rescued 0), max iterations 12 | -| fir | SIN-5000Hz--40dBFS | 0.703 | -138.5 dB | PASS | processBlock 0.067 s for 0.200 s of audio: 33.5 % of one core, real-time factor 2.98 +| fir | SIN-5000Hz--40dBFS | 0.703 | -138.5 dB | PASS | processBlock 0.067 s for 0.200 s of audio: 33.3 % of one core, real-time factor 3.00 latency 58 samples, Newton failures 0 (rescued 0), max iterations 18 | -| fir | SIN-5000Hz--20dBFS | 0.706 | -138.5 dB | PASS | processBlock 0.078 s for 0.200 s of audio: 38.9 % of one core, real-time factor 2.57 +| fir | SIN-5000Hz--20dBFS | 0.706 | -138.5 dB | PASS | processBlock 0.077 s for 0.200 s of audio: 38.7 % of one core, real-time factor 2.59 latency 58 samples, Newton failures 0 (rescued 0), max iterations 19 | -| fir | SIN-5000Hz--6dBFS | 0.744 | -138.5 dB | PASS | processBlock 0.145 s for 0.200 s of audio: 72.5 % of one core, real-time factor 1.38 +| fir | SIN-5000Hz--6dBFS | 0.744 | -138.5 dB | PASS | processBlock 0.143 s for 0.200 s of audio: 71.7 % of one core, real-time factor 1.39 latency 58 samples, Newton failures 449 (rescued 191), max iterations 32 | -| fir | DI-GTR L | 0.741 | -138.5 dB | PASS | processBlock 0.889 s for 5.000 s of audio: 17.8 % of one core, real-time factor 5.63 +| fir | DI-GTR L | 0.741 | -138.5 dB | PASS | processBlock 0.877 s for 5.000 s of audio: 17.5 % of one core, real-time factor 5.70 latency 58 samples, Newton failures 0 (rescued 0), max iterations 17 | -| fir | DI-GTR LL | 0.756 | -138.5 dB | PASS | processBlock 0.870 s for 5.000 s of audio: 17.4 % of one core, real-time factor 5.74 +| fir | DI-GTR LL | 0.756 | -138.5 dB | PASS | processBlock 0.858 s for 5.000 s of audio: 17.2 % of one core, real-time factor 5.83 latency 58 samples, Newton failures 0 (rescued 0), max iterations 17 | -| fir | DI-GTR R | 0.783 | -138.5 dB | PASS | processBlock 0.873 s for 5.000 s of audio: 17.5 % of one core, real-time factor 5.73 +| fir | DI-GTR R | 0.783 | -138.5 dB | PASS | processBlock 0.860 s for 5.000 s of audio: 17.2 % of one core, real-time factor 5.81 latency 58 samples, Newton failures 0 (rescued 0), max iterations 20 | -| fir | DI-GTR RR | 0.758 | -138.5 dB | PASS | processBlock 0.890 s for 5.000 s of audio: 17.8 % of one core, real-time factor 5.62 +| fir | DI-GTR RR | 0.758 | -138.5 dB | PASS | processBlock 0.882 s for 5.000 s of audio: 17.6 % of one core, real-time factor 5.67 latency 58 samples, Newton failures 0 (rescued 0), max iterations 18 | -| iir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.028 s for 0.200 s of audio: 14.0 % of one core, real-time factor 7.12 +| iir | SIN-100Hz--40dBFS | 0.111 | -138.5 dB | PASS | processBlock 0.028 s for 0.200 s of audio: 13.9 % of one core, real-time factor 7.21 latency 13 samples, Newton failures 0 (rescued 0), max iterations 2 | -| iir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.031 s for 0.200 s of audio: 15.5 % of one core, real-time factor 6.45 +| iir | SIN-100Hz--20dBFS | 0.719 | -138.5 dB | PASS | processBlock 0.031 s for 0.200 s of audio: 15.3 % of one core, real-time factor 6.54 latency 13 samples, Newton failures 0 (rescued 0), max iterations 6 | -| iir | SIN-100Hz--6dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.033 s for 0.200 s of audio: 16.3 % of one core, real-time factor 6.14 +| iir | SIN-100Hz--6dBFS | 0.826 | -138.5 dB | PASS | processBlock 0.031 s for 0.200 s of audio: 15.5 % of one core, real-time factor 6.46 latency 13 samples, Newton failures 0 (rescued 0), max iterations 6 | -| iir | SIN-1000Hz--40dBFS | 0.358 | -138.5 dB | PASS | processBlock 0.046 s for 0.200 s of audio: 22.9 % of one core, real-time factor 4.36 +| iir | SIN-1000Hz--40dBFS | 0.358 | -138.5 dB | PASS | processBlock 0.045 s for 0.200 s of audio: 22.3 % of one core, real-time factor 4.48 latency 13 samples, Newton failures 0 (rescued 0), max iterations 7 | -| iir | SIN-1000Hz--20dBFS | 0.729 | -138.5 dB | PASS | processBlock 0.049 s for 0.200 s of audio: 24.6 % of one core, real-time factor 4.06 +| iir | SIN-1000Hz--20dBFS | 0.729 | -138.5 dB | PASS | processBlock 0.047 s for 0.200 s of audio: 23.6 % of one core, real-time factor 4.23 latency 13 samples, Newton failures 0 (rescued 0), max iterations 9 | -| iir | SIN-1000Hz--6dBFS | 0.766 | -138.5 dB | PASS | processBlock 0.054 s for 0.200 s of audio: 27.1 % of one core, real-time factor 3.69 +| iir | SIN-1000Hz--6dBFS | 0.766 | -138.5 dB | PASS | processBlock 0.054 s for 0.200 s of audio: 26.9 % of one core, real-time factor 3.72 latency 13 samples, Newton failures 0 (rescued 0), max iterations 22 | -| iir | SIN-5000Hz--40dBFS | 0.682 | -138.5 dB | PASS | processBlock 0.067 s for 0.200 s of audio: 33.5 % of one core, real-time factor 2.99 +| iir | SIN-5000Hz--40dBFS | 0.682 | -138.5 dB | PASS | processBlock 0.067 s for 0.200 s of audio: 33.4 % of one core, real-time factor 3.00 latency 13 samples, Newton failures 0 (rescued 0), max iterations 16 | -| iir | SIN-5000Hz--20dBFS | 0.788 | -138.5 dB | PASS | processBlock 0.076 s for 0.200 s of audio: 38.1 % of one core, real-time factor 2.63 +| iir | SIN-5000Hz--20dBFS | 0.788 | -138.5 dB | PASS | processBlock 0.080 s for 0.200 s of audio: 40.2 % of one core, real-time factor 2.49 latency 13 samples, Newton failures 0 (rescued 0), max iterations 22 | -| iir | SIN-5000Hz--6dBFS | 0.803 | -138.5 dB | PASS | processBlock 0.107 s for 0.200 s of audio: 53.7 % of one core, real-time factor 1.86 +| iir | SIN-5000Hz--6dBFS | 0.803 | -138.5 dB | PASS | processBlock 0.108 s for 0.200 s of audio: 54.0 % of one core, real-time factor 1.85 latency 13 samples, Newton failures 163 (rescued 86), max iterations 32 | -| iir | DI-GTR L | 0.796 | -138.5 dB | PASS | processBlock 0.871 s for 5.000 s of audio: 17.4 % of one core, real-time factor 5.74 +| iir | DI-GTR L | 0.796 | -138.5 dB | PASS | processBlock 0.869 s for 5.000 s of audio: 17.4 % of one core, real-time factor 5.76 latency 13 samples, Newton failures 0 (rescued 0), max iterations 18 | -| iir | DI-GTR LL | 0.750 | -138.5 dB | PASS | processBlock 0.844 s for 5.000 s of audio: 16.9 % of one core, real-time factor 5.93 +| iir | DI-GTR LL | 0.750 | -138.5 dB | PASS | processBlock 0.843 s for 5.000 s of audio: 16.9 % of one core, real-time factor 5.93 latency 13 samples, Newton failures 0 (rescued 0), max iterations 23 | -| iir | DI-GTR R | 0.837 | -138.5 dB | PASS | processBlock 0.849 s for 5.000 s of audio: 17.0 % of one core, real-time factor 5.89 +| iir | DI-GTR R | 0.837 | -138.5 dB | PASS | processBlock 0.846 s for 5.000 s of audio: 16.9 % of one core, real-time factor 5.91 latency 13 samples, Newton failures 0 (rescued 0), max iterations 17 | -| iir | DI-GTR RR | 0.843 | -138.5 dB | PASS | processBlock 0.872 s for 5.000 s of audio: 17.4 % of one core, real-time factor 5.73 +| iir | DI-GTR RR | 0.843 | -138.5 dB | PASS | processBlock 0.864 s for 5.000 s of audio: 17.3 % of one core, real-time factor 5.78 latency 13 samples, Newton failures 0 (rescued 0), max iterations 20 | diff --git a/harness/reports/M6/20261015_x2_rates_d057_half2x.md b/harness/reports/M6/20261015_x2_rates_d057_half2x.md new file mode 100644 index 0000000..c99573c --- /dev/null +++ b/harness/reports/M6/20261015_x2_rates_d057_half2x.md @@ -0,0 +1,48 @@ +# M6 A1: full engine (exact J4) at lower internal rates, plugin model check + +- Date 2026-10-15; options: divisor 2, tol 0.0001; rates 48 kHz × [2]. +- Plugin model check (D-051): ESR ≤ -20 dB vs SPICE (cached 384000 Hz references, 48 kHz band, first 5 s of every DI track, first 0.1 s excluded). Unseen settings: the 16 X4 validation settings, 10 s, vs the engine at 16x (first 1 s excluded). Cost: ampwav --stats on 10 s of DI GTR L, one run at a time; oversampling filters not included. + +## Summary + +| Rate | vs SPICE worst (CRUNCH / DRIVE / HOT) | Check | Unseen settings vs 16x (worst / median) | Newton failures (not rescued) / runs, max iterations | Cost default / HOT (% of one core) | +|---|---|---|---|---|---| +| 2x (96 kHz) | -38.8 / -38.0 / -22.4 dB | PASS | -18.9 / -30.1 dB | 12 (2) / 28, 32 | 19.3 / 29.1 | + +## vs SPICE per clip + +| Clip | 2x | +|---|---| +| CRUNCH GTR L | -39.3 dB | +| CRUNCH GTR LL | -38.8 dB | +| CRUNCH GTR R | -40.2 dB | +| CRUNCH GTR RR | -39.6 dB | +| DRIVE GTR L | -38.0 dB | +| DRIVE GTR LL | -39.0 dB | +| DRIVE GTR R | -39.2 dB | +| DRIVE GTR RR | -38.1 dB | +| HOT GTR L | -22.6 dB | +| HOT GTR LL | -23.6 dB | +| HOT GTR R | -23.5 dB | +| HOT GTR RR | -22.4 dB | + +## Aliasing (power outside the harmonics of f0, relative to the total; 16x for reference) + +Each cell: non-harmonic power, then Newton failures (not rescued) in that 2 s run. Non-converged samples add non-harmonic power, so a cell with many failures measures solver error as well as aliasing. + +| Preset | f0 | amplitude (FS) | 16x | 2x | +|---|---|---|---|---| +| CRUNCH | 5000 Hz | 0.1 | -26.0 dB, 0 (0) | -14.4 dB, 0 (0) | +| CRUNCH | 5000 Hz | 0.4 | -25.7 dB, 0 (0) | -12.1 dB, 2000 (1992) | +| CRUNCH | 9000 Hz | 0.1 | -39.9 dB, 0 (0) | -8.9 dB, 0 (0) | +| CRUNCH | 9000 Hz | 0.4 | -38.5 dB, 0 (0) | -4.8 dB, 24397 (22062) | +| HOT | 5000 Hz | 0.1 | -30.0 dB, 387 (0) | -11.2 dB, 0 (0) | +| HOT | 5000 Hz | 0.4 | -29.8 dB, 408 (0) | -10.2 dB, 655 (653) | +| HOT | 9000 Hz | 0.1 | -38.2 dB, 60 (0) | -10.3 dB, 0 (0) | +| HOT | 9000 Hz | 0.4 | -36.0 dB, 25 (0) | -7.6 dB, 16562 (12679) | + +## Cost + +| Rate | ns per engine sample (default / HOT) | % of one core at 48 kHz | +|---|---|---| +| 2x | 2007 / 3030 | 19.3 / 29.1 |