[warning: OP meatware issue for sure] please help me trying to understand why pvcollect doesn't behave as I think it should?

Hello

I am slowly converting old dirty patches from Max to SC and in FFT land, it is always a challenge. Now I know that @josh has made PV_BinDelay - and I use it to test this patch - it behaves like it should with the c external, but with pvcollect i still get artefacts (resonance and ring mod at the fft frame rate).

help?

~size = 2048;
~buf = Buffer.alloc(s,~size*100);
~del = Buffer.alloc(s, ~size / 2);

// x blocks of random delays
x = 5; ~del.sendCollection((x.collect{1.0.rand}.normalizeSum * (~size / 2.01) + 1).round(1).collect{|i|100.rand.dup(i)}.flat)

// look at them, woo
~del.plot

// play something pitchy
(
y = {arg buf;
	var source = SinOsc.ar(Line.ar(220,440,10,doneAction: 2));
	var fftbuf = LocalBuf(~size);
	var chain = FFT(fftbuf, source, hop: 0.25, wintype: 1);
	var chainB = PV_Copy(chain, LocalBuf(~size));
	var cue = chain > -1;
	var count = Stepper.kr(cue, max: 100);
	FluidBufCompose.kr(fftbuf,destination: ~buf, destStartFrame: count * ~size, trig: cue);
	chainB = chainB.pvcollect(~size, { |mag, phase, bin, index|
		var fftindex = (bin*2);
		var readhead = (count - BufRd.kr(1,~del,bin,0,0)).mod(100) * ~size;
		var real = BufRd.kr(1,~buf, readhead + fftindex,0,0);
		var imag = BufRd.kr(1,~buf, readhead + fftindex + 1,0,0);
		[((real**2) + (imag**2)) ** 0.5, imag.atan2(real)];
	}, frombin: 0, tobin: ~size/2+1, zeroothers: 0);
	[source, IFFT(chainB,1)];
	}.play
)


/// ringy
// same thing with Josh's sounds good - the C code (and my max patch and the patch above) do the same thing IIUC...

(
~fb = Buffer.alloc(s, ~size / 2);

y = {arg buf;
	var source = SinOsc.ar(Line.ar(220,440,10,doneAction: 2));
	var fftbuf = LocalBuf(~size);
	var chain = FFT(fftbuf, source, hop: 0.25, wintype: 1);
	chain = PV_BinDelay(chain, 1, ~delsec, ~fb, 0.25);
	[source, IFFT(chain)];
	}.play
)
1 Like

I’d also encountered amplitude modulation artefacts with FFT. The informal explanation that I finally came up with is: the AM artefacts don’t happen when each FFT window (or grain – I think FFT is a special case of granular synthesis) has a Hann window and the overlap is an even integer, because then the fade-in is the first quarter of a sin^2 wave and the fade-out is the first quarter of cos^2, and sin^2(x) + cos^2(x) = 1, so the total aggregate envelope is a constant = no AM.

In SC, FFT by default applies a half-sine envelope to the time-domain samples before FFT-ing, and IFFT applies another identical envelope to the resulting time-domain samples. Multiplying by the same envelope twice = squaring it = sin^2 = Hann window. So FFT → IFFT without anything else should sound perfectly smooth.

Audio → half-sine window → FFT encodes the window as a non-intuitive interaction among the FFT’s partials. Messing around with the partials will distort this envelope. Then at the end you get “not a half-sine” times “half-sine” = not a Hann window. So the sum of midpoint-aligned overlapping windows will no longer be a constant, and AM will factor into the result. Messing around with the partials is, of course, the point! So the default windowing may or may not work for every application. You might be able to try other window types, probably discussed in FFT help.

Alternately, it was just announced that DynGen can now do FFT! (“Add FFT support via phase vocoder.”) This might let you handle windowing in a more flexible way (though I haven’t looked at details) and it looks like it will be a great solution to the long-standing “I want a new FFT operation but I don’t want to use C++, and pvcollect isn’t doing the job” problem.

hjh

1 Like

100% agree here - did you see and played with Alex Harker’s genius framelib? I think he and @Sam_Pluta made an version for SC, maybe I should see the state of it…

I have tried Sine and Hann, but I get different flavours of AM artefacts in both cases. Still there though.

This looks amazing, I don’t know how I managed to miss it. I’ll continue my investigations!

Are you sure it isn’t just clipping?

(
y = {arg buf;
	var source = SinOsc.ar(Line.ar(220,440,10,doneAction: 2));
	var fftbuf = LocalBuf(~size);
	var chain = FFT(fftbuf, source, hop: 0.25, wintype: 1);
	var chainB = PV_Copy(chain, LocalBuf(~size));

	[IFFT(chainB,1)];
	}.play
)

Sam

If you do a PV_RandComb with a high value for wipe, I think you’ll get AM artifacts at a low volume (because most of the partials are removed). At least that’s my recollection.

hjh

Deffo - in the test above I might but the AM problem is more in line with @jamshark70 observations… replacing the source with:
var source = SinOsc.ar([111,333,555],mul: 0.1).sum;
showcases the problem clearly. I’m happy it is not me.

I’ll give DynGen a spin. And see where Alex is up to with FrameLib because that is the next level for frame processing for real. Multiresolution fft stuff for instance. funky :slight_smile: