DSP
← devlog

Why I stopped trusting `tanh` for kick-drum distortion

The first time I mixed a kick through Niner’s saturation stage, I had a symmetric tanh sitting in it and the transient punched. The second time, with the same tanh pushed a little harder, it didn’t. Envelope was fine. Sub was fine. The kick just got softer and slightly duller, in the way that reads as “I lost a dB of presence and can’t find it again.”

I spent three days on that distortion stage. The short version: tanh is a polite waveshaper, and a kick drum is not a polite signal. The long version is the rest of this post.

What tanh actually does to a drum hit

tanh is bounded, smooth, and odd-symmetric. That last word is where it falls over on a kick.

An odd-symmetric waveshaper can only generate odd harmonics. 3rd, 5th, 7th, and so on. It cannot, by construction, add a 2nd. And the 2nd harmonic is most of what your ear reads as body when analog gear thickens a signal. Transformers, tubes, class-A front-ends, a pushed tape head: the reason any of them sound “bigger at the same level” is they’re producing even harmonics that sit an octave above the fundamental and fill in the lower midrange. Plain tanh can’t do that.

So what does it do instead? It smears the transient. The first 30–80 ms of a kick carry most of the perceptual weight, and symmetric saturation softens that edge while packing odd-harmonic energy into a region that already has odd content (mostly from the pitched component of the kick itself). The result is a drum that measures hotter and sounds flatter. It fattens without getting bigger.

There is no bug. It’s the curve.

What Niner ships instead

Three modes. Each one was supposed to have a genuinely different harmonic fingerprint, not three shades of the same symmetric idea. None of them are tanh alone.

Clip — rational shaper, split-band

The first replacement is still symmetric, which looks like a contradiction. The trick is that it doesn’t see the whole signal.

The shaper itself is x / sqrt(1 + x²). Bounded to ±1 like tanh, but with a harder shoulder. On its own it would have the same 2nd-harmonic problem. What makes it useful on a kick is split-band: a one-pole lowpass at 60 Hz sits in front of the shaper, and everything below that pivot bypasses the curve entirely. The sub-bass fundamental passes through clean, and only the 200 Hz body and the 2–6 kHz click actually hit the shape.

// src/dsp/saturation.rs
const CLIP_SPLIT_HZ: f32 = 60.0;

let lows  = self.clip_lp_z;
let highs = x - lows;
let clipped_highs = highs / (1.0 + highs * highs).sqrt();
lows + clipped_highs

It’s a symmetric clipper wearing a split-band jacket. On an 808-style kick the 50 Hz fundamental walks through untouched, the body hardens, and the click takes on a bright edge that reads as “clipped-but-precise” rather than “compressed.” It sounds nothing like plain tanh on the same input, even though the harmonic math is technically the same family.

Diode — asymmetric, even-harmonic

The Diode stage is where 2nd-harmonic content finally shows up. It’s a pair of 1 - exp(-x) curves, set up asymmetrically: positive half uses the full curve, negative half is scaled by 0.8 before inversion.

const DIODE_NEG_ASYM: f32 = 0.8;

if d >= 0.0 { 1.0 - (-d).exp() }
else        { -(1.0 - d.exp()) * DIODE_NEG_ASYM }

Asymmetric clipping is how you generate even harmonics without actual diodes or magnets. A physical diode pair has slightly different forward-drop characteristics on each half-cycle, and that mismatch is what imparts diode character. The 0.8 here is a caricature of real hardware, but it’s the right kind of caricature: it reliably puts even harmonics in the output, which the ear reads as thickness rather than smear. Diode is the mode I reach for when I want a kick to feel bigger, not brighter.

One unglamorous detail worth naming: the input is clamped to ±20 before .exp(). Past that you’re overflowing f32 for no musical benefit, and nothing ruins the “analog character” pitch faster than a stray NaN from a loud sample.

Tape — yes, still tanh under the hood

Here’s the part I had to be honest about.

Tape mode does still use tanh. The memoryless shape is:

const TAPE_BIAS: f32 = 0.08;

let fx_raw = (x + TAPE_BIAS).tanh() - TAPE_BIAS.tanh();

The bias shifts the curve asymmetrically around zero, which gives even harmonics the same way Diode does, by breaking the odd symmetry. If that were all there was to Tape, I wouldn’t have shipped it as its own mode. It would collapse into “Clip with a tilt.”

What makes Tape actually tape-flavoured is two things a static nonlinearity can’t do, layered on top of the biased tanh.

Drive-reactive HF loss. A one-pole lowpass sits downstream of the shaper, and its cutoff tracks the drive knob. Low drive parks it near 10 kHz (barely there). Full drive drops it to 2.5 kHz. So hitting Tape harder makes it darker, and the saturation-generated harmonics get rolled off by the same filter as the carrier. That’s how a real machine degrades under level. Most static shapers get brighter as you crush them, which is the opposite of the instinct tape gives your ear.

One-pole hysteresis. Each output sample blends in 18% of the previous divergence between the filtered and unfiltered shape. Translated: the curve the signal traverses on the way up is slightly different from the curve it traverses on the way down. That’s a first-order model of magnetic hysteresis. It’s subtle, it’s smeary, and two identical input samples arriving from different histories produce measurably different outputs. That’s the property that separates “tape model” from “tape-coloured EQ.”

const TAPE_HYSTERESIS: f32 = 0.18;

let y = fx + TAPE_HYSTERESIS * (self.tape_prev_y - self.tape_prev_fx);

So Tape is: bias → tanh → drive-reactive lowpass → hysteresis memory. The tanh isn’t the character. It’s the bounded soft-knee in the middle of a memory-based chain. I didn’t remove tanh. I gave it one job and built the rest of the machine around it.

When tanh is still the right answer

The goal was never to dunk on tanh. It’s a good shaper with a specific fingerprint, and on the right source that fingerprint is exactly what you want.

Steady-state bass: a held synth note, a low pad, an organ sustain. The odd-harmonic character is inaudible as character because there’s no transient to smear, and the smooth shoulder keeps the tone consistent across velocity.

Vocal saturation, where the symmetric sheen is the point. Tape’s drive-reactive darkening feels wrong on breath content, and Diode’s grit is too gnarled for most pop vocals.

Limiter soft-knees. When you want to round off a peak without adding harmonic character, tanh is the cheapest, most predictable way to do it.

tanh didn’t fail. A kick drum just isn’t its job.

What’s in the repo

Niner is GPL and open-source. The saturation stage lives in src/dsp/saturation.rs, pinned to a commit so nothing rots under you. Read the Tape process function if you’re curious what 438 lines of drum-kick-specific saturation look like after six drafts.

Niner ships 2026-07-15, free. If you want a heads-up, there’s a signup on the home page.

Update — 2026-04-28

v0.6.0 added a per-voice clip stage that sits before the amp envelope inside each kick voice. Three modes: Tanh, Diode, Cubic. Default off — every v0.5.x preset keeps its harmonic content bit-identical.

The reasoning in this post still applies to the master SAT chain, which still runs Clip / Diode / Tape — none of them plain tanh. The new stage solves a different problem: a 909-style soft-clipper between the source and the VCA, where the curve shapes the attack instead of smearing a tail. Source: src/dsp/voice_clip.rs.

Different stage, different tool. tanh came back where it fits.