Friday, February 15, 2013

Mandlebrot Sonifications

Here are 2 studies on sonifying iterations of z=z^2+c. I'm using the same instrument from the Carotid Kundalini studies, and mapping the real, imaginary, angle and radius of each iteration to instrument parameters (see the code comments). Each note is actually a chord of iteration values for one point in the set. As the music moves forward in time, parallel rows move along the real axis. What's most interesting to me here is how you can definitely hear cycles, and loops within loops in the pitches, especially in the 2nd study.
//load up all of our servers into an array
~z = Server.all.asArray;

//our instrument - phase multiplication of sine waves, in a cluster of 8 phase / pitch shifted voices
~z.collect({|z|
SynthDef("sine-cluster",{|freq=100, atk=1, rel=1, slope=1, amp=0.005,pan =0,gate=0,m=0,p=0,ph=0,pl=0,pf=1|
 var sin = SinOsc.ar(  [1,1]  * freq *.x (p+[1-p,1,1+p,1+p+p]) ,pl*SinOsc.ar(freq/(pf),ph*(0..3)/3,XLine.ar(freq.sqrt.ceil*pf,pf*slope.abs,pf/(rel*freq.log2.ceil))),(1/(rel+1))+((2-1)/2)*(amp)/(freq+512));
 OffsetOut.ar(0,Mix.ar(Pan2.ar(sin
 ,[-1,1])) * EnvGen.ar(Env.perc(atk,rel, 1,slope),gate, doneAction:2) );}).send(z);
});

//the Mandelbrot function, where z is a complex number and c is a real number
~m = {|z,c|squared(z)+c};

//utility function - returns unique values in an array
~unique = {|a|var t = Array.fill(a.size);
  a.collect({|i,j|(t.includes(i)).if({t.put(j,nil)}, {t.put(j,a[j])})}).reject({|i|i==nil});
  t.reject({|i|i==nil});
};

 

//the pitch set
~p = ~unique.((16..32).reject({|x|x.factors.detect({|n|n>5})!=nil})*.x(2**((-6)..3))).sort;

//some sonification parameters

~globalSustain =2;
~d = 10;
~minIters =5;

//how many iterations to generate the pitches for an orbit
~maxIters = 1024;
~rows =2;
//minibrot on the the real axis
~d = 12;
~minIters =4;
~cx = -1.635;
~ci = -0.0001220703125;
~globalSustain =1.5;
~rows =2;

//on our way out to the western point
~cx = -1.875;
~ci = 0;

~cx = -1.95125;
~ci = -1/(2**30);

~globalSustain =0.5;
~cx = -1.957125;
~ci = 0;
~rows =2;



//5 arm spirals
~globalSustain =2;
~cx = -0.52482350635;
~ci = 0.62534492645;
~rows =3;
~d = 12;
~minIters =6;



(
//start a loop to crawl along the real number axis of the graph
//each cycle zooms in 2x closer to the original point, and crawls 1/2 as quickly
Routine {
(9..9).do({|y|
 var max = (2**(5+y)).asInt;
max.clip(1,2048).do({|x|
 //the player function is defined outside this loop so it can be tweaked in realtime while the loop is playing 
 ~player.(x,y,max);
 })
})
}.play;

/*recursive function to generate a collection of orbit points - bail out if we reach infinity, zero, or a non-number.   Always quit after reaching "safe" number of iterations
this function returns an array of "points" which are the Complex values for a particular iteration
*/
~mr = {|z,c,safe,points|
 ((safe > 0).and(z.real.isNaN.not).and(z.real!=inf).and(z.real !=0)).if({
 ~mr.(squared(z)+c,c,safe-1,points.add(z))
 },points)
};
)
(
~player = {|x,y,max|
 //we sonfiy a line of points on the imaginary axis together, based on the value of ~rows
 ~rows.asInt.do({|ii|
  //slowly crawl along the real axis
  var c= ~cx+(x/(2**(8+y))) - (max/(2**(9+y))) , z = Complex.new(c,~ci+(1/(2**(8+y))*ii)),points;

  //our initial point to iterate on
  z = ~m.(z,c);
 /*
  alternate traversal strategy, move our sonification point in a tightening spiral around the orginal values for real and imaginary.
  var z = Complex(~cx,~ci),points,c=~ci;
  //z is the original center point.  We add the orbit point to this
  z = (z.asPolar+ Polar(1/2**y,0).rotate(((x*pi*2)/128)).scale(1+((1-(x/1024))/(2**(y-ii))))).asComplex;
*/

  //generate an array of orbit points
  points= ~mr.(z,c,~maxIters,[]);
  /* 
  take a slice of the array to sonify it
  the size of this slice can be tweaked by adjusting ~minIters and ~d.
  the lower end of the array tends to be more chaotic and the higher end of the array more constant.
  all of these points sound simultaneously, as a chord
  */
  points.reverse[~minIters.asInt..~d.asInt].collect({|p,i|
   var ss;
   z = p;
   /*
   z is given as a complex number, but we can also treat it as a polar to get its angle (theta) and radius (rho) from the origin (0,0).  it would probably be more interesting to get rho and theta  relative to the original z point.   */
   ((z.real.isNaN)).if({},{
   ss = Synth("sine-cluster",nil,~z.wrapAt(x));
   //frequency of the pitch, based on the distance from the origin
   //fundamental pitch of 66hz multiplied by an overtone in the pitch set ~p
   ss.set(\freq, 66 * ((~p.wrapAt(z.rho*~p.size*~cx))));
   //phase offset of the pitch, also based on radius
   ss.set(\ph,z.rho);
   //add a small pitch bend 
   ss.set(\p,1/512);
   ss.set(\gate,1);
   //this is the amplitude of the 2nd (phase-shifting) oscillator, based on the imaginary value of of z
   ss.set(\pl, z.imag.abs.log2.clip(-1,1));
   //frequency of the 2nd oscillator, based on one of the harmonics in the pitch set ~p.
   ss.set(\pf, 1/(~p.wrapAt(~p.size *z.theta/pi)));
   //length of the note, based on the real value of z
   ss.set(\rel,~globalSustain/((z.real.abs+2).log2).clip(2,32));
   //pannign position
   ss.set(\pan, (z.imag%1) * (z.real%1));
   //note attack value, based on the reciprocal of the imaginary of z (maximum value 1 second)
   ss.set(\atk,1/(1+z.imag.abs));
   //slope of the peak amplitude drop - higher negative value = sharper attack, quieter sustain
   ss.set(\slope,z.real.abs.log * (z.real/z.real.abs));
   //tweak the amplitude a little to offset notes with a longer attack
   ss.set(\amp,2.5  * (2/(1+(z.imag.abs.clip(0,1)))));
   });
  });
 });
 (1/10).wait;
};
)

1 comment: