Reputation: 3583
I'm trying to port some python numpy code to Swift using the Accelerate framework.
In python I write
import numpy as np
frames = np.array([1.0, 2.0, 3.0, 4.0])
fftArray = np.fft.fft(frames, len(frames))
print(fftArray)
And the output is:
[10.+0.j -2.+2.j -2.+0.j -2.-2.j]
So in Swift I'm trying to calculate the FFT like this:
import Foundation
import Accelerate
func fftAnalyzer(frameOfSamples: [Float]) {
// As above, frameOfSamples = [1.0, 2.0, 3.0, 4.0]
let analysisBuffer = frameOfSamples
let frameCount = frameOfSamples.count
var reals = [Float]()
var imags = [Float]()
for (idx, element) in analysisBuffer.enumerated() {
if idx % 2 == 0 {
reals.append(element)
} else {
imags.append(element)
}
}
var complexBuffer = DSPSplitComplex(realp: UnsafeMutablePointer(mutating: reals), imagp: UnsafeMutablePointer(mutating: imags))
let log2Size = Int(log2f(Float(frameCount)))
guard let fftSetup = vDSP_create_fftsetup(vDSP_Length(log2Size), Int32(kFFTRadix2)) else {
return []
}
// Perform a forward FFT
vDSP_fft_zrip(fftSetup, &(complexBuffer), 1, UInt(log2Size), Int32(FFT_FORWARD))
let realFloats = Array(UnsafeBufferPointer(start: complexBuffer.realp, count: Int(frameCount)))
let imaginaryFloats = Array(UnsafeBufferPointer(start: complexBuffer.imagp, count: Int(frameCount)))
print(realFloats)
print(imaginaryFloats)
// Release the setup
vDSP_destroy_fftsetup(fftSetup)
return realFloats
}
The realFloats and imaginaryFloats are printed like so:
[20.0, -4.0, 0.0, 0.0]
[-4.0, 4.0, 0.0, 0.0]
Any ideas on what I should be doing differently?
Upvotes: 1
Views: 1660
Reputation: 694
There's a bit of confusion around the difference between vDSP_fft_zip and vDSP_fft_zrip. The most immediate impact in your case is that the zrip output is packed, and needs to be scaled by 1/2 to equal the normal math output of a standard FFT.
This is buried somewhat in the Apple documentation: rather than appearing on the page for vDSP_fft_zrip, it's in the vDSP Programming Guide. However, it's still not clear from the guide how the input data is prepared in each case - and OOPer's answer is a big help with this.
import Foundation
import Accelerate
var input: [Float] = [1.0, 2.0, 3.0, 4.0]
let length = input.count
let log2n = log2(Float(length))
let fftSetup = vDSP_create_fftsetup(vDSP_Length(log2n), FFTRadix(kFFTRadix2))!
print("--vDSP_fft_zip---")
var real = input
var imag = [Float](repeating: 0.0, count: length)
var splitComplexBuffer = DSPSplitComplex(realp: &real, imagp: &imag)
vDSP_fft_zip(fftSetup, &splitComplexBuffer, 1, vDSP_Length(log2n), FFTDirection(FFT_FORWARD))
print("real: \(real)")
print("imag: \(imag)")
print("-----------------")
print("DC : real[0]")
print("Nyquist : real[2]")
print()
print()
print("--vDSP_fft_zrip--")
// only need half as many elements because output will be packed
let halfLength = length/2
real = [Float](repeating: 0.0, count: halfLength)
imag = [Float](repeating: 0.0, count: halfLength)
// input is alternated across the real and imaginary arrays of the DSPSplitComplex structure
splitComplexBuffer = DSPSplitComplex(fromInputArray: input, realParts: &real, imaginaryParts: &imag)
// even though there are 2 real and 2 imaginary output elements, we still need to ask the fft to process 4 input samples
vDSP_fft_zrip(fftSetup, &splitComplexBuffer, 1, vDSP_Length(log2n), FFTDirection(FFT_FORWARD))
// zrip results are 2x the standard FFT and need to be scaled
var scaleFactor = Float(1.0/2.0)
vDSP_vsmul(splitComplexBuffer.realp, 1, &scaleFactor, splitComplexBuffer.realp, 1, vDSP_Length(halfLength))
vDSP_vsmul(splitComplexBuffer.imagp, 1, &scaleFactor, splitComplexBuffer.imagp, 1, vDSP_Length(halfLength))
print("real: \(real)")
print("imag: \(imag)")
print("-----------------")
print("DC : real[0]")
print("Nyquist : imag[0]")
vDSP_destroy_fftsetup(fftSetup)
printed:
--vDSP_fft_zip---
real: [10.0, -2.0, -2.0, -2.0]
imag: [0.0, 2.0, 0.0, -2.0]
-----------------
DC : real[0]
Nyquist : real[2]
--vDSP_fft_zrip--
real: [10.0, -2.0]
imag: [-2.0, 2.0]
-----------------
DC : real[0]
Nyquist : imag[0]
Input is placed in the real array of the DSPSplitComplex structure and the imaginary array is zeroed out.
Output is complex and requires double the memory of the input. However, much of this output is symmetrical - which is how the packed output of zrip is able to represent the same information in half the memory.
Input is spread across DSPSplitComplex with DSPSplitComplex.init(fromInputArray: ) or by using vDSP_ctoz.
The fromInputArray: method does the same thing as ctoz, but it's an easier/safer approach - don't have to use UnsafeRawPointer and bindMemory.
Output is packed complex. When packed, output requires the same amount of memory as the input.
Scaling: results are 2x relative to standard Math FFT and need to be scaled as such.
Upvotes: 6
Reputation: 47896
I'm not good at numpy, but according to the doc, fft
takes complex-input. Then its equivalent would be vDSP_fft_zip
, not vDSP_fft_zrip
.
And your code causes buffer overflow or might cause dangling pointer, with all such things fixed I get this:
func fftAnalyzer(frameOfSamples: [Float]) -> [Float] {
// As above, frameOfSamples = [1.0, 2.0, 3.0, 4.0]
let frameCount = frameOfSamples.count
let reals = UnsafeMutableBufferPointer<Float>.allocate(capacity: frameCount)
defer {reals.deallocate()}
let imags = UnsafeMutableBufferPointer<Float>.allocate(capacity: frameCount)
defer {imags.deallocate()}
_ = reals.initialize(from: frameOfSamples)
imags.initialize(repeating: 0.0)
var complexBuffer = DSPSplitComplex(realp: reals.baseAddress!, imagp: imags.baseAddress!)
let log2Size = Int(log2(Float(frameCount)))
print(log2Size)
guard let fftSetup = vDSP_create_fftsetup(vDSP_Length(log2Size), FFTRadix(kFFTRadix2)) else {
return []
}
defer {vDSP_destroy_fftsetup(fftSetup)}
// Perform a forward FFT
vDSP_fft_zip(fftSetup, &complexBuffer, 1, vDSP_Length(log2Size), FFTDirection(FFT_FORWARD))
let realFloats = Array(reals)
let imaginaryFloats = Array(imags)
print(realFloats)
print(imaginaryFloats)
return realFloats
}
Printed
[10.0, -2.0, -2.0, -2.0] [0.0, 2.0, 0.0, -2.0]
Upvotes: 6