高速フーリエ変換を実施する(vDSPを使う)

iPhone や Mac などでフーリエ変換を実施する場合は、Apple社の Accelerate フレームワークという信号処理用の高速なライブラリを使いましょう。そこに vDSP_fft_zip という FFT に使えるメソッドがあります。Apple開発者サイトを調べまくって、なんとか実行できるコードを作ることができましたので紹介します。

ちなみに下記の記事は、むかしむかしに独自で書いたC言語のFFTのコードを Swift に翻訳したものを紹介しています。( 独自で書いたというよりは、トラ技に乗っていたコードを自分テイストに書き換えただけですが... )

高速フーリエ変換を実施する(独自コード)

Apple の Accelerate フレームワークを使わずに高速フーリエ変換を実施するコードを紹介します。

自分でいろいろ高速化を考えたりするよりも、プライド捨ててサッサと vDSP を使ったほうが速いのがくやしいですッ!

Fig. 1 オリジナル波形
Fig. 2 ローパス21次の波形
Fig. 3 ハイパス21次の波形
Fig. 4 ローパス901次の波形
Fig. 5 ハイパス901次の波形
import SwiftUI

// データ配列の要素をなす構造体の型.
struct X_FX {
    var  x: Double
    var fx: Double
}

// スライダ用の定数.
struct SLIDER {
    static let MIN:  Double =    0.0
    static let MAX:  Double = 1022.0
    static let STEP: Double =    1.0
}

struct ContentView: View {

    // 背景の色と波形の色.
    let COLOR_BG   = Color( red: 0.0, green: 0.0, blue: 0.3, opacity: 1.0 )
    let COLOR_WAVE = Color( red: 0.0, green: 1.0, blue: 0.0, opacity: 1.0 )

    // データの配列.
    let NUM_DATA = 2048
    @State var Data: [X_FX] = []

    // フーリエ変換オブジェクト.
    let Fourier = MyFourierObject()

    // スライダと関係するフィルタリングの次数.
    @State var SliderValue: Double = 60.0
    @State var Threshold = 60

    // ローパスフィルタリングするかハイパスフィルタリングするか.
    @State var ModePassLow: Bool = true

    // f(x) 方向の描画倍率
    let DRAW_RATE = 0.8

    // FFTとIFFTの実行時間.
    @State var MsecExec: Int = 0

    var body: some View {

        GeometryReader {

            geom in

            ZStack{

                // 画面のうち ZStack が占めるサイズ.
                let RectW = geom.size.width
                let RectH = geom.size.height

                // 波形描画の背景の矩形を描画する.
                Rectangle()
                    .foregroundColor( COLOR_BG )
                    .frame( width: RectW, height: RectH )

                // 目安になるセンター十字線と振幅の高さを描画する.
                Path{ path in

                    let plot_x = RectW * 0.5
                    let plot_y = RectH * 0.5

                    // センターの垂直線.
                    let pnt00 = CGPoint( x: plot_x, y:     0 )
                    let pnt01 = CGPoint( x: plot_x, y: RectH )
                    path.move( to: pnt00 )
                    path.addLine( to: pnt01 )

                    // センターの水平線.
                    let pnt10 = CGPoint( x:     0, y: plot_y )
                    let pnt11 = CGPoint( x: RectW, y: plot_y )
                    path.move( to: pnt10 )
                    path.addLine( to: pnt11 )

                    // 振幅 f(x) のプラス側のめやす水平線.
                    let tmp0 = ( plot_y - ( plot_y * DRAW_RATE ))
                    let pnt20 = CGPoint( x:     0, y: tmp0 )
                    let pnt21 = CGPoint( x: RectW, y: tmp0 )
                    path.move( to: pnt20 )
                    path.addLine( to: pnt21 )

                    // 振幅 f(x) のプラス側のめやす水平線.
                    let tmp1 = ( plot_y + ( plot_y * DRAW_RATE ))
                    let pnt30 = CGPoint( x:     0, y: tmp1 )
                    let pnt31 = CGPoint( x: RectW, y: tmp1 )
                    path.move( to: pnt30 )
                    path.addLine( to: pnt31 )

                }
                .stroke( .gray, lineWidth: 1.0 )
                .frame( width: RectW, height: RectH ) // ここが重要.

                // 波形を描画する.
                Path{ path in

                    let amp = RectH * 0.5 * DRAW_RATE
                    let off_y = RectH * 0.5

                    if ( Data.count > 0 )
                    {

                        var plot_x = Double(0)
                        var plot_y = -( Data[0].fx ) * amp + off_y
                        var pnt = CGPoint( x: plot_x, y: plot_y )
                        path.move( to: pnt )

                        let loop = Int( RectW )
                        for i in 1 ..< loop {
                            let rate = Double(i)/Double( RectW - 1 )
                            let n = Int( rate * Double( Data.count - 1 ))
                            plot_x = Double(i)
                            plot_y = -( Data[n].fx ) * amp  + off_y
                            pnt = CGPoint( x: plot_x, y: plot_y )
                            path.addLine( to: pnt )
                        }

                    }

                }
                .stroke( COLOR_WAVE, lineWidth: 1.5 )
                .frame( width: RectW, height: RectH ) // ここが重要.


                VStack{

                    // データをセットするボタンを水平に並べる.
                    HStack{

                        // ローパスフィルタを選択するボタン.
                        Button( action: {
                            ModePassLow = true
                            processing( &Data, NUM_DATA, Threshold, ModePassLow )
                        }){
                            Text( "LowPass" )
                                .foregroundColor( .white )
                        }
                        .padding()
                        .background( .blue )

                        // ハイパスフィルタを選択するボタン.
                        Button( action: {
                            ModePassLow = false
                            processing( &Data, NUM_DATA, Threshold, ModePassLow )
                        }){
                            Text( "HighPass" )
                                .foregroundColor( .white )
                        }
                        .padding()
                        .background( .blue )

                        // フィルタ次数をひとつ増加するボタン.
                        Button( action: {
                            SliderValue += 1
                            if SliderValue > SLIDER.MAX
                            {
                                SliderValue = SLIDER.MAX
                            }
                            Threshold = Int( SliderValue )
                            processing( &Data, NUM_DATA, Threshold, ModePassLow )
                        }){
                            Text( "+" )
                                .foregroundColor( .white )
                        }
                        .padding()
                        .background( .blue )

                        // フィルタ次数をひとつ減らすボタン.
                        Button( action: {
                            SliderValue -= 1
                            if SliderValue < SLIDER.MIN
                            {
                                SliderValue = SLIDER.MIN
                            }
                            Threshold = Int( SliderValue )
                            processing( &Data, NUM_DATA, Threshold, ModePassLow )
                        }){
                            Text( "-" )
                                .foregroundColor( .white )
                        }
                        .padding()
                        .background( .blue )

                    }

                    // スライダの値を示す.
                    Text( String( format: "%f", SliderValue ) )
                        .foregroundColor( .white )

                    // フィルタ次数の値を示す.
                    Text( String( format: "%d / %d", Threshold, NUM_DATA ) )
                        .foregroundColor( .white )

                    // 実行時間を示す.
                    Text( String( format: "%d msec", MsecExec ) )
                        .foregroundColor( .white )

                    // フィルタ次数を決定するスライダ.
                    Slider(
                        value: $SliderValue,
                        in: SLIDER.MIN...SLIDER.MAX,
                        step: SLIDER.STEP,
                        onEditingChanged: {
                            editing in
                            // これをコメントインしてもなんだか違いがわからない.
                            //Threshold = Int( SliderValue )
                            //processing( &Data, NUM_DATA, Threshold, ModePassLow )
                        }
                    ).onChange( of: SliderValue ){ new_value in
                        Threshold = Int( new_value )
                        processing( &Data, NUM_DATA, Threshold, ModePassLow )
                    }

                }
                .frame( maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom )
                .padding( .bottom )

            }
            .onAppear(){

                // 起動時に矩形波データを仕込む.
                SetDataSquare( &Data, NUM_DATA )

            }

        }

    }

    // フーリエ変換して周波数領域でフィルタリングして逆フーリエ変換する.
    func processing( _ data: inout[X_FX], _ num_data: Int, _ threshold: Int, _ mode_filter: Bool )->Void
    {

        // 矩形波を仕込む.
        let _ = SetDataSquare( &data, num_data )

        // 実部と虚部のデータ領域を確保する.
        let data_re = UnsafeMutablePointer<Float>.allocate( capacity: num_data )
        let data_im = UnsafeMutablePointer<Float>.allocate( capacity: num_data )
        let buf_for_dsp = UnsafeMutablePointer<Float>.allocate( capacity: num_data )

        // f(x) のデータを実部に仕込み、虚部に 0.0 を仕込む.
        for n in 0 ..< num_data
        {
            data_re[n] = Float( data[n].fx )
            data_im[n] = Float( 0.0 )
        }

        // 時間計測を開始する.
        let date_beg = Date()

        // フーリエ変換で周波数領域の実部と虚部のデータを取得する.
        Fourier.Transform(
            MyFourierObject.TransformType.TimeToFreq,
            data_re,
            data_im,
            buf_for_dsp,
            num_data
            )

        // 周波数領域でフィルタリングを実施する.
        if ( mode_filter )
        {
            // ローパスフィルタ.
            let _ = FilteringPassLow(
                        data_re,
                        data_im,
                        num_data,
                        threshold
                        )
        }
        else
        {
            // ハイパスフィルタ.
            let _ = FilteringPassHigh(
                        data_re,
                        data_im,
                        num_data,
                        threshold
                        )
        }

        // フーリエ逆変換する.
        Fourier.Transform(
            MyFourierObject.TransformType.FreqToTime,
            data_re,
            data_im,
            buf_for_dsp,
            num_data
            )

        // 時間計測を終わる.
        let date_fin = Date()

        // 実行時間を取得する.
        let sec = date_fin.timeIntervalSince( date_beg )
        let msec = sec * 1000.0
        MsecExec = Int( msec.rounded())

        // 実部のデータを元のデータ f(x) に書き戻す.
        for n in 0 ..< num_data
        {
            data[n].fx = Double( data_re[n] )
        }

        data_re.deallocate()
        data_im.deallocate()
        buf_for_dsp.deallocate()

        return

    }

    // サイン波形を格納する.
    func SetDataSin( _ data: inout [X_FX], _ req_num_data: Int )->Void {

        // いったん配列をクリアする.
        data.removeAll()

        let RANGE = 2.0
        let MUL = 2.0

        for n in 0 ..< req_num_data {

            let rate = Double(n)/Double( req_num_data - 1)
            var v = RANGE * rate
            v -= 1.0 // この時点で -1.0 〜 0.0 〜 +1.0 になる.

            let t = v * ( 2.0 * ( Double.pi )) * MUL
            let ft = sin( t )

            // データ要素を追加する.
            let element = X_FX( x: t, fx: ft )
            data.append( element )

        }

    }

    // 矩形波を格納する.
    func SetDataSquare( _ data: inout [X_FX], _ req_num_data: Int )->Void {

        // いったん配列をクリアする.
        data.removeAll()

        // サイン波形をしこむ.
        SetDataSin( &data, req_num_data )

        // サイン波形の正負を評価して、矩形波をつくる.
        for n in 0 ..< req_num_data {

            if ( data[n].fx >= 0.0 )
            {
                data[n].fx = +1.0;
            }
            else
            {
                data[n].fx = -1.0
            }

        }

    }

    // ローパスフィルタリングを実施する.
    func FilteringPassLow(
        _ data_re: UnsafeMutablePointer<Float>,
        _ data_im: UnsafeMutablePointer<Float>,
        _ num_data: Int,
        _ threshold: Int
        )->Bool
    {

        let beg = threshold
        let fin = num_data - threshold

        if ( beg < 0 ){ return false }
        if ( beg >= num_data ) { return false }
        if ( fin < 0 ){ return false }
        if ( fin >= num_data ) { return false }

        for k in beg ... fin
        {
            data_re[k] = 0.0
            data_im[k] = 0.0
        }

        return true

    }

    // ハイパスフィルタリングを実施する.
    func FilteringPassHigh(
        _ data_re: UnsafeMutablePointer<Float>,
        _ data_im: UnsafeMutablePointer<Float>,
        _ num_data: Int,
        _ threshold: Int
        )->Bool
    {

        let half = num_data >> 1

        let beg = half - threshold
        let fin = half + threshold

        if ( beg < 0 ){ return false }
        if ( beg >= num_data ) { return false }
        if ( fin < 0 ){ return false }
        if ( fin >= num_data ) { return false }

        var a = 0
        var b = 0

        a = 0
        b = beg
        for k in a ..< b
        {
            data_re[k] = 0.0
            data_im[k] = 0.0
        }

        a = fin + 1
        b = num_data - 1
        for k in a ..< b
        {
            data_re[k] = 0.0
            data_im[k] = 0.0
        }

        return true

    }

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

下記が vDSP を用いたフーリエ変換とフーリエ逆変換を実行するクラスです。

Transform というメソッドの buffer_for_dsp というメモリ領域は、内部で DSP 乗算をするので、それに用いる領域です。このコードを改造して引数を減らしたい場合は、メソッドの内部で buffer_for_dsp メモリをアロケートしてください。中でいちいちメモリ確保するので、その分が速度に影響するかもしれませんが。

データの個数は 2 のべき乗で実行してください

import Accelerate

class MyFourierObject {

    public enum TransformType {
        case TimeToFreq
        case FreqToTime
    }

    init()
    {

    }

    public func Transform(
            _ transform_type: TransformType,
            _ data_re: UnsafeMutablePointer<Float>,
            _ data_im: UnsafeMutablePointer<Float>,
            _ buffer_for_dsp: UnsafeMutablePointer<Float>,
            _ num_data: Int
            ) -> Void
    {

        // 複素型データの準備をする.
        var split_complex = DSPSplitComplex( realp: data_re, imagp: data_im )

        // FFTの準備をする.
        let dsp_length = vDSP_Length( log2( Float( num_data )))
        let fft_setup = vDSP_create_fftsetup( dsp_length, FFTRadix( kFFTRadix2 ))

        let MY_FFT_STRIDE = 1

        // 場合分け.
        if ( transform_type == TransformType.TimeToFreq )
        {
            // TimeToFreq.
            vDSP_fft_zip(
                fft_setup!,
                &split_complex,
                MY_FFT_STRIDE,
                dsp_length,
                FFTDirection( FFT_FORWARD )
                )
        }
        else
        {
            // FreqToTime.
            vDSP_fft_zip(
                fft_setup!,
                &split_complex,
                MY_FFT_STRIDE,
                dsp_length,
                FFTDirection( FFT_INVERSE )
                )
        }

        // FFTの後処理をする.
        vDSP_destroy_fftsetup( fft_setup )

        // 場合分け.
        if ( transform_type == TransformType.TimeToFreq )
        {
            // NOP.
        }
        else
        {

            // スケーリングのための係数を計算する.
            var scale = Float( 1.0 )/Float( num_data )

            // DSP乗算のストライド.
            let MY_DSP_STRIDE = 1

            // 実部のDSP乗算を実施する.
            vDSP_vsmul(
                data_re,
                MY_DSP_STRIDE,
                &scale,
                buffer_for_dsp,
                MY_DSP_STRIDE,
                vDSP_Length( num_data )
                )

            // 引数の実部データ列に戻す.
            for n in 0 ..< num_data
            {
                data_re[n] = buffer_for_dsp[n]
            }

            // 虚部のDSP乗算を実施する.
            vDSP_vsmul(
                data_im,
                MY_DSP_STRIDE,
                &scale,
                buffer_for_dsp,
                MY_DSP_STRIDE,
                vDSP_Length( num_data )
                )

            // 引数の虚部データ列に戻す.
            for n in 0 ..< num_data
            {
                data_im[n] = buffer_for_dsp[n]
            }

        }

        return

    }

}