構造体配列による信号波形を描画する

1次元信号処理のプログラミングをする場合、x と f(x) のように、横軸の値と縦軸の値をワンセットにした構造体を定義して、それを一つの配列要素としてデータ列として扱う場合が定番です。

本記事では、このような信号データの波形を画面に表示する方法を紹介します。具体的には、下記のような3種類の波形を画面に表示します。

Fig. 1 シンク関数
Fig. 2 サイン関数
Fig. 3 コサイン関数

いきなりコード解説をします。

3〜6行目、構造体の定義です。1次元信号において x が横軸のデータです。fx が縦軸のデータです。
10〜11行目、データの個数を定義します。きめ細かいデータを表示したい場合 NUM_DATA を大きくとってください。ためしに 1000 を 100 にしてみると効き目がわかると思います。

125行目、シンク関数をセットするメソッドです。
161行目、サイン関数をセットするメソッドです。
186行目、コサイン関数をセットするメソッドです。
120行目、起動時はシンク関数をセットします。

36〜54行目、波形を描画している部分です。
40、49行目、描画座標系は左上隅が(0, 0)なので信号データにマイナスをかけています。
46、47、49行目、波形描画の連続性を優先して描画座標から実データの要素インデックスを逆算します。
58、78行目、Path の描画範囲を決定する .frame() の値は、背景の Rectangle() と同じサイズにします。

import SwiftUI

struct X_FX {
    var  x: Double
    var fx: Double
}

struct ContentView: View {

    let NUM_DATA = 1000
    @State var Data: [X_FX] = []

    var body: some View {

        ZStack{

            // 全体背景の矩形を描画する.
            Rectangle()
                .foregroundColor( Color.mint )

            // 波形描画の矩形サイズ.
            let RECT_W = 300.0
            let RECT_H = 400.0

            // 波形描画の背景の矩形を描画する.
            Rectangle()
                .foregroundColor( Color.black )
                .frame( width: RECT_W, height: RECT_H )

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

                let amp = RECT_H * 0.5
                let off_y = RECT_H * 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( RECT_W )
                    for i in 1 ..< loop {
                        let rate = Double(i)/Double( RECT_W - 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( .green, lineWidth: 2.0 )
            .frame( width: RECT_W, height: RECT_H ) // ここが重要.

            // 目安になるセンター十字線を描画する.
            Path{ path in

                let plot_x = RECT_W * 0.5
                let plot_y = RECT_H * 0.5

                let pnt00 = CGPoint( x: plot_x, y:      0 )
                let pnt01 = CGPoint( x: plot_x, y: RECT_H )
                path.move( to: pnt00 )
                path.addLine( to: pnt01 )

                let pnt10 = CGPoint( x:      0, y: plot_y )
                let pnt11 = CGPoint( x: RECT_W, y: plot_y )
                path.move( to: pnt10 )
                path.addLine( to: pnt11 )

            }
            .stroke( .white, lineWidth: 1.0 )
            .frame( width: RECT_W, height: RECT_H ) // ここが重要.

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

                // シンク関数をセットするボタン.
                Button( action: {
                    let _ = SetDataSinc( &Data, NUM_DATA )
                }){
                    Text( "sinc" )
                        .foregroundColor( .white )
                }
                .padding()
                .background( .blue )

                // サイン関数をセットするボタン.
                Button( action: {
                    let _ = SetDataSin( &Data, NUM_DATA )
                }){
                    Text( "sin" )
                        .foregroundColor( .white )
                }
                .padding()
                .background( .blue )

                // コサイン関数データをセットするボタン.
                Button( action: {
                    let _ = SetDataCos( &Data, NUM_DATA )
                }){
                    Text( "cos" )
                        .foregroundColor( .white )
                }
                .padding()
                .background( .blue )

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

        }
        .onAppear(){
            // 起動時にはシンク関数を描画する.
            SetDataSinc( &Data, NUM_DATA )
        }

    }

    func SetDataSinc( _ 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

            var ft = 0.0

            // ゼロ割チェック.
            if ( t != 0.0 )
            {
                ft = sin( t ) / t
            }
            else
            {
                ft = 1.0
            }

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

        }

    }

    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 SetDataCos( _ 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 = cos( t )

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

        }

    }

}

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