Splay all the bins of a FFT

Hi!

Is the a more low processing cost option for splaying all the bins of a FFT?

c.free; c = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
(
{
    var in, chain, buf = c;
    in = PlayBuf.ar(1, buf, BufRateScale.kr(buf), loop: 1);
    chain = FFT(LocalBuf(1024), in)!75;

	chain.do({arg item,i;
    chain[i] = chain[i].pvcalc(1024, {|mags, phases|
        [mags , phases ]; 
    }, frombin: i, tobin: i, zeroothers: 1);
	});
	Out.ar(0,Splay.ar(IFFT(chain)*0.5, center: {rrand(-1.0,1.0)}!75))
}.play
)

By any change, from a C++ perspective, would it be possible to have IFFT outputting each individual bin as a multichannel UGen?

FFT doesn’t work that way. You can do what you want, but I think you need to do it with multiplying the magnitudes in left and right channel versions of the FFT. So, in the code below I have taken the FFT, then copied the buffer so we have a left and right channel. To pan each bin to a different location, I come up with a magnitude value for each bin in each channel. Because an FFT has both mag and phase, I need to make buffers that have both mag and phase values, so each buffer is [mag0, phase0, mag1, phase1…etc] (I am ignoring the first two values of the FFT because they don’t actually matter in this situation). Then once I have the mags for left and right channels I use PV_MagMul to multiply that buffer by the FFT channels. It doesn’t sound like much, but each time you run it, it should sound a little different.

Sam

//create two buffers
//each buffer needs to have 1024 values that go mag0, phase0, mag1, phase1, etc
(
~pan = Array.fill(512, {rrand(0,1.0)});  //random panning for each bin

~magsL = [(1-~pan).sqrt]; //left volumes for each bin
~magsL = ~magsL.add(Array.fill(512, {0})).flop.flatten; //add phases of 0 and fold them into the 

~magsR = [~pan.sqrt].add(Array.fill(512, {0})).flop.flatten;  //right values done in one line instead of two

//load them into Buffers
~magBufL = Buffer.loadCollection(s, ~magsL);
~magBufR = Buffer.loadCollection(s, ~magsR);
)

c = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
(
{
    var in, chainL, chainR, buf = c;
	var localB = LocalBuf(1024);
	
    in = PlayBuf.ar(1, buf, BufRateScale.kr(buf), loop: 1);
    chainL = FFT(LocalBuf(1024), in);
	chainR = PV_Copy(chainL, localB); //duplicate the FFT
	
	chainL = PV_MagMul(chainL, ~magBufL); //multiply the left fft by the left mags
	chainR = PV_MagMul(chainR, ~magBufR); //multiply the right fft by the right mags
	
	IFFT([chainL, chainR]) //IFFT in "stereo"
}.play
)

//with pan locations of just left or right, we can actually hear it working 
(
~pan = Array.fill(512, {[0,1].choose});
~magsL = [(1-~pan).sqrt].add(Array.fill(512, {0})).flop.flatten;
~magsR = [~pan.sqrt].add(Array.fill(512, {0})).flop.flatten;

~magBufL = Buffer.loadCollection(s, ~magsL);
~magBufR = Buffer.loadCollection(s, ~magsR);
)

//with mags of 0, we should hear nothing.
(
~magBufL = Buffer.loadCollection(s, Array.fill(1024, 0));
~magBufR = Buffer.loadCollection(s, Array.fill(1024, 0));
)

Minor correction: the first two values are mag0, magNyquist. The phase values are dropped for these two because DC has no phase, and Nyquist is just a series of up-down samples where a phase shift implies information higher than Nyquist, which doesn’t exist.

Technically, removing DC could affect continuity of the signal, especially with strong low frequency energy. Removing Nyquist is mathematically an issue, but basically inaudible. Your dog’s hearing is that good but certainly not mine.

hjh

A fun alternative using multislider to manually pan the bins (taking into account the correction around nyquist magnitude):

//create two buffers
//each buffer needs to have 1024 values that go mag0, phase0, mag1, phase1, etc
(
~pan = Array.fill(512, {rrand(0,1.0)});  //random panning for each bin

~magsL = [(1-~pan).sqrt]; //left volumes for each bin
~magsL = ~magsL.add([rrand(0,1.0)].addAll(Array.fill(511, {0}))).flop.flatten; //add phases of 0 and fold them into the

~magsR = [~pan.sqrt].add([rrand(0,1.0)].addAll(Array.fill(511, {0}))).flop.flatten;  //right values done in one line instead of two

//load them into Buffers
~magBufL = Buffer.loadCollection(s, ~magsL);
~magBufR = Buffer.loadCollection(s, ~magsR);
)

c = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
(
{
    var in, chainL, chainR, buf = c;
	var localB = LocalBuf(1024);

    in = PlayBuf.ar(1, buf, BufRateScale.kr(buf), loop: 1);
    chainL = FFT(LocalBuf(1024), in);
	chainR = PV_Copy(chainL, localB); //duplicate the FFT

	chainL = PV_MagMul(chainL, ~magBufL); //multiply the left fft by the left mags
	chainR = PV_MagMul(chainR, ~magBufR); //multiply the right fft by the right mags

	IFFT([chainL, chainR]) //IFFT in "stereo"
}.play
)

//play with the panning of each bin by moving the sliders
(
w = Window();
m = MultiSliderView(w).size_(512).elasticMode_(1);
m.value = Array.fill(512, {|i| ~pan[i]});
m.action_{|ms|
	var pan = ms.value.insert(1, ms.value.pop);

	~magsL = (1-pan).sqrt.as(Signal);
	~magBufL.sendCollection(~magsL.asWavetableNoWrap);
	~magsR = pan.sqrt.as(Signal);
	~magBufR.sendCollection(~magsR.asWavetableNoWrap);
};
w.layout_(VLayout([m]));
w.front
)

//or set them to a fun algorithm
(
var sig = Signal.sineFill(512, [1,0.5,0.25], [0]);
m.valueAction_(sig.abs);
)

(
var sig = Signal.fill(512, {1.0.rand});
m.valueAction_(sig.abs);
)
1 Like

Thanks a lot, exactly that!

What was wrong in the first code is that I have not realized that SC Panners do not allow multichannel expansion, right? (pity this would be really handy). I should be like:

(
{
    var in, chain, buf = b, sig;
    in = PlayBuf.ar(1, buf, BufRateScale.kr(buf), loop: 1);
    chain = FFT(LocalBuf(256), in)!100;

	chain.do({arg item,i;
    chain[i] = chain[i].pvcalc(256, {|mags, phases|
        [mags , phases ]; 
    }, frombin: i, tobin: i, zeroothers: 1);
	});

	sig = IFFT(chain)*0.5;

	sig = sig.sum {
		|chan, i|
		PanAz.ar(2, chan, {rrand(-1.0,1.0)},width: 2.0); // stereo version
		//PanAz.ar(13, chan, {rrand(-1.0,1.0)},width: 2.0); // 13 channels version
	};
	Out.ar(0, sig)
}.play;
)

This is not correct. FFTs are not signals. They are buffers that store an analysis of a signal. I think this is your primary misconception.

What I did was I took the FFT of a mono signal, copied the analysis buffer into two buffers, and then controlled the magnitude of each bin in the two buffers, effectively panning each bin left or right. So, bin 30 has a magnitude of 1 (mags are not between 0 and 1, I am just using this to simplify) in fft buffer 1 and a mag of 0 in fft buffer 2, so it pans to the right, etc for every bin.

What you are doing is trying to duplicate the FFT, which shouldn’t do anything. What I think it might be doing is performing the chain operation on the same buffer 100 times.

I think you need to go back and reread the FFT Overview page and go through the help. FFTs are confusing. They operate on their own logic, separate from the rest of SC, so you can’t treat them like signals.

Sam

edit: changed volume to magnitude