Skip to content

feat: add 12AX7 triode clipper and inter-stage filtering#211

Merged
OpenSauce merged 4 commits intomainfrom
feat/triode-clipper
Mar 2, 2026
Merged

feat: add 12AX7 triode clipper and inter-stage filtering#211
OpenSauce merged 4 commits intomainfrom
feat/triode-clipper

Conversation

@OpenSauce
Copy link
Owner

Summary

  • Add a new Triode clipper type using the Koren 12AX7 triode equation via a 256-point lookup table with cubic Hermite interpolation — physically-modeled asymmetric clipping with correct even-harmonic content
  • Add a one-pole lowpass filter (10kHz) between the preamp's cascaded waveshapers to model plate load capacitance, reducing digital fizz across all clipper types
  • Update Clean preset to use the new Triode clipper

Details

The Koren triode model computes plate current from the standard 12AX7 parameters (mu=100, Kp=600, Kvb=300, Kg1=1060, Ex=1.4). The table is built at init time in f64 precision, normalized to [-1, 1], and interpolated at runtime with cubic Hermite — same CPU cost as tanh.

The inter-stage LP filter models plate load capacitance that rolls off upper harmonics before they reach the second nonlinearity. Without this, cascaded waveshapers re-distort the full harmonic spectrum, which is the primary cause of "digital fizz" in amp sims.

Test plan

  • make lint passes
  • make test passes (5 new clipper tests + all existing)
  • Manual A/B listening: Triode vs Soft/ClassA clippers
  • Verified smoother tone with inter-stage filtering at high gain

Add a new Triode clipper type using the Koren 12AX7 triode equation via
a 256-point lookup table with cubic Hermite interpolation. This produces
physically-modeled asymmetric clipping with correct even-harmonic content.

Add a one-pole lowpass filter (10kHz) between the preamp's cascaded
waveshapers to model plate load capacitance. This shapes upper harmonics
before re-distortion, reducing digital fizz across all clipper types.
Copilot AI review requested due to automatic review settings March 2, 2026 17:56
Replace the linear approximation (2πfc/fs) with the proper analog-derived
discretization (1 - exp(-2πfc/fs)). The linear form is only accurate when
fc << fs and diverges near Nyquist.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new physically-modeled “Triode” preamp clipper option and inserts an inter-stage lowpass filter between the preamp’s cascaded nonlinearities, then updates the Clean preset to use the new sound.

Changes:

  • Add ClipperType::Triode backed by a lookup table and interpolation, plus i18n/UI integration.
  • Add an inter-stage one-pole lowpass filter in the preamp stage to reduce high-frequency “fizz” in cascaded waveshapers.
  • Update presets/Clean.json to use the Triode clipper (and adjust preset values).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/i18n/mod.rs Adds translation key/strings for the new Triode clipper label.
src/gui/stages/preamp.rs Exposes the Triode clipper in the GUI clipper-type picker.
src/amp/stages/preamp.rs Inserts an inter-stage LP filter between the two nonlinear stages.
src/amp/stages/common.rs Introduces a reusable one-pole lowpass filter implementation.
src/amp/stages/clipper.rs Adds ClipperType::Triode using a lazily-initialized lookup table and tests.
presets/Clean.json Switches Clean preset to Triode and adjusts related preset parameters.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +60 to +63
pub fn new(cutoff_hz: f32, sample_rate: f32) -> Self {
let coeff = 1.0 - (-2.0 * PI * cutoff_hz / sample_rate).exp();
Self { y_prev: 0.0, coeff }
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OnePoleLP::new() computes coeff as (2π * cutoff / sample_rate).min(1.0), which is a forward-Euler approximation and becomes 1.0 for common settings (e.g. 10 kHz at 44.1 kHz => coeff>1 => clamped to 1). That effectively disables the intended lowpass (output becomes the input after one-sample lag) and makes the tone depend on oversampling/sample-rate.

Consider using a standard one-pole coefficient (e.g. a = exp(-2π * fc / fs) and then y = a*y_prev + (1-a)*x, or equivalently coeff = 1-a in the current form) so the cutoff is correct and stable across sample rates.

Copilot uses AI. Check for mistakes.
Comment on lines +74 to +75
Self::Triode => TRIODE_TABLE.process(driven),
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TRIODE_TABLE is lazily initialized, and the first ClipperType::Triode.process() call will trigger allocation/table construction and one-time synchronization. If that first call happens on the real-time audio thread (e.g. when loading a preset using Triode), it can cause an audible glitch.

Consider forcing initialization off the audio thread (e.g. during app/engine startup, or when building the amp chain if any stage uses ClipperType::Triode) so audio processing never pays the init/lock cost.

Copilot uses AI. Check for mistakes.
- Replace Vec<f32> with [f32; 256] for the triode lookup table to avoid
  heap allocation and improve cache locality
- Add #[inline] to ClipperType::process() and TubeTable::process() hot paths
- Force LazyLock initialization in Manager::new() before the JACK audio
  thread starts, avoiding lazy init on the RT callback
- Add doc comments to TubeTable and Koren model constants
@OpenSauce OpenSauce enabled auto-merge (squash) March 2, 2026 18:05
@OpenSauce OpenSauce merged commit ae964d5 into main Mar 2, 2026
7 checks passed
@OpenSauce OpenSauce deleted the feat/triode-clipper branch March 2, 2026 18:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants