Difference between revisions of "Fractal Computation in ChucK - The Julia Class"

IDEA

To create a class that utilizes proper fractal computation as a means for generating a well-defined array that, in use, sounds random or chaotic but is entirely deterministic and thus perfectly repeatable, while still containing more varied content than a standard recursion function.

FRACTALS?

A fractal is a geometric shape that, when split into parts, will look approximately (or exactly, if framed perfectly) like the whole. Some can even be scale-invariant. They are 'produced' through iterations of an equation. The recursion is what produces the self-similarity.

The two most popular set of fractal-generating processes are known as the Julia set and the Mandelbrot set. While both produce fractals that are infinitely complex, the Julia set allows for much more variation in the resultant fractal with very small changes in the constants given (it is more chaotic).

The calculation of the shape is fairly simple, but requires a few inputs. It takes a recursive equation and plots the number of iterations necessary at each point before the equation hits a max value (generally seen to be when it's growth will become wildly exponential). Since some points will only reach this value after an infinite number of iterations, the calculation will either stop at the max value or at a given max number of iterations.

To make the calculation, then, we need to give it: an equation, initial constants, max output, max number of iterations, plot boundaries and plot grid density (the plot's subdivisions; ie, how many points to begin iteration at).

The equation for the Julia set is:

Zn+1 = Zn2 + c

Fractals are drawn in the complex plane (thus points in the plane will take the form z = a+bi). For the equation above, this means that the Zn is the point in the plot where the iteration starts/takes place, and c is the constant defined in your initial conditions; both are in a+bi form.

The boundaries of the real and complex axes on the plane can be whichever you choose, as they can be infinitely small or large. Given fractals self-similar nature, simple plot numbers are often chosen, such as both axes being bounded between -1 and 1.

The plot grid density is the number of divisions that you break each axis into. Choosing 2 for each axis will make your plot resemble the standard Cartesian coordinate drawing (four squares), but will be much too course of a resolution for any useful application. Somewhere in the range of 10-20 in each axis should be basically sufficient. The finer the resolution, the more expensive the computation, so be careful.

A standard convention for the max iteration value, Zmax, is generally two. After this point it will increase exponentially, so a higher number is usually unnecessary.

The max number of iterations taken at each grid point until Zmax is reached is up to the user. The higher the number gives you finer resolution on the printed image, which depending on your other chosen constants may or may not be necessary. A very large iteration max is very computationally expensive, however, so experiment with the set of constants you chose to find what's appropriate. Sometimes nothing in the picture will take more than 5 iterations, while sometimes they may infinitely many. Start small, work your way up. More than 200 is generally useless for audio applications.

Now that those are established, it's necessary to explain how images of fractals are actually produced. They are done the same way as explained above- making a defined grid in a defined coordinate system, with all other constants defined- and using the Z value at the centroid of each grid square to calculate the number of iterations taken to either reach Zmax or the max number of iterations and storing that to each grid location. Then, in the programming, a range of iteration numbers are mapped to colors to produce the image.

In the case below, for example, the black may have been mapped to 1-2 iterations, crimson to 3-6 iterations, red-brown to 6-12 iterations, red-orange to 12-20 iterations, etc etc, to the baby blue which may have been 150-200 iterations. The largest number of iterations generally occur near the plot origin as they will grow the slowest because the initial point is the smallest in magnitude.

Excessively large example:

DESIGN

Instead of plotting the gridded iteration counts to colors, we can use the array as an output to control sound. And thus the Julia class is born!

The output array will literally be the iteration count at the point mapped to the [row][column] specified in the initial parameters. So if you picked an 20 subdivisions on the real axis and 20 in the complex, you will have 400 grid squares and therefore 400 points in the array in [20][20] form.

How the output array is utilized is up to the end user. The nice part is that very small variations in the equation's input constants yield wildly different outputs so there is an infinite amount of variation in both size/scope/length and magnitude difference between plotted points. It can be as simple or as complex as desired and as constant or 'random' as desired. In any case, though, if you return the constants to a known set that produced an output that you liked, you can go back to it and get the same results each time because it is completely deterministic.

IMPLEMENTATION

Because this is a newly defined class in ChucK that isn't (yet?) internal to the system, the class script has to open and it's shred added to the Virtual Machine to be called in another shred. However, the computation constants are not defined in the class script, so you can have multiple shreds working that are being run with completely different fractal arrays.

To use it, open up Julia.ck and add it's shred to the Virtual Machine. Any shred after that can call on Julia foo (or whatever) just as you would any other object or unit generator, and the constants can be defined however you'd like. The necessary parameters and array walking script are included in Julia.ck and can be added into the program of your choice.

SAMPLE CODE WITH SPACING MESSED UP AND HARD TO READ

Julia.ck

   public class Julia
{
public void setC(complex c)
{
c => c_;
}

public void setLowerLeft(complex lower_left)
{
lower_left => lower_left_;
}

public void setUpperRight(complex upper_right)
{
upper_right => upper_right_;
}

public void setNumPointsX(int num_points_x)
{
num_points_x => num_points_x_;
}

public void setNumPointsY(int num_points_y)
{
num_points_y => num_points_y_;
}

public void setMaxIter(int max_iter)
{
max_iter => max_iter_;
}

public int[][]compute()
{
(upper_right_.re - lower_left_.re) / num_points_x_ => float x_spacing;
(upper_right_.im - lower_left_.im) / num_points_y_ => float y_spacing;

int iter_values[num_points_x_][num_points_y_];

for(0 => int row; row < num_points_x_; row++)
{
lower_left_.re + (row + 0.5) * x_spacing => float cur_x;
for(0 => int column; column < num_points_y_; column++)
{
lower_left_.im + (column + 0.5) * y_spacing => float cur_y;

#(cur_x, cur_y) => complex z;
<<<"Iterating at", z>>>;

compute_iteration(z) => iter_values[row][column];

<<<"\tSetting iter[", row, "][", column, "] to: ", iter_values[row][column]>>>;
}
}

return iter_values;
}

private int compute_iteration(complex z)
{
0 => int cur_iter;

while (cur_iter < max_iter_)
{
<<<"\t\titeration", cur_iter, ": |z| =", magnitude(z)>>>;
if(magnitude(z) > 2.0)
{
break;
}
z * z + c_ => z;
cur_iter++;
}

return cur_iter;
}

private float magnitude(complex c)
{
return Math.sqrt(c.re * c.re + c.im * c.im);
}


The embedded code necessary to call on Julia:

   #(0.3, 0.9) => complex c;
#(-1, -1) => complex lower_left;
#(1, 1) => complex upper_right;
20 => int num_points_x;
20 => int num_points_y;
30 => int max_iter;
Julia j;
j.setC(c);
j.setLowerLeft(lower_left);
j.setUpperRight(upper_right);
j.setNumPointsX(num_points_x);
j.setNumPointsY(num_points_y);
j.setMaxIter(max_iter);
j.compute() @=> int iter_values[][];
for(0 => int row; row < num_points_x; row++)
{
for(0 => int column; column < num_points_y; column++)
{
<<<"Val[", row, "][", column, "]:", iter_values[row][column]>>>;
}
}


Fully implemented example code of melody player:

   .5::second => dur T;
T - (now % T) => now;
SinOsc s => dac;
0.4 => s.gain;
0.5 => float note_length;
#(0.2, 0.6) => complex c;
#(-1, -1) => complex lower_left;
#(1, 1) => complex upper_right;
30 => int num_points_x;
30 => int num_points_y;
250 => int max_iter;
Julia j;
j.setC(c);
j.setLowerLeft(lower_left);
j.setUpperRight(upper_right);
j.setNumPointsX(num_points_x);
j.setNumPointsY(num_points_y);
j.setMaxIter(max_iter);
j.compute() @=> int iter_values[][];
[0, 2, 4, 5, 8, 10, 12, 14, 24, 26, 28, 30, 32, 34, 36, 38] @=> int whole_tone_scale[];
[0, 2, 3, 5, 7, 8, 10, 12, 24, 26, 27, 29, 31, 32, 34, 36] @=> int minor_scale[];
[0, 2, 4, 5, 7, 9, 11, 12, 24, 26, 28, 29, 31, 33, 35, 36] @=> int major_scale[];
[0, 2, 4, 5, 6, 8, 10, 12, 24, 26, 28, 29, 30, 32, 34, 36] @=> int locrian_major_scale[];
[0, 2, 3, 5, 7, 9, 11, 12, 24, 26, 27, 29, 31, 33, 35, 36] @=> int melodic_minor_scale[];
[0, 2, 3, 5, 7, 8, 11, 12, 24, 26, 27, 29, 31, 32, 35, 36] @=> int harmonic_minor_scale[];
[0, 1, 3, 4, 6, 8, 9, 12, 24, 25, 27, 28, 30, 32, 33, 36] @=> int altered_scale[];
[0, 1, 4, 5, 7, 8, 10, 12, 24, 25, 28, 29, 31, 32, 34, 36] @=> int spanish_phrygian_scale[];
[0, 2, 3, 6, 7, 8, 10, 12, 24, 26, 27, 30, 31, 32, 34, 36] @=> int gypsy_minor_scale[];
[0, 1, 4, 5, 7, 8, 11, 12, 24, 25, 28, 29, 31, 32, 35, 36] @=> int byzantine_scale[];
[0, 2, 3, 6, 7, 8, 11, 12, 24, 26, 27, 30, 31, 32, 35, 36] @=> int hungarian_gypsy_minor_scale[];
melodic_minor_scale @=> int scale[];
for (0 => int row; row < num_points_x; row++)
{
for (0 => int column; column < num_points_y; column++)
{
iter_values[row][column] => int cur_iter;
scale[cur_iter % scale.cap()] => float midi_number;
std.mtof(50 + midi_number) => float freq;
<<<"Playing iter value", cur_iter, ", midi number", midi_number, ", frequency", freq>>>;
freq => s.freq;
note_length::T => now;
}
}


KNOWN ISSUES

The initial computation is very demanding, and thus on less powerful computers it can definitely cause ChucK to think the Virtual Machine is hanging. If you let it work, though, and don't hit abort, it will always come through. BUT, if you are trying to use it on-the-fly, it will often stop time momentarily and just give you noise. On a powerful computer it will not do this, but user beware. This can be prevented mostly by simplifying the size and resolution of the fractal.

Therefore, WARNING: ON ALL CCRMA LINUX MACHINES, CHUCK WILL TRY TO HANG ITSELF. GIVE IT TIME AND SPACE, AND IT WILL COME BACK YOU SAFELY AND HAPPILY. DO NOT AGREE WITH IT AND LET IT HANG; YOU'LL NEVER FORGIVE YOURSELF.

FUTURE USAGES AND IMPROVEMENTS

OMG who knows. Only time will tell!

Actually, the way that the "fractalkick.ck" and "fractalsnare.ck" actually work is not what I had expected based on how I coded it, and I'm not quite sure why (the hack version is actually closer to what I expected), so in time I will resolve this issue. Still sounds nifty, though.

Main files:

julia.ck The main source code for running the Julia class.

fractalmultiplescale.ck A simple melody generator using the Julia class. You have a couple random scales to choose from and can change them on the fly. It's even synchronized to run with the programs below!

fractalkick.ck Based on Perry Cook & Ge's On-The-Fly programming basic drum sample (otf_01 - otf_07), it uses the fractal array to control when the kick drum hits (though it is modulated to remain in time). Not the initial way that I had planned to control this (the other way was a few extra lines of code), but nevertheless it sounds really cool.

fractalsnare.ck Same as above, but with the snare. Sounds very cool with either kick just keeping time or with both snare and kick get their play signals from the fractal array.

fractalhackkick.ck A pretty hack test to see how the in-time control of the kick drum sounded. It sounds and works differently from the version above- and is a pretty cheesy method!- but nevertheless sounds pretty good.

fractalhacksnare.ck Same as above, but for the snare.

Other files necessary for the drum loop with the above four files:

hihat.ck Standard hi-hat program. (otf_02.ck)

hihat-open.ck Standard hi-hat(open) program. (otf_03.ck)

snare-hop.ck Standard snare-hop program. (otf_07.ck)

kick.ck Standard kick program. Run if you want a baseline or just want to run the fractalsnare.ck. (otf_01.ck)

snare.ck Standard snare program. Run if you want a baseline or just want to run fractalkick.ck. (otf_04.ck)

Quick MP3 examples:

fractalmultiplescale.mp3 Quick example (using melodic minor) demonstrating the sound of the the fractal array controlling note choice over two octaves of a scale.

standarddrumloop.mp3 The standard OTF drum loop- this is just meant to give the baseline from which the subsequent files deviate. Who wants to dance?! (Sorry, no ungst ungst ungst noises.)

drumloopwithfractalsnare.mp3 Standard drum loop with the fractal array controlling when the snare hits. Changing the Julia set parameters make huge differences in what's played (same is true for all the fractal drum loop examples). Kick drum is the standard OTF unit.

drumloopwithfractalkick.mp3 Same as above, but now controlling the kick, and the snare is back to normal.

drumloopwithfractalsnareandfractalkick.mp3 Let's go wild!

drumloopwithfractalhacksnare.mp3 A hacked test version using the array to control the snare. Sounds completely different but just as useable.

drumloopwithfractalhackkick.mp3 Same as above, but with the kick instead of the snare.

drumloopwithfractalhacksnareandfractalhackkick.mp3 Let's go wild, again!

ZOMG Pictures:

scaleplayerfractalmapping.jpg The image of the fractal being sonified in the fractalmultiplescale.ck file above.

fractalsnaremapping.jpg The image of the fractal being sonified in the fractalsnare.ck file above.

fractalkickmapping.jpg The image of the fractal being sonified in the fractalkick.ck file above.