Comments (28)
DSP guy here. I've managed to match Surge's FFT and numpy's FFT. They are not equivalent as it stands. Here are the differences:
- Surge's FFT actually calculates the power spectrum with
vDSP_zvmags
and then reverses that operation later on by taking the square root and scaling. If you remove the subsequent line of code withvDSP_vsmul(sqrt(magnitudes), 1, [2.0 / Float(input.count)]
then you get the power spectrum. - Numpy has no normalization. In other libraries, sometimes I have seen a scaling coefficient on the magnitudes, sometimes not. Surge's line
vDSP_vsmul(sqrt(magnitudes), 1, [2.0 / Float(input.count)]
scales the magnitudes by 2/N where N is the window size. - Surge's FFT doesn't handle non powers of 2 for window size. It's probable that the person commenting above didn't choose a power of 2 and that's why the frequency response graph looks of similar shape but scaled and translated.
- Surge's FFT doesn't have a windowing function at all. Common choices are hamming or hann windows. Very rarely are FFTs done with a boxcar (rectangular) window.
- Surge's FFT returns the full magnitude array which is unnecessary for real valued signals since it's mirrored about the nyquist rate.
I rewrote the Surge FFT code to window with a hamming window and return the power spectrum (no scaling) to match the librosa one-liner: S = np.abs(librosa.core.stft(x, n_fft=w, hop_length=w, window=signal.hamming, center=False)) ** 2
. Under the hood, librosa uses Numpy's FFT.
I tested on 1 frame of the same audio wav file within Python and on iOS using surge and got basically the same results (except for some floating point precision differences).
public func powerSpectrum(_ input: [Float]) -> [Float] {
// apply hamming window
// window has slightly different precision than scipy.window.hamming (float vs double)
var win = [Float](repeating: 0, count: input.count)
vDSP_hamm_window(&win, vDSP_Length(input.count), 0)
let input_windowed = mul(input, win)
var real = [Float](input_windowed)
var imaginary = [Float](repeating: 0.0, count: input_windowed.count)
var splitComplex = DSPSplitComplex(realp: &real, imagp: &imaginary)
let length = vDSP_Length(floor(log2(Float(input_windowed.count))))
let radix = FFTRadix(kFFTRadix2)
let weights = vDSP_create_fftsetup(length, radix)
withUnsafeMutablePointer(to: &splitComplex) { splitComplex in
vDSP_fft_zip(weights!, splitComplex, 1, length, FFTDirection(FFT_FORWARD))
}
// zvmags yields power spectrum: |S|^2
var magnitudes = [Float](repeating: 0.0, count: input_windowed.count)
withUnsafePointer(to: &splitComplex) { splitComplex in
magnitudes.withUnsafeMutableBufferPointer { magnitudes in
vDSP_zvmags(splitComplex, 1, magnitudes.baseAddress!, 1, vDSP_Length(input_windowed.count))
}
}
vDSP_destroy_fftsetup(weights)
return magnitudes
}
Not sure what the actionable items are for the Surge maintainer(s) to patch up the FFT function. It's not necessarily wrong, I think it just needs some more parameters so people can match output from other libraries. Possibly adding a window parameter to the Surge FFT function as well as an option to specify whether to scale the result. In my opinion letting people scale the result outside of this function is probably best.
from surge.
would you say our current implementation is wrong and should be fixed, or is it simply a different way of using FFT and we might want to provide a choice to the user?
I would say the Surge FFT algorithm returns a scaled amplitude spectrum (something like np.abs(S)
where you take the absolute value of the complex components in the FFT) so I wouldn't really call it an FFT, which typically returns the complex components e.g., scipy rfft. I would say your standard Surge user doesn't really care because most people in audio end up wanting the amplitude spectrum anyways so they don't have to deal with complex numbers.
My personal recommendation would be to try to match output of heavy-use libraries like numpy, scipy for compatibility between experimental systems (done off device, e.g., machine learning experimentation) and production systems (e.g., prediction on device).
Specific Recommendations:
- I would remove the scaling of 2/N. People scale FFTs in different ways and nobody likes looking through source code to see how something is scaled to reverse the operation and add their own scaling. Removing scaling would also match NumPy. Keep the sqrt though, so you don't have the power spectrum.
- Allowing the user to specify a window function (hamming, hann, etc.) is critical and this is something that Surge lacks. Nobody runs FFTs with a rectangular window due to bad spectral leakage. In fact, by default a lot of other FFT libraries use a hamming window by default and therefore default output in Surge can be quite different.
- Enforce window size power of 2 or snap to nearest power of 2: all FFT libraries I've encountered require the input window size size or number of points in the FFT to be a power of 2. DFTs usually allow non powers of 2, but with FFTs this is usually a requirement due to the butterfly algorithm (divide and conquer strategy). This is the issue the OP ran into without knowing why it was happening.
from surge.
Hi @abokhalel2, my example is just for the FFT on a single window, not an STFT. To get the STFT you would sliding window the samples and send it to the function above. Typically both the window and hop size are powers of 2 with the hop being some division of the window size. Hope that helps.
from surge.
@vade I see you're contributing to an OpenAI whisper CoreML project. I like that.
Find my email online and I'll share my Accelerate vDSP Mel Spectrogram code written in swift privately. It took a while to make it match librosa output. Not sure if it will match Torches STFT code but might give you another reference point. Also, from what I remember, it's really fast.
from surge.
Hi @brett-eugenelabs, it's element-wise multiplication: same function as vDSP_vmul
in Accelerate framework or Surge.elmul
in Surge. Hope that helps π
from surge.
Looks like they are using different scales. How are you computing the x-axis? Maybe try removing pow
. Can you share the contents of signal
?
from surge.
Yeah. Here is the dummy data used to create the above signals/graphs above.
Column 1, "RightLegAngleSignal", is what I used to construct the periodogram in Swift as well as numpy/python.
The last column, "pgram", is the periodogram which Surge outputted (First graph above).
If you run the python code on Column 1, "RightLegAngleSignal", you will get the second graph.
Thanks so much
from surge.
I see there are a ton of nan
s, did you try filtering those out?
from surge.
Yes. In both Swift and Python, I filtered them out.
from surge.
I get a different result, are you on the latest version?
from surge.
I'm on Surge 2.0.0. Are both your Python and Swift periodograms matching?
from surge.
Try 2.2.0
from surge.
Thanks I'll try that right now
from surge.
Ok I just ran the app again (using Surge 2.2.0) and generated a new CSV, Swift Periodogram, and Numpy Periodogram.
Data attached below:
They are still different (but have some similarities in shape). Do you think this has something to do with the symmetry of the FFT around the center point? Also it also looks like the scales are different, do you know why that is?
Thanks so much
all-signals.csv.zip
from surge.
Hey thanks for writing that. What does the mul(input,win) do? does it multiply the two arrays? hows does that work?
from surge.
Element-wise multiplication
from surge.
I'm personally not familiar enough with the FFT, so β¦
@gburlet would you consider your modified implementation a bug fix or an alternative to our current one? Or put differently: would you say our current implementation is wrong and should be fixed, or is it simply a different way of using FFT and we might want to provide a choice to the user?
Either way I'd be more than happy to merge a PR addressing the issue appropriately. π
from surge.
This is great, thanks a lot @gburlet!
from surge.
@gburlet Do you have the source for the mul() in the following line?
let input_windowed = mul(input, win)
from surge.
@gburlet is there a possibility in your example to make hop_length
and window_size
different numbers? I'm trying to find a way to make them match the results of torch.stft or librosa.core.stft when hop_length
and window_size
being different numbers.
This is my torch.stft example in case needed.
import torch
import torchaudio
win_length = 4096
hop_length = 1024
win = torch.hann_window(win_length)
wav, sr = torchaudio.load('audio_example.wav')
stft = torch.stft(wav, win_length, hop_length=hop_length, window=win)
from surge.
I have a perhaps naive question for @gburlet - (im not a DSP guy at all so forgive me if this is a newbie q) - why do you fill only the real components of of the DSP Split Complex with the source audio frame?
Looking at Apples Mel sample code on the vDSP page, they bind the incoming time domain audio signal via vDSP_ctoz
into a split complex, filling both the real and imaginary components.
With their implementation, for a FFT size of 400, you'd get 200 complex samples back, or 200 |S|^2 magnitude values.
With your implementation, you'd get 400 magnitude values back? Am I understanding that correctly?
And if so, what are the differences in the FFT output when populating the Split Complex with only real components, vs running vDSP_ctoz
?
Thank you in advance. This thread has been INCREDIBLY helpful in trying to match Torches STFT code to produce a Log Mel Spectrogram. Im not there yet, but im getting closest thanks to this thread.
from surge.
@vade I unfortunately don't have time / brain bandwidth to jump into this in more detail but see above, this point specifically, which may help you understand:
Surge's FFT returns the full magnitude array which is unnecessary for real valued signals since it's mirrored about the nyquist rate.
from surge.
Hey @gburlet - no worries, Your input here has been super helpful. I appreciate the prompt reply! thank you!
from surge.
Much obliged! For real!
from surge.
@gburlet I'm trying to use your powerSpectrum()
code above and I'm getting the error:
No exact matches in call to global function 'mul'
At the line:
let input_windowed = mul(input, win)
Am I missing something? What kind of multiplication is this element wise, or ...? Sorry if this question seems obtuse.
from surge.
@annoybot see above, it has been answered
from surge.
Ah yes, indeed it has been answered, thanks. π¬
from surge.
@gburlet, I hope the following question is not too off topic for this thread.
Do you think there would be an advantage in using Welch's method over a plain FFT when processing sound data from an instrument?
I was wondering if it would help eliminate noise?
from surge.
Related Issues (20)
- Move Project to Separate Organization HOT 5
- How to read the results of "eigenDecompose" HOT 2
- Efficient non-allocating column/row-based Matrix iterators
- Efficient randomized initializers for Array/Vector/Matrix
- Auto-generate unit tests & benchmarks
- Migrate from `Float`/`Double` to `T: FloatingPoint` HOT 6
- Migrate API to a more idiomatic naming scheme HOT 1
- 3.0 Roadmap HOT 4
- feature request more numpy like dot
- is there some missing matrix functionality? HOT 1
- Warning: "Inout expression creates a temporary pointer, but argument 'imagp' should be a pointer that outlives the call to 'init(realp:imagp:)'" HOT 3
- missing power operator HOT 1
- Add documentation comments to public declarations
- Availability on Apple Watch? HOT 4
- randomNormal not documented HOT 1
- Not working on Watch app HOT 3
- some comfuse HOT 2
- Project Dead? HOT 5
- test_sqrt_in_place_array_float() fails on M1 Pro
- εΊ
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. πππ
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google β€οΈ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from surge.