fftea
一个简单高效的FFT实现。
使用Cooley-Tukey算法支持实数或复数幂次为2的数组的FFT。还包括一些相关实用程序,如窗口函数、STFT和逆FFT。
之所以构建此包,是因为package:fft不再积极维护,并且实现效率不高。有几项改进使此实现更有效。
- 主FFT类使用给定的大小进行构造,这样只需计算一次扭曲因子。这对于STFT特别有用。
- 不使用复数的包装器类,仅使用Float64x2List。每个小型包装器类都是单独分配和解引用的。对于FFT复数数学等内部循环代码,这会产生很大差异。Float64x2还可以利用SIMD优化。
- FFT算法是就地执行的,因此不会分配额外的数组。
- 基于循环的FFT,而不是递归。
- 使用三角函数技巧,只计算四分之一的扭曲因子。
用法
运行基本的实值FFT
// myData.length must be a power of two. Eg 2, 4, 8, 16, ...
List<double> myData = ...;
final fft = FFT(myData.length);
final freq = fft.realFft(myData);
freq是一个Float64x2List,表示复数列表。有关Float64x2List的有用扩展方法,请参阅ComplexArray。
为了效率,请避免每次都重新创建FFT对象。而是创建一个所需大小的FFT对象,并重复使用它。
运行STFT计算频谱图
// audio.length *doesn't* need to be a power of two. It can be any legnth.
List<double> audio = ...;
final chunkSize = 1024; // Must be a power of two.
final stft = STFT(chunkSize, Window.hanning(chunkSize));
final spectrogram = <Float64List>[];
stft.run(audio, (Float64x2List freq) {
spectrogram.add(freq.discardConjugates().magnitudes());
});
实值FFT的结果大约有一半是冗余数据,因此discardConjugates会从结果中删除这些数据。
[和项,...项...,奈奎斯特项,...共轭项...] ^—— 这些项被保留 ——^ ^- 已丢弃 -^
然后magnitudes会丢弃复数的相位数据,只保留幅度,这通常是进行频谱图分析时所需的。
如果你想知道频谱图中某个元素的频率,请使用stft.frequency(index, samplingFrequency),其中samplingFrequency是audio录制时的频率,例如44100。频谱图的最大频率(也称为奈奎斯特频率)将是stft.frequency(chunkSize / 2, samplingFrequency)。
有关更详细的用法,请参阅示例。
基准测试
我发现了一些其他有前景的FFT实现,因此决定也对它们进行基准测试:scidart和smart_signal_processing。
运行基准测试
- 转到bench目录并运行pub get,
cd bench && dart pub get - 运行
dart run bench.dart
fftea通过缓存两次运行之间的扭曲因子以及就地执行FFT来获得其部分速度。因此,基准测试被分为缓存和就地变体。在适当的情况下,缓存和就地执行也应用于其他一些库。
在下表中,“cached”表示FFT对象的构造不计入基准测试时间。而“in-place”表示使用就地FFT,并且将输入数据转换为FFT所需格式的转换和复制时间不计入基准测试。在Ubuntu上的Dell XPS 13上运行。
| Size | package:fft | smart | smart,就地 | scidart | scidart,就地* | fftea | fftea,缓存 | fftea,就地,缓存 |
|---|---|---|---|---|---|---|---|---|
| 16 | 410.6 微秒 | 28.3 微秒 | 8.3 微秒 | 347.2 微秒 | 164.0 微秒 | 42.2 微秒 | 11.6 微秒 | 10.9 微秒 |
| 64 | 416.4 微秒 | 8.2 微秒 | 2.7 微秒 | 169.0 微秒 | 194.0 微秒 | 73.5 微秒 | 41.6 微秒 | 41.8 微秒 |
| 256 | 470.6 微秒 | 14.0 微秒 | 11.0 微秒 | 904.9 微秒 | 812.8 微秒 | 34.1 微秒 | 8.4 微秒 | 6.1 微秒 |
| 2^10 | 1.54 毫秒 | 47.2 微秒 | 44.1 微秒 | 8.28 毫秒 | 8.26 毫秒 | 36.3 微秒 | 30.5 微秒 | 27.3 微秒 |
| 2^12 | 7.65 毫秒 | 206.1 微秒 | 197.3 微秒 | 132.99 毫秒 | 121.27 毫秒 | 159.2 微秒 | 143.9 微秒 | 129.3 微秒 |
| 2^14 | 39.83 毫秒 | 940.1 微秒 | 899.4 微秒 | 1.96 秒 | 1.96 秒 | 750.1 微秒 | 695.5 微秒 | 590.2 微秒 |
| 2^16 | 261.38 毫秒 | 6.04 毫秒 | 5.62 毫秒 | 41.44 秒 | 41.60 秒 | 4.71 毫秒 | 5.89 毫秒 | 3.56 毫秒 |
| 2^18 | 1.31 秒 | 27.65 毫秒 | 27.84 毫秒 | 已跳过 | 已跳过 | 20.80 毫秒 | 24.92 毫秒 | 15.66 毫秒 |
| 2^20 | 7.29 秒 | 168.84 毫秒 | 151.12 毫秒 | 已跳过 | 已跳过 | 119.93 毫秒 | 106.00 毫秒 | 88.26 毫秒 |
实际上,您通常会提前知道FFT的大小,因此可以轻松地一次性构造FFT对象,以利用缓存。有时也可以利用就地加速,例如,如果您必须从其他源复制数据,那么您最好自己构造扁平的复数数组。由于并非总是可能这样,因此“fftea, cached”时间可能最具代表性。在这种情况下,fftea比package:fft快约60-80倍,比smart_signal_processing快约30%。不确定scidart出了什么问题,但它似乎是O(n^2)。
* Scidart的FFT没有就地模式,但它们确实使用了自定义格式,因此就地表示转换为该格式的时间不包含在基准测试中。