Inside POLSCHWARM: resonances with mass

Inside POLSCHWARM: How a filter gives its resonances mass

Inside POLSCHWARM: resonances with mass

POLSCHWARM has been in the shop since October 12. The product page says what the filter can do. This post explains how it works: with the formulas from the code, the numbers behind them and the bug I found just before release. If you only want to hear how it sounds, grab the demo. If you want to know why it sounds that way, read on.

What always bugged me about filter sweeps

A normal filter goes exactly where you send it. When CUTOFF jumps from 300 Hz to 2 kHz, it computes 2 kHz from the next block on, or a smoother drags the value across in a straight line. Both sound as if someone drew the movement with a ruler. I wanted a filter where the path to the target tells its own story: who gets there first, who lags behind, who overshoots.

The idea behind it is simple. Every resonance gets a mass and hangs on a spring. The rest of this post explains how that turns into a filter that, at rest, still does exactly what you set, down to the bit.

Seven bodies instead of seven sets of coefficients

POLSCHWARM swarm field with targets, bodies, trails and frequency response
The swarm field: rings are the targets, dots are the bodies with their trails, grey ones are parked. Below it, the frequency response you are hearing right now.

POLSCHWARM is a chain of seven sections. Each section is a TPT state variable filter, so it preserves the topology and has no delay in the feedback loop. Every section has a body. A body is an analog pole s = 2π(−a + jf), with f as the damped frequency and a as the decay rate, plus a shape made of four parts (cL, cB, cH, cX). The shape mixes lowpass, bandpass, highpass and the dry signal of the section.

The most important sentence for everything that follows: TYPE, CUTOFF and RESO do not set the filter, they only set the target of each body. There are eight types: LP2, LP4, LP6, HP2, HP4, BP4, Formant and Overtones. Whatever a type does not need gets parked. A parked section has the shape (0, 0, 0, 1) and passes the signal through bit-exact, no matter where its pole currently sits.

In the field on the UI, frequency runs horizontally and damping vertically. Every body drags a trail behind it there, so you see what you hear.

How a body's position becomes a filter coefficient

The position (f, a) gives three values for the state variable filter:

  • the natural frequency f0 = √(a² + f²)
  • the damping k = 2a / f0, at least 0.01
  • the tuning value g = tan(π · min(f0, 0.45 · fs) / fs)

This happens once per control block of 16 host samples, and within the block the values ramp linearly. What matters is what does not happen here: no modulation, no impulse and no knob touches g or k directly. Everything that moves the filter pushes the bodies. The coefficients follow only from where they are.

The spring, solved exactly instead of approximated

POLSCHWARM knobs TEMPO, FRICTION, SWARM and REPULSION
The four swarm knobs. TEMPO is the spring rate, FRICTION the damping, SWARM spreads the mass.

Every body follows a damped oscillation around its target: y″ + 2ζω y′ + ω² y = 0, where y is the distance to the target. The same equation applies to pole position and shape. A simple integration method would distort the damping at large steps. So POLSCHWARM computes the closed-form solution in every step: underdamped with cos and sin, overdamped with cosh and sinh, and close to ζ = 1 via a series expansion, so it never divides by a number that almost vanishes. There are up to 16 substeps per control block, chosen so that dt · ω_max stays at 0.1 or below.

The knobs feed straight into this equation. TEMPO (0.2 to 40 Hz) is the spring rate, FRICTION sets ζ. SWARM spreads the mass: body i has the mass m = 2^(SWARM · u), with u in seven steps between −1 and +1, and oscillates at ω = 2π · TEMPO / √m. At SWARM 0 all seven bodies move the same way. At SWARM 100 % the heaviest body is four times as heavy as the lightest and therefore half as fast. The light ones lead, the heavy ones follow, and exactly this offset is what you hear in the frequency response.

Repulsion that vanishes by itself at rest

Two bodies push each other away with a force proportional to 1/(d² + 0.05²) − 1/(d_t² + 0.05²). Here d is their relative distance and d_t the relative distance of their targets. The second term is the trick: the repulsion only acts while two bodies are closer together than their targets, and at the target it is zero. In flight the bodies dodge each other, and that creates peaks and gaps in the spectrum that no static setting has. At rest the repulsion does not get in the way. If two bodies sit exactly on top of each other, they move apart in a fixed direction per pair, without randomness.

Why rest is exactly the filter you set

Mathematically, a spring never quite arrives. So POLSCHWARM lets the bodies snap into place: once relative distance and relative velocity are both below 10⁻⁷, the body jumps exactly onto its target and its velocity becomes zero. Since the repulsion is zero there as well, nothing pulls it away again. The filter at rest is not an approximation, it is exactly the filter you set.

To keep things from getting out of hand, there are walls for frequency and decay rate. A body that overshoots its target and flies into a wall bounces back with half its velocity.

What else pushes the swarm

POLSCHWARM swarm field with WIND PITCH and WIND DAMPING
With WIND the bodies rest next to their target rings instead of on them.

All other movements follow the same rule: they only change velocities, never a position and never a coefficient.

IMPULSE gives the bodies a push on every detected transient, MIDI IMPULSE does the same for incoming notes. WIND acts like a second spring that pushes frequency or damping relative to its own position. The equilibrium then sits at target / (1 − 0.5 · WIND): at WIND +1 one octave above the target, at −1 at two thirds of it. SHAKE is a random force that grows with the square root of the block length. That way it acts the same at every sample rate. At TEMPO 3 Hz, FRICTION 50 % and SHAKE 100 % the bodies wobble by about three semitones. GATHER pulls the swarm toward its center, SCATTER drives it apart.

On top of that come eight stations, each with its own TYPE, CUTOFF and RESO. PATH switches between them in sync with the host. The target jumps discretely, and the transition is shaped only by the movement of the bodies. DRIVE saturates before each of the seven sections, a level compensation keeps the loudness steady, and the signal path runs at 1x, 2x or 4x oversampling.

The bug I found just before release

Until October 12 the spring did not move the band part cB itself but a factor b, and cB only came out as b · k with the k of the current position. That sounds harmless, but it was not. A section that was still wide on its way to a narrow peak already carried the full boost of that peak along the way. Seven such sections together boosted the signal by 70 dB and more.

The fix: the spring now moves cB itself. The target carries cB = b · k with the k of the target position, and cB stays between −2k and 4k. At 4k a section boosts by 14 dB at its natural frequency, at −2k it is an allpass. Since then, flights into a peak no longer boost broadband. The release shipped with this fix, but I am telling the story anyway, because it shows how easily a physically elegant idea fails on a single badly chosen variable.

Try POLSCHWARM yourself

The demo has every feature. Once a minute it mixes in three seconds of quiet noise, and after 30 launches it goes silent until a serial number is entered. The full version costs 49 €. POLSCHWARM comes as VST3, CLAP and standalone for macOS 11 or later (Intel and Apple Silicon), Windows 10 or later and Linux from Ubuntu 22.04, plus AU on the Mac. It ships with 70 factory presets in eleven banks.

Demo and full version: kreuzberg-audio.com/en/downloads/polschwarm

If you have questions about the math or would solve something differently, write to me. I am happy about any feedback.

Cheers,
Frank