在 SuperCollider 中设置 FFT 链的各个 bin

Set individual bins of FFT chain in SuperCollider

我正在从事图像到声音的项目,并试图在 SuperCollider 中实现加法合成。我想使用逆 DFFT 对(数百个)正弦波求和,而不是为每个正弦波创建一个 SinOsc 合成器。

所有 SuperCollider 文档都说 IFFT 消耗由 FFT 生成的称为 "FFT chain" 的东西(并由 PV_* 函数转换):

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

但对于我的应用程序,我不需要 FFT 阶段,因为我已经知道我的信号在频域中的表示方式。我想要的是:

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

"frequency-domain signal" 是一个 numpy 数组序列,表示我在 Python 应用程序中已有的频域信号。所以,我需要将这些信息传递给 SuperCollider。

据我了解FFT链表示某种数据流,但我不明白如何手动将数据写入其中。

我也尝试过使用静默 FFT 链(例如,获取 Silence.arFTT),但我也不知道如何手动设置各个频率仓。

这里有几个选项。

  1. 使用 PackFFT Ugen。这使您可以使用任意 UGen 数组来表示幅度和相位。这是一个示例,希望比帮助文件中的示例更清楚您的目的:

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

    注意这里FFT缓冲区的大小是频段数的两倍。我相信这是正确的,但不是 100% 肯定。

  2. 使用PV_ChainUGen.pvcollect.pvcalc方法。 (有关示例代码,请参阅帮助文件。)理论上,您可以使用 Array ref 作为 SynthDef 参数,并使用它来任意设置动态 运行 合成器的幅度和相位。在实践中,我发现这是一种脆弱的方法:SC 对 FFT 块大小很挑剔(它们受音频设备块大小的限制);非常大的 SynthDef 是有问题的;无论如何,语法最终变得非常糟糕。

  3. 我实际上不会打折直接使用正弦波,特别是 FSinOsc UGen,它使用非常有效的正弦近似,或者 DynKlang,它采用数组参考。

    这是一个包含 1000 个 FSinOsc 实例的示例,发出安静的隆隆声;它目前在我的 i5 Macbook 上使用 22% CPU(这包括任意平移每个振荡器):

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

当然,选项 1 的效率要高得多 - 看起来效率大约提高了 10 倍。但是为了简单起见,您无法击败选项 3。