Skip to content
Sample of FFT-Spectrum Particle Field (Synthetic Audio)

audio_reactive · PasmaFFTSpectrum

FFT-Spectrum Particle Field (Synthetic Audio)

A particle field whose velocity is driven by three FFT-magnitude bands (low / mid / high) computed from a sliding window of synthetic audio. The audio signal is a chain of 9 sinusoids (3 per band) plus a 1 Hz impulse train; a Hann-windowed real FFT gives the magnitude at each band's centre frequency, and the three band magnitudes push the particles along x (low vs mid) and y (mid vs high) with a swirl component modulated by the mid band. Particles leave low-alpha strokes on a fading canvas, with ink brightness boosted by the high-band magnitude — the spectrum particle look Piter Pasma uses across his 4 kB demos, *Skulptuur*, and the *Genuary* prompts.

Original: http://piterpasma.nl/

▶ Try the live demo (JS port) ▶ Run reference.py via Pyodide 10 formulas have instant JS ports. The Pyodide path runs the actual Python in your browser — first load is ~6 MB, then instant.

Family
audio_reactive
Source
audio-reactive
Evolution
fft
Render
trail_accumulation
Complexity
medium
Agents
1400
Year
2024
Author
Piter Pasma (algorithmic animation, FFT-driven bands); technique adapted from openFrameworks ofBook *Sound* chapter

Math essence

audio(t) = Σ amp_i * sin(2π freq_i t + phase_i) + Σ impulse_kick * δ(t mod 1). Each step the latest 256 samples (window_size) are Hann-windowed and FFT'd: bands = [|FFT(low_freq)|, |FFT(mid_freq)|, |FFT(high_freq)|]. fx, fy = field_gain * (band_diff_low_mid + 0.4, band_diff_mid_high + 0.4) + swirl * (ny, -nx). v *= 0.985. canvas *= trail_decay; canvas[p.y, p.x] += ink * (0.08 + 0.18 * high_band).

Parameters

Name Type Default Range Description
dim int 400 200–800 Square canvas side length in pixels.
num_particles int 1400 200–4000 Particle count.
window_size int 256 64–1024 FFT window size (samples).
sample_rate float 256 64–1024 Synthetic audio sample rate (Hz).
low_freq tuple 0.7,1.6,3 — Low-band centre frequencies.
mid_freq tuple 8,14,22 — Mid-band centre frequencies.
high_freq tuple 40,64,96 — High-band centre frequencies.
low_amp tuple 0.55,0.4,0.3 — Low-band amplitudes.
mid_amp tuple 0.35,0.28,0.2 — Mid-band amplitudes.
high_amp tuple 0.2,0.15,0.1 — High-band amplitudes.
kick_freq float 1 0.1–8 Beat / kick frequency (Hz) added to low band.
kick_strength float 1.6 0–4 Strength of the kick impulse.
field_gain float 6 0.1–16 Gain applied to the FFT-band-driven force field.
max_speed float 5.5 0.5–14 Speed clamp (px/frame).
trail_decay float 0.94 0.7–0.999 Per-frame multiplicative fade of the canvas.
hue_drift float 0.004 0–0.05 Hue rotation per step (turns).
bg_color tuple 4,6,14 — Background colour.
seed int null 0–999999 RNG seed.

Reference

First 60 lines of reference.py. Full source: library_external/audio_reactive/PasmaFFTSpectrum/reference.py

from __future__ import annotations

from typing import ClassVar

import numpy as np


class PasmaFFTSpectrum:
    """FFT-spectrum particle field inspired by Piter Pasma's algorithmic
    animation work.

    A synthetic "audio" signal is generated each frame as a chain of
    sinusoids across low / mid / high bands, plus a 1 Hz impulse
    train (kick). A sliding window of the most recent samples is
    Hann-windowed and run through a real FFT; the magnitudes at the
    three band centres (low / mid / high) drive particle velocities
    along the x / y axes and a radial "pressure" pulse.

    Particles leave low-alpha strokes on a fading canvas (the
    classic "FFT visualizer" look), colour-cycled with the band's
    spectral centroid. This mirrors the way Pasma's pieces use
    magnitude bands (low / mid / high) to drive multiple visual
    parameters at once — one of his signature moves across the
    4 kB demos, *Skulptuur*, and the *Genuary* prompts.
    """

    family: ClassVar[str] = "audio_reactive"
    sketch: ClassVar[str] = "pasmafftspectrum"

    def __init__(
        self,
        dim: int = 400,
        num_particles: int = 1400,
        window_size: int = 256,
        sample_rate: float = 256.0,
        low_freq: tuple[float, ...] = (0.7, 1.6, 3.0),
        mid_freq: tuple[float, ...] = (8.0, 14.0, 22.0),
        high_freq: tuple[float, ...] = (40.0, 64.0, 96.0),
        low_amp: tuple[float, ...] = (0.55, 0.40, 0.30),
        mid_amp: tuple[float, ...] = (0.35, 0.28, 0.20),
        high_amp: tuple[float, ...] = (0.20, 0.15, 0.10),
        kick_freq: float = 1.0,
        kick_strength: float = 1.6,
        field_gain: float = 6.0,
        max_speed: float = 5.5,
        trail_decay: float = 0.94,
        hue_drift: float = 0.004,
        bg_color: tuple[int, int, int] = (4, 6, 14),
        seed: int | None = None,
    ) -> None:
        self.dim = dim
        self.num_particles = num_particles
        self.window_size = window_size
        self.sample_rate = sample_rate
        self.low_freq = np.array(low_freq, dtype=np.float32)
        self.mid_freq = np.array(mid_freq, dtype=np.float32)
        self.high_freq = np.array(high_freq, dtype=np.float32)
        self.low_amp = np.array(low_amp, dtype=np.float32)
        self.mid_amp = np.array(mid_amp, dtype=np.float32)
        self.high_amp = np.array(high_amp, dtype=np.float32)

Try running it locally

The reference implementation is self-contained Python with numpy. Install the package and run:

python library_external/audio_reactive/PasmaFFTSpectrum/reference.py

Need help? See Learn for the standard environment setup, or Playground for an in-browser sketchbook.

More from audio-reactive

See all audio_reactive artworks →