Skip to content
Sample of Reactive Concentric Rings (FFT-Band-Driven)

audio_reactive · PasmaReactiveCircle

Reactive Concentric Rings (FFT-Band-Driven)

N concentric rings around the canvas centre, each driven by a different FFT band (low / mid / high). Each ring's radius, thickness, and hue are modulated by the magnitude of its band; a global 1 Hz kick impulse momentarily inflates every ring (the beat-driven throb). Rings also slowly rotate with per-ring phase offsets — Pasma's 'interesting rotating things' approach, where simple concentric structures turn into one another over time. The colour ramp uses the FFT-band magnitudes as a 3-stop HSV gradient; the result is read against a dark radial vignette, like Pasma's *Epihyperderpflardioids* project on a phone screen.

Original: http://piterpasma.nl/articles/rotating

▶ 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
concentric_rings
Complexity
medium
Agents
18
Year
2024
Author
Piter Pasma (algorithmic animation; technique adapted from openFrameworks ofBook *Sound* chapter and Pasma's 'interesting rotating things' article)

Math essence

rings = linspace(ring_min*W/2, ring_max*W/2, N). For each ring i: low = Σ low_amp_k * sin(2π low_freq_k * t + phase_k); mid, high analogously. radius_i = base_radii[i] + ((1-u)*low + u*high) * W * 0.04 + kick * W * 0.5; thickness_i = base_thick + 0.7 * tanh(mid * 4). ink_i = HSV(hue_base + u * hue_per_ring*6 + 180*(mid-low), 0.65+0.35*tanh(high*4), 0.85+0.15*tanh((low+mid+high)*3)). Draw ring at radius_i with thickness_i; canvas *= 0.92 per step.

Parameters

Name Type Default Range Description
dim int 400 200–800 Square canvas side length in pixels.
num_rings int 18 3–48 Number of concentric rings.
ring_min float 0.1 0.02–0.45 Innermost ring radius as a fraction of canvas half-width.
ring_max float 0.46 0.3–0.49 Outermost ring radius as a fraction of canvas half-width.
ring_thickness float 1.4 0.5–4 Base ring thickness in pixels.
low_freq tuple 0.21,0.51,0.83 — Low-band centre frequencies.
mid_freq tuple 1.1,1.9,2.7 — Mid-band centre frequencies.
high_freq tuple 3.5,5.1,7.4 — High-band centre frequencies.
low_amp tuple 0.2,0.14,0.08 — Low-band amplitudes.
mid_amp tuple 0.14,0.1,0.07 — Mid-band amplitudes.
high_amp tuple 0.08,0.06,0.04 — High-band amplitudes.
kick_freq float 1 0.1–6 Beat / kick frequency (Hz).
kick_strength float 0.06 0–0.2 Strength of the kick impulse (as fraction of canvas half-width).
rotation_speed float 0.07 -0.4–0.4 Per-step rotation rate of the ring arcs (radians/step).
hue_per_ring float 18 0–90 Hue shift per ring (degrees, multiplied by 6 internally).
bg_color tuple 4,4,10 — Background colour.
fg_color tuple 235,235,245 — Foreground tint.
seed int null 0–999999 RNG seed.

Reference

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

from __future__ import annotations

from typing import ClassVar

import numpy as np


class PasmaReactiveCircle:
    """Pasma-style reactive concentric circles.

    N concentric circles around the canvas centre. Each ring has
    its own "frequency band" — a synthetic sin chain (low / mid /
    high) whose instantaneous magnitude drives (a) the ring's
    radius offset, (b) its thickness, and (c) its hue. A global
    "kick" signal (1 Hz impulse train) momentarily inflates
    every ring's radius, like a beat-driven throb. Rings also
    have a slowly rotating phase, evoking Pasma's "interesting
    rotating things" article (parametric curves that slowly turn
    into one another).

    The colour ramp uses the FFT-band magnitudes as a 3-stop
    gradient (low→blue, mid→magenta, high→white). Background is a
    dark radial vignette so the rings read as bright halos, the
    way Pasma's *Epihyperderpflardioids* project reads on a
    phone screen.
    """

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

    def __init__(
        self,
        dim: int = 400,
        num_rings: int = 18,
        ring_min: float = 0.10,
        ring_max: float = 0.46,
        ring_thickness: float = 1.4,
        low_freq: tuple[float, ...] = (0.21, 0.51, 0.83),
        mid_freq: tuple[float, ...] = (1.1, 1.9, 2.7),
        high_freq: tuple[float, ...] = (3.5, 5.1, 7.4),
        low_amp: tuple[float, ...] = (0.20, 0.14, 0.08),
        mid_amp: tuple[float, ...] = (0.14, 0.10, 0.07),
        high_amp: tuple[float, ...] = (0.08, 0.06, 0.04),
        kick_freq: float = 1.0,
        kick_strength: float = 0.06,
        rotation_speed: float = 0.07,
        hue_per_ring: float = 18.0,
        bg_color: tuple[int, int, int] = (4, 4, 10),
        fg_color: tuple[int, int, int] = (235, 235, 245),
        seed: int | None = None,
    ) -> None:
        self.dim = dim
        self.num_rings = num_rings
        self.ring_min = ring_min
        self.ring_max = ring_max
        self.ring_thickness = ring_thickness
        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)

Try running it locally

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

python library_external/audio_reactive/PasmaReactiveCircle/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 →