Freeing a Phasor when it reaches its end + how to playback sample indexed slices with Patterns?

Hi, I’m trying to do a really simple thing: I’m using FluCoMa (here using FluidBufTransientSlice) to automatically slice a soundfile (it’s me crinkling a piece of plastic in my hand, so it’s full of transients), and I simply want to play the slices how I want, using Patterns.
FluidBufTransientSlice fills a Buffer with the slices indexes in samples. I’m filling an Array with the same values for easier retrieval.

Below is my code till now.
I have two problems: I need a way to free the Phasor when it reaches its end, otherwise it keeps looping; and I need a way to correctly calculate the end index for each slice. I know I should just get the next value in the indices Array but I can’t figure out how to do it with this setup. I know the solution should be fairly easy but I’ve been wrapping my head around this for the last hour lol. Thanks!

~src = Buffer.read(s, "path/to/soundfile");

(
fork {
~indices = Buffer(s);
FluidBufTransientSlice.process(s, ~src, indices:~indices, blockSize:256, padSize:128, skew:0, windowSize:14,
	clumpLength:25, minSliceLength:1000, action: {
		"found % slice points".format(~indices.numFrames).postln;
		"with an average length of % seconds per slice".format(~src.duration / ~indices.numFrames).postln;
	}
);

2.wait;

// loading indices into a new Array
~indices.loadToFloatArray(0, -1, {|arr| ~indArr = arr; "indices array (indArr) created!".postln});
}
)

// player
(
SynthDef(\slices, {
	|rate = 0, startPos = 0, endPos = 1, amp = 1, pan = 0, out = 0|
	var snd, phasor;
	phasor = Phasor.ar(rate: BufRateScale.kr(~src) * rate, start:startPos, end:endPos);
	snd = BufRd.ar(2, ~src, phasor, loop:0, interpolation:4);
	snd = Balance2.ar(snd[0], snd[1], pan, amp);
	Out.ar(out, snd)
}).add;
)

// pattern
(
Pdef(\pat1, Pbind(
	\instrument, \slices,
	\dur, 0.1,
	\startPos, ~indArr.at(rrand(1, ~indArr.size - 2)),
	\endPos, ~indArr.at(???)
)).play
)
1 Like

You could use Sweep or Line for a one-shot ramp triggered by the language instead of Phasor. Do you know how long each slice is? Otherwise you would know that the next start frame is the current end frame or?

not the solution yet, but maybe an intermediate step:

i have normalized the slices array with:

~normalizedSlicesArray = ~indArr / ~src.numFrames;

and set up Phasor accordingly, so that posLo and posHi are normalized values between 0 and 1. Then you can index into your slices array and pick a start and end slice.

SynthDef(\slices, { |sndBuf|

	var sig, pos;

	pos = Phasor.ar(
		trig: DC.ar(0),
		rate: BufRateScale.kr(sndBuf) * \rate.kr(1) * SampleDur.ir / BufDur.kr(sndBuf),
		start: \posLo.kr(0),
		end: \posHi.kr(1)
	);

	sig = BufRd.ar(1, sndBuf, pos * BufFrames.kr(sndBuf), loop: 0, interpolation: 4);
	sig = Pan2.ar(sig, \pan.kr(0));

	sig = sig * Env.asr(0.001, 1, 0.001).ar(Done.freeSelf, \gate.kr(1));
	
	sig = sig * \amp.kr(-15.dbamp);

	Out.ar(\out.kr(0), sig);
}).add;
)

~normalizedSlicesArray = ~indArr / ~src.numFrames;

Synth(\slices, [\sndBuf, ~src, \posLo, ~normalizedSlicesArray[1], \posHi, ~normalizedSlicesArray[7]]);
1 Like

The data relationship is like this:

When you write \startPos, ~indArr.at(rrand(1, ~indArr.size - 2)), then only the result of startPos is available. The index is hidden. But two expressions have a dependency on the same index value.

So good way to do this in a pattern is to start by putting the index into the event.

Also note that at resolves at the time of creating the pattern, not per event, so as written, it would play the same slice over and over again. You’ll need a pattern to access values from the array.

Pdef(\pat1, Pbind(
	\instrument, \slices,
	\dur, 0.1,
	\index, Pwhite((1, ~indArr.size - 2), inf),
	\startPos, Pindex(~indArr, Pkey(\index)),
	\endPos, Pindex(~indArr, Pkey(\index) + 1)
)).play

hjh

1 Like

Hey I have a solution for this that I have used for while with flucoma, it’s kind of complex but allows you to sort the slices by any of the flucoma measures.

(
~clearAnalysisDict = {|dict|
	dict.keysValuesDo{ |key, value|
		if(value.isKindOf(Buffer)){
			value.free;
		};
		dict.removeAt(key);
	};
	dict = ();
};

~getSlices = {|file, dict, threshold|
	dict.put(\filepath, file);
	dict.put(\file, Buffer.readChannel(s, file, channels: [0]));
	dict.put(\indices, Buffer(s));
	//nrt onset slice of source
	FluidBufOnsetSlice.processBlocking(s, dict.at(\file), metric: 9, threshold: threshold, indices: dict.at(\indices), action: {"found slices".postln});
};

~sortSlices = {|measure=\centroid, dict|
	//get
	var indices = dict.at(\indices);
	var file = dict.at(\file);
	var spec, stats, meanfeatures;
	//get and set new analysis buffers
	dict.put(\spec, Buffer(s));
	dict.put(\stats, Buffer(s));
	dict.put(\meanfeatures, Buffer(s));
	//vars
	spec = dict.at(\spec);
	stats = dict.at(\stats);
	meanfeatures = dict.at(\meanfeatures);
	//analysis
	indices.loadToFloatArray(action: {
		arg fa;
		//iterate through adjacent pairs of indices (tuple like)
		fa.doAdjacentPairs{
			arg start, end, i;
			var numSamps = end - start;
			// i.postln;
			//compute spectral features per fft frame (w selected feature)
			FluidBufSpectralShape.processBlocking(s, file, start, numSamps, features: spec, select:[measure]);
			//buf stats channels: mean std skew kurtosis min median max
			FluidBufStats.processBlocking(s, spec, stats: stats, select:[\mean]);
			FluidBufCompose.processBlocking(s, stats, destination: meanfeatures, destStartFrame: i);
		};
		//get indices
		dict.put(\onsetArr, fa);
		dict.put(\size, fa.size);
		//get INDICES of sorted features
		meanfeatures.loadToFloatArray(action: { arg fa; dict.put(\sortedIndices, fa.order) });
		"done analysis".postln;
	});

};

~analyzeSlices = {|file, dict, thresh, metric|
	~clearAnalysisDict.(dict);
	~getSlices.(file, dict, thresh);
	~sortSlices.(metric, dict);
};


~getSlice = {|slice, dict|
	var sortedIndices = dict.at(\sortedIndices);
	var onsets = dict.at(\onsetArr);
	var startIdx = sortedIndices.wrapAt(slice);
	var endIdx = startIdx + 1;
	var startOnset = onsets.at(startIdx);
	var endOnset = onsets.at(endIdx);
	[startOnset, endOnset];
};

~pGetSlice = {|generator, dict|
	Pcollect ({ |i| ~getSlice.(i, dict).asRef; }, generator);
};

SynthDef(\segPlayer, {|buf, slice = #[0, 1], oneshot=1|
	var bufFrames, sampsDur, offset, startsamp, endsamp, numChans, sig, pan, panned, env, posRate, line, phasor, phase;
    
	bufFrames = BufFrames.ir(buf);
	numChans = buf.numChannels;

	offset = \offset.kr(0);
	startsamp = slice[0] + offset;
	endsamp = slice[1] + offset;
	sampsDur = endsamp - startsamp;

	posRate = \posRate.kr(1);

	//select looped or oneshot NOTE: doneaction controlled by release
	line= Line.ar(
			start: startsamp,
			end: endsamp,
			dur: (sampsDur / s.sampleRate) * BufRateScale.kr(buf) * posRate.reciprocal
	);

    phasor = Phasor.ar(
		rate: (1 / bufFrames * BufRateScale.kr(buf)) * posRate,
		start: startsamp,
		end: endsamp,
		resetPos: startsamp
	);
    
	phase = Select.ar(oneshot, [phasor, line]);
	
    sig = BufRd.ar(numChans, buf, line, interpolation: 2);

	//handle channels
	pan = \pan.kr(0);
	panned = case
	{numChans == 1} {Pan2.ar(sig, pan)}
	{numChans == 2} {Balance2.ar(sig[0], sig[1], pan)}
	{numChans > 2} { var splay = Splay.ar(sig); Balance2.ar(splay[0], splay[1], pan); } ;
	
	//envelope
	env = EnvGen.ar(Env.perc(\atk.kr(0.01), \rel.kr(0.5), curve: \curve.kr(-4)), gate: \gate.kr(1), doneAction: 2);
	
	//out
	sig = panned * env * \gain.kr(0).dbamp;
	sig = sig * \amp.kr(1);
	Out.ar(\out.kr(0), sig);
}).add;
)

Where the usage is something like:

(
~sliceBuf = Dictionary();
// eg centroid, spread, skewness, kurtosis, rolloff, flatness, crest
~analyzeSlices.(file: Platform.resourceDir +/+ "sounds/a11wlk01.wav", dict: ~sliceBuf, thresh: 0.1, metric: \centroid);
)

(
	Pdef(\seg_pattern,
		Pbind(
			\instrument, \segPlayer,
			\amp, 1,
			\buf, ~sliceBuf.at(\file),
			//lowest to highest
			\slice, ~pGetSlice.(Pseries(0, 1, inf).wrap(0, 32), ~sliceBuf),
			\dur, 0.25,
			\oneshot, 0
		)
	).play(t);
)
2 Likes

thats really cool. Im currently trying to set this up for server side sequencing. Where the phasors scheduling period which drives a window function should match the current slice of the sequence of slices. In my opinion the duration should match the current slice length or a set of slices and not be independent from the slice lengths. Whats the difference between FluidBufTransientSlice and FluidBufOnsetSlice in this context?

if I understand you correctly you can just work off this

    phasor = Phasor.ar(
		rate: (1 / bufFrames * BufRateScale.kr(buf)) * posRate,
		start: startsamp,
		end: endsamp,
		resetPos: startsamp
	);

And if you’re running granular scheduling use that as the master phasor

I like having a separate envelope because then you can make the sound decay shorter than the slice length. (This is a simple use case I use for percussion fairly often)

No idea about the difference between those two classes, I just adapted that part from the examples. You could swap it out for whatever though.

Ah I remember, the reason I decided to use onset rather than transient slice was that I felt it was more generally useful to detect onsets for covering non-percussive sound sources too - eg, start of a sound with a fade in wouldn’t be counted as a transient (I think), but it would be counted as an onset.