【SwiftUI】「ボイスメモ」の音声波形をPathで再現する

## はじめに
iPhoneの標準アプリ「ボイスメモ」を使って音声を録音するときに、録音開始とともに音声波形が表示されます。
この記事では、SwiftUIのPath構造体を使って「ボイスメモ」で表示される音声波形を再現してみようと思います（下図）。
<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/655077/bc051e3b-c263-1dab-86d0-3b7bc4b663c4.gif">

## 動作環境
- Xcode 14.1
- iOS 16

## 実装
音声波形は下記の順で実装していきます。
1. ある点の線を描画 
2. 指定した横幅まで、1.の線を任意の間隔で描画
3. 2.で描画した音声波形を右から左へ流れるように更新

まず、下図のiPhone中央に位置する音声波形に対して、xy座業系を考えます（iOSデバイスはデバイス下部に向かってy軸が正の値を取るため、y軸が下向きになっていることに注意してください）。
xy座標内の音声波形では、$\{y=y_0\}$を中心として上下長さが対称となる線をx軸に対して一定間隔ごとに描画しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/655077/dac3e8ed-5d57-f98f-4dff-844171af6278.png)

### ① 線の描画
Pathを使ってある点での線を描画するため、上の図の点線の枠①に注目します（拡大したグラフが下図）。
グラフ内の変数$\{l_n\}$は、マイク入力から随時取得できる音の大きさなどを想定しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/655077/dbeaed0b-b7eb-65f6-656f-613e06175274.png)
このグラフでは、$\{(x_n, y_0-l_n)\}$を起点として、$\{(x_n, y_0+l_n)\}$までの線を示しており、SwiftUIの`Path`を使い、下記のように実装できます。
```swift
// 起点
Path { path in
    path.move(
        to: .init(
            x: x, // x_n
            y: y0 - l // y_0 - l_n
        )
    )
    // 終点
    path.addLine(
        to: .init(
            x: x, // x_n
            y: y0 + l // y_0 + l_n
        )
    )
}
```

### ② 任意の間隔で線を描画
次に、指定した横幅$\{W\}$まで任意の間隔$\{(=x_{n+1}-x_n)\}$で線を配置していきます。(下図)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/655077/c0a7f896-bcce-fc71-8576-04036dc3eab9.png)
実装では、`(横幅)/(任意の間隔)`個の要素 $\{l_n\}$を持つ配列`levelArray`を用意し、`levelArray`の要素数分の線を描画していきます。
```swift
Path { path in
    let y_0 = view.size.height * 0.5
    var x = 0.0
    
    for l in levelArray {
        path.move(
            to: .init(
                x: x,
                y: y0 - l
            )
        )
        path.addLine(
            to: .init(
                x: x,
                y: y0 + l
            )
        )
        x += interval
    }
}
```

### ③ 音声波形の更新
「はじめに」でお見せした音声波形のgif画像では、波形が右から左に流れているように見えると思います。これを実装で表現するためには、音の大きさなどを取得するたびに、`levelArray`の末尾に新しい値 $\{l_n\}$を追加し、一番最初の要素を削除する処理が必要です。このような処理を行うことで音声波形の再描画が右から左に進んでいるように見えます。
以上から、①と②の実装も踏まえると、右から左に流れていく音声波形は下記のように実装できます。

```swift
struct SpectrumView: View {
    @State var levelArray: [CGFloat] = []
    @Binding var value: CGFloat

    private let interval:CGFloat = 5.0
    
    var body: some View {
        GeometryReader { render in
            Path { path in
                let y_0 = render.size.height * 0.5
                var x = 0.0
                
                for l in levelArray {
                    path.move(
                        to: .init(
                            x: x,
                            y: y0 - l
                        )
                    )
                    path.addLine(
                        to: .init(
                            x: x,
                            y: y0 + l
                        )
                    )
                    
                    x += interval
                }
            }
            .stroke(lineWidth: 1.0)
            .fill(Color.red)
            .onChange(of: value, perform: { newValue in
                if levelArray.isEmpty { return }
                levelArray.append(newValue)
                levelArray.remove(at: 0)
            })
            .onAppear {
                if levelArray.isEmpty == false { return }
                let count = render.size.width / interval
                levelArray = Array(repeating: 0.01, count: Int(count))
            }
        }
    }
}
```

## GitHub
https://github.com/mlballack/AudioSpectrumView.git

## おわりに
最後までご覧いただきありがとうございました。音声を録音するなどのアプリを作る際には参考にしていただければと思います。
他に音声波形の描画方法などありましたら、コメントで教えていただけると幸いです。

## 参考
- [【SwiftUI】Path構造体の使い方と図形描画方法！円/三角/扇型](http://tech.amefure.com/swift-path)
- [【SwiftUI】GeometryRederでViewのサイズや座標（位置）を取得する](https://tech.nri-net.com/entry/get_coord_by_geometryreder)