MScottWaller
MScottWaller

Reputation: 3583

Why is FFT different in Swift than in Python?

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

Answers (2)

lentil
lentil

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]


vDSP_fft_zip

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.


vDSP_fft_zrip

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.


see: vDSP Programming Guide

  • Data Packing for Real FFTs
  • Scaling Fourier Transforms

Upvotes: 6

OOPer
OOPer

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

Related Questions