Skip to content

fix: bound poweramp saturation, fix crossover distortion, and precompute sag#187

Merged
OpenSauce merged 1 commit intomainfrom
fix/poweramp-saturation-and-sag
Feb 14, 2026
Merged

fix: bound poweramp saturation, fix crossover distortion, and precompute sag#187
OpenSauce merged 1 commit intomainfrom
fix/poweramp-saturation-and-sag

Conversation

@OpenSauce
Copy link
Owner

  • ClassA/ClassB: apply tanh() to both positive and negative halves with asymmetric gain, fixing unbounded negative output (was linear, could reach -3.6 at high drive)
  • ClassAB: replace discontinuous 3-region piecewise function with smooth deadzone x³/(x²+dz²) for authentic crossover distortion, eliminating ~0.02 magnitude jumps at ±0.15 thresholds
  • Precompute sag envelope exp() coefficients in constructor instead of per-sample, removing ~1.5M redundant exp() calls/sec at 16x oversampling
  • Track sag envelope from post-drive signal for consistent behavior across presets regardless of upstream gain staging
  • Add denormal protection on sag envelope
  • Fix comment: 0.002s time constant is 2ms, not 5ms

…ute sag

- ClassA/ClassB: apply tanh() to both positive and negative halves with
  asymmetric gain, fixing unbounded negative output (was linear, could
  reach -3.6 at high drive)
- ClassAB: replace discontinuous 3-region piecewise function with smooth
  deadzone x³/(x²+dz²) for authentic crossover distortion, eliminating
  ~0.02 magnitude jumps at ±0.15 thresholds
- Precompute sag envelope exp() coefficients in constructor instead of
  per-sample, removing ~1.5M redundant exp() calls/sec at 16x oversampling
- Track sag envelope from post-drive signal for consistent behavior
  across presets regardless of upstream gain staging
- Add denormal protection on sag envelope
- Fix comment: 0.002s time constant is 2ms, not 5ms
Copilot AI review requested due to automatic review settings February 14, 2026 16:53
@OpenSauce OpenSauce enabled auto-merge (squash) February 14, 2026 16:53
@OpenSauce OpenSauce merged commit 875b36e into main Feb 14, 2026
11 checks passed
@OpenSauce OpenSauce deleted the fix/poweramp-saturation-and-sag branch February 14, 2026 16:55
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

This PR refines the power amp stage’s DSP to improve numerical stability, realism of crossover distortion, and runtime efficiency in oversampled processing.

Changes:

  • Bounds Class A/Class B negative half-wave behavior by applying tanh() to both polarities with asymmetric gain.
  • Replaces Class AB’s discontinuous crossover with a smooth deadzone function for continuous crossover distortion.
  • Precomputes sag envelope follower coefficients and updates the sag envelope from the post-drive signal (plus adds denormal protection).

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

Comment on lines 50 to 56
// Calculate dynamic drive reduction from sag
let sag_amount = 1.0 - (self.sag * self.sag_envelope * 0.5).min(0.5);
let dynamic_drive = self.drive * sag_amount;

// Apply power amp clipping based on type
// Apply power amp drive
let driven = input * (1.0 + dynamic_drive * 3.0);

Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

sag_amount / dynamic_drive are computed before updating sag_envelope, so sag drive reduction is based on the previous envelope value while the envelope is now derived from the post-drive signal of the current sample. This effectively adds a 1-sample delay and also makes the envelope depend on already sag-reduced drive (feedback), which may not match the intended “track sag from post-drive” behavior. Consider updating the sag envelope first using a post-drive signal that excludes sag reduction (e.g., driven using self.drive only), then compute sag_amount/dynamic_drive from that updated envelope for the same sample.

Copilot uses AI. Check for mistakes.
Comment on lines 71 to 95
match self.amp_type {
PowerAmpType::ClassA => {
// Class A - smooth, asymmetric clipping
if driven > 0.0 {
// Class A: smooth, asymmetric saturation.
// Both halves bounded via tanh; negative side has reduced gain
// for the even-harmonic asymmetry characteristic of single-ended amps.
if driven >= 0.0 {
driven.tanh()
} else {
driven * 0.8 // Less gain for negative side
(driven * 0.8).tanh()
}
}
PowerAmpType::ClassAB => {
// Class AB - asymmetric with crossover characteristics
if driven > 0.15 {
// Positive values above crossover
driven.tanh()
} else if driven < -0.15 {
// Negative values below crossover
0.9 * driven.tanh() // Slightly less gain on negative
// Class AB: smooth deadzone around zero for crossover distortion,
// then asymmetric saturation on both halves.
// f(x) = x³/(x²+dz²) is C∞ smooth, gain→0 at zero crossing,
// gain→1 for |x|>>dz — models reduced transconductance near zero.
let dz: f32 = 0.1;
let x2 = driven * driven;
let crossover = driven * x2 / (x2 + dz * dz);
if crossover >= 0.0 {
crossover.tanh()
} else {
// Crossover region with slight distortion
driven * (1.0 + 0.2 * driven.abs())
(crossover * 0.9).tanh()
}
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The new Class A/B bounding and the new Class AB crossover function are significant DSP behavior changes, but poweramp.rs currently has no unit tests. Since other stage implementations include #[test] coverage, it would be good to add tests here (e.g., output always bounded for extreme inputs for each PowerAmpType, and Class AB continuity/no jump around the deadzone).

Copilot uses AI. Check for mistakes.
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