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

From CCRMA Wiki
Jump to: navigation, search
([)
Line 133: Line 133:
 
     }
 
     }
  
Sample usage:
 
  
 +
The code necessary to embed to call on Julia:
  
 
     #(0.3, 0.9) => complex c;
 
     #(0.3, 0.9) => complex c;
Line 155: Line 155:
 
         }
 
         }
 
     }
 
     }
 +
 +
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, 3, 5, 7, 9, 10, 12, 24, 26, 27, 29, 31, 33, 34, 36] @=> int melodic_minor_scale[];
 +
[0, 1, 3, 4, 6, 8, 9, 12, 24, 25, 27, 28, 30, 32, 33, 36] @=> int altered_scale[];
 +
melodic_minor_scale @=> int scale[];
 +
for (0 => int row; row < num_points_x; row++)
 +
{
 +
    for (0 => int column; column < num_points_y; column++)
 +
    {
 +
        // pick something from the scale
 +
        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;
 +
    }
 +
  }
 +
  
  

Revision as of 00:37, 10 December 2008

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: Julia (Fractal).png

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 code necessary to embed 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 i; i < num_points_x; i++) {
        for(0 => int j; j < num_points_y; j++) {
           <<<"Val[", i, "][", j, "]:", iter_values[i][j]>>>;
       }
   }

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, 3, 5, 7, 9, 10, 12, 24, 26, 27, 29, 31, 33, 34, 36] @=> int melodic_minor_scale[];
[0, 1, 3, 4, 6, 8, 9, 12, 24, 25, 27, 28, 30, 32, 33, 36] @=> int altered_scale[];
melodic_minor_scale @=> int scale[];
for (0 => int row; row < num_points_x; row++) 
{
   for (0 => int column; column < num_points_y; column++) 
   {
       // pick something from the scale
       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;
    }
 }


SAMPLE SOUND CLIPS

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.

FUTURE USAGES AND IMPROVEMENTS

OMG who knows. Only time will tell!

DOWNLOADS