FluidPlotter visualization of the 2D location controlled externally

Hi all,

I am trying to control externally the navigation of a 2D corpora in Supercollider. I reckon I can do it passing the parameter to set the point to navigate the FluidKDTree. However, it cannot be visualized. Is there a way to visualize the location of the X and Y coordinates not controlled by the mouse using the FluidPlotter perhaps it can be done using the GUI elements of Supercollider

thanks a lot

Hello!

The trick I used in Max and Pd was to add a ‘pointer’ item in the dictionary that I was updating. @tedmoore will know better / faster than me if we can do that in SC.

I’m in a multi-project marathon now, but in a few weeks I can come back to this if that is not possible

I’ve used UserView in the past to overlay on top of FluidPlotter. Then you can draw a dot (and whatever else you might want) over the plotter. In this example I’m doing it over FluidWaveform to draw a line, so a bit different, but you can probably use it as an example to do what you want.

1 Like

thanks,

I’ll try to adapt your example to the FluidPlotter

Hi, I have been trying to adapt the code of your video tutorial using an OSC control. So far I can navigate the corpus using an OSCTouch xy control, but I have not been able to visualize the point over the FluidPlotter. I reckon the problem is more about Supercollider than FluCoMa, It seems that the function for drawing the Pen.fillOval within the OSCdef does not get the argument msg. Perhaps I am missing something about the defer functions, or perhaps there is an easier way of doing it. Please can you see quickly whether I am missing something:

(

var win = Window.new;
var uv = UserView.new;

~normed.dump({
 arg dict;
 var point = Buffer.alloc(s, 2);
 var previous = nil;
 var uv, win;
 dict.postln;

 defer{
 	win = Window(bounds:Rect(0, 0, 400, 400));
 	FluidPlotter(
 		dict: dict,
 		xmin: 0,
 		xmax: 1,
 		ymin: 0,
 		ymax: 1,
 		bounds: Rect(0, 0, win.bounds.width, win.bounds.height));
 	uv = UserView(win, Rect(0, 0, win.bounds.width, win.bounds.height));
 	}});
 
 OSCdef(\\xy, {
 arg msg;

 defer{	
 	uv.drawFunc = { arg x, y;
 		x = msg[1];
 		y = msg[2];
 		Pen.color_(Color.red);
 		Pen.fillOval(Rect(x * win.bounds.width, y * win.bounds.height, 20, 20));
 	};
 	w.refresh;};
 
 fork{
 	arg view, x, y;
 	var point = Buffer.alloc(s, 2);
 	var previous = nil;
 
 	x = msg[1];
 	y = msg[2];
 	point.setn(0, [x, y]);
 
 	~tree.kNearest(point, 1, {
 		arg nearest;
 		if(nearest != previous){
 			nearest.postln;
 			~play_slice.(nearest.asInteger);
 			previous = nearest;
 		};
 	};)
 };
 

 }, ‘/3/xy’);

 )

Thanks a lot

hi all, I have got it. I am sharing the code below. I think there is a way to improve it, perhaps by snapping the red circle to the nearest point.


(
var circle, w, uv, point, previous, fp;

point = Buffer.alloc(s, 2);
previous = nil;

circle = {
	|x, y, r|
	Pen.fillColor = Color.new255(255, 0, 0, 80);
	Pen.strokeColor = Color.red;
	Pen.addArc(x@y + ((r@r)), r, 0, 2pi);
	Pen.fillStroke;
};

w = Window("Monitor", bounds: Rect(0, 0, 800, 800)).front;
w.view.background_(Color.hsv(1, 1, 1, alpha: 0));

~normed.dump({
	arg dict;
	dict.postln;
	defer{	
		fp = FluidPlotter(
			w,
			dict: dict,
			xmin: 0,
			xmax: 1,
			ymin: 0,
			ymax: 1,
			bounds: Rect(0, 0, w.bounds.width, w.bounds.height));
		uv = UserView(w, Rect(0, 0, w.bounds.width, w.bounds.height));
	}
});
	
OSCdef(\xy, {
	arg msg;

	defer{
		uv.drawFunc = { |uvview|
			circle.value(
				msg[1].postln * w.bounds.width,
				msg[2].postln * w.bounds.height,
				10
			);
		};
		w.refresh;
		uv.refresh;
	};

	fork{
		arg view, x, y;

		x = msg[1];
		y = msg[2];
		point.setn(0, [x, y]);

		~tree.kNearest(point, 1, {
			arg nearest;
			if(nearest != previous){
				nearest.postln;
				~play_slice.(nearest.asInteger);
				previous = nearest;
			};
		};)
	};

	
}, '/3/xy');
)
1 Like

Hi @p3mb ,

nice work. here’s another approach that i think is more idiomatic. i haven’t tested it (because i don’t have the data set or touchosc file), so i’m sure it needs some modifications, but essentially, it is more common to create the drawFunc just once when you initialize the UserView. Inside the draw func a variable will be used that is from a larger scope (declared out side the draw func) so that it can be updated separately (here it’s circle which is a Point). then in the oscdef, circle can be updated and uv.refresh can be called (which re-runs the drawFunc) and this time draw func runs the value for circle is udpated so it will draw it in the new location.

like i said, i didn’t test this code, but i think you’ll see the pattern, and based on what you’re up to here it seems you’ll be able to run with it anyway!

if it doesn’t work out, paste here a more ‘easily testable’ version for me, even with some dummy data just so i can run it, and i can debug a bit easier.

t

(
var circle, w, uv, point, previous, fp;

point = Buffer.alloc(s, 2);
previous = nil;

circle = Point(0,0);

w = Window("Monitor", bounds: Rect(0, 0, 800, 800)).front;
w.view.background_(Color.hsv(1, 1, 1, alpha: 0));

~normed.dump({
	arg dict;
	dict.postln;
	defer{	
		fp = FluidPlotter(
			w,
			dict: dict,
			xmin: 0,
			xmax: 1,
			ymin: 0,
			ymax: 1,
			bounds: Rect(0, 0, w.bounds.width, w.bounds.height)
		);
		uv = UserView(w, Rect(0, 0, w.bounds.width, w.bounds.height));
		uv.drawFunc = {
			Pen.fillColor = Color.new255(255, 0, 0, 80);
			Pen.strokeColor = Color.red;
			Pen.addArc(point + (r@r), r, 0, 2pi);
			Pen.fillStroke;
		};
	}
});

OSCdef(\xy, {
	arg msg;
	
	arg view, x, y;
	
	x = msg[1];
	y = msg[2];
	point.setn(0, [x, y]);
	
	~tree.kNearest(point, 1, {
		arg nearest;
		if(nearest != previous){
			nearest.postln;
			~play_slice.(nearest.asInteger);
			defer{
				// not sure if you need w.refresh?
				w.refresh;
				uv.refresh;
			};
			previous = nearest;
		};
	};)
	
}, '/3/xy');
)

Hi @tedmoore,

thanks for the answer. I reckon I know what you mean. I have copied the whole code, which is basically part of the teaching material you have make. The last part is the OSC part with the idiomatic change. Also, I have added a OSC message sender so there is no need to have touchOSC at the same time that keeps the OSCdef that receives the message.

I was wondering about snapping the red circle to the coordinates of the nearest point given by FluidKDTree. In other words, that the red point snaps on top of the point that is sounding. Similar how FluidPlotter works. However, I need to figure out how to do it.

Open to any suggestion. thx

~folder = FluidFilesPath();
~loader = FluidLoadFolder(~folder).play(s,{"done loading folder".postln;});

(
if(~loader.buffer.numChannels > 1){
	~src = Buffer(s);
	~loader.buffer.numChannels.do{
		arg chan_i;
		FluidBufCompose.processBlocking(s,
			~loader.buffer,
			startChan:chan_i,
			numChans:1,
			gain:~loader.buffer.numChannels.reciprocal,
			destination: ~src,
			destGain:1,
			action:{"copied channel: %".format(chan_i).postln}
		);
	};
}{
	"loader buffer is already mono".postln;
	~src = ~loader.buffer;
};
)

// SLICE

(
~indices = Buffer(s);
FluidBufOnsetSlice.processBlocking(
	s,
	~src,
	metric: 9,
	threshold: 0.05,
	indices: ~indices,
	action: {
	"found % slice points".format(~indices.numFrames).postln;
	"average duration per slice: %".format(~src.duration / (~indices.numFrames+1)).postln;
});
)

// 5. ANALYSIS


(
~analyses = FluidDataSet(s);
~indices.loadToFloatArray( action: {
	arg fa;
	var mfccs = Buffer(s);
	var stats = Buffer(s);
	var flat = Buffer(s);
	fa.doAdjacentPairs{
		arg start, end, i;
		var num = end - start;

		FluidBufMFCC.processBlocking(
			s, 
			~src,
			start,
			num,
			features: mfccs, //Buffer de salida
			numCoeffs: 13, 
			startCoeff:1
		);

		FluidBufStats.processBlocking(
			s,
			mfccs,
			stats: stats,
			select: [\mean]
		);

		FluidBufFlatten.processBlocking(
			s,
			stats,
			destination: flat //el Buffer de salida
		);

		~analyses.addPoint(i,flat);
		"analyzing slice % / %".format(i+1,fa.size-1).postln;
	};
	s.sync;
	~analyses.print;
});
)


(
~umapped = FluidDataSet(s);
FluidUMAP(
	s,
	numNeighbours: 15,
	minDist: 0.9
).fitTransform(~analyses, ~umapped, action:{"umap done".postln});
)

(
~normed = FluidDataSet(s);
FluidNormalize(s).fitTransform(~umapped,~normed);
)

~tree = FluidKDTree(s).fit(~normed);

// 8. PLAYER

(
~play_slice = {
	arg index;
	{
		var startsamp = Index.kr(~indices, index);
		var stopsamp = Index.kr(~indices, index + 1);
		var phs = Phasor.ar(
			0,
			BufRateScale.ir(~src),
			startsamp,
			stopsamp);
		
		var sig = BufRd.ar(1,~src,phs);
		var dursecs = (stopsamp - startsamp) / BufSampleRate.ir(~src);
		var env;

		dursecs = min(dursecs,1);

		env = EnvGen.kr(
			Env([0,1,1,0], [0.03,dursecs-0.06,0.03]),
			doneAction:2
		);
		sig.dup * env;
	}.play;
};
)

// DRAWING, PLAYER, RED POINT. OSC CONTROLLED

(
var circle, w, uv, fp;
var xpos = 0;
var ypos = 0;
var point = Buffer.alloc(s, 2);
var previous = nil;

circle = {
	|x, y, r|
	Pen.fillColor = Color.new255(255, 0, 0, 80);
	Pen.strokeColor = Color.red;
	Pen.addArc(x@y + ((r@r)), r, 0, 2pi);
	Pen.fillStroke;
};

w = Window("Monitor", bounds: Rect(0, 0, 800, 800)).front;
w.view.background_(Color.hsv(1, 1, 1, alpha: 0));

~normed.dump({
	arg dict;
	dict.postln;
	defer{	
		fp = FluidPlotter(
			w,
			dict: dict,
			xmin: 0,
			xmax: 1,
			ymin: 0,
			ymax: 1,
			bounds: Rect(0, 0, w.bounds.width, w.bounds.height));
		uv = UserView(w, Rect(0, 0, w.bounds.width, w.bounds.height))
		.drawFunc = { |uvview|
			circle.value(
				xpos * w.bounds.width,
				ypos * w.bounds.height,
				10
			);
		};
	}
});
	
OSCdef(\xy, {
	arg msg;

	msg.postln;
	
	xpos = msg[1];
	ypos = msg[2];

	fork{
		point.setn(0, [xpos, ypos]);
		~tree.kNearest(point, 1, {
			arg nearest;
			if(nearest != previous){
				nearest.postln;
				~play_slice.(nearest.asInteger);
				previous = nearest;
			};
		};)
	};
	
	defer{uv.refresh};
	
}, '/3/xy');
)


// Test with random point to not use OSCtouch

(
m = NetAddr.new("localhost", 57120);
~rndmesg = Routine{loop {
	m.sendMsg("/3/xy", 1.0.rand, 1.0.rand);
	1.wait;
}};

~rndmesg.play;

)

~rndmesg.stop;


This seems to be working. Note that the y axis is inverted because FluidPlotter makes the bottom left as 0,0 while on the UserView it is the top left.

~folder = FluidFilesPath();
~loader = FluidLoadFolder(~folder).play(s,{"done loading folder".postln;});

(
if(~loader.buffer.numChannels > 1){
	~src = Buffer(s);
	~loader.buffer.numChannels.do{
		arg chan_i;
		FluidBufCompose.processBlocking(s,
			~loader.buffer,
			startChan:chan_i,
			numChans:1,
			gain:~loader.buffer.numChannels.reciprocal,
			destination: ~src,
			destGain:1,
			action:{"copied channel: %".format(chan_i).postln}
		);
	};
}{
	"loader buffer is already mono".postln;
	~src = ~loader.buffer;
};
)

// SLICE

(
~indices = Buffer(s);
FluidBufOnsetSlice.processBlocking(
	s,
	~src,
	metric: 9,
	threshold: 0.05,
	indices: ~indices,
	action: {
		"found % slice points".format(~indices.numFrames).postln;
		"average duration per slice: %".format(~src.duration / (~indices.numFrames+1)).postln;
});
)

// 5. ANALYSIS


(
~analyses = FluidDataSet(s);
~indices.loadToFloatArray( action: {
	arg fa;
	var mfccs = Buffer(s);
	var stats = Buffer(s);
	var flat = Buffer(s);
	fa.doAdjacentPairs{
		arg start, end, i;
		var num = end - start;

		FluidBufMFCC.processBlocking(
			s,
			~src,
			start,
			num,
			features: mfccs, //Buffer de salida
			numCoeffs: 13,
			startCoeff:1
		);

		FluidBufStats.processBlocking(
			s,
			mfccs,
			stats: stats,
			select: [\mean]
		);

		FluidBufFlatten.processBlocking(
			s,
			stats,
			destination: flat //el Buffer de salida
		);

		~analyses.addPoint(i,flat);
		"analyzing slice % / %".format(i+1,fa.size-1).postln;
	};
	s.sync;
	~analyses.print;
});
)


(
~umapped = FluidDataSet(s);
FluidUMAP(
	s,
	numNeighbours: 15,
	minDist: 0.9
).fitTransform(~analyses, ~umapped, action:{"umap done".postln});
)

(
~normed = FluidDataSet(s);
FluidNormalize(s).fitTransform(~umapped,~normed);
)

~tree = FluidKDTree(s).fit(~normed);

// 8. PLAYER

(
~play_slice = {
	arg index;
	{
		var startsamp = Index.kr(~indices, index);
		var stopsamp = Index.kr(~indices, index + 1);
		var phs = Phasor.ar(
			0,
			BufRateScale.ir(~src),
			startsamp,
			stopsamp);

		var sig = BufRd.ar(1,~src,phs);
		var dursecs = (stopsamp - startsamp) / BufSampleRate.ir(~src);
		var env;

		dursecs = min(dursecs,1);

		env = EnvGen.kr(
			Env([0,1,1,0], [0.03,dursecs-0.06,0.03]),
			doneAction:2
		);
		sig.dup * env;
	}.play;
};
)

// DRAWING, PLAYER, RED POINT. OSC CONTROLLED

(
var circle, w, uv, fp;
var xpos = 0;
var ypos = 0;
var point = Buffer.alloc(s, 2);
var previous = nil;

circle = {
	|x, y, r|
	Pen.fillColor = Color.new255(255, 0, 0, 80);
	Pen.strokeColor = Color.red;
	Pen.addArc(x@y /*+ ((r@r))*/, r, 0, 2pi);
	Pen.fillStroke;
};

w = Window("Monitor", bounds: Rect(0, 0, 800, 800)).front;
w.view.background_(Color.hsv(1, 1, 1, alpha: 0));

~normed.dump({
	arg dict;
	~normed_dict = dict;
	dict.postln;
	defer{
		fp = FluidPlotter(
			w,
			dict: dict,
			xmin: 0,
			xmax: 1,
			ymin: 0,
			ymax: 1,
			bounds: Rect(0, 0, w.bounds.width, w.bounds.height));
		uv = UserView(w, Rect(0, 0, w.bounds.width, w.bounds.height))
		.drawFunc = { |uvview|
			circle.value(
				xpos * w.bounds.width,
				(1-ypos) * w.bounds.height,
				10
			);
		};
	}
});

OSCdef(\xy, {
	arg msg;

	point.setn(0, [msg[1], msg[2]]);
	~tree.kNearest(point, 1, {
		arg nearest;
		if(nearest != previous){
			# xpos, ypos = ~normed_dict["data"][nearest.asString];
			nearest.postln;
			~play_slice.(nearest.asInteger);
			defer{ uv.refresh };
			previous = nearest;
		};
	};)

}, '/3/xy');
)


// Test with random point to not use OSCtouch

(
m = NetAddr.new("localhost", 57120);
~rndmesg = Routine{loop {
	m.sendMsg("/3/xy", 1.0.rand, 1.0.rand);
	1.wait;
}};

~rndmesg.play;

)

~rndmesg.stop;

Hi,

it works perfect. However, at least using OSCtouch the inverted Y axis makes the OSC control to be inverted. I reckon is that the 0, 0 value in the xy fader of the touchOSC is in the botton left, differently than the graphic languages such as processing and the Supercollider Window class..

I’ve learnt a lot

thanks!!

1 Like