Grit no longer turns the volume down as you turn it up. It used to lose 4.4 dB across the knob at mixing level and 13.3 dB on hot material, so turning it up sounded like turning the plugin down. It is now within 0.3 dB across its whole range, and it is roughly three times stronger at the same knob position — 0.10–0.30 is a normal working range now. Output is no longer a hidden drive control. And every factory preset is far gentler on delicate material: vocal presets measure 0.37–1.08 % distortion at mixing level where they used to be 2.40–2.97 %.
All of that shipped in 1.0.0. It is repeated here because 1.0.0's notes opened with a paragraph about test tooling and buried it three paragraphs down — and the first paragraph is the part most people read. The 1.0.0 notes are left as they were published rather than quietly rewritten.
> 1.0.1 changes no audio. Same DSP, same presets, same behaviour — the plugin binaries > are functionally identical to 1.0.0. If you are already on 1.0.0 there is nothing here for > you but a better manual. This is a documentation release.
BiasMeasure's Setting had no field for stage_gain2 and never set it, so the sweep row labelled *"everything at minimum"* was quietly running the second stage at its 6 dB default. The gap is exactly Stage 2 — the authoring grid puts Drive 0 / Stage 2 0 at 1.884 % and Drive 0 / Stage 2 6 at 2.759 %. A probe whose name does not match what it sets is worse than no probe. Setting now carries the field, defaulted to 6 dB so every canned setting measures exactly what it always has (verified unchanged: clean 20.69, driven 37.39, gritty 40.31, full 28.23), with the "minimum" row explicitly at zero. The manual's chart is redrawn from the corrected sweep.strong hardcodes its colour, so it survived the print stylesheet entirely and whole phrases were simply missing from the page. And section{page-break-inside:avoid} cost six near-blank pages out of 41, because a section longer than a page cannot avoid breaking and gets pushed whole onto a fresh one.docs/manual/Bias-X-User-Manual.pdf — 29 pages, and it keeps the dark theme rather than inverting to a light document, because that look is the manual's identity and a PDF of it gets read on a screen far more often than it gets printed. Making that survive an export needs print-color-adjust:exact (Chrome drops every background otherwise) and @page{margin:0} with the inset moved inside, because Chrome leaves the page-margin band unpainted whatever the canvas background says and framed every page in white. Generated by build-manual.py --pdf via headless Chrome or Edge, so it is reproducible rather than hand-made.Bias-X 1.0. The version number is the point of this release as much as anything in it: every knob has been measured to do what its label says at the level people actually mix at, every factory preset is voiced to a distortion budget a test can fail, and the whole thing is gated by six harnesses plus pluginval at strictness 10, two sanitizers and clang-tidy.
Upgrading from 0.7.0, which is the last public build. Everything below landed after it.
A full audit, thirty-four fixes, and the whole factory library rebuilt — twice. The second rebuild is the one that matters: the library is now voiced to a declared distortion budget per category, enforced by a gate that fails, instead of to taste with the measurement taken afterwards.
If you only read one thing: presets for delicate material are far gentler than any previous version. Vocal now measures 0.37–1.08 % THD at mixing level where it used to be 2.40–2.97 % — about 2.7× quieter — and the same is true of the mix-bus (Vintage) and texture/pad (FX) categories. Drums went the other way, on request.
Two of the fixes change how a control behaves, and if you have used an earlier build you will notice: Grit is roughly three times as strong at the same knob position, and no longer changes the output level as you turn it, and the Output control is no longer a hidden drive control. Both are corrections, not re-voicings — the details are below. Every factory preset was re-solved against them.
User presets are untouched: their stored parameter values recall exactly as before.
Every measurement in this project until now used a 0.4–0.5 amplitude test tone (−8 to −6 dBFS). That answers *"how far can this be pushed"* but not *"what does it do to my mix"*, and for Bias-X those have very different answers: TriodeCurve.h's kSwing = 1.2 V of grid swing per unit of input, on a −1.2 V operating point, means a full-scale signal drives the modelled 12AX7 to its rated maximum. THD is therefore steeply level-dependent.
Measured at a realistic −18 dBFS, the v0.7.0 library ran 5–40 % THD. "Silk Polish", commented in-source as *"barely-there polish… the lightest preset in the library"*, measured 15.7 %. There was no subtle option anywhere in it. The batch was authored honestly against the only ruler the project had; the ruler was pointed at the wrong question.
That same blind spot is why two v0.7.0 fixes had to be reopened — see below.
The first rebuild in this release aimed at "much more conservative" and still came out too hot for voices, pads, textures and whole mixes. The cause was a method, not a mis-hearing: both this release's first pass and v0.6.0's chose parameter values, measured afterwards, and documented whatever the measurement said — so no preset could ever fail. What changed:
BiasMeasure exits non-zero if a preset exceeds it. Vocal 1.10 %, FX 1.10 %, Vintage 1.30 %, Bass 1.80 %, Guitar 2.40 %, Drums 7.00 %, Drive 9.00 %, Extreme 24.00 %.BiasMeasure renders a six-tone probe through each preset and compares tonal shape, distortion and modulation pairwise within a category. On its first run it found near-duplicates in six of the eight categories: Lead Sustain and Crunch Rhythm scored 0.04, i.e. the same preset twice. Every pair was separated using the free controls, so none of them got hotter in the process.stage_output to within ±1.5 dB of unity at −18 dBFS — measured, and now automatically flagged if it drifts. 0 of 40 flagged.drive — but that cancels the shaper's small-signal prediction, which is only exact while the shaper is not compressing, and saturation *is* the output falling below that prediction. So the correction over-shoots, and over-shoots harder the further the knob goes: at drive 10 a −8 dBFS input reaches the shaper at 4.0 and comes back at about 1.0, so dividing by 10 over-corrects roughly fourfold.TransformerSaturation.cpp's comment already spelled it out — *"exactly right at low drive and progressively wrong as drive rises, because saturation is precisely the actual response falling below that prediction"* — while the identical mistake was being written into the pentode. It is fixed the same way: the stage now measures its own output against its own input over ~80 ms and equalises them, so drive shapes harmonic content and mix carries amount. Worst level deviation across the whole macro is now 0.30 dB, at every input level from −30 to −3 dBFS, and THD stayed monotonic while getting steeper through the middle (9.85 % vs 8.54 % at 0.50).BiasKnobAudit now asserts level at six input levels, and that assertion was verified able to fail before its clean result was believed. And the stage_output-with-Grit span widens from 1.38× to 1.93× against the 2.0× ceiling: that is the physically correct direction, since feeding a saturator 18 dB less signal *should* cost it harmonics and the old figure was partly the broken normalisation masking it, but the margin is now thin and the ceiling was left at 2.0 rather than raised to fit.grit, ironOut and age sit downstream of it, so through the whole engine an Output sweep moved THD 15.84 % → 3.45 % at Grit 0.6, a 4.6× swing. The BX-023 fix incidentally reduced that to 1.38×, which is ordinary level-dependence rather than a hidden control. Both halves are now asserted.std::clamp does not sanitise NaN (both its comparisons are false), so a NaN reached CubicLut's (int) cast and read ~2 GB out of bounds. Separately, the transformer's Jiles-Atherton state is a feedback integrator, so a NaN latched there and silenced the plugin for the rest of the session. Hardening those two individually was not enough — the new regression test proved it, since this chain has feedback state in a dozen places including JUCE's own oversampling filters. Non-finite input is now scrubbed at the engine boundary and the chain resets.moreThanOneInstanceAllowed() returned true, which made the already-correct restore handler dead code; relaunching the shortcut started a second process competing for the exclusive ASIO device.visibilityChanged() is never delivered for an ancestor's visibility change, so both kept running at 30 Hz with the standalone window hidden to the tray.paint() was advancing its filter.--target Bias builds JUCE's shared-code *static library*, not the plugin — the .vst3 and .exe are separate targets that depend on it. The stale-bundle guard then passed against the previous build, and the pipeline signed, packaged, tagged, pushed and announced the old binary as the new version. Now uses Bias_All, plus a timestamp assertion that refuses to package an artefact the build did not touch.main, and the push is explicit.MISMATCH and returned 0, sat outside the gate, and went unmentioned in TESTING.md, while six BUGS.md entries cited it as their regression test. It now asserts every measured value and is in the gate. It immediately caught BX-023.signtool verify is now a post-condition on both artefacts, and -SkipSigning renames the installer so it cannot be mistaken for a signed build.python docs/manual/build-manual.py.stage_output cleanliness probe, and a per-octave tone-response probe for Iron and Age. The factory audit gained a THD@-18 column and a gain dB level-match column that flags anything more than ±1.5 dB off unity.createInit() assertion), a silence-recovery check for Auto Gain, NaN/Inf injection, oversized-block processing, and 0.5×/2× editor renders.Two of the three turned out to be blocked on a wrong belief rather than on real work, which is the more useful finding.
BiasMeasure's canned settings only ever ran at Standard quality. Measured at both: driven −31.0 → −55.5 dB (−24.5 dB), gritty −24.9 → −38.0 dB (−13.1 dB), full −21.6 → −32.8 dB (−11.2 dB); clean was already at −98.5 dB. So the original framing was right, the mitigation has been shipping since v0.4.0, and what was actually missing was the measurement and the guidance. BiasMeasure now prints the Standard-vs-High delta on every run, the seven hottest factory presets select High, and the manual says which settings warrant it.-G Ninja + clang-cl toolchain". Only the Ninja half was true — clang-tidy needs a compilation *database*, not clang as the compiler, and Ninja + cl.exe produces a perfectly good one. Both Ninja and clang-tidy already ship with Visual Studio. Added scripts/tidy-configure.bat and scripts/run-clang-tidy.ps1 (with -Changed and -Filter for an edit-check loop, since a full sweep is 30–60 minutes). 0 diagnostics across all 30 of Bias-X's own translation units, after fixing four real misc-const-correctness findings in Palette.cpp. Deliberately not release-gating: a clang-tidy version bump adding a new check must not be able to block shipping a fix.add_subdirectory, so local builds are byte-identical and no slower; a machine without one fetches the pinned tag. Both paths verified end to end — a clean configure with -DBIAS_JUCE_DIR= cloned JUCE 8.0.13 from upstream and built a working 8.6 MB Bias-X.vst3. The CI-rebuild prerequisite recorded in CLAUDE.md as a v1 blocker is therefore removed; adding a rebuild-verification job is now ordinary work.-fsanitize=undefined, and Visual Studio's "C++ Clang tools" component ships clang-tidy.exe but no compiler, so BX-056's trick of reusing the VS toolchain does not extend to it. With LLVM installed (clang-cl 22.1.8) and a BIAS_UBSAN CMake option, 0 reports across five harnesses — including BiasEnginePhase5's NaN/Inf injection check, so the BX-034/035 hardening does not itself invoke UB. Three obstacles worth recording: clang-cl reports as Clang while MSVC is *also* true, so the existing if(MSVC) branch would have silently dropped UBSan; Windows ships UBSan only as a static-CRT runtime, so the build dir has to be /MT; and -fno-omit-frame-pointer is silently ignored by the cl driver (/Oy- instead). Driven by scripts/ubsan-run.ps1, and verified able to report before its clean result was believed.docs/TESTING.md told you to build four harnesses with -DBIAS_SANITIZE=ON; the first failed at link with LNK2038: mismatch detected for 'annotate_string', because MSVC's ASan annotates STL container internals and Catch2 — pulled in by FetchContent — is not instrumented. The other three do not link Catch2, so whoever ran ASan got three greens and one error that reads like a toolchain fault. Fixed with _DISABLE_STL_ANNOTATION, plus the second trap: every instrumented binary exits 127 before `main()` unless MSVC's clang_rt.asan_dynamic-x86_64.dll is on PATH. scripts/asan-run.ps1 handles both — 0 findings across all four.Start-Process -ArgumentList does not quote > array elements, so the -p path — which contains a space in this repo — was truncated, > clang-tidy silently found no compilation database, and reported nothing. That is > indistinguishable from a clean result. It was caught only by deliberately injecting a > defect and checking the harness noticed, the same discipline Wave 0 applied to > BiasKnobAudit. Do it for every new gate.build-manual.py held all 40 presets as a hardcoded string its own docstring described as "a paste of BiasMeasure's factory preset audit" — and a paste cannot know it is out of date. It now reads the audit at build time and fails with instructions if the file is missing, rather than emitting a manual from remembered numbers. docs/PRESETS.md is likewise emitted from the same data as the C++ table by scripts/presets/, so the two cannot disagree. Both of these were found while updating the docs for BX-060, and both are the same class of defect as BX-060 itself: a number that was already measured, already written down, and never checked against what it sat next to.clean was documented at −77.0 dB alias / −72.1 dB DC; the v1.0.0 build measures −98.5 / −77.3. clean uses no Grit at all, so BX-060 could not have changed it — which is what gave it away. The numbers trace back to the v0.7.0 changelog's own "Known trade-off" section, so they were copied from the previous release's notes rather than from a measurement run. All four rows regenerated. The manual's other five measured claims were then re-verified rather than assumed — Drive, Sag, Output Iron and Age all reproduce exactly; Stage 2's "20–45 % per 3 dB" actually measures 17–23 % and was corrected.BiasMeasure's factory audit — 40 headings, 6 trims, 40 table rows, 8 category cells — so a missed one fails loudly instead of silently.kSwing puts unity input at a 12AX7's rated maximum — and it was simply never written down. The manual now leads with it.A user reported that some controls seemed to do something other than what they claimed. A full audit — every knob swept across its whole range, measured for THD, level and spectral tilt — found four that measurably contradicted their own labels, plus two calibration faults. All six are fixed, and the factory library was re-solved against the result.
This changes the sound. Every preset was re-voiced; user presets will also sound different, most obviously brighter (see Age) and, at high gain settings, more saturated (see Output).
isPopupMenu() test in the mouse-up handler could never be true. The menu is now opened from mouse-down, where the button is still present — which is also the more correct place on Windows, since that is the only branch where JUCE gives the tray window foreground ownership.BiasIconGen from the *same* live-vector draw code as the plugin's header glyph, so the installer art cannot drift from the product. Inno accepts BMP only for these and JUCE ships no BMP encoder, so the generator gained one.Bias-X.vst3 folder tree (plus the desktop.ini/Plugin.ico that mark it as a system folder) behind in the user's VST3 path. The chosen VST3 location is now recorded in the registry at install time and read back at uninstall — the uninstaller runs no wizard, so it had no other way to know where a redirected bundle had gone..ico rather than the Standalone's embedded icon, so it resolves even if the .exe is gone and Windows can pick its own size. The Start Menu's Uninstall shortcut carries the brandmark too.The clean canned setting's alias@11k went from −95.9 dB to −77.0 dB. Nothing got noisier — the old number was Age's permanent lowpass filtering the aliasing away, and that lowpass was the BX-024 bug. −77 dB is far below the noise floor of any real material. The hot settings moved the other way and genuinely improved: driven −27.6 → −31.0 dB, gritty −19.1 → −24.2 dB, full −19.5 → −20.0 dB.
Full regression gate (BiasUnit 4296 assertions, BiasRenderSmoke, BiasPresetRoundtrip, BiasEnginePhase5, BiasUiSnapshot, BiasPresetRender 40/40, BiasKnobAudit, BiasMeasure 0/40 presets flagged) and pluginval strictness 10 → SUCCESS all pass.
A rebuilt factory preset library — 40 presets, 5 per category, every one of them measured through the live engine rather than eyeballed from a parameter table. Measuring them is also what surfaced the one DSP fix in this release: the Mix knob has been comb-filtering since Mix was introduced in v0.4.0.
BiasEnginePhase5 now checks the blend at 4 kHz as well as 220 Hz, and the low-frequency probe is kept deliberately — it passes on the *broken* engine, documenting the blind spot in the test itself.BiasEngine on a musical probe (BiasPresetRender), all 35 non-Extreme presets land within roughly ±1.5 dB of unity (rms 0.074–0.103 against the probe's 0.087) and peak between 0.31 and 0.57 — so switching presets compares tone, not loudness. Auto Gain carries that for anything that saturates; the parallel-Mix and tremolo/chop presets, where Auto Gain would fight the blend or the modulation, are hand-trimmed to the same target instead. The five Extreme presets are deliberately hot but still peak below full scale (0.82–0.99).stage_output is applied *into* TubePreamp's final fastTanh ceiling (so it sets how hard the stage squares off, and is not a clean makeup trim); age_capacitance is effectively the brightness/weight control, because CapacitorColorizer's LP/HP pair never bypasses (8k/80 Hz Small, 4k/30 Hz Medium, 2k/10 Hz Large); and age_coloration, grit_*, ironIn_* and ironOut_* must never appear in a preset at all, because BiasEngine drives them from age_age and the three macros every block and would silently override any stored value.BiasUnit, BiasRenderSmoke, BiasPresetRoundtrip, BiasEnginePhase5, BiasUiSnapshot, BiasPresetRender, BiasMeasure) and pluginval strictness 10 → SUCCESS all pass. BiasMeasure's factory audit flags 0 of 40 presets. No CPU change from the BX-021 fix — the dry delay is one block copy at base rate, and the measured difference is inside the benchmark's own run-to-run spread on this machine (full engine, Standard: 7.6% / 10.6% / 8.0% across three consecutive runs).Three UX improvements, all user-requested after reviewing v0.4.1's installer: the Standalone gains a real system tray icon, and the installer gains real choices it didn't offer before. No DSP changes.
$LASTEXITCODE came back 0 and the release proceeded to packaging/signing/publishing anyway (manual re-validation afterward showed the actual shipped binary was fine, but the gate itself didn't gate). Added a defense-in-depth check that scans pluginval's own output for its unambiguous failure markers, not just the exit code. Release tooling only — no plugin change.Five fixes from real-world use and a follow-up knob-correctness audit of v0.4.0 — full detail in the "Post-release follow-up" entries under [0.4.0] below and in docs/BUGS.md BX-016 through BX-020. No new features; this release exists to get already-fixed bugs into users' hands.
BiasKnobAudit (new tool) and expanded BiasMeasure coverage — every exposed knob is now verified for real, audible magnitude (not just direction of change), and all 128 factory presets are measured on every run.BiasUnit, BiasRenderSmoke, BiasPresetRoundtrip, BiasEnginePhase5, BiasUiSnapshot, BiasMeasure, BiasKnobAudit) and pluginval strictness 10 → SUCCESS all pass.DSP was byte-stable from v1 through 0.3.0; this release deliberately breaks that streak — a full quality assessment (DSP algorithms, performance, UI, completeness) led to a ground-up pass on the nonlinear models, full-chain anti-aliasing, a real performance pass, a reference-grade hero visual, and a session's worth of workflow features. Every phase below was independently gated (unit + integration harnesses + pluginval strictness 10) before moving to the next.
BiasEngine oversamples the entire nonlinear chain once (4× Standard, 8× High — see Quality below) instead of only TubePreamp's old internal 2×. Measured alias-to-signal ratio at 11 kHz: driven +3.9 dB → −26.7 dB, full-chain setting −4.0 dB → −32.1 dB, clean −22.2 dB → −99.5 dB. Latency (near-zero, minimum-phase IIR) is now reported to the host via setLatencySamples().TriodeCurve.h) replaces the old 3rd-order polynomial "lookalike" — a real Norman-Koren 12AX7 plate-current model with smooth grid-conduction limiting (no hard corner) and an authentic bias-dependent harmonic balance. THD@1kHz fell from a cartoonish 120% (driven) to a musical ~21%; the plugin is genuinely more tube-like, not just cleaner.PentodeDistortion) — a single smooth asymmetric tanh shaper riding on a bias-dependent operating point, replacing the old tanh/atan split (which had a slope discontinuity at the zero crossing) and its discontinuous "add-a-square-term" 2nd-harmonic bolt-on.TransformerSaturation, both irons) — proper implicit dM/dH integrated with RK2 midpoint (verified numerically identical to RK4 at this oversampling factor, half the cost), a numerically-stable Langevin function + derivative, and the dead dMdt_prev state removed. No more clamp-as-stability-crutch.langevin() is called once per sample instead of twice; TubePreamp's Bias modulation is now smoothed (was a raw per-block jump).prepareToPlay, so switching is instant and allocation-free; switching also re-prepares the five DSP modules at the new effective rate and forwards the latency change to the host.BiasEngine, reading like a real analog scope.VectorKnob now sets its slider's accessible title from its caption, covering every knob (present and future) from one shared code path.docs/AUDIT-LOG.md) — the chain's recursive, branchy per-sample state makes a branchless rewrite higher-risk than the CPU headroom currently justifies.BiasMeasure (THD / alias-to-signal / DC, the objective before/after ruler for the DSP work), BiasBench (per-module + full-engine CPU timing), BiasUnit (Catch2 unit layer — DSP-primitive invariants + parameter-layout checks), BiasEnginePhase5 (functional tests proving Mix/Auto-Gain/Quality-switch actually work, not just that they're wired), BiasPresetRender (renders every factory preset to WAV for audition). An opt-in BIAS_SANITIZE CMake option builds any target with AddressSanitizer (-DBIAS_SANITIZE=ON on a fresh build dir).docs/BUGS.md BX-009 / BX-010.BiasUnit (9 cases / 4290 assertions), BiasEnginePhase5 (7/7 functional checks), BiasRenderSmoke, BiasPresetRoundtrip (32/32 params round-trip + factory + init), BiasUiSnapshot regression, and pluginval strictness 10 → SUCCESS all pass on the final build. Warning-clean.
JUCE_DIRECT2D=0).drawButtonBackground's ambient drop-shadow was larger than the button and got hard-clipped into a square by JUCE's component-bounds clipping. Disabled the ambient shadow for buttons (kept for the larger group/master panels, which have real margin).SignalPathPanel, restyled for Bias's single-accent Blaze system (the modulation loop uses the same accent family dimmed to accentDeep, not a second hue).BiasLookAndFeel gained getTooltipBounds/drawTooltip overrides for this).-6px offset from each zone's edge that didn't match the zones' own symmetric content-centering. Dividers now draw exactly on the zone boundary, and both buttons were also narrowed (AUTO 84→64px, A/B 50→36px) to read as tighter, more proportionate controls.BiasLookAndFeel::drawPopupMenuItem previously discarded the hasSubMenu flag entirely; it now draws the arrow in the same hand-drawn triangle language as the combo box's own dropdown chevron.pow/softplus, the Jiles-Atherton transformer's double-precision tanh/sinh in langevin(), the pentode's per-sample tanh — running inside the 4×/8× oversampled chain, up to ~1.5M pow calls/sec. Replaced with Catmull-Rom lookup tables (FastMath.h, new) for the triode plate curve, langevin(), and the pentode/output-ceiling tanh (<1e-6 error vs. the closed form, unit-tested); age (CapacitorColorizer) moved outside the oversampled island — it's mild/near-linear, verified inaudible rather than a real timbral change; the transformer low-shelf now only redesigns on an actual value change (was every 32 samples unconditionally); BiasEngine's per-sample meter/mix/scope loops replaced with vectorised AudioBuffer/FloatVectorOperations calls. Full engine (Standard, 48 kHz/512): 17.29% → 7.19% of a core (2.4×); High: 14.25%. Real-world validated (Standalone + Ableton, via Task Manager): 2-3% — matches the benchmark and lands in the range of comparable plugins.docs/BUGS.md BX-015.BiasLookAndFeel explicitly overriding tooltip drawing to a regular weight. Root cause: the editor's TooltipWindow was constructed with a nullptr parent, making it a desktop-level window sitting outside the editor's component tree entirely — its getLookAndFeel() could never actually find lnf, so it silently fell back to JUCE's stock styling no matter what the override said. Now parented to the editor, per JUCE's own documented guidance for plugin editors.MeterBar's IN/OUT labels and the SignalScope's IN/STAGE/GRIT/OUT/TRANSFER/GRID labels.drawFittedText auto-shrink-to-fit was quietly compressing text below its stated size whenever copy ran long. Both now word-wrap normally and grow a vertical scrollbar only when content genuinely doesn't fit; the control glossary changed from fixed equal-height row slices (which silently clipped detail text on any stage with more than 2-3 controls) to a real scrollable stacked list.BiasUiBench (new CMake target) — a headless GUI repaint-cost benchmark isolating SignalScope/MeterBar paint cost from DSP cost, motivated by the CPU investigation above. BiasBench gained CLI-overridable sample rate / block size and a Standard+High dual-pass in one run, so DSP cost can be checked under realistic DAW conditions instead of one fixed config. BiasUnit gained two regression cases (FastMath::fastTanh / CubicLut accuracy vs. the closed forms).BiasUnit (11 cases / 4296 assertions), BiasRenderSmoke, BiasPresetRoundtrip (32/32 params round-trip + factory + init), BiasEnginePhase5 (7/7 functional checks), BiasUiSnapshot regression, and pluginval strictness 10 → SUCCESS (v1.0.4) all pass on the current build.
Added an About pop-over with credits and a live link back to vst.tools.