Apple FFT дает противоречивые результаты - PullRequest
3 голосов
/ 11 октября 2019

Я реализовал в алгоритме FFT, используя родные классы Apple. Я взял код прямо с их сайта здесь:

https://developer.apple.com/documentation/accelerate/vdsp/fast_fourier_transforms/finding_the_component_frequencies_in_a_composite_sine_wave

Тем не менее, когда я запускаю код, он каждый раз дает разные результаты. Я создал модульный тест, который запускает его несколько раз и сравнивает, если результаты одинаковы, в модульном тесте не удаетсяМое единственное предположение, что это проблема с памятью. Только так я могу представить, что результаты могут отличаться каждый раз.

import Foundation
import Accelerate

class AppleFFT{
    var windowSize = 512
    var n = vDSP_Length(512)
    var halfN = Int(512 / 2)
    var fftSetUp : FFTSetup?
    var log2n : vDSP_Length?
    init(windowSize: Int){
        self.windowSize = windowSize 
        n = vDSP_Length(windowSize)
        halfN = Int(n / 2)
        initialize()
    }
    private init(){
        initialize()
    }
    func initialize(){
        log2n = vDSP_Length(log2(Float(n)))
        if log2n == nil { return }
        fftSetUp = vDSP_create_fftsetup(log2n!, FFTRadix(kFFTRadix2))

    }
    func process(signal : [Float], n: vDSP_Length) ->DSPSplitComplex{
        let window = vDSP.window(ofType: Float.self,
                                 usingSequence: .hanningDenormalized,
                                 count: Int(n), 
                                 isHalfWindow: false)

        let signal2 = vDSP.multiply(signal, window)
        let observed: [DSPComplex] = stride(from: 0, to: Int(n), by: 2).map {
            return DSPComplex(real: signal[$0],
                              imag: signal[$0.advanced(by: 1)])
        }

        var forwardInputReal = [Float](repeating: 0, count: halfN)
        var forwardInputImag = [Float](repeating: 0, count: halfN)

        var forwardInput = DSPSplitComplex(realp: &forwardInputReal,
                                           imagp: &forwardInputImag)

        vDSP_ctoz(observed, 2,
                  &forwardInput, 1,
                  vDSP_Length(halfN))

        //Create some empty arrays we can put data into
        var forwardOutputReal = [Float](repeating: 0, count: halfN)
        var forwardOutputImag = [Float](repeating: 0, count: halfN)
        var forwardOutput = DSPSplitComplex(realp: &forwardOutputReal,
                                            imagp: &forwardOutputImag)

        //Perform actual fft, placing results in forwardOutput
        vDSP_fft_zrop(fftSetUp!,
                      &forwardInput, 1,
                      &forwardOutput, 1,
                      log2n!,
                      FFTDirection(kFFTDirection_Forward))

        //Do cheap analysis to figure out original frequencies
        let componentFrequencies = forwardOutputImag.enumerated().filter {
            $0.element < -1
        }.map {
            return $0.offset
        }
        return forwardOutput
    }
}

import XCTest
import Accelerate

class testAppleFFT: XCTestCase {

    func testFFTConsistency(){
        let signal = genSignalWith(frequencies:[100, 500], numSamples: 512, sampleRate: 44100)
        let fft = AppleFFT(windowSize: 512)
        let complex1 = fft.process(signal: signal , n: 512)
        for i in 0..<10{
            print("i = \(i)")
            let complex2 = fft.process(signal: signal, n: 512)
            var complex1realp = complex1.realp
            var complex1imagp = complex1.imagp
            var complex2realp = complex2.realp
            var complex2imagp = complex2.imagp
            for j in 0..<512 {
                let r1 = complex1realp.pointee
                let i1 = complex1imagp.pointee
                let r2 = complex2realp.pointee
                let i2 = complex2imagp.pointee
                XCTAssert(abs(r1 - r2) < 0.00001)
                XCTAssert(abs(i1 - i2) < 0.00001)
                if !(abs(r1 - r2) < 0.00001){
                    print(" error: i: \(i) j: \(j) r1: \(r1) r2: \(r2)")
                }
                if !(abs(i1 - i2) < 0.00001){
                    print(" error: index: \(i) i1: \(i1) i2: \(i2)")
                }
                complex1realp = complex1realp.advanced(by: 1)
                complex1imagp = complex1imagp.advanced(by: 1)
                complex2realp = complex2realp.advanced(by: 1)
                complex2imagp = complex2imagp.advanced(by: 1)

            }    
        }
    }
    func genSignalWith(frequencies: [Float], numSamples: Int, sampleRate: Float, amplitudes: [Float] = []) -> [Float]{
        var sig : [Float] = []
        for t in 0..<numSamples{
            var sum : Float = 0.0
            for i in 0..<frequencies.count{
                let f = frequencies[i]
                var a : Float = 1.0
                if(amplitudes.count > i){
                     a = amplitudes[i] 
                }
                let thisValue = sin(Float(t) / sampleRate * 2 * .pi * f)
                sum += thisValue
            }
            sig.append(sum)
        }
        return sig
    }
}

1 Ответ

13 голосов
/ 23 октября 2019

Это:

var forwardInput = DSPSplitComplex(realp: &forwardInputReal,
                                   imagp: &forwardInputImag)
vDSP_ctoz(observed, 2, &forwardInput, 1, vDSP_Length(halfN))

делает не то, что вы хотите. Проблема с этим немного тонкая, особенно если вы пришли из C или C ++ фона. Массивы в swift не похожи на массивы в C или C ++;в частности, они не имеют фиксированных адресов в памяти. Это объекты, которые Свифт может выбрать для перемещения. Это хорошо, когда вы работаете в Swift, но иногда вызывает боль, когда вам нужно взаимодействовать с функциями C (и особенно с типами C, которые хотят сохранять указатели на вызовы функций, как вы заметили).

Когда вы вызываете DSPSplitComplex(realp: &forwardInputReal, ...), & неявно создает UnsafeMutablePointer<Float> в памяти forwardInputReal, но , этот указатель действителен только во время вызова init. Когда вы передаете forwardInput в vDSP_ctoz, указатель уже вышел из области видимости и больше не действителен, поэтому вы вызываете неопределенное поведение. В частности, компилятор может предположить, что вызов vDSP_ctoz не изменяет содержимое forwardInputReal или forwardInputImag, поскольку функция не получает действительный указатель на их содержимое.

ЛучшийЧтобы обойти это, нужно быть более явным:

forwardInputReal.withUnsafeMutableBufferPointer { r in
  forwardInputImag.withUnsafeMutableBufferPointer { i in
    var splitComplex = DSPSplitComplex(realp: r.baseAddress!, imagp: i.baseAddress!)
     vDSP_ctoz(observed, 2, &splitComplex, 1, vDSP_Length(halfN))
  }
}
// forwardInput[Real,Imag] now contain the de-interleaved data.
// splitComplex is out-of-scope and cannot be used, so the invalid pointers
// are discarded.

Есть несколько вещей, которые могли бы сделать это проще.

Во-первых, в компилятор Swift приходит изменение , который будет диагностировать эту ошибку для вас.

Во-вторых, мы можем обернуть маленький танец, который я показал, в некоторые вспомогательные функции:

/// De-interleave the real and imaginary parts of a complex buffer into two
/// new Float arrays.
func ctoz<T>(_ data: T) -> (real: [Float], imag: [Float])
where T: AccelerateBuffer, T.Element == DSPComplex {
  var imag = [Float]()
  let real = [Float](unsafeUninitializedCapacity: data.count) { r, n in
    imag = [Float](unsafeUninitializedCapacity: data.count) { i, n in
      ctoz(data, real: &r, imag: &i)
      n = data.count
    }
    n = data.count
  }
  return (real, imag)
}

/// De-interleave the real and imaginary parts of a complex buffer into two
/// caller-provided Float buffers.
///
/// - Precondition: data, real, and imag must all have the same length.
func ctoz<T, U, V>(_ data: T, real: inout U, imag: inout V)
where T: AccelerateBuffer, T.Element == DSPComplex,
      U: AccelerateMutableBuffer, U.Element == Float,
      V: AccelerateMutableBuffer, V.Element == Float
{
  precondition(data.count == real.count && data.count == imag.count)
  real.withUnsafeMutableBufferPointer { r in
    imag.withUnsafeMutableBufferPointer { i in
      var split = DSPSplitComplex(realp: r.baseAddress!, imagp: i.baseAddress!)
      data.withUnsafeBufferPointer { d in
        vDSP_ctoz(d.baseAddress!, 2, &split, 1, vDSP_Length(data.count))
      }
    }
  }
}

С этими определенными вспомогательными функциями вы можете просто сделать:

var forwardInputReal = [Float](repeating: 0, count: halfN)
var forwardInputImag = [Float](repeating: 0, count: halfN)
ctoz(observed, real: &forwardInputReal, imag: &forwardInputImag)

или даже:

let (forwardInputReal, forwardInputImag) = ctoz(data)

Я поговорю с командой vDSP и посмотрю, сможем ли мы добавить что-то подобное в Framework для будущего выпуска, поэтомувам не нужно писать это самостоятельно.

...