PV Ugens and DXEnvFan (miSCellaneous)

Hey there!

I’m having some odd errors when using most PV Ugens with miSCellaneous’s DXEnvFan (kudos to the author of the quark btw :wink: ).
Using with a PV_BufRd works without problems, but for example, swapping it with a PV_BinBufRd crashes and stops the server. Or adding, let’s say, a PV_BinGap in the FFT chain gives the following error:
ERROR: Message 'miSC_getFFTbufSizes' not understood.

But most of the time, whichever PV i add to the chain results in the server crashing.

I thought maybe it was just a CPU or memory overload but even with lighter parameters and high memory allocation, the problem persists.
Is there any cautions i should be aware of when using DXEnvFan with PV Ugens?

A simple reproducer example would help to proceed in troubleshooting.

Sweet, i can try and put that together ASAP, as i can’t just send my code as it is because it’s part of a bigger project with more dependencies (more about sound banks and DAW bridging, so it’s not influencing this particular bit of code, as one would perhaps point out).

But before i send that, maybe it’s best to tell that the code is based on one of the Buffer Granulation tutorial examples:
Ex.1f: Buffer granulation Synth with per-grain effect processing (DXEnvFan)

So it’s more or less the same code, just adapted to use it with a PV_BufRd (loaded with scpv files) and PV processing instead of the BPF (in theory, but that’s where errors start to happen).
The LocalBuf is expanded to maxOverlap, so the pointer of PV_BufRd can read independent FFT buffers with the env generated by DXEnvFan, that triggers a Phasor to read through PV_BufRd.
Until then it works just fine.

Let me know if that’s enough information for now or you’d prefer the actual code?

Ok, I will have a look into it. But that might need some time, we are in the middle of end term rush, which will decay within some days hopefully …

No worries!
Good luck with the end terms :slight_smile:

Hello again,

probably a combination of two different things: I’ve tried Ex 1f now with PV_BufRd / PV_BinBufRd and didn’t encounter any server crashes. Independent from this there’s a method missing preventing PV_BinGap and PV_BinRange from working within a chain, but this can easily be added (for variants with PV_BinGap see second block of code, replace in miSCellaneous_lib’s file PV_UGens.sc and recompile before playing examples 1 and 2).

Regarding the server crashes, check for following possible mistakes:

.) it needs an Array of different LocalBufs, so

{ LocalBuf(~windowSize) } ! ~maxOverlap

and not

LocalBuf(~windowSize) ! ~maxOverlap

.) In contrast to PV_BufRd PV_BinBufRd needs an FFT before

.) some type of multichannel mismatch (e.g. using a nested array where a flat one were needed)

For the sake of simplicity the examples below use 12 analysis buffers derived from a single buffer played at different rates.

// analysis adapted from PV_BufRd help file
// prepare 12 analysis buffers from one sound file played at 12 different rates


s.boot

(
~path = Platform.resourceDir +/+ "sounds/a11wlk01.wav";
~windowSize = 1024;
~hopSize = 0.25;

~soundfile = SoundFile.new(~path);
~soundfile.openRead;
~soundfile.close;

~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);

// Hann window
~winType = 1;
)

// 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 buffer played back at different speeds

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

// playback

(
SynthDef(\pvplay, { arg out = 0, recBuf = 1;
    var in, chain, bufnum;
    bufnum = LocalBuf.new(1024);
    chain = PV_BufRd(bufnum, recBuf, MouseX.kr(0.0, 1.0));
    Out.ar(out, IFFT(chain, 1).dup);
    }).add;
)

// check with a specific rate

b = Synth("pvplay", [\out, 0, \recBuf, ~recBufs[10]]);

b.free;


// Ex. 1, Granulation with PV_BufRd

// check also with PV_BinGap uncommented
// therefore it needs adapted class definition

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

// overlap only settable in SC versions >= 3.9

SynthDef(\gran_1f_a, { |out = 0, posLo = 0.1, posHi = 0.9,
	posRateE = 0, posRateM = 1, overlap = 2, trigRate = 1,
	panMax = 0.8, loBin = 10, hiBin = 10, amp = 1|
	var sig, bpFreq, dUgen, bufDur, pos, posRate, playbuf, env, maxOverlap = ~maxOverlap;
	var chains, 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
	);


	chains = PV_BufRd(fftBufs, ~recBufs, pos);

	// check with PV_BinGap
	// chains = PV_BinGap(chains, loBin, hiBin);

	playbuf = IFFT(chains, ~winType);

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

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

	// mix to out
	Out.ar(0, Mix(sig) * amp)
}, metadata: (
	specs: (
		posLo: [0.01, 0.99, \lin, 0.01, 0],
		posHi: [0.01, 0.99, \lin, 0.01, 0.5],
		posRateE: [-3, 4, \lin, 1, -1],
		posRateM: [0.1, 10, \exp, 0.01, 1.35],
		trigRate: [1, 200, \lin, 0.01, 90],
		overlap: [0.2, 12, \lin, 0.01, 7],
		panMax: [0.0, 1, \lin, 0.005, 0.75],
		loBin: [0, 200, \lin, 1, 5],
		hiBin: [0, 200, \lin, 1, 12],
		amp: [0.0, 3, \lin, 0.005, 1]
	)
)).add;
)

(
\gran_1f_a.sVarGui.gui(
	tryColumnNum: 1,
	synthColorGroups: (0..9).clumps([4,2,1,2,1])
)
)


// Ex. 2, Granulation with PV_BinBufRd

// check also with PV_BinGap uncommented
// therefore it needs adapted class definition


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

SynthDef(\gran_1f_b, { |out = 0, posLo = 0.1, posHi = 0.9,
	posRateE = 0, posRateM = 1, overlap = 2, trigRate = 1,
	panMax = 0.8, loBin = 10, hiBin = 10, clear = 0.0, amp = 1|
	var sig, bpFreq, dUgen, bufDur, pos, posRate, playbuf, env, maxOverlap = ~maxOverlap;
	var chains, 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, ~soundBuf, ~rates, loop: 1));
	chains = PV_BinBufRd(chains, ~recBufs, pos, 0, 2, 10, clear);

	// check with PV_BinGap
	// chains = PV_BinGap(chains, loBin, hiBin);

	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));

	// mix to out
	Out.ar(0, Mix(sig) * amp)
}, metadata: (
	specs: (
		posLo: [0.01, 0.99, \lin, 0.01, 0],
		posHi: [0.01, 0.99, \lin, 0.01, 0.5],
		posRateE: [-3, 4, \lin, 1, -1],
		posRateM: [0.1, 10, \exp, 0.01, 1.35],
		trigRate: [1, 200, \lin, 0.01, 90],
		overlap: [0.2, 12, \lin, 0.01, 7],
		panMax: [0.0, 1, \lin, 0.005, 0.75],
		clear: [0, 1, \lin, 1, 0],
		loBin: [0, 200, \lin, 1, 5],
		hiBin: [0, 200, \lin, 1, 12],
		amp: [0.0, 3, \lin, 0.005, 1]
	)
)).add;
)

(
\gran_1f_b.sVarGui.gui(
	tryColumnNum: 1,
	synthColorGroups: (0..10).clumps([4,2,1,1,2,1])
)
)

s.freqscope

Here the modified code for PV_UGens.sc within miSCellaneous, replace and recompile

PV_BinRange : PV_ChainUGen {

	*new { |buffer, loBin, hiBin|
		^this.multiNew(\control, buffer, loBin, hiBin)
	}

	*new1 { |rate, buffer, loBin, hiBin|
		var wipe, bufSize, chain_clipped;

		bufSize = buffer.miSC_getFFTbufSize;
		chain_clipped = LocalBuf(bufSize);
		chain_clipped = PV_Copy(buffer, chain_clipped);

		wipe = loBin * 2 / bufSize;
		chain_clipped = PV_BrickWall(chain_clipped, wipe);

		wipe = hiBin * 2 / bufSize - 1;
		chain_clipped = PV_BrickWall(chain_clipped, wipe);

		^chain_clipped
	}
}

PV_BinGap : PV_ChainUGen {

	*new { |buffer, loBin, hiBin|
		^this.multiNew(\control, buffer, loBin, hiBin)
	}

	*new1 { |rate, buffer, loBin, hiBin|
		var wipe, bufSize, chain_gap_1, chain_gap_2;

		bufSize = buffer.miSC_getFFTbufSize;
		chain_gap_1 = LocalBuf(bufSize);
		chain_gap_2 = LocalBuf(bufSize);

		chain_gap_1 = PV_Copy(buffer, chain_gap_1);
		chain_gap_2 = PV_Copy(buffer, chain_gap_2);

		wipe = loBin - 1 * 2 / bufSize - 1;
		chain_gap_1 = PV_BrickWall(chain_gap_1, wipe);

		wipe = hiBin + 1 * 2 / bufSize;
		chain_gap_2 = PV_BrickWall(chain_gap_2, wipe);

		^PV_Add(chain_gap_1, chain_gap_2);
	}
}


+ FFT {
	miSC_getFFTbufSize {
		^this.fftSize
	}
}

+ PV_ChainUGen {
	miSC_getFFTbufSize {
		^this.inputs[0].miSC_getFFTbufSize
	}
}

+ LocalBuf {
	miSC_getFFTbufSize {
		^this.inputs[1]
	}
}
1 Like

Great!
Hmm so i assumed (wrongly) that PV_BinBufRd would get its FFT data from a scpv file…

Thank you so much for the in-depth reply, it is deeply appreciated!

I have learned from your example: interestingly, it is altering the idea of fx processing per grain. At least in my interpretation of your description we have now a pre-granulation fx processing (or post fx granulation) – where the fx params are not changed per grain, although this would be possible too (e.g. the env could trigger positions).
Post granulation FFT processing per grain (I have done this once in the kitchen studies project) would lead to different results especially with short grains, that produce strong amplitude modulation, which means a difference in the spectrum.

Oh nice :slight_smile:
Yes, that’s basically it. I’ve been also looking into buffer processing before granulation, only tricky part is the smooth interpolation between changes/updates. (I did experiment with that in Max/MSP but not yet in Supercollider).

BTW the bug in PV_BinGap / PV_BinRange is fixed in the last update v0.24.

Ah yes i saw! That’s brilliant! Thanks :slight_smile: