Tooling for generating audio-reactive Mandelbrot-zoom animations in Ultra Fractal. Takes a song, analyses its beats and frequency spectrum with librosa, and emits a complete UF .upr parameter file whose camera, colors, and 3D transformations are driven by the audio.
Built incrementally while figuring out UF's parser quirks the hard way. The companion blog-post-as-comments inside the source files explains a number of UF gotchas (line length limits, integer index requirements, the difference between gradient rotation= display and actual per-stop animation, scientific-notation parser failures) that aren't in any UF documentation.
A .upr for UF whose entry combines:
- Mandelbrot deep zoom into a chosen target (default: Seahorse Valley at
−0.7454288 + 0.1130088i) over the song's main section, going to magn10¹⁰by ~1:26. - Beat-locked zoom + rotation kicks synchronized to detected beats. Per-section
pulse_scaleconfig means the intro can be calm, verses normal, and a designated solo section can be amplified or zeroed entirely. - 8 frequency-band-driven color stops — each color stop's gradient-index position drifts proportionally to its paired band's cumulative energy. Sub-bass → charcoal, bass → indigo, low-mid → plum, …, air → smoky teal.
- Section-based dynamics: 0:00–0:12 intro (no kicks), 0:12–1:26 verses (full kicks), 1:26+ "guitar solo" (camera flies through multiple Mandelbrot regions via waypoints).
- True plane↔sphere morph during the solo via a custom
3d-morphtransform (morph.uxf) — the whole composite image smoothly bends into a spinning sphere and back. - Per-pixel
Smoothoutside coloring with a curated 8-stop "moody psychedelic" palette +OrbitTrapsring overlay +Gradient(Cone) inside coloring.
fractal-audio-sync/
├── README.md ← this file
├── analyze.py ← runs librosa on a song, dumps analysis to .npz
├── generate_upr.py ← the main generator. reads analysis cache, emits .upr
├── morph.uxf ← custom UF transform (plane↔sphere morph)
└── venv/ ← Python venv (not tracked)
# One-time setup
cd ~/Desktop/fractal-audio-sync # or wherever
python3 -m venv venv
./venv/bin/pip install librosa numpy
# Custom UF transform: copy morph.uxf to your UF Formulas folder so UF can find it
cp morph.uxf "~/Documents/Ultra Fractal 6/Formulas/"# 1. Analyze a song (only needed once per song; cached as Songname.analysis.npz)
./venv/bin/python3 analyze.py ~/Downloads/Yoursong.wav
# 2. Edit generate_upr.py to point at your analysis cache and tune parameters,
# then run the generator
./venv/bin/python3 generate_upr.py
# 3. Open the generated .upr in Ultra Fractal
# File → Browse → audio-mandelbrot.upr → AudioMandelbrotThe generated .upr is written to ~/Documents/Ultra Fractal 6/Parameters/audio-mandelbrot.upr so UF's browser picks it up immediately.
All tunable parameters live at the top of generate_upr.py as ALL_CAPS constants:
| Parameter | What it does |
|---|---|
PREVIEW_MODE |
True for fast iteration (480×854, no AA); False for full HD render |
CENTER_END, MAGN_AT_SOLO_START |
Where the verse zoom is heading and how deep |
SOLO_START_SEC |
When the guitar-solo flying phase begins |
SOLO_WAYPOINTS |
List of (time, center_re, center_im, magn) tuples for the camera fly-through |
MORPH_* |
Plane↔sphere morph timing |
SECTIONS |
Per-section pulse_scale overrides |
BAND_DRIFT_AMOUNT |
Max drift per color stop. Must stay below inter-stop spacing (50) or stops will cross over and the gradient flips/inverts visibly. |
BAND_SAMPLE_INTERVAL |
Seconds between band-energy keyframes |
SMOOTH_GRADIENT_STOPS |
The 8 paired (band → color) stops |
BEAT_STRIDE |
2 = half-notes, 4 = downbeats only |
PULSE_MAGN_FACTOR, PULSE_ROT_DELTA |
Amplitude of beat-driven kicks |
| Audio feature | Drives |
|---|---|
| Detected beats (every 2nd one by default) | Magnification + rotation pulses, scaled per section |
| 8 frequency bands (40 Hz–20 kHz, cumulative energy) | One color stop each — colors drift independently through the gradient |
| Section timestamps | pulse_scale per section (intro calm, solo morph) |
| Solo waypoints | Manual camera fly-through targets |
Documented for future debugging. UF's .upr parser:
magn=lines tolerate continuations + arbitrary length, butindex="..." color=Nlines have a logical-line-length cap (500 chars). Wrapping with300 chars by sampling sparsely.\<newline>is supported on both, butindex=still chokes if the unwrapped logical content exceeds the limit. Workaround: keepindex=animations to ≤- Gradient
index=keyframe values must be integers. Float values cause silent parse failures. - Animating gradient
rotation=alone is display-only — the rendered colors don't shift. To actually rotate the rendered gradient, every color stop'sindex=must be animated by the same offset in lockstep. - Python's
:.10gformat emits lowercase1e+10for 11-digit integers, which collides with UF's@#time separator parsing. Use.12gor higher to avoid scientific notation in the magn range you care about. - The built-in
3d-mappingtransform has a discreteshapeenum (Plane/Sphere/Egg) so you can't smoothly morph between them. Solution: write a custom.uxf(this repo'smorph.uxf) that exposes a continuousmorph∈ [0, 1] parameter and linearly blends(u, v)between plane and sphere intersections. - The built-in 3d-mapping's Plane mode is a perspective ray-plane projection ("looking at a floor"), not a flat identity passthrough.
morph.uxf's Plane mode is an identity passthrough so morph=0 is indistinguishable from no transform at all.
- A full song at 720×1280 + AA takes many hours to render. Use
PREVIEW_MODE = Truefor fast iteration at 480×854 without AA. - UF's render dialog lets you restrict to a frame range — useful for spot-checking specific moments (e.g. frames 2700–2900 for the morph onset).
- For very deep zooms, UF's perturbation algorithm engages automatically with
Standard.ufm > Mandelbrot (Built-in)(FastMandel) which this project uses. - The sphere/morph transform doesn't currently use perturbation, so deep-zoom sphere phases may render slowly.
Personal/artistic project. No formal license — use as you like, attribution appreciated.