PV_BufRd analysis of multiple soundfiles

thank you. I understand. But is it possible to do an iteration here or to use a Pattern?

The problem is that Synth.new is the thing that needs to be iterated – so the loop should surround Synth.new. But you tried to put the loop inside Synth.new.

~recBufs.do { |buf, i|
	Synth(\pv_rec, [
		\recBuf, buf,
		\soundBufnum, ~soundBufs[i]
	]);
};

Possibly in a Routine with some waiting/onFree magic so that you’re not doing all of them at once.

It is perhaps a bit confusing because Events multichannel-expand arguments, but Synths don’t. A Synth is really just a thin wrapper around direct server messages, so it isn’t actually very powerful (despite the fact that it’s used everywhere).

hjh

thanks for the explanation. i have tested it with a Routine and just 2 subfolders filled with 3 files each and had a noisy, strange result when playing one of the analysed files from ~recBufs with PV_BufRd.
maybe im wrong but doesnt ~soundBufs also need this syntax: ~soundBufs[\subfolder][i] in the example its just ~soundBufs[i]? thanks :slight_smile:

Probably – I didn’t read your example extremely carefully – I was just explaining about the looping with respect to Synth.new. I was assuming you’d be responsible for your own data structures :wink:

hjh

I was assuming you’d be responsible for your own data structures

Im trying to, but still confused haha :wink:

The reason for putting the iterations inside of the SynthDef in the firstplace (thanks for your correction) was that i didnt and still dont know how to iterate over ~recBufs and access the subfolders in ~soundBufs when its not just the index which has to be changed for each iteration.
Because the structure for ~recBufs and ~soundBufs is identical: ~*dictionary*[*\subfolder from ~path*][*index of item in \subfolder*] I was trying:

(
r = Routine.new({
~recBufs.do {
		arg buf, i;
		Synth(\pv_rec, [
			\recBuf, buf,
			\soundBufnum, ~soundBufs[buf][i]
		]);
	};
	s.sync;
	"done".postln;
}).play(AppClock);
)

which is not working. do i need a variable for the \subfolder of ~soundBufs? or how can this be done?
I was also trying to come up with a pattern solution with Pdict where i also got confused with syntax. Is Pchain working here?

(
Pdef(\pv_rec,
	Pbind(
		\instrument, \pv_rec,
		\recBuf, Pdict(~recBufs, Pseq([~recBufs.keys(Array)],1),inf),
		\soundBufnum, Pdict(~soundBufs, Pseq([~soundBufs.keys(Array)],1),inf)
	):
).play;
)

If you want to keep this data structure, you’ll need two nested loops, one to iterate over the keys of your dictionary, and one to iterate over the array of buffers that is the value for that key (iiuc).

But I wonder if you really need this complex structure. Seems to me you could just collect all the buffers into a flat array … then the processing would be simpler, too.

cheers,
eddi
https://soundcloud.com/all-n4tural/sets/spotlight
https://alln4tural.bandcamp.com

Yes, alln4tural is correct – a single flat loop is not enough to handle a nested collection.

It’s helpful, when writing these loops, to choose accurate argument names. In ~recBufs.do { |something| ... }, “something” is a member of the collection – which here you’ve clarified is an array, and not a buffer. When you write ~recBufs.do { |buf| ... }, the fact that you called it buf doesn’t change the fact that it’s an array.

So

~recBufs.keysValuesDo { |i, arrayOfBufs|
    arrayOfBufs.do { |recBuf, j|
        // now you have recBuf, and ~soundBufs[i][j]
    }
}

But another data structure is a single flat array of events: (recBuf: /* a buffer */, soundBuf: /* a buffer */), and you can add any other descriptors to this.

It took me awhile to realize it, but keeping parallel data structures is a pain – very easy to break, raises code complexity. It’s often better to put related objects together into a namespace and then collect a number of these namespaces.

hjh

(
// path to a sound file here
~path = PathName(thisProcess.nowExecutingPath).parentPath ++ "buffers/";
// the frame size for the analysis - experiment with other sizes (powers of 2)
~windowSize = 1024;
// the hop size
~hopSize = 0.25;
// Hann window
~winType = 1;
//different playback rates
~rates = (1..12) * 0.1 + 0.4;

~buffers = Dictionary.new;
PathName(~path).entries.do {
	arg subfolder;
	var soundfile, data;
	
	// 'protect' guarantees the soundfiles will be closed in case of error
	// however there is still a misunderstanding about how to use it
	// it should be 'protect { body } { cleanup }'
	// you've written 'protect { body; cleanup }'
	// so, upon error, the cleanup won't run
	// I'll fix it below
	// also the protect should be per file, not per subdirectory
	
	~recBufs.add(
		subfolder.folderName.asSymbol ->
		Array.fill(
			subfolder.entries.size,
			{
				arg i;
				// read size of Soundfiles
				soundfile = SoundFile.openRead(subfolder.entries[i].fullPath);
				
				// you aren't using the data in the client
				// so there is no need to waste memory and time reading the whole file
				// don't do this:
				// data = Signal.newClear(soundfile.numFrames);
				// soundfile.readData(data);
				protect {
					(
						pvRecBuf: Buffer.alloc(s, soundfile.duration.calcPVRecSize(~windowSize, ~hopSize)),
						sndBuf: Buffer.read(s, subfolder.entries[i].fullPath)
					)
				} { |err|
					if(err.notNil) {
						"Error opening '%'".format(subfolder.entries[i].fullPath.basename).warn;
					};
					soundfile.close
				};
			}
		);
	);
};
)

Then…

~buffers.do { |subdirArray|
	subdirArray.do { |buf|
		Synth(\pv_rec, [
			\recBuf, buf.pvRecBuf,
			\soundBufnum, buf.sndBuf
		]);
	};
};

… and the need to keep access to two levels of indices disappears – the analysis loop is simpler.

hjh

1 Like

thank you so much also for the additional explanations. Im so happy :slight_smile:

i think instead of ~recBufs.add(subfolder.folderName.asSymbol it should be ~buffers.add(subfolder.folderName.asSymbol

ive adjusted the code with ~rate from this example PV Ugens and DXEnvFan (miSCellaneous) - #6 by dkmayer so you have each analysed Buffer with 12 different rates for playing back with PV_BufRd.
The syntax for a Pbind would be \buf, ~buffers[*\subfoldername*][*index of buffer*][\pvRecBuf][*index of playbackrate*],

see the code below, i hope my implementation is correct. I have tested it with
~buffers[*\subfoldername*][*index of buffer*][\pvRecBuf][*index of playbackrate*].numFrames; for the different playbackrates and it was working nicely.

(
// path to a sound file here
~path = PathName(thisProcess.nowExecutingPath).parentPath ++ "buffers/";
// the frame size for the analysis - experiment with other sizes (powers of 2)
~windowSize = 1024;
// the hop size
~hopSize = 0.25;
// Hann window
~winType = 1;
//different playback rates
~rates = (1..12) * 0.1 + 0.4;

~buffers = Dictionary.new;
PathName(~path).entries.do {
	arg subfolder;
	var soundfile, data, rates;

	~buffers.add(
		subfolder.folderName.asSymbol ->
		Array.fill(
			subfolder.entries.size,
			{
				arg i;
				// read size of Soundfiles
				soundfile = SoundFile.openRead(subfolder.entries[i].fullPath);

				protect {
					(
						pvRecBuf: ~rates.collect { |rate, i|
							Buffer.alloc(s, (soundfile.duration / rate).calcPVRecSize(~windowSize, ~hopSize))
						},

						sndBuf: Buffer.read(s, subfolder.entries[i].fullPath)

					)
				} { |err|
					if(err.notNil) {
						"Error opening '%'".format(subfolder.entries[i].fullPath.basename).warn;
					};
					soundfile.close
				};
			}
		);
	);
};

// this does the analysis and saves it to ~recBufs... frees itself when done (in addition uses rate)
SynthDef(\pvrec_2, {
	arg recBuf, soundBufnum, rate = 1;
    var in, chain;
    Line.kr(1, 1, BufDur.kr(soundBufnum) / rate, doneAction: 2);
    in = PlayBuf.ar(1, soundBufnum, rate * BufRateScale.kr(soundBufnum), loop: 0);
    chain = FFT(LocalBuf(~windowSize), in, ~hopSize, ~winType);
    chain = PV_RecordBuf(chain, recBuf, 0, 1, 0, ~hopSize, ~winType);
    }).add;
)

(
~buffers.do { |subdirArray|
	subdirArray.do { |buf|
		~rates.do { |rate, i|
			Synth(\pvrec_2, [
				\recBuf, buf.pvRecBuf[i],
				\soundBufnum, buf.sndBuf,
				\rate, rate
			]);
		};
	};
};
)

hey, ive recognized that if i run

~buffers.do { |subdirArray|
	subdirArray.do { |buf|
		Synth(\pv_rec, [
			\recBuf, buf.pvRecBuf,
			\soundBufnum, buf.sndBuf
		]);
	};
};

i have synth nodes on the server until i hit cmd + period.
Synth(\pv_rec) is not beeing freed after doing the analysis. what can i do here?

Okay, it just takes a lot of time when i have a few more subfolders with soundfiles.
Done a test with 1 soundfile in 1 subfolder then all the Synth Nodes are gone right away.

The Problem now is, accessing the analysed soundfiles via the Pattern Library causes heavy Server Overload, up to 140%. Same SynthDef with hardcoded path for sndBuf/recBuf ca. 7%. I already have read this thread Performance of SC on OSX about better using Arrays.
The question now is: how can I use an Array instead of a Dictionary in this case or are there any other solutions?
For PV_BinBufRd I would need an Array including one SubArray for the sndBuf and one SubArray for the corresponding recBufs with their different rates. thanks alot.

(
s.waitForBoot({

	s.sync;
	
	~path = PathName(thisProcess.nowExecutingPath).parentPath ++ "samples/";
	// the frame size for the analysis - experiment with other sizes (powers of 2)
	~windowSize = 1024;
	// the hop size
	~hopSize = 0.25;
	// Hann window
	~winType = 1;
	//different playback rates
	~rates = (1..12) * 0.1 + 0.4;

	~buffers = Dictionary.new;
	PathName(~path).entries.do {
		arg subfolder;
		var soundfile, data;

		~buffers.add(
			subfolder.folderName.asSymbol ->
			Array.fill(
				subfolder.entries.size,
				{
					arg i;
					// read size of Soundfiles
					soundfile = SoundFile.openRead(subfolder.entries[i].fullPath);

					protect {
						(
							pvRecBuf: ~rates.collect { |rate, i|
								Buffer.alloc(s, (soundfile.duration / rate).calcPVRecSize(~windowSize, ~hopSize))
							},

							sndBuf: Buffer.read(s, subfolder.entries[i].fullPath)
						)
					} { |err|
						if(err.notNil) {
							"Error opening '%'".format(subfolder.entries[i].fullPath.basename).warn;
						};
						soundfile.close
					};
				}
			);
		);
	};

// this does the analysis and saves it to ~recBufs... frees itself when done (in addition uses rate)
SynthDef(\pvrec, {
	arg recBuf, soundBufnum, rate = 1;
    var in, chain;
    Line.kr(1, 1, BufDur.kr(soundBufnum) / rate, doneAction: 2);
    in = PlayBuf.ar(1, soundBufnum, rate * BufRateScale.kr(soundBufnum), loop: 0);
    chain = FFT(LocalBuf(~windowSize), in, ~hopSize, ~winType);
    chain = PV_RecordBuf(chain, recBuf, 0, 1, 0, ~hopSize, ~winType);
	}).add;

	s.sync;

~buffers.do { |subdirArray|
	subdirArray.do { |buf|
		~rates.do { |rate, i|
			Synth(\pvrec, [
				\recBuf, buf.pvRecBuf[i],
				\soundBufnum, buf.sndBuf,
				\rate, rate
			]);
		};
	};
};

	s.sync;

	"fft analysis done".postln;

});
)

Ok, so we’re now talking about the playback synths… but you’ve reposted the analysis code, not the playback code.

With no idea how you’re playing the synths, there’s no way to answer :wink:

hjh

hey thanks, @jamshark70 for testing ive been using the code from this example for buffer granulation with PV_BufRd / PV_BinBufRd https://scsynth.org/t/pv-ugens-and-dxenvfan-miscellaneous/2327/5and just added an overall amp envelope but also checked with Pmono without one.

  (
    ~maxOverlap = ~rates.size;
    ~audioBus = Bus.audio(s, ~maxOverlap);

    SynthDef(\pv_BinBufRd, {
    	arg out=0, posLo=0.1, recBufs=0, sndBuf=0, posHi=0.9,
    	posRateE=0, posRateM=1, overlap=2, trigRate=1,
    	panMax=0.8, clear=0.0, amp=1;

    	var gainEnv = \gainEnv.kr(Env.newClear(8).asArray);
    	var sig, chains, pos, posRate, playbuf, env;
    	var maxOverlap = ~maxOverlap;
    	var fftBufs = { LocalBuf(~windowSize) } ! ~maxOverlap;

    	posRate = 10 ** posRateE * posRateM;

    	// phasor bounds must be between 0 and 1
    	pos = Phasor.ar(0, posRate * SampleDur.ir, posLo, posHi);

    	// multichannel trigger
    	env = DXEnvFan.ar(
    		Dseq((0..maxOverlap-1), inf),
    		trigRate.reciprocal,
    		size: maxOverlap,
    		maxWidth: maxOverlap,
    		width: (Main.versionAtLeast(3, 9)).if { overlap }{ 2 },
    		// option to avoid unwanted triggers
    		zeroThr: 0.002,
    		// take equalPower = 0 for non-squared sine envelopes
    		// more efficient with helper bus
    		equalPower: 0,
    		bus: ~audioBus
    	);

    	// need FFT before PV_BinBufRd !
    	chains = FFT(fftBufs, PlayBuf.ar(1, sndBuf, ~rates));
    	chains = PV_BinBufRd(chains, recBufs, pos, 0, 2, 10, clear);
    	playbuf = IFFT(chains, ~winType);

    	// generate grains by multiplying with envelope
    	sig = playbuf * env;

    	// generate array of ~maxOverlap stereo signals
    	sig = Pan2.ar(sig, Demand.ar(env, 0, Dseq([-1, 1], inf) * panMax));

    	sig = Mix(sig) * 35.neg.dbamp * amp;

    	//amp envelope
    	gainEnv = EnvGen.kr(gainEnv, doneAction:2);
    	sig = sig * gainEnv;

    	// mix to out
    	Out.ar(out, sig)
    }).add;
    )

    (
    Pdef(\pv_BinBufRd,
    	Pbind(
    		\type, \hasEnv,
    		\instrument, \pv_BinBufRd,

    		\bufIndex, 0,

    		\sndBuf, Pfunc{|event|
    		~buffers[\laser][event[\bufIndex]][\sndBuf]
    		},
    		\recBufs, Pfunc{|event|
    		~buffers[\laser][event[\bufIndex]][\pvRecBuf]
    		},
    		
    		\dur, 0.125,

    		\posLo, 0.01,
    		\posHi, 0.99,
    		\posRate, 1,
    		\posRateE, 1,
    		\posRateM, 0.1,
    		\trigRate, 1,
    		\overlap, 7,
    		\panMax, 0.80,

    		\legato, 0.8,
    		\atk, Pwhite(0.01,0.03,inf), // between 0 and 1
    		\sus, (1 - Pkey(\atk)) * Pexprand(0.35,0.50,inf), // between 0 and 1

    		\gainEnv, Pfunc{|e|
    			var rel = (1 - e.atk - e.sus);
    			var c1 = exprand(2,6);
    			var c2 = exprand(-2,-6);
    			Env([0,1,1,0],[e.atk, e.sus, rel],[c1,0,c2])
    		},

    		\amp, 0.35,
    		\finish, ~utils[\hasEnv],
    	),
    ).play(t, quant:1);
    )

when i put the the dictionarypath directly into the synthdef its just 1/10 of the cpu.

// need FFT before PV_BinBufRd !
	chains = PV_BufRd(fftBufs, ~buffers[\laser][0][\pvRecBuf], pos);
	playbuf = IFFT(chains, ~winType);

	// generate grains by multiplying with envelope
	sig = playbuf * env;

It looks like you have some confusion about handling the array of record buffers.

	pvRecBuf: ~rates.collect { |rate, i|
		Buffer.alloc(s, (soundfile.duration / rate).calcPVRecSize(~windowSize, ~hopSize))
	},

… so you will have one buffer per rate, for each file.

 SynthDef(\pv_BinBufRd, {
    	arg out=0, posLo=0.1, recBufs=0, sndBuf=0, posHi=0.9,

… but the synthdef permits only one value for recBufs. recBufs is a plural name, but 0 is a singular value – so, the argument (which knows nothing about English grammar) will accept only a singular value.

    		\recBufs, Pfunc{|event|
   	 		~buffers[\laser][event[\bufIndex]][\pvRecBuf]
    		},

Then, this is providing an array of buffers to the event.

In the default event prototype, an array for a parameter means to create one synth per array element. You have 12 rates → 12 buffers → 12 synths… hold on… \dur, 0.125,… 12 synths, 8 times per second. Synth duration is 1 (based on your envelope) so you should have up to 96 synths at any moment. (Note also that, because the SynthDef accepts only one value here, there is literally no other way for the event to handle an array for this parameter.)

And the SynthDef multichannel-expands chains by ~rates so you have 96 synths x 12 FFT chains per synth = 1152 simultaneous FFT chains.

So a high degree of CPU usage is not exactly surprising.

I guess what you’re expecting is a single synth doing 12 FFT chains, 8 times per second = 96 FFT chains. For that, you would need sndBufs to be an array argument. The size is determined by ~rates, so you cannot declare sndBufs as an arg. You can use NamedControl. So, delete it from the arg list, and then in the variable declarations:

var sndBufs = NamedControl.kr(\sndBufs, Array.fill(~rates.size, 0));

Then in your pattern, you need the array of buffers to be treated not as multi-synth expansion, but as an array included in a single message. This is done by wrapping the array in another array level:

    		\recBufs, Pfunc{|event|
    			[ ~buffers[\laser][event[\bufIndex]][\pvRecBuf] ]
    		},

hjh

thanks for your help @jamshark70 .

1.)
Then I think I have a general misunderstanding of PV_BinBufRd.
In the Example the analysed Soundfiles are stored with different ~rates in ~recBufs and the original Soundfile is stored in ~soundBuf. In the SynthDef the original Soundfile stored in~soundBuf has been put into Playbuf.ar together with the array of ~rates for the rate argument. The Array of analysed Soundfiles with their corresponding rates stored in ~recBufs has been put into PV_BinBufRd.

Analysis from Example:

~rates = (1..12) * 0.1 + 0.4;

// need FFT buffers from different lengths
~recBufs = ~rates.collect { |rate, i|
	Buffer.alloc(s, (~soundfile.duration / rate).calcPVRecSize(~windowSize, ~hopSize));
};

// one sound buffer is enough in this case
~soundBuf = Buffer.read(s, ~path);

// make analysis of same buffer played back at different speeds

(
~rates.do { |rate, i| Synth(\pvrec_2, [\recBuf, ~recBufs[i], \soundBufnum, ~soundBuf, \rate, rate]) }
)

SynthDef from Example :

// need FFT before PV_BinBufRd !
chains = FFT(fftBufs, PlayBuf.ar(1, ~soundBuf, ~rates, loop: 1));
chains = PV_BinBufRd(chains, ~recBufs, pos, 0, 2, 10, clear);

when I now declare:

var sndBufs = NamedControl.kr(\sndBufs, Array.fill(~rates.size, 0));

in my SynthDef and change PV_BinBufRd to:

// need FFT before PV_BinBufRd !
chains = FFT(fftBufs, PlayBuf.ar(1, sndBufs, BufRateScale.kr(sndBufs), loop: 1));
chains = PV_BinBufRd(chains, recBufs, pos, 0, 2, 10, clear);

and put

\sndBufs, Pfunc{|event|
		~buffers[\laser][event[\bufIndex]][\sndBuf]
		},

and

\recBufs, Pfunc{|event|
    			[ ~buffers[\laser][event[\bufIndex]][\pvRecBuf] ]
    		},

in the Pbind i have to exchange the rate argument in Playbuf.ar from ~rates to BufRateScale.kr(sndBufs) right? otherwise i get a really harsh, high frequency.

2.)
when i use the dictionary in other SynthDefs without any arrays I can just use the path to an analysed soundfile with a specific rate in the Pbind like this, right?

\recBufs, ~buffers[\laser][0][\pvRecBuf][5]

I don’t understand why you’re switching to sndBufs now. The problem was with recBufs.

My suggestion was to make recBufs an arrayed input, but this latest code snippet looks like you applied that advice to sndBuf(s) instead. The behavior shouldn’t be expected to be the same. (I think this explains the high frequency too.)

Yes, if it’s not an arrayed control, then you can just put one buffer into the event directly.

hjh

hey, im sorry you said sndBufs in your post. I changed everything and its working now. thanks alot.

smacks forehead

You’re right, it’s a typo in my post.

Really sorry about that… a bit embarrassed. Just one word wrong, and it was kinda the most important one.

hjh

haha no problem, thanks for your help :slight_smile: