// 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; "";
)