PV_BufRd analysis of multiple soundfiles

hey, i have implemented ~path and ~makeBuffers in order to analyse multiple soundfiles. how has ~soundfile, ~recBufs and ~rates.do from the PV_BufRd helpfile to be changed to do the analysis for multiple soundfiles when booting the server? thanks.

 (
    // path to a sound files 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;

    // get some info about the files
    ~soundfile = SoundFile.new(~path);
    ~soundfile.openRead;
    ~soundfile.close;

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

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


    // allocate the soundfiles you want to analyze
    ~makeBuffers = {
    	~samples = Dictionary.new;
    	PathName(~path).entries.do{
    		arg subfolder;
    		~samples.add(
    			subfolder.folderName.asSymbol ->
    			Array.fill(
    				subfolder.entries.size,
    				{
    					arg i;
    					Buffer.read(s, subfolder.entries[i].fullPath);
    				}
    			)
    		);
    	};
    };

    ServerBoot.add(~makeBuffers);
    )

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

    // make analysis of same buffers played back at different speeds
    (
    ~rates.do { 
    	arg rate, i;
    	Synth(\pvrec_2, [\recBuf, ~recBufs[i], \soundBufnum, ~samples, \rate, rate]) 
    };
    )

hey, i have still problems writing a function for analysing the Soundfiles and writing functions in general. Hopefully someone can help. I was trying something like this:

(
// 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;

~analyseSoundFiles = {
	var soundfile = SoundFile.new;
	PathName(~path).entries.do{
		arg subfolder, i;
		
		// 'protect' guarantees the soundfiles will be closed in case of error
		protect {
			
			// read size of Soundfiles				
			soundfile.openRead(subfolder.entries[i].fullPath);
			
		} {
			"Could not read soundfile".warn;
		};
	};
	soundfile.close;
};
)

i get this error 7 times for each subfolder in ++ “buffers/”, so at least ~analyseSoundFiles detects the right amount of subfolders and protect is also working.

WARNING: Could not read soundfile
WARNING: Could not read soundfile
WARNING: Could not read soundfile
WARNING: Could not read soundfile
WARNING: Could not read soundfile
WARNING: Could not read soundfile
WARNING: Could not read soundfile

The structure now is:

  • Make soundfile object
  • do loop
    • open file, do stuff
  • end do loop
  • close file object

“Close file” should be inside the loop… actually best to put it in the second ‘protect’ function (so that it always happens, even in case of error).

hjh

thanks @jamshark70.

I´m not sure but I think this is working now:

(
// 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;

~recBufs = Dictionary.new;
PathName(~path).entries.do{
	arg subfolder, i;
	var soundfile, data;
	
	// 'protect' guarantees the soundfiles will be closed in case of error
	protect {
		~recBufs.add(
			subfolder.folderName.asSymbol ->
			Array.fill(
				subfolder.entries.size,
				{
					arg i;
					// read size of Soundfiles
					soundfile = SoundFile.openRead(subfolder.entries[i].fullPath);
					data = Signal.newClear(soundfile.numFrames);
					soundfile.readData(data);
					
					// allocate memory to store FFT data to... SimpleNumber.calcPVRecSize(frameSize, hop) will return
					// the appropriate number of samples needed for the buffer
					Buffer.alloc(s, soundfile.duration.calcPVRecSize(~windowSize, ~hopSize));
				}
			);
		);
		{
			"Could not read soundfile".warn;
		};
		soundfile.close;
	};
};
)
(
// 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;

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

	// 'protect' guarantees the soundfiles will be closed in case of error
	protect {
		~recBufs.add(
			subfolder.folderName.asSymbol ->
			Array.fill(
				subfolder.entries.size,
				{
					arg i;
					// read size of Soundfiles
					soundfile = SoundFile.openRead(subfolder.entries[i].fullPath);
					data = Signal.newClear(soundfile.numFrames);
					soundfile.readData(data);

					// allocate memory to store FFT data to... SimpleNumber.calcPVRecSize(frameSize, hop) will return
					// the appropriate number of samples needed for the buffer
					Buffer.alloc(s, soundfile.duration.calcPVRecSize(~windowSize, ~hopSize));
					/*
					~rates.collect {
						arg rate, i;
						Buffer.alloc(s, (soundfile.duration / rate).calcPVRecSize(~windowSize, ~hopSize));
					};
					*/
				}
			);
		);
		{
			"Could not read soundfile".warn;
		};
		soundfile.close;
	};
};

// allocate the soundfiles you want to analyze
~soundBufs = Dictionary.new;
PathName(~path).entries.do{
	arg subfolder;
	~soundBufs.add(
		subfolder.folderName.asSymbol ->
		Array.fill(
			subfolder.entries.size,
			{
				arg i;
				Buffer.read(s, subfolder.entries[i].fullPath);
			}
		)
	);
};

// this does the analysis and saves it to ~recBufs... frees itself when done
SynthDef(\pv_rec, {
	arg recBuf=1, soundBufnum=2;
    var in, chain;
    Line.kr(1, 1, BufDur.kr(soundBufnum), doneAction: 2);
    in = PlayBuf.ar(1, soundBufnum, BufRateScale.kr(soundBufnum), loop: 0);
    // note the window type and overlaps... this is important for resynth parameters
    chain = FFT(LocalBuf(~windowSize), in, ~hopSize, ~winType);
    chain = PV_RecordBuf(chain, recBuf, 0, 1, 0, ~hopSize, ~winType);
    // no ouput ... simply save the analysis to ~recBufs
    }).add;
)

The analysis is working now:

(
Synth(\pv_rec, [\recBuf, ~recBufs[\fx][0], \soundBufnum, ~soundBufs[\fx][0]]);
Synth(\pv_rec, [\recBuf, ~recBufs[\fx][1], \soundBufnum, ~soundBufs[\fx][1]]);
Synth(\pv_rec, [\recBuf, ~recBufs[\fx][2], \soundBufnum, ~soundBufs[\fx][2]]);
Synth(\pv_rec, [\recBuf, ~recBufs[\voice][0], \soundBufnum, ~soundBufs[\voice][0]]);
Synth(\pv_rec, [\recBuf, ~recBufs[\perc][0], \soundBufnum, ~soundBufs[\perc][0]]);
// etc...........
)

But how can I do the analysis for all the files from the subfolders at once? I was trying:

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

The synth can handle one and only one buffer.

So you will have to run a separate synth for each buffer.

hjh

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;