View this PageEdit this PageUploads to this PageHistory of this PageTop of the SwikiRecent ChangesSearch the SwikiHelp Guide

particlechamber

Home   How To   Code Pool   Public Library   Theory   Events
//  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; "";
)


Link to this Page