// Particlechamber // by Derek Holzer (http://macumbista.net/?page_id=514) // ported from puredata by redFrik // quickstart: // load a soundfile with the grey button labelled '0'. // start the granulator with the big red button in the upper left corner //071028 - initial release //090406 - fixes: set relativeOrigin to false and changed boxColor_ to background_ //090621 - removed relativeOrigin and using ViewRedirect instead of GUI //130219 - gui fixes for sc3.6 /* GUI.cocoa; GUI.swing; GUI.qt; */ ( //--settings var numSamples= 8; var numVoices= 32; var fontName= "Courier"; var busOutIndex= 0; //main stereo sound output bus var windowShape= 'wel'; //lin, sin, wel, cub, sqr var maxLength= 5; //max grain size in seconds //--variables var guiWin, docWin, //main and readme windows guiOff, guiLen, //variables needed for auto gui update stopTime= 0, //float that holds when user asked to stop stopTimes= 0.dup(numVoices), //array of differences from stopTime locator, //interpolates offsets and adds jitter routines, //array of engines that updates synths samples, //array of buffers sample, //current buffer index (sample_number) grp, //group for all synths synMix, synRev, //a mixer and a reverb synth busMix, busRev, //internal busses spd= 0, //scan_speed in sec spdFactor, //scale factor for scan_speed (gear) jit= 0.1, //jitter for offsets in percent voices= 1, //number of active voices (vox) offTrg= 0.1, offSrc= 0.1, //target and source for interpolation offMin= 0.1, offSprd= 0.1, //offset of start position in percent envMin= 0.1, envSprd= 0.05, //envelope (atk/rel) in percent susMin= 0.1, susSprd= 0.1, //length in percent ampMin= 0.75, ampSprd= 0.2, //gain in percent panSprd= 0.5, //pan spread in percent async= 0.5; //mute probability in percent //--setup var init= { "\nparticlechamber initialising...".postln; Routine.run{ var hang= Condition.new; s.latency= 0.05; s.bootSync(hang); initBuffers.value; s.sync(hang); initSynthDefs.value; s.sync(hang); initSynths.value; s.sync(hang); {initGui.value}.defer; CmdPeriod.doOnce({free.value}); 1.wait; engine.value; }; }; var initBuffers= { samples= {Buffer(s, 0, 1)}.dup(numSamples); //make room - no loading yet }; var initSynthDefs= { SynthDef(\particlechamberReverb, {|in= 0, out= 0, room= 0.85, damp= 0.5, mute= 0, amp= 0.85| var input= In.ar(in, 2); var z= FreeVerb2.ar(input[0], input[1], 1, room, damp, amp*mute); Out.ar(out, z+input); }, #[\ir, \ir, 0.05, 0.05, 0.05, 0.05]).send(s); SynthDef(\particlechamberMixer, {|in= 0, out= 0, pan= 0, amp= 0.85| var input= In.ar(in, 2); var z= Balance2.ar(input[0], input[1], pan, amp); Out.ar(out, z); }, #[\ir, \ir, 0.05, 0.05]).send(s); SynthDef(\particlechamberGranReader, {|out= 0, bufnum, off= 0, pan= 0, env= 0, sus= 1, amp= 1| var e= EnvGen.ar(Env(#[0, 1, 1, 0], [env, sus, env], windowShape), 1, amp, 0, 1, 2); var z= PlayBuf.ar(1, bufnum, BufRateScale.ir(bufnum), 1, BufFrames.ir(bufnum)*off, 0); Out.ar(out, Pan2.ar(z*e, pan)); }, #[\ir, \ir, \ir, \ir, \ir, \ir, \ir]).send(s); }; var initSynths= { grp= Group(s); busMix= Bus.audio(s, 2); busRev= Bus.audio(s, 2); synMix= Synth.tail(grp, \particlechamberMixer, [\in, busMix.index, \out, busRev.index]); synRev= Synth.tail(grp, \particlechamberReverb, [\in, busRev.index, \out, busOutIndex]); }; //--main var engine= { var len= 0, off= 0; "particlechamber running...".postln; locator= Routine{ inf.do{ { guiOff.value_(off); //update gui current_position guiLen.value_(len); //update gui ave_grain_length }.defer; if(offMin<offTrg, { offMin= (offMin+(offTrg-offSrc/(spd.max(0.01)*spdFactor)).abs+(jit.rand2*0.5)).max(0); }, { offMin= (offMin-(offTrg-offSrc/(spd.max(0.01)*spdFactor)).abs+(jit.rand2*0.5)).max(0); }); 0.1.wait; }; }; routines= {|i| //one for each voice Routine{ var sus, env, amp; stopTimes[i].wait; stopTimes[i]= 0; while({stopTime==0}, { if(i<voices, { //vox filter off= (offMin+offSprd.rand2).fold(0, 1).max(0.0001); sus= (susMin*maxLength+0.001+(susSprd.rand2*sample.duration)).max(0.0001); env= (envMin*0.2+(envSprd.rand2*0.5)).max(0.002); len= env*2+sus; amp= (ampMin+ampSprd.rand2).max(0); Synth.head(grp, \particlechamberGranReader, [ \out, busMix.index, \bufnum, sample.bufnum, \off, off, \pan, panSprd.rand2, \env, env, \sus, sus, \amp, amp*async.coin.binaryValue ]); len.wait; //time in seconds }, { 0.01.wait; //poll rate for voices filter }); }); stopTimes[i]= Main.elapsedTime-stopTime; }; }.dup(numVoices); }; //--cleanup var free= { "\nparticlechamber stopping...".postln; locator.stop; routines.do{|x| x.stop}; busMix.free; busRev.free; synRev.free; synMix.free; grp.free; samples.do{|x| x.free}; guiWin.onClose_(nil); if(guiWin.isClosed.not, {guiWin.close}); if(docWin.notNil, { docWin.onClose_(nil); if(docWin.isClosed.not, {docWin.close}); }); }; //--gui var initGui= { var vTitle, vSamples, vFile, vTable, vReverb, vMain, vCredit; var guiSpd, guiTrg; var guiRadioSample, guiRadioGears; //variables needed for radio buttons var fnt11= Font(fontName, 11), fnt12= Font(fontName, 12), fnt14= Font(fontName, 14); guiWin= Window("particlechamber", Rect(250, 100, 695, 515), false); guiWin.view.background_(Color.white); guiWin.front; guiWin.onClose_({free.value}); //--title vTitle= CompositeView(guiWin, Rect(10, 10, 470, 60)) .background_(Color.new255(224, 224, 224)); StaticText(vTitle, Rect(5, 5, 200, 25)) .font_(Font(fontName, 18)) .string_("Particlechamber"); StaticText(vTitle, Rect(140, 35, 310, 25)) .font_(fnt12) .string_(""++numVoices++"-voice asynchronous granular synthesizer"); //--file_granulator vFile= CompositeView(guiWin, Rect(10, 75, 470, 225)) .background_(Color.new255(68, 136, 240)); Button(vFile, Rect(10, 10, 20, 20)) //mute .font_(fnt12) .states_([ ["", Color.black, Color.new255(252, 40, 40)], ["X", Color.black, Color.new255(252, 40, 40)] ]) .action_{|but| if(but.value==1, { locator.play; stopTime= 0; routines.do{|x| x.reset; x.play}; }, { locator.stop; stopTime= Main.elapsedTime; }); }; Slider(vFile, Rect(45, 10, 20, 136)) //volume .background_(Color.new255(40, 244, 244)) .value_(0.85) .action_{|sld| synMix.set(\amp, sld.value.linlin(0, 1, -60, 6).dbamp)}; StaticText(vFile, Rect(80, 22, 100, 15)) .font_(fnt11) .string_("sample_number"); guiRadioSample= {|i| //sample_number Button(vFile, Rect(i*15+75, 10, 15, 15)) .font_(fnt11) .states_([["", Color.black, Color.blue(1, 0.25)], ["", Color.black, Color.black]]) .action_{|but| guiRadioSample.do{|x| if(x!=but, {x.value_(0)})}; if(but.value==1, { sample= samples[i]; }, { but.value= 1; }); }; }.dup(numSamples); guiRadioSample[0].valueAction_(1); StaticText(vFile, Rect(80, 55, 100, 15)) .font_(fnt11) .string_("envelope"); Slider(vFile, Rect(75, 45, 100, 15)) //envelope .background_(Color.clear) .value_(envMin) .action_{|sld| envMin= sld.value}; StaticText(vFile, Rect(80, 85, 100, 15)) .font_(fnt11) .string_("length"); Slider(vFile, Rect(75, 75, 100, 15)) //length .background_(Color.clear) .value_(susMin) .action_{|sld| susMin= sld.value}; StaticText(vFile, Rect(80, 115, 100, 15)) .font_(fnt11) .string_("gain"); Slider(vFile, Rect(75, 105, 100, 15)) //gain .background_(Color.clear) .value_(ampMin) .action_{|sld| ampMin= sld.value}; StaticText(vFile, Rect(80, 145, 100, 15)) .font_(fnt11) .string_("stereo_spread"); Slider(vFile, Rect(75, 135, 100, 15)) //stereo_spread .background_(Color.clear) .value_(panSprd) .action_{|sld| panSprd= sld.value}; StaticText(vFile, Rect(80, 175, 100, 15)) .font_(fnt11) .string_("async"); Slider(vFile, Rect(75, 165, 100, 15)) //async .background_(Color.clear) .value_(async) .action_{|sld| async= sld.value}; StaticText(vFile, Rect(80, 205, 100, 15)) .font_(fnt11) .string_("jitter"); Slider(vFile, Rect(75, 195, 100, 15)) //jitter .background_(Color.clear) .value_(jit) .action_{|sld| jit= sld.value}; StaticText(vFile, Rect(225, 45, 100, 15)) .font_(fnt11) .string_("range"); NumberBox(vFile, Rect(180, 45, 40, 15)) //range .font_(fnt12) .value_(envSprd) .action_{|num| num.value= envSprd= num.value.clip(0, 1)}; StaticText(vFile, Rect(225, 75, 100, 15)) .font_(fnt11) .string_("range"); NumberBox(vFile, Rect(180, 75, 40, 15)) //range .font_(fnt12) .value_(susSprd) .action_{|num| num.value= susSprd= num.value.clip(0, 1)}; StaticText(vFile, Rect(225, 105, 100, 15)) .font_(fnt11) .string_("range"); NumberBox(vFile, Rect(180, 105, 40, 15)) //range .font_(fnt12) .value_(ampSprd) .action_{|num| num.value= ampSprd= num.value.clip(0, 1)}; StaticText(vFile, Rect(185, 145, 100, 15)) .font_(fnt11) .string_("pan"); Slider(vFile, Rect(180, 135, 100, 15)) //pan .background_(Color.clear) .value_(0.5) .action_{|sld| synMix.set(\pan, sld.value*2-1)}; StaticText(vFile, Rect(185, 175, 100, 15)) .font_(fnt11) .string_("vox"); Slider(vFile, Rect(180, 165, 100, 15)) //vox .background_(Color.clear) .action_{|sld| voices= (sld.value*(numVoices-1)+1).round.asInteger}; StaticText(vFile, Rect(335, 45, 100, 15)) .font_(fnt11) .string_("scan_target"); guiTrg= NumberBox(vFile, Rect(290, 45, 40, 15))//scan_target .background_(Color.new255(60, 80, 252)) .font_(fnt12) .value_(0); StaticText(vFile, Rect(335, 75, 130, 15)) .font_(fnt11) .string_("scan_interpolation"); guiSpd= NumberBox(vFile, Rect(290, 75, 40, 15))//scan_interpolation .background_(Color.new255(60, 80, 252)) .font_(fnt12) .value_(spd); StaticText(vFile, Rect(335, 105, 120, 15)) .font_(fnt11) .string_("current_position"); guiOff= NumberBox(vFile, Rect(290, 105, 40, 15))//current_position .background_(Color.new255(60, 80, 252)) .font_(fnt12) .value_(offMin); StaticText(vFile, Rect(335, 135, 100, 15)) .font_(fnt11) .string_("grain_spread"); NumberBox(vFile, Rect(290, 135, 40, 15)) //grain_spread .font_(fnt12) .value_(offSprd) .action_{|num| num.value= offSprd= num.value.clip(0, 1)}; StaticText(vFile, Rect(335, 165, 120, 15)) .font_(fnt11) .string_("ave_grain_length"); guiLen= NumberBox(vFile, Rect(290, 165, 40, 15))//ave_grain_length .background_(Color.new255(60, 80, 252)) .font_(fnt12) .value_(0); StaticText(vFile, Rect(330, 197, 130, 20)) .font_(fnt14) .string_("file_granulator"); //--table_locator vTable= CompositeView(guiWin, Rect(10, 305, 470, 200)) .background_(Color.new255(68, 136, 240)); UserView(vTable, Rect(10, 10, vTable.bounds.width-20, vTable.bounds.height-40)) .drawFunc_{ var ww= vTable.bounds.width-20; var hh= vTable.bounds.height-40; var stepx= ww/(30-1); var stepy= hh/(20-1); Pen.smoothing= false; Pen.strokeColor= Color.white; 30.do{|x| Pen.line((x*stepx)@0, (x*stepx)@hh); 20.do{|y| Pen.line(0@(y*stepy), ww@(y*stepy)); }; }; Pen.stroke; }; Slider2D(vTable, Rect(10, 10, vTable.bounds.width-20, vTable.bounds.height-40)) .knobColor_(Color.red) .background_(Color.blue(0.2, 0.5)) .action_{|sld| offTrg= sld.x; //sample offset 0 - 1 offSrc= offMin; spd= sld.y*10; //interpolation time 0 - 10 guiTrg.value_(offTrg); guiSpd.value_(spd*spdFactor); }; StaticText(vTable, Rect(330, 170, 110, 20)) .font_(fnt14) .string_("table_locator"); //--reverb vReverb= CompositeView(guiWin, Rect(485, 75, 200, 160)) .background_(Color.new255(68, 136, 240)); Button(vReverb, Rect(10, 10, 20, 20)) //mute .font_(fnt12) .states_([ ["", Color.black, Color.new255(252, 40, 40)], ["X", Color.black, Color.new255(252, 40, 40)] ]) .action_{|but| synRev.set(\mute, but.value)}; Slider(vReverb, Rect(45, 10, 20, 136)) //volume .background_(Color.new255(40, 244, 244)) .value_(0.85) .action_{|sld| synRev.set(\amp, sld.value.linlin(0, 1, -40, 6).dbamp)}; StaticText(vReverb, Rect(80, 25, 100, 15)) .font_(fnt11) .string_("room_size"); Slider(vReverb, Rect(75, 15, 100, 15)) //room_size .background_(Color.clear) .value_(0.85) .action_{|sld| synRev.set(\room, sld.value)}; StaticText(vReverb, Rect(80, 55, 100, 15)) .font_(fnt11) .string_("damping"); Slider(vReverb, Rect(75, 45, 100, 15)) //damping .background_(Color.clear) .value_(0.5) .action_{|sld| synRev.set(\damp, sld.value)}; /*Slider(vReverb, Rect(75, 65, 100, 15)) //dry/wet .background_(Color.clear) .action_{|sld| "not in use".postln}; StaticText(vReverb, Rect(80, 65, 100, 15)) .font_(fnt11) .string_("dry/wet"); Slider(vReverb, Rect(75, 85, 100, 15)) //stereo_width .background_(Color.clear) .action_{|sld| "not in use".postln}; StaticText(vReverb, Rect(80, 85, 100, 15)) .font_(fnt11) .string_("stereo_width");*/ StaticText(vReverb, Rect(135, 137, 100, 20)) .font_(fnt14) .string_("reverb"); //--main vMain= CompositeView(guiWin, Rect(485, 240, 200, 200)) .background_(Color.new255(68, 136, 240)); Button(vMain, Rect(10, 10, 100, 20)) //readme .font_(fnt12) .states_([["README", Color.black, Color.blue(1, 0.25)]]) .action_{ docWin= Window("README", Rect(100, 100, 800, 780), false); docWin.view.background_(Color.white); docWin.front; StaticText(docWin, Rect(230, 10, 350, 25)) .font_(fnt12) .string_("Particlechamber by Derek Holzer [Umatic.nl]"); StaticText(docWin, Rect(10, 50, 380, 80)) .font_(fnt11) .string_("Particlechamber is a 32-voice asynchronous granular synthesizer for real-time transformation of a soundfile. It is loosely based on the famous Travelizer instrument from Reaktor 3, however I think it's much better because it is FREE!"); StaticText(docWin, Rect(10, 140, 380, 210)) .font_(fnt11) .string_("This abstraction can be used to time-stretch or -compress a soundfile, although there are other tools [such as Frank Barknecht's Synchgrain object] which do this 'nicer', but it's main strength is in generating clouds of sonic particles, time-scrambling a file, or creating abstract textures. If one does a bit of reverse engineering, it can also be used as a tool for learning about the techniques of granular synthesis. I have left a subpatch inside the granreader subpatch where others can add their own grain-level events, such as randomized or constant-Q filters or windowed envelopes, to see what is possible with this technique. Of course, I can only highly recommend Curtis Road's amazing book 'Microsound' for those interested in learning more."); StaticText(docWin, Rect(400, 50, 380, 106)) .font_(fnt11) .string_("Particlechamber requires a few externals to run. It uses Freeverb~ for its reverb section [although you could easily replace it with another reverb], and uses Grid as a major GUI element [although you could hack your way around it if you can't install Grid by sending numbers directly to the table_locator subpatch. Look inside for details...]"); StaticText(docWin, Rect(400, 170, 380, 40)) .font_(fnt11) .string_("It would be best to take Grid from the PD External Repository: http://pure-data.sourceforge.net"); StaticText(docWin, Rect(400, 220, 380, 40)) .font_(fnt11) .string_("However, Yves Degoyon's Grid external can also be found here: http://ydegoyon.free.fr/software.html"); StaticText(docWin, Rect(400, 270, 380, 40)) .font_(fnt11) .string_("The freeverb~ external can be downloaded here: http://www.akustische-kunst.org/puredata/main.html"); StaticText(docWin, Rect(10, 400, 380, 25)) .font_(fnt11) .string_("Operation of Particlechamber:"); StaticText(docWin, Rect(20, 430, 370, 65)) .font_(fnt11) .string_("Open the Load subpatch and click the bangs to load samples. It is better to do this before you start playing, because loading soundfiles can cause audible glitches in PD's performance. Sorry..."); StaticText(docWin, Rect(20, 500, 370, 80)) .font_(fnt11) .string_("The red buttons mute and unmute the file granulator and the reverb. The large vertical sliders are gain for each section. The reverb is post-fader from the file granulator, and can be bypassed either by muting it or with the wet/dry slider."); StaticText(docWin, Rect(20, 585, 370, 105)) .font_(fnt11) .string_("The envelope is a linear ramp, so a length of 0 means a pure triangular 'window' for each grain. [(envelope x 2)+ length= average grain length in ms]. Stereo_spread randomly pans each grain to a wider or lesser field. The range feature randomizes each parameter as a percentage plus or minus the given number."); StaticText(docWin, Rect(400, 430, 380, 120)) .font_(fnt11) .string_("Asynch randomizes which grains are passed through, from almost none to all. Vox activates or deactivates each of the 32 voices. Grain_spread makes small adjustments in the read position, which can be used to either add a reverb- or chorus-like effect, or to completely time-smear a file. Jitter makes larger adjustments in the read position, resulting in various degrees of time-scrambling."); StaticText(docWin, Rect(400, 555, 380, 105)) .font_(fnt11) .string_("The XY controller at the bottom determines the read position in the sample. This controller has an interpolation scale, adjustable by the gear-shift on the right. This means that Particlechamber will scan more slowly or quickly through the file depending on the cursor's Y position multiplied by the interpolation factor."); StaticText(docWin, Rect(200, 700, 380, 50)) .font_(fnt11) .string_("Particlechamber is free software and comes without any warrenty that it will do ANYTHING like what I say it will. Enjoy it all the same."); StaticText(docWin, Rect(450, 750, 180, 25)) .font_(fnt11) .string_("derek@umatic.nl"); }; numSamples.do{|i| Button(vMain, Rect(i*15+10, 55, 15, 15)) .font_(fnt11) .states_([[i.asString]]) .action_{ File.openDialog("", {|path| var sf= SoundFile.openRead(path); var ch= sf.numChannels; sf.close; if(ch==1, { samples[i].allocRead(path, 0, -1, {|buf| AppClock.sched(0.1, {buf.updateInfo; nil})}); }, { samples[i].allocReadChannel(path, 0, -1, 1, {|buf| AppClock.sched(0.1, {buf.updateInfo; nil})}); "multichannel soundfile - only loaded first (left) channel".warn; }); }, {}); }; }; StaticText(vMain, Rect(10, 70, 180, 15)) .font_(fnt11) .string_("click to load samples"); guiRadioGears= {|i| StaticText(vMain, Rect(30, i*15+110, 40, 15)) .font_(fnt11) .string_("x"+10.pow(i)); Button(vMain, Rect(10, i*15+110, 15, 15)) .font_(fnt11) .states_([["", Color.black, Color.blue(1, 0.25)], ["", Color.black, Color.black]]) .action_{|but| guiRadioGears.do{|x| if(x!=but, {x.value_(0)})}; if(but.value==1, { spdFactor= 10.pow(i); guiSpd.value_(spd*spdFactor); }, { but.value= 1; }); }; }.dup(3); guiRadioGears[1].valueAction_(1); StaticText(vMain, Rect(85, 110, 110, 50)) .background_(Color.new255(68, 136, 240)) .font_(fnt11) .string_("gear-shift for interpolation speed"); //--credit vCredit= CompositeView(guiWin, Rect(485, 445, 200, 60)) .background_(Color.new255(224, 224, 224)); StaticText(vCredit, Rect(35, 10, 120, 20)) .font_(fnt12) .string_("derek@umatic.nl"); StaticText(vCredit, Rect(35, 35, 180, 20)) .font_(fnt12) .string_("pd->sc port by redFrik"); }; init.value; ""; )