sound-synthesissupercollider

Set individual bins of FFT chain in SuperCollider


I am working on image-to-sound project and trying to implement additive syntheses in SuperCollider. I want to use inverse DFFT to sum over (hundreds of) sine waves instead of creating a SinOsc synth for each of them.

All SuperCollider documentation says that IFFT consumes something called "FFT chain" produced by FFT (and transformed by PV_* functions):

Time-domain signal -> FFT -> [PV_* -> PV_* -> ...] -> IFFT

But for my application I don't need FFT stage since I already know how my signal is represented in frequency domain. What I want instead is:

Frequency-domain signal -> Manually constructed FFT chain -> IFFT

The "frequency-domain signal" is a sequence of numpy arrays representing a signal in frequency domain which I already have in my Python application. So, I need to pass this information to SuperCollider.

From what I understand FFT chain means some kind of data stream but I don't understand how to manually write data into it.

I've also tried to play with silent FFT chain (e.g. get FTT of Silence.ar) but I have no clue how to manually set individual frequency bins either.


Solution

  • Here are a few options.

    1. use the PackFFT Ugen. This lets you use an arbitrary array of UGens for magnitude and phase. Here's an example, hopefully clearer for your purpose than the one in the helpfile:

      s = Server.local;
      s.waitForBoot {
      
      Routine {    
          n = 512;
      
          // massively multichannel control busses
          ~magBus = Bus.control(s, n);
          ~phaseBus = Bus.control(s, n);
      
          s.sync;
      
          ~synth = { var mags, phases, chain, snd;
              mags = n.collect ({ |i| In.kr(~magBus.index + i) });
              phases = n.collect ({ |i| In.kr(~phaseBus.index + i) });
              chain = FFT(LocalBuf(n*2), Silent.ar);
              chain = PackFFT(chain, n, [mags, phases].flop.flatten);
              Out.ar(0, IFFT(chain).dup);
          }.play(s);
      
          s.sync;
      
          // raise each bin magnitude in a random order.
          // eventually results in wide-band noise, so watch your ears...
          Array.series(n).scramble.do({ arg i;
              i.postln;
              ~magBus.setAt(i, -16.dbamp.rand);
              (0.01 + 0.2.rand).wait;
          });    
      }.play;    
      };
      

      Note that the size of the FFT buffer here is twice the number of frequency bands. I believe this is correct but not 100% sure.

    2. use the .pvcollect and .pvcalc methods of PV_ChainUGen. (See the helpfile for example code.) In theory, you can use an Array ref as a SynthDef argument and use that to arbitrarily set magnitudes and phases of a running synth on the fly. In practice I've found this to be a fragile approach: SC is picky about FFT block sizes (they are limited by audio device block size); very large SynthDefs are problematic; and anyway the syntax ends up being pretty horrible.

    3. I actually wouldn't discount just using sine waves directly, specifically the FSinOsc UGen, which uses a very efficient sine approximation, or DynKlang, which takes an Array reference.

      Here's an example with 1000 instances of FSinOsc, making a quiet rumbling noise; it is currently using 22% CPU on my i5 Macbook (and this includes arbitrarily panning each oscillator):

      s = Server.local;
      s.waitForBoot { 
      
      n = 1000;
      
      ~freq = Array.rand(n, 20.0, 60.0).midicps;
      ~amp = Array.rand(n, 1/n * 0.01, 1/n * 0.5);
      ~pan= Array.rand(n, -1.0, 1.0);
      
      ~sines = Array.fill(n, { arg i; 
          { Pan2.ar( FSinOsc.ar(~freq[i], 0, ~amp[i]), ~pan[i]) }.play;
      });
      
      };
      

    Of course option 1 is much, much more efficient - looks like by about a factor of 10. But you can't beat option 3 for simplicity.