Another 2 months later and kf-2.15.4 is ready. Kalles Fraktaler 2 + is fast deep zooming Free Software for fractal graphics (Mandelbrot, Burning Ship, etc). Full change log:

kf-2.15.4 (2021-07-22)

- new: rewritten GUI for window size / image size (by popular request)
- new: “imaginary axis points up” option in the transformation dialog (requested by saka and others, makes complex plane comply with maths conventions)
- new: Hidden Mandelbrot formula (thanks to FractalAlex, Bruce Dawson) https://fractalforums.org/f/22/t/3576/msg22122#msg22122
- new: Hidden Mandelbrot a la Cos formula (thanks to 3Dickulus) https://fractalforums.org/f/74/t/3591/msg22215#msg22215

- set Factor A real and imaginary parts to control shape (e.g. 1+1i)
- new: Polarbrot formula, for p = 2, 3, 4 (thanks to gerrit) https://fractalforums.org/f/15/t/1916/msg23377#msg23377

- set Factor A real part to control power a
- fractional and/or negative power a is possible
- known issue: need to set Bailout escape radius very high (but not so high that derivatives overflow: try 1e10 or so)
- known issue: for positive a, reference at 0 fails (blank image) (workaround: offset the center very slightly in the Location dialog)
- known issue: for negative a, blank image (workaround: set Formula seeds non-zero (1e-300); this will reduce accuracy for deeper zooms)
- known issue: seams with numeric differences DE (analytic DE is ok)
- known issue: Newton zooming is not functional yet
- known issue: auto-skew is not functional yet
- new: convert between built-in formulas and hybrid formulas (when possible) with new buttons in the Formula dialog
- new: option Ignore Hybrids in the Formula dialog to list only the built-in formulas that don’t have hybrid equivalents
- new: optimized some built-in formulas using common subexpression elimination (6%-58% faster perturbation calculations)

- Burning Ship power 2, 3, 4, 5
- Buffalo power 2, 3, 4
- new: optimized some hybrid OpenCL perturbation calculations
- fix: Hybrid operator multiplication works with OpenCL
- fix: Omnibrot works with OpenCL
- fix: Mandelbrot power 4 and above with derivatives works with OpenCL
- fix: formulas 52, 53, 69, 70, 71 now work with OpenCL
- fix: formulas 4 (power 3), 20, 23-26, 42-50 now have correct derivatives for analytic DE
- fix: renamed some formulas (Abs General Quadratic Plus/Minus, Omnibrot) (suggested by gerrit)
- fix: Zoom Amount spinner in Transformation dialog works live
- fix: Transformation dialog Zoom Amount sign inversion
- fix: right mouse button drag in Transformation dialog stretches in a more intuitive way
- fix: Transformation dialog displays/edits total transformation instead of difference from last set transformation
- fix: Newton zoom dialog (atom domain) size of period <=1 is set to 1
- fix: OpenCL error dialog no longer appears and disappears again instantly
- internal: formula build system refactored for parallel building and much faster incremental builds
- internal: include structure rationalized for faster builds
- internal: use intermediate ar archives for linking many object files
- internal: formula preprocessor supports temporary variables (can be used for common subexpression elimination)
- upgrade to gsl-2.7
- upgrade to openexr-2.5.7

Get it from mathr.co.uk/kf/kf.html.

]]>A small techno piece I made this week. Created with Clive, my thing for livecoding audio in the C programming language. Lots of biquad filters in this one, about half of them at subsonic frequencies as envelopes for rhythms.

Source code at code.mathr.co.uk/clive.

Video made with FragM, a scene with 3 spheres and varying refraction with dispersion. Not physically accurate...

**- - -**

*smoltech* is a movement to reduce wasteful technology use. See
Marloes de Valk's paper at Computing Within Limits 2021
for an overview of related terms.
I find it inspiring, but I'm not yet fully immersed: for example, while
the sound was computationally very light, the video in this post took
some hours to render on a powerful GPU; I also maintain/extend software
for fractal art, which is a computationally expensive genre.

I do hope not to buy another computer (mine are sufficient for my needs), will see how that goes: NVIDIA EOL'd support for my 10+-year-old laptop's GPU (the Free/Libre Nouveau driver was too full of bugs last time I checked), so I won't be able to upgrade beyond a certain point (maybe Buster is the last working Debian release). And I may need access to a recent Microsoft operating system for some things, which I'm not happy about at all.

]]>Julia morphing, an artistic Mandelbrot set zooming technique, gives angled internal addresses that end with a regular structure like:

\[ \cdots p \to_{1/3} 2p+k \to_{1/3} 2(2p+k)+k \cdots \]

Back in 2013, I made an animation of an embedded Julia set orbit in the hairs around several period 7 islands. Embedded Julia sets are (relatively) surface features with simple angled internal addresses, while Julia morphs are much deeper and the addresses are more complicated.

My first attempt simply replaced a prefix of a template Julia morph
with 2N prefixes based on the period patterns in the hairs
and while I got images (the angled internal addresses were realizable by
actual locations in the Mandelbrot set) they didn't form a coherent
animation in any way. I asked
a question on mathoverflow
about this.

I went back to the graphical Mandelbrot set explorers, and figured out that the suffix needed to change when changing the prefix, because k (as in 2p+k) changes according to which child bulb the hair is attached to. Moreover, there is another case to consider, whether the child bulb's period appears in the address (=> address has more numbers) or not (=> address is the simple 2p+k pattern).

I did the initial graphical analysis in m-perturbator-gtk, heavily using its annotation features such as periodic point marking and external ray tracing to find external angles. Then I used mandelbrot-symbolics in ghci, to convert between angles and addresses. Here's some of the Haskell code:

-- ... main = do [depth] <- map read <$> getArgs let prefixes = (".(" ++) . (++ "0111)") . concat <$> replicateM depth [lo, hi] julia = 3 * depth + 4 morph = 4 lo = "011" hi = "100" period (Sym.Angled _ _ a) = period a period (Sym.Unangled p) = p writeFile "a.txt" . unlines . map (\prefix -> let Just addressPrefix@(Sym.Angled 1 _ (Sym.Angled 2 _ (Sym.Angled 3 pq _))) = (Sym.angledAddress . Sym.rational) =<< Txt.parse prefix addressSuffix (Sym.Angled p t a) = Sym.Angled p t (addressSuffix a) addressSuffix (Sym.Unangled p) = Sym.Angled p (1 Sym.% 2) (go morph p0) where p0 | m < julia = julia + m | m > julia = 2 * p go 0 p | m < julia = Sym.Unangled p go n p | m < julia = Sym.Angled p (1 Sym.% 3) (go (n - 1) (2 * p + m)) go 0 p | m > julia = Sym.Angled p (1 Sym.% 2) (Sym.Angled (p + 1) (1 Sym.% 2) (Sym.Unangled (p + 2))) go n p | m > julia = Sym.Angled p (1 Sym.% 2) (Sym.Angled (p + 1) (1 Sym.% 2) (Sym.Angled (p + 2) (1 Sym.% 3) (go (n - 1) (2 * (p + 2) + m - 2)))) m = 3 * fromIntegral (Sym.denominator pq) address = addressSuffix addressPrefix node (Sym.Angled p1 _ (Sym.Unangled p2)) | m < julia = p1 + p2 node (Sym.Angled p1 _ (Sym.Angled _ _ (Sym.Angled _ _ (Sym.Unangled p2)))) | m > julia = p1 + p2 node (Sym.Angled _ _ a) = node a Just ray = Txt.plain . Sym.binary . <$> Sym.addressAngles address in ray ++ "\t" ++ show (period address) ++ "\t" ++ show (node address)) $ prefixes

That outputs one of the external angles of the angled internal address,
of the Julia morph corresponding to each prefix, as well as its period, and
the period of a neighbouring node in the Julia morph which I used in some
C code to align all the morphs in screen-space. I use the command line tool
`parallel -k`

to parallelize tracing these external rays using
`m-exray-in`

from `mandelbrot-numerics`

, then found
the periodic nucleus of the minibrot islands with `m-nucleus`

, and
the approximate zoom level using `m-domain-size`

and some
`ghc -e`

scripting (starting `ghc`

is slow, better
to run it once to handle multiple inputs). I fed this to some custom C code
that aligns the view with the node period:

#include <stdio.h> #include <mandelbrot-numerics.h> int main(int argc, char **argv) { if (! (argc > 1)) return 1; int Period = atoi(argv[1]); // hardcoded precision, should be based on zoom level mpfr_t Re, Im, Zoom; mpfr_init2(Re, 1000); mpfr_init2(Im, 1000); mpfr_init2(Zoom, 53); // parse KFR input (no error checking) getc(stdin); getc(stdin); getc(stdin); getc(stdin); mpfr_inp_str(Re, stdin, 10, MPFR_RNDN); getc(stdin); getc(stdin); getc(stdin); getc(stdin); getc(stdin); mpfr_inp_str(Im, stdin, 10, MPFR_RNDN); getc(stdin); getc(stdin); getc(stdin); getc(stdin); getc(stdin); getc(stdin); getc(stdin); mpfr_inp_str(Zoom, stdin, 10, MPFR_RNDN); getc(stdin); // find nearby node nucleus and use it to align view mpfr_d_div(Zoom, 2, Zoom, MPFR_RNDN); mpfr_mul_d(Zoom, Zoom, 0.3, MPFR_RNDN); mpc_t c1; mpc_init2(c1, 1000); mpfr_add(mpc_realref(c1), Re, Zoom, MPFR_RNDN); mpfr_set(mpc_imagref(c1), Im, MPFR_RNDN); m_r_nucleus(c1, c1, Period, 64, 1); mpfr_sub(mpc_realref(c1), mpc_realref(c1), Re, MPFR_RNDN); mpfr_sub(mpc_imagref(c1), mpc_imagref(c1), Im, MPFR_RNDN); mpc_abs(Zoom, c1, MPFR_RNDN); mpfr_div_d(Zoom, Zoom, 0.3, MPFR_RNDN); mpfr_d_div(Zoom, 2, Zoom, MPFR_RNDN); mpfr_printf("Re: %Re\r\nIm: %Re\r\nZoom: %.18e\r\n", Re, Im, mpfr_get_d(Zoom, MPFR_RNDN)); mpc_arg(Zoom, c1, MPFR_RNDN); printf("RotateAngle: %.18f\r\nIterations: 15000\r\n", mpfr_get_d(Zoom, MPFR_RNDN) / (2 * 3.141592653589793) * 360); return 0; }

Then I rendered the final images with `kf.x86_64.exe`

.
All of this was orchestrated by a Bash shell script, `a.txt`

generated by the Haskell with depth 12 (4096 lines) (this took the most
time, almost 2 days):

#!/bin/bash prec=1000 cat a.txt | while read ray period node do echo "m-exray-in 100 '${ray}' 8 $((2 * 8 * ${period}))" done | parallel -k | paste a.txt - | tee b.txt cat b.txt | while read ray period node re im do echo m-nucleus $prec $re $im $period 64 1 done | parallel -k | paste b.txt - | tee c.txt cat c.txt | while read ray period node ere eim nre nim do echo m-domain-size $prec $nre $nim $period done | parallel -k | paste c.txt - | tee d.txt cat d.txt | while read ray period node ere eim nre nim ds do echo $ds done | ghc -e "interact $ unlines . map (show . (\s -> 2 / (10 * s**1.125)) . read) . lines" | paste d.txt - | tee e.txt cat -n e.txt | while read n ray period node ere eim nre nim ds zoom do echo -e "Re: $nre\r\nIm: $nim\r\nZoom: $zoom\r" | m-align-nodes ${node} > $(printf %04d.kfr $n) done for i in ????.kfr do kf.x86_64.exe -s settings.kfs -l "$i" -c palette.kfp -t "$i.tif" 2>&1 | grep -v fixme # silence Wine error flood convert -colorspace RGB $i.tif -geometry 1920x1080 -colorspace sRGB "${i%kfr}tif" && rm "$i.tif" done ffmpeg -r 60 -i "%04d.tif" -pix_fmt yuv420p -profile:v high -level:v 5.1 -crf:v 20 spin.mkv

I asked another question about the magic number 1.125 in this script.

While the rays were tracing (using the CPU) I ran some of the commands
on the first ray to get the first KFR file, and I rendered a zoom video
(using the GPU) to the minibrot at its center, controlling the zoom speed
and depth in `zoomasm`

so that the zoom video slowed down and
paused at the exact zoom depth of the spin video (I tried to use maths
to work out the numbers to enter into zoomasm, but couldn't figure it out
so I eventually used binary search by hand and got it close enough).

I also made a soundtrack, using the pairs of rays of each spin video
frame, angles expressed in binary, converted to wavetables and stretched
to the length of the frame using FFT/zeropad/IFFT. I processed the result
of this in `audacity`

, to extend it to the length of the whole
video (making sure to keep the spin section correctly aligned in time):

#include <complex.h> #include <math.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sndfile.h> #include <fftw3.h> int main(int argc, char **argv) { const int SR = 192000; const int FPS = 60; const int length = SR / FPS; float audio[length][2]; double _Complex *ifft_in = fftw_malloc(length * sizeof(*ifft_in)); double _Complex *ifft_out = fftw_malloc(length * sizeof(*ifft_out)); fftw_plan backward = fftw_plan_dft_1d(length, ifft_in, ifft_out, FFTW_BACKWARD, FFTW_PATIENT | FFTW_DESTROY_INPUT); int old_period = 0; double _Complex *fft_in = 0; double _Complex *fft_out = 0; fftw_plan forward = 0; SF_INFO onfo = { 0, SR, 2, SF_FORMAT_WAV | SF_FORMAT_FLOAT, 0, 0 }; SNDFILE *ofile = sf_open("audio.wav", SFM_WRITE, &onfo); while (true) { int period = 0; char *ray[2] = { 0, 0 }; if (3 != scanf("%d\t%ms\t%ms\n", &period, &ray[0], &ray[1])) { break; } if (period != old_period) { if (fft_in) { fftw_free(fft_in); } if (fft_out) { fftw_free(fft_out); } if (forward) { fftw_destroy_plan(forward); } fft_in = fftw_malloc(period * sizeof(*fft_in)); fft_out = fftw_malloc(period * sizeof(*fft_out)); forward = fftw_plan_dft_1d(period, fft_in, fft_out, FFTW_FORWARD, FFTW_MEASURE | FFTW_DESTROY_INPUT); } for (int c = 0; c < 2; ++c) { int v = 0; for (int p = 0; p < period; ++p) { fft_in[p] = v; v += ray[c][p] == '0' ? -1 : 1; } double s = v / (double) period; double _Complex dc = 0; for (int p = 0; p < period; ++p) { dc += (fft_in[p] -= p * s); } dc /= period; for (int p = 0; p < period; ++p) { fft_in[p] -= dc; } fftw_execute(forward); memset(ifft_in, 0, length * sizeof(*ifft_in)); for (int p = 0; p < period/2; ++p) { ifft_in[p] = fft_out[p]; ifft_in[length - 1 - p] = fft_out[period - 1 - p]; } fftw_execute(backward); double rms = 0; for (int p = 0; p < length; ++p) { audio[p][c] = creal(ifft_out[p]); rms += audio[p][c] * audio[p][c]; } rms = 0.25 / sqrt(rms / length); for (int p = 0; p < length; ++p) { audio[p][c] *= rms; } free(ray[c]); } sf_writef_float(ofile, &audio[0][0], length); old_period = period; } sf_close(ofile); return 0; }

Then I split the zoom video at this point and splicing in the spin video:

ffmpeg -i zoom.mkv -t 136 -codec:v copy zoom-1.mkv ffmpeg -i zoom.mkv -ss 136 -codec:v copy zoom-2.mkv cat > zoom.txt <<EOF file 'zoom-1.mkv' file 'spin.mkv' file 'zoom-2.mkv' EOF ffmpeg -f concat -i zoom.txt -i soundtrack.m4a \ -codec:v copy -codec:a copy "Julia morph orbit in the hairs.mkv"

Then two-pass encoding to a lower bitrate version for web streaming:

ffmpeg -i "Julia morph orbit in the hairs.mkv" \ -codec:a copy -profile:v high -level:v 5.1 -g 120 \ -x264opts keyint=120:no-scenecut -b:v 6M -movflags +faststart \ -pass 1 -y "Julia morph orbit in the hairs.mp4" ffmpeg -i "Julia morph orbit in the hairs.mkv" \ -codec:a copy -profile:v high -level:v 5.1 -g 120 \ -x264opts keyint=120:no-scenecut -b:v 6M -movflags +faststart \ -pass 2 -y "Julia morph orbit in the hairs.mp4"

And that's the video linked from the image at the top of the page.

]]>*Old Wood Dish* (2010) by James W. Morris is a fractal artwork,
a zoomed-in view of part of the Mandelbrot set. The magnification factor
of 10152 is quite shallow by today's standards, but in 2010 the
perturbation and series approximation techniques for speeding up image
generation had not yet been developed: this is a deep zoom for that era.
Thankfully JWM's (now defunct) gallery included the parameter files, the
image linked above is a high resolution re-creation in Kalle's Fraktaler,
thanks to a parameter file conversion script I wrote. You can find out
more about JWM's software MDZ and see more of his images on my
mirror of part of his old website.

*Old Wood Dish* is an example of what would now be called
"Julia morphing", using the property that zooming in towards baby Mandelbrot
set islands doubles-up (and then quadruples, octuples, ...) the features
you pass. This allows you to sculpt patterns, here the pattern has a tree
structure.

Each baby Mandelbrot set islands has a positive integer associated to
it: its period. Iteration of the center of its cardioid repeats with that
period, returning to 0. Atom periods are "near miss" periods, where the
iteration gets nearer to 0 than it ever did before. They indicate a nearby
baby Mandelbrot set island (or child bulb) of that period.
The atom periods of the center of *Old Wood Dish* are:

1, 2, 34, 70, 142, 286,574,862, 1438, 2878, 5758

One can see a pattern: 2 * 34 + 2 = 70; 2 * 70 + 2 = 142; 2 * 142 + 2 = 286. But this pattern is broken at the numbers highlighted: 2 * 574 + 2 = 1150 != 862.

Using Newton's root-finding method in one complex variable,
one can find the nearby baby Mandelbrot sets with those periods. When zooming
out, these eventually each become the lowest period island in the view in turn
(higher periods are closer to the starting point), and the zoom level at which
this happens is usually significant in terms of the decisions made when performing
Julia morphing. These zoom levels (log base 10) for *Old Wood Dish* are:

0.114, 0.591, 4.69, 8.44, 14.0, 22.4, 30.8, 43.4, 66.6, 101, 152

and the successive ratios of these numbers are

5.15, 7.94, 1.79, 1.66, 1.59,1.37,1.40, 1.53, 1.52, 1.50

Repeated Julia morphing leads to these ratios tending to a constant (often 1.5), but the two numbers highlighted are clearly outside the curve: one can see that these correspond to the two mismatching periods. I'll have to ask him to see if this was intentional or an accident.

A list of atom domain periods is related to a concept called an
internal address, which is an ascending list of the lowest periods of
the hyperbolic components (cardioid-like or disk-like shapes) that you
pass through along the filaments on the way to the target from the origin.
An extension, angled internal addresses, removes the ambiguity of which
way to turn (for example, there are two period 3 bulbs attached to the
period 1 cardioid, they have internal angles 1/3 and 2/3). One can find
angled internal addresses by converting from external angles, and one
can find external angles by tracing rays outwards from a point towards
infinity. The angled internal address of *Old Wood Dish* starts:

1 1/2 2 16/17 33 1/2 34 1/3 69 1/2 70 1/3 141 1/2 142 1/3 285 1/2 286 ...

and the pattern can be extended indefinitely by

... 1/3 (p-1) 1/2 p 1/3 (2p+1) 1/2 (2p+2) ...

The numerators of the angles in an angled internal address can be varied
freely, so one can create a whole family of variations. Varying the 1/3 to
2/3 only changes the alignments of the decorations outside the tree structure,
but varying 16/17 changes the shapes that tree is built from. Here are
*Old Wood Dish* variations 1-16, with the irregular zoom pattern
adjusted to a fully-regular zoom ending up with period 9214:

I found the center coordinates for these images by tracing external rays towards each period 9214 inner island. This took almost 5 hours wall-clock time with 16 threads in parallel (one for each ray). I then found the approximate view radius by atom domain size raised to the power of 1.125, multiplied by 10. These magic numbers were found by exploring shallower versions graphically. Using this radius I used Newton's method again, to find the pair of period 13820 minibrots at the first junctions near the center. I found these periods using KF-2.15.3's newly improved Newton zooming dialog. I used their locations to rotate and scale all the images for precise alignment. Animated it looks quite hypnotic I think:

Software used:

- mandelbrot-numerics m-describe program and script to get rough idea of period structure;
- mandelbrot-perturbator GTK program to explore the shallow layers and trace external rays to find external angles;
- mandelbrot-symbolics Haskell library in GHCI REPL to convert (both directions) between external angles and angled internal addresses;
- mandelbrot-numerics m-exray-in program to trace rays inwards given external angles;
- mandelbrot-numerics m-nucleus program to find periodic root from ray end point;
- mandelbrot-numerics m-domain-size program to find approximate view size;
- kf-2.15.3 interactive Newton zooming dialog to find period of the first junction nodes;
- custom code in C to align views, using mandelbrot-numerics library;
- custom code in bash shell to combine everything into KFS+KFR files;
- kf-2.15.3 command line mode to render each KFS+KFR to very large TIFF files;
- ImageMagick convert program to downscale for anti-aliasing (PNG for web, and smaller GIFs);
- gifsicle program to combine the 16 frames into 1 animated GIF.

Has it been a year already?

Co.Lab Sounds is back again with Fringe Art Bath for their 2021 festival!

Co.Lab Sound is an experimental testbed for artists to develop new work and collaborate live. Informed by a long line of experimental music, sound art and performance, these events embrace the processes of improvisation and experimentation to showcase artists expanding the field of sound art and what it means to experience art live.

Performing is inherently participatory, our aim is to develop a new dialogue between the audience and our exhibiting artists, collectives and musicians by making the audience a part of the making process. We encourage the disruptive, the active and outspoken. Take part, take in and be live.

I have a new video in the online exhibition, featuring a selection of algorithmically generated fractal zoom videos made by my fractal explorer bot named Rodney. The soundtrack is synced to the video fames: there is a 2D bank of resonant filters controlled by colours, each with a sawtooth waveshaper based on combining [hilbert~] ported from Pure-data with Chebyshev polynomials.

]]>After almost 2 months of work I'm happy to announce a new release of Kalles Fraktaler 2 +, fast deep zooming Free Software for fractal graphics (Mandelbrot, Burning Ship, etc). Most of the focus has been on speed improvements, with rescaled iterations ala Pauldelbrot providing a big speedup for most deep zoom locations. Full change log:

kf-2.15.3 (2021-05-26)

- new: updated progress reporting in status bar to include more information
- new: texture resize control in colouring dialog (disable to get actual image pixels in OpenGL GLSL shaders)
- new: texture browse dialog allows selecting BMP and PNG images as well as JPEG
- new: Newton-Raphson zooming dialog changes

- user interface redesigned from scratch
- new absolute zooming modes (previous mode is called relative)
- new atom domain mode (for Mandelbrot set power 2 and hybrids only)
- new size factor control
- can auto-capture zoom depth after Newton zoom
- auto skew (escape) moved to transformation dialog
- new: transformation dialog changes

- new zoom adjustment control (with rotation on left mouse button)
- now shows the difference between the original transformation and the new transformation, instead of the total transformation
- stretch amount now displayed in cents for a more friendly range
- spin buttons added so scroll wheel and arrows can adjust values, which live-update the transformed image
- auto skew (escape) moved from Newton-Raphson zooming dialog
- new: single precision floating point support for shallow zooms (until zoom e20) (disabled by default due to some locations having undetected glitches)
- new: single precision floatexp extended floating point for arbitrarily deep zooms (disabled by default due to some locations having undetected glitches)
- new: OpenCL can work in single precision mode, for example on devices that don’t support double precision
- new: rescaled perturbation calculations for arbitrarily deep zooms (usually faster than old long double and floatexp implementations; with or without derivatives; with or without OpenCL; single or double precision, single precision disabled by default due to some locations having undetected glitches); supported formulas:

- Mandelbrot power 2
- Mandelbrot power 3
- Burning Ship power 2
- hybrid formula editor
- new: rescaled series approximation calculations for Mandelbrot power 2 (about 30% faster than the all-floatexp implementation, can be disabled if necessary in the perturbation and series approximation tuning dialog)
- new: number type selection dialog (advanced menu) allows fine-tuning allowed implementations
- new: “reuse reference” (advanced menu) can be used together with “auto solve glitches” (this uses additional memory for the primary reference)
- fix: “reuse reference” re-calculates reference when the used number type changes (fixes some issues with bad images and/or crashes)
- new: “reference strict zero” control in perturbation and series approximation tuning dialog (advanced menu, experimental); affects rescaled iterations only
- new: lower level implementation of reference calculations for hybrids is over 7x faster (now only 10% slower than built in versions)
- new: OpenCL can run threaded to improve user interface responsiveness (enabled by default; can be disabled in OpenCL device selection dialog)
- new: “‘Open’ resets default parameters” setting can be disabled to load minimal KFR/KFP without resetting missing parameters to defaults (this setting is enabled by default for backwards compatibility)
- new: “glitch low tolerance” can be a fraction between 0 and 1
- new: “approx low tolerance” can be a fraction between 0 and 1
- new: crash recovery offers to restore settings as well as parameters
- fix: correct power calculation for multiplied hybrid operators (symptom: seams between iteration bands with numeric DE)
- fix: documentation uses subsections instead of lists for improved navigation and table of contents
- known issue: some locations (especially Burning Ship “deep needle”) are much slower and need much more memory; workaround:

- disable “rescaled single” in number type selection dialog; and if still slow:
- disable “rescaled double” in number type selection dialog
- known issue: some locations have undetected glitches in single precision; workaround:

- disable “single”, “rescaled single” and “floatexp single” in number type selection dialog; or
- enable “glitch low tolerance” in perturbation and series approximation tuning dialog

Get it from mathr.co.uk/kf/kf.html.

Now I'll probably take a break from coding KF until after the summer, apart from bugfixes, as I don't have a big exciting idea to inspire and motivate me. Some ideas for when I come back include:

- stripe average colouring via a ring buffer of last few iterations
- auto skew method based on directional DE distribution in an image
- OpenCL/OpenGL sharing
- use multiple OpenCL platforms and devices at the same time
- GLSL file watcher so you can use your favourite text editor when writing colouring algorithms
- plain iterations (without perturbation) for very shallow zooms
- port/embed mandelbrot-perturbator engine for glitch correction of power 2 Mandelbrot by "rebasing and carrying on"
- store starting zoom in Newton-Raphson progress updates and add resume functionality
- rip out 75% of the built-in formulas and replace with hybrid formula designer versions
- refactor the build system for faster incremental development

If anyone out there wants to work on any of these or other ideas, I'll be more than happy to help you get started navigating the code to know where the changes should be made.

]]>*(click the picture to view the video on diode.zone)*

The **legendary colour palette technique** embeds an image in the iteration
bands of an escape time fractal by linearizing it by scanlines and
synchronizing the scan rate to the iterations in the fractal spirals
so they line up to reconstruct the original image. Historically this has
been done by preparing palettes for fractal software using external
tools, and mostly only for small images (KF for example has a palette
limited to 1024 colour slots, which means 32x32 or 64x16 at most).

Kalles Fraktaler 2 has an image texture feature, which historically only allowed you to warp a background through the semi-transparent fractal. I added the ability to create custom colouring algorithms in OpenGL shader language (GLSL), with which it is possible to repurpose this texture and (for example) use it as a legendary palette.

Here I scaled my avatar (originally 256x256) to 128x16 pixels, and fine tuned the iteration count divisor by hand after zooming to a spiral in the Seahorse Valley of the Mandelbrot set. Then the face from the icon is visible in the spirals all the way down to the end of the video. I used a work-in-progress (not yet released) build of KF 2.15.3, which has a new setting not to resize the texture to match the frame size: this allows the legendary technique to work much more straightforwardly.

I rendered exponential map EXR frames from KF and assembled into a zoom video with zoomasm. From KF I exported just the RGB channels with the legendary palette colouring, and the distance estimate channels. I did not colour the RGB with the distance estimate in KF, because with the exponential map transformation they would not be screen-space correct (the details would be smaller in the center of the reprojected video than at the edges). I could not do all the colouring in zoomasm either, because it does not support image textures. I added the boundary of the fractal in zoomasm afterwards, by mixing pink with the RGB from KF according to the length of the screen-space distance estimate channels (which zoomasm scales properly when reprojecting the exponential map).

This post was inspired by Fractal Universe's YouTube videos from 2017:

- Mandelbrot zoom 10^49 - Legendary color palette #1 : Phantom minibrot
- Mandelbrot zoom 10^94 - Legendary color palette #2 : Sierpinsky triangle
- Mandelbrot zoom 10^94 - Legendary color palette #3 : the word "MANDELBROT"

I hope to release KF 2.15.3 before the end of this month. There are big changes (aside from the texture resize setting) that I'm keen to get out into the world.

]]>The complex beauty of the world's most famous fractal, the Mandelbrot set, emerges from the repeated iteration of a simple formula:

\[z \to z^2 + c\]

Zooming into the intricate boundary of the shape reveals ever more detail, but one needs higher precision numbers and higher iteration counts as you go deeper. The computational cost rises quickly with the classical rendering algorithms which use high precision numbers for each pixel.

In 2013, K.I. Martin's SuperFractalThing and accompanying white paper sft_maths.pdf popularized a pair of new acceleration techniques. First one notes that the formula \(z \to z^2 + c\) is continuous, so nearby points remain nearby under iteration. This means you can iterate one point at high precision (the reference orbit) and compute differences from the reference orbit for each pixel in low precision (the perturbed orbits). Secondly, iterating the perturbed formula one ends up with a polynomial series in the initial pertubation in \(c\), which depends only on the reference. The degree rises rapidly but you can truncate it to get an approximation. This means you can compute the series approximation coefficients once, and substitute in the perturbed \(c\) values for each pixel, allowing you to initialize the perturbed orbits at a later iteration, skipping potentially lots of per-pixel work.

The perturbation technique has since been extended to the Burning Ship fractal and other "abs variations", and it also works for hybrid fractals combining iterations of several formulas.

Prerequisites for the rest of this article: a familiarity with complex numbers and algebraic manipulation; knowing how to draw the unzoomed Mandelbrot set; understanding the limitations of computer implementation of numbers (see for example Hardware Floating Point Types).

In the remainder of this post, lower case and upper case variables with the same letter mean different things. Upper case means unperturbed or reference, usually high precision or high range. Lower case means perturbed per pixel delta, low precision and low range.

In perturbation, on starts with the iteration formula [1]:

\[Z \to Z^2 + C\]

Perturb the variables with unevaluated sums [2]:

\[(Z + z) \to (Z + z)^2 + (C + c)\]

Do symbolic algebra to avoid the catastrophic absorption when adding tiny values \(z\) to large values \(Z\) (e.g. 1 million plus 1 is still 1 million if you only have 3 significant digits to work with) [3]:

\[z \to 2 Z z + z^2 + c\]

\(C, Z\) is the "reference" orbit, computed in high precision using [1] and rounded to machine double precision, which works fine most of the time. \(c, z\) are the "pixel" orbit, you can do many of these near each reference (e.g. an entire image).

There is a problem that can be noticed when you zoom deeper near certain features in the fractal. There are parts that can have a "noisy" appearance, or there may be weird flat blobs that look out of place. These are the infamous perturbation glitches. It was observed that adding references in the glitches and recomputing the pixels could fix them, but there was no reliable way to detect them programmatically until Pauldelbrot discovered/invented a method: Perturbation Theory Glitches Improvement.

The solution: if [4]:

\[|Z+z| << |Z|\]

at any iteration, then glitches can occur. The solution: retry with a new reference, or (for well-behaved formulas like the Mandelbrot set) rebase to a new reference and carry on.

Perturbation assumes exact maths, but some images have glitches when naively using perturbation in low precision. Pauldelbrot found his glitch criterion by perturbing the perturbation iterations: one has perturbed iteration as in [3] (recap: \(z \to 2 Z z + z^2 + c\)). Then one perturbs this with \(z \to z + e, c \to c + f\) [5]:

\[e \to (2 (Z + z) + e) e + f\]

We are interested what happens to the ratio \(e/z\) under iteration, so rewrite [3] as [6]:

\[z \to (2 Z + z) z + c\]

Pattern matching, the interesting part (assuming \(c\) and \(f\) are small) of \(e/z\) is \(2(Z + z) / 2 Z\). When \(e/z\) is small, the nearby pixels "stick together" and there is not enough precision in the number type to distinguish them, which makes a glitch. So a glitch can be detected when [7]:

\[|Z + z|^2 < G |Z|^2\]

where G is a threshold (somewhere between 1e-2 and 1e-8, depending how strict you want to be). This does not add much cost, as \(|Z+z|^2\) already needs to be computed for escape test, and \(G|Z^2|\) can be computed once for each iteration of the reference orbit and stored.

The problem now is: How to choose G? Too big and it takes forever as glitches are detected all over, too small and some glitches can be missed leading to bad images.

The glitched pixels can be recalculated with a more appropriate reference point: more glitches may result and adding more references may be necessary until the image is finished.

Double precision floating point (with 53 bits of mantissa) is more than enough for computing perturbed orbits: even single precision (with 24 bits) can be used successfully. But when zooming deeper another problem occurs: double precision has a limited range, once values get smaller than about 1e-308 then they underflow to 0. This means perturbation with double precision can only zoom so far, as eventually the perturbed deltas are smaller than can be represented.

An early technique for extending range is to store the mantissa as a double precision value, but normalized to be near 1 in magnitude, with a separate integer to store the exponent. This floatexp technique works for arbitrarily deep zooms, but the performance is terrible because it needs to handle every arithmetic operation in software (instead of them being a single CPU instruction).

The solution for efficient performance turned out to be using an unevaluated product (compare with the unevaluated sum of perturbation) to rescale the double precision iterations to be nearer 1 and avoid underflow: substitute \(z = S w\) and \(c = S d\) to get [8]:

\[S w \to 2 Z S w + S^2 w^2 + S d\]

and now cancel out one scale factor \(S\) throughout [9]:

\[w \to 2 Z w + S w^2 + d\]

Choose \(S\) so that \(|w|\) is around \(1\). When \(|w|\) is at risk of overflow (or underflow) after some iterations, redo the scaling; this is typically a few hundred iterations as \(|Z|\) is bounded by \(2\) except at final escape.

Optimization: if \(S\) underflowed to \(0\) in double precision, you don't need to calculate the \(+ S w^2\) term at all when \(Z\) is not small. Similarly you can skip the \(+ d\) if it underflowed. For higher powers there will be terms involving \(S^2 w^3\) (for example), which might not need to be calculated either due to underflow. Ideally these tests would be performed once at rescaling time, instead of in every inner loop iteration (though they would be highly predictable I suppose).

There is a problem: if \(|Z|\) is very small, it can underflow to \(0\) in unscaled double in [9]. One needs to store the full range \(Z\) and do a full range (e.g. floatexp) iteration at those points, because \(|w|\) can change dramatically. Rescaling is necessary afterwards. This was described by Pauldelbrot: Rescaled Iterations in Nanoscope.

To do the full iteration, compute \(z = S w\) in floatexp (using a floatexp for \(S\) so that there is no underflow), do the perturbed iteration [3] with all variables in floatexp. To rescale afterwards, compute \(S = |z|\) and \(w = z/S, d = c/S\) (computed in floatexp with \(w\) and \(d\) rounded to double precision afterwards). Then a double precision \(s\) can be computed for use in [9].

The Burning Ship fractal modifies the Mandelbrot set formula by taking absolute values of the real and imaginary parts before the complex squaring [10]:

\[X + i Y \to (|X| + i |Y|)^2 + C\]

When perturbing the Burning Ship and other "abs variations", one ends up with things like [11]:

\[|XY + Xy + xY + xy| - |XY|\]

which naively gives \(0\) by catastrophic absorption and cancellation. laser blaster made a case analysis Perturbation Formula for Burning Ship which can be written as [12]:

diffabs(c, d) := |c+d| - |c| = c >= 0 ? c + d >= 0 ? d : -(2*c+d) : c + d > 0 ? 2*c+d : -d

when \(d\) is small the \(\pm d\) cases are much more likely. With rescaling in the mix [11] works out as [13]:

\[\operatorname{diffabs}(XY/s, Xy + xY + sxy)\]

which has the risk of overflow when \(s\) is small, but the signs work out ok even for infinite \(c\) as \(d\) is known to be finite. Moreover, if \(s = 0\) due to underflow, the \(\pm d\) branches will always be taken (except when \(XY\) is small, when a full floatexp iteration will be performed instead), and as \(s \ge 0\) by construction, [13] reduces to [14]:

\[\operatorname{sign}(X) * \operatorname{sign}(Y) * (X y + x Y)\]

(Note: this formulation helps avoid underflow in \(\operatorname{sign}(XY)\) when \(X\) and \(Y\) are small.)

For well-behaved functions like the Mandelbrot set iterations, one needs to do full iterations when \(Z\) gets small. For the Burning Ship and other abs variations, this is not sufficient: problems occur if either X and Y are small, not only when both are small at the same time. Full iterations need to be done when either variable is small. This makes rescaled iterations for locations near the needle slower than just doing full floatexp iterations all the time (because of the extra wasted work handling the rescaling). This is because near the needle all the iterations have Y near 0, which means floatexp iterations will be done anyway. Using floatexp from the get go avoids many branches and rescaling in the inner loop, so it's significantly faster. The problem is worse in single precision because it has much less range: it underflows below 1e-38 or so, rather than 1e-308 for double precision.

The problem of automatically detecting these "deep needle" locations (which may be in the needles of miniships) and switching implementations to avoid the extra slowdown remains unresolved in KF.

The Mandelbrot set has lovely logarithmic spirals all over, and the Burning Ship has interesting "rigging" on the miniships on its needle. Hybridization provide a way to get both these features in a single fractal image. The basic idea is to interleave the iteration formulas, for example alternating between [1] and [10], but more complicated interleavings are possible (eg [1][10][1][1] in a loop, etc).

Hybrid fractals in KF are built from stanzas, each has some lines, each line has two operators, and each operator has controls for absolute x, absolute y, negate x, negate y, integer power \(p\), complex multiplier \(a\). The two operators in a line can be combined by addition, subtraction or multiplication, and currently the number of lines in a stanza can be either 1 or 2 and there can be 1, 2, 3 or 4 stanzas. The output of each line is fed into the next, and at the end of each stanza the +c part of the formula happens. There are controls to choose how many times to repeat each stanza, and which stanza to continue from after reaching the end.

Implementing perturbation for this is quite methodical. Start from an operator, with inputs \(Z\) and \(z\). Set mutable variables:

z := input W := Z + z B := Z

If absolute x enabled in formula, then update

re(z) := diffabs(re(Z), re(z)) re(W) := abs(W) re(B) := abs(B)

Similarly for the imaginary part. If negate x enabled in formula, then update

re(z) := -re(z) W := -W B := -B

Similarly for the imaginary part. Now compute

\[S = \sum_{i=0}^{p-1} W^i B^{p-1 - i}\]

and return \(a z S\). Combining operators into lines may be done by Perturbation Algebra. Combining lines into stanzas can be done by iterating unperturbed \(Z\) alongside perturbed \(z\); only the \(+C\) needs high precision, and that is not done within a stanza.

Rescaling hybrid iterations seems like a big challenge, but it's not that hard: if either or both the real and imaginary parts of the reference orbit \(Z\) are small, one needs to do a full range iteration with floatexp and recalculate the scale factor afterwards, as with formulas like Burning Ship. Otherwise, thread \(s\) through from the top level down to the operators. Initialize with

W := Z + z*s

and modify the absolute cases to divide the reference by \(s\):

re(z) := diffabs(re(Z/s), re(z))

Similarly for imaginary part. When combining operators (this subterm only occurs with multiplication) replace \(f(o_1, Z + z)\) with \(f(o_1, Z + z s)\).

And that's almost all the changes that need to be made!

For distance estimation of hybrid formulas I use dual numbers for automatic differentiation. One small adjustment was needed for it to work with rescaled iterations: instead of initializing the dual parts (before iteration) with 1 and scaling by the pixel spacing at the end for screen-space colouring, initialize the dual parts with the pixel spacing and don't scale at the end. This avoids overflow of the derivative, and the same rescaling factor can be used for regular and dual parts.

Naive implementations of parametric hybrids are very slow due to all the branches in the inner loops (checking if absolute x enabled at every iteration for every pixel, etc). Using for example OpenCL, these branches can be done once when generating source code for a formula, instead of every iteration for every pixel. This runs much faster, even when compiled to run on the same OpenCL device that is interpreting the parametric code.

The other part of the thing that K I Martin's SuperFractalThing popularized was that iteration of [3] gives a polynomial series in \(c\) [15]:

\[z_n = \sum A_{n,k} c^k\]

(with 0 constant term). This can be used to "skip" a whole bunch of iterations, assuming that truncating the series and/or low precision doesn't cause too much trouble. Substituting [15] into [3] gives [16]:

\[\sum A_{n+1,k} c^k = 2 Z \sum A_{n,k} c^k + (\sum A_{n,k} c^k)^2 + c\]

Equating coefficients of \(c^k\) gives recurrence relations for the series coefficients \(A_{n,k}\). See Simpler Series Approximation.

The traditional way to evaluate that it's ok to do the series approximation at an iteration is to check whether it doesn't deviate too far from regular iterations (or perturbed iterations) at a collection of "probe" points. When it starts to deviate, roll back an iteration and initialize all the image pixels with [15] at that iteration.

Later, knighty extended the series approximation to two complex variables. If the reference \(C\) is a periodic point (for example the center of a minibrot), the biseries in \(z, c\) allows skipping a whole period of iterations. Then multiple periods can be skipped by repeating the biseries step. This gives a further big speedup beyond regular series approximation near minibrots. An escape radius is needed for \(z\), based on properties of the reference, so as not to perform too many biseries iterations. After that, regular perturbed iterations are performed until final escape. This is available in KF as NanoMB1.

Current research by knighty and others involves a chain of minibrots at successively deeper zoom levels. One starts with the deepest minibrot, performing biseries iterations until it escapes its \(z\) radius. Then rebase the iterates to the next outer minibrot, and perform biseries iterations with that. Repeat until final escape. This is available in KF as NanoMB2, but it's highly experimental and fails for many locations. Perhaps it needs to be combined with more perturbation or higher precision: sometimes the iterates may still be too close to each other when they escape a deep minibrot, such that catastrophic absorption occurs. In progress...

For Burning Ship and other abs variations (and presumably hybrids too), series approximation can take the form of two bivariate real series in \(\Re(c)\) and \(\Im(c)\) for the real and imaginary parts of \(z\). But these are only good so long as the region is not folded by an absolute value, so typically only a few iterations can be skipped. Maybe the series can be split into two (or more) parts with the other side(s) shifted when this occurs? In progress...

Perturbation techniques that greatly reduce the quantity of high precision iterations needed, as well as (for well-behaved formulas) series approximation techniques that reduce the quantity of low precision iterations needed still further, provide a vast speedup over classical algorithms that use high precision for every pixel. Rescaling can provide an additional constant factor speedup over using full range floatexp number types for most (not "deep needle") locations. Chained biseries approximation ("NanoMB2") and series approximation for abs variations and hybrids are still topics of research.

It remains open how to choose the \(G\) for Pauldelbrot's glitch detection criterion, and how to robustly compute series approximation skipping: there is still no complete mathematical proof of correctness with rigourous error bounds, although the images do most often look plausible and different implementations do tend to agree.

]]>*Wurgo* is a track from my album *Inside Outside* I released in 2005.
This week I made a video for it, showcasing the new features of *kf-2.15.2*
and *zoomasm-3.0*, available at:

I recorded a short *Making Of* video to explain the new features:

The Making Of is flawed, mostly because I forgot to route zoomasm to OBS in JACK when recording the first time, and the second time I'd lost a bit of enthusiasm...

]]>Zoom videos are a genre of 2D fractal animation. The rendering of the final video can be accelerated by computing exponentially spaced rings around the zoom center, before reprojecting to a sequence of flat images.

Some fractal software supports rendering EXR keyframes in exponential map form, which *zoomasm* can assemble into a zoom video. zoomasm works from EXR, including raw iteration data, and colouring algorithms can be written in OpenGL shader source code fragments.

New in *zoomasm-3.0* is support for loading KFR/KFP files containing OpenGL GLSL colouring algorithms direct from *kf-2.15.2* or later.

zoomasm 3.0 (2021-04-02)

- API: backwards incompatible change: GLSL shaders now need to define
`vec3 colour(void)`

and call`getX()`

functions instead of having everything as arguments. See the manual for API documentation.- New: support for KFR/KFP palettes with OpenGL GLSL shaders from
Kalle’s Frakaler 2 +version 2.15.2 or later. Some features are missing:

- Iterations, IterationsMin, IterationsMax;
- Jitter compensation for numerical differences-based DE;
- Texture images.
- Win: change binary names to x86_64 and i686: maybe aarch64 or armv7 will be possible later using llvm-mingw.
- Win: put
`presets/`

in a subfolder of the distribution- Win: update
`openexr`

to 2.5.5- Win: update
`glew`

to 2.2.0- Win: update
`glfw`

to 3.3.3- Win: update
`imgui`

,`ìmgui-filebrowser`

,`miniaudio`

, and`tomlplusplus`

to latest versions.

Get it from mathr.co.uk/zoomasm

]]>Kalle's Fraktaler 2 + is fast deep zooming Free Software for fractal graphics (Mandelbrot, Burning Ship, etc).
The new version *kf-2.15.2* I released a couple of days ago has a big new feature:
you can design your own custom colouring algorithms in OpenGL GLSL shader fragments,
compatible with *zoomasm-3.0*.
Several examples are included. Full change log:

kf-2.15.2 (2021-03-31)

- new: custom OpenGL colouring algorithms (compatible with
zoomasm 3.0)

- button bottom left of the colors dialog or Ctrl+G in main window
- text area for GLSL editing, or import and export to `*.glsl` files
- GLSL is stored in KFP palettes, KFR files and image metadata
- user API allows access of KFP parameters within GLSL
- new: OpenGL implementation of colouring algorithm

- default shader gives similar results as the regular implementation
- there are tiny differences (1 or 2 ULP) due to different rounding
- uses portions of libqd ported to GLSL for higher precision (49 bits, vs 24 bits for float and 53 bits for double on CPU)
- some features remain unimplemented in this version:

- least squares 2x2 numerical differencing algorithm
- least squares 3x3 numerical differencing algorithm
- new: experimental ARM builds are possible using llvm-mingw

- 64bit aarch64 can be built but crashes on start
- 32bit armv7 is blocked on a GMP bug(?) (missing symbols in library)
- still needs gcc windres because llvm windres is incomplete
- new: option to save EXR files without their 8bit RGBA preview images (makes files smaller and saves faster)
- new: sRGB gamma-correct downscaling (using patched pixman)
- fix: removed special case for zooms less than 1e3, fixes rendering of Redshifter 1 cubic a=2 z0=-4/3 (reported by gerrit)
- fix: make undo and redo refresh the colors dialog
- fix: remove warning about slow unused derivatives, because it is hard to tell if GLSL colouring might need them
- fix: dependency preparation script adapted to new architectures
- fix: dependency preparation script can build subsets of libraries
- fix: correctly initialize "no glitch detection" vector in SIMD
- fix: use `delete[]` instead of `delete` in some places
- fix: use `static inline` to prevent redefinition errors in llvm
- upgrade to pixman 0.40+git (claudeha/kf branch)
- upgrade to tiff 4.2.0
- upgrade to openexr 2.5.5

Get it from mathr.co.uk/kf/kf.html

For the next version, I plan on trying to implement the optimized rescaling replacement for the floatexp number type described by Pauldelbrot and implemented in rust-fractal (among others): by properties of iterations, it should be possible to renormalize much less frequently (about once every 200 iterations or more) instead of after every single arithmetic operation (which is very very slow). Hopefully the speedup should be dramatic, which will be very beneficial for the OpenCL engine because it uses floatexp much sooner (due to lack of support for 80bit x87 long double).

]]>This Saturday evening (GMT) the great London venue IKLECTIK is streaming some performances:

Sonic Electronics - Williams / Yorke / Allen / Vo Ezn / Netz

IKLECTIK [off-site] presents,

SONIC ELECTRONICSwith Jake Williams / Loula Yorke / Claude Heiland-Allen / Vo Ezn / Laura Netz

Saturday 27 February 2021 | 8.30pm (GMT)

IKLECTIK YOUTUBE channel: https://youtu.be/OdNE9WSyA7M

IKLECTIK TWITCH channel: https://www.twitch.tv/iklectik

Please support the artists! If you can donate, please do it at the following link: https://buytickets.at/iklectik/480926

Sonic Electronics is an experimental event which happens 1st Wednesday monthly The Others – Stoke Newington. Alternatively, Sonic Electronics plays one-off events at IKLECTIK. We propose an anti-techno-capitalist approach to music genres like ambient, drone, techno, experimental, electronics, acousmatic, live coding, noise, vaporwave, glitch, industrial, post-punk, new wave…..

Sonic Electronics proposes an inclusive event to the LGBT community, female artists, no discrimination on gender, races, MH, disability.

Artists:

- Jake Williams
- http://www.jfbwilliams.com/sonituslive/
- Loula Yorke
- https://loulayorke.com/
- Claude Heiland-Allen
- https://mathr.co.uk/clive/
- Vo Ezn
- http://ezn.leverburns.blue/
- Laura Netz
- http://netzzz.net/medial-ages-live/

I'll be live-coding audio in the C programming language.

]]>