【iOS】Core Audioでシンセサイザーを作る


iOSのCore Audioを使って、波形を生成するシンセサイザーを作ってみました。あわせてエフェクターもいくつか実装しています。

その作り方を解説します。

### できたもの

音のサンプルをSoundCloudにアップしています。アナログシンセのような感じが出ていると思います。

https://soundcloud.com/fssjk0veihjz/ios-core-audio-synthesizer

画面はこんな感じです。鍵盤ではなく、スライダーで音程を調整します。

<img width="200px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/2c76101b-b3e5-c2a5-5362-59a0dd145d46.png">

ソースコードはこちらです。複数のサンプルがありますが、今回解説するのは[「Synthesizer」](https://github.com/TokyoYoshida/CoreAudioExamples/tree/main/CoreAudioExamples/Samples/Synthesizer)になります。
https://github.com/TokyoYoshida/CoreAudioExamples


### 作り方の概要

Core Audioの構成要素であるAVAudioEngineを使います。波形を生成するレンダー関数を作り、これをソースノードとしてミキサーノードにつなぎ、出力ノードから出力することで、任意の波形を音声出力することができます。

**<処理の流れ>**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/ac5c635c-4740-da9e-3a61-0684bd433413.png)

### 作り方

本稿は[「Synthesizer」](https://github.com/TokyoYoshida/CoreAudioExamples/tree/main/CoreAudioExamples/Samples/Synthesizer)の解説記事ですが、波形の作り方は[「AudioEngeneGenerateWave」](https://github.com/TokyoYoshida/CoreAudioExamples/tree/main/CoreAudioExamples/Samples/AudioEngeneGenerateWave)の方がシンプルで分かりやすいので、まずはこれをベースに解説していきます。

**1. AVAudioEngineを生成する**

AVAudioEngineを生成して各種の情報を取得します。

```swift:AudioEngeneWaveGenerator.swift
class AudioEngeneWaveGenerator {
    var audioEngine: AVAudioEngine = AVAudioEngine()
    var sampleRate: Float = 44100.0
    var deltaTime: Float = 0
    var mainMixer: AVAudioMixerNode?
    var outputNode: AVAudioOutputNode?
    var format: AVAudioFormat?

    func initAudioEngene() {
        mainMixer = audioEngine.mainMixerNode
        outputNode = audioEngine.outputNode
        format = outputNode!.inputFormat(forBus: 0)


        sampleRate = Float(format!.sampleRate)
        deltaTime = 1 / Float(sampleRate)                                        
    }
// 〜略〜
```

**2. レンダー関数を作る**

波形をレンダーする関数を作ります。ここではサイン波を生成しています。

sin関数に周波数(currentTone)と、2π（単位時間で1周する）、時間を与えて生成します。currentToneに、440.0を与えればラの音(A)になります。

```swift:AudioEngeneWaveGenerator.swift
# class AudioEngeneWaveGeneratorの一部
static let toneA: Float = 440.0
var time: Float = 0

lazy var sourceNode = AVAudioSourceNode { [self] (_, _, frameCount, audioBufferList) -> OSStatus in
    let abl = UnsafeMutableAudioBufferListPointer(audioBufferList)
    for frame in 0..<Int(frameCount) {
        let sampleVal: Float = sin(AudioEngeneWaveGenerator.toneA * 2.0 * Float(Double.pi) * self.time)
        self.time += self.deltaTime
        for buffer in abl {
            let buf: UnsafeMutableBufferPointer<Float> = UnsafeMutableBufferPointer(buffer)
            buf[frame] = sampleVal
        }
    }
    return noErr
}
```

**3. 音を鳴らす**

実際に音を鳴らします。

AVAudioEngineに、AVAudioSourceNodeやAVAudioOutputNodeをコネクトしていくことで、音の生成から出力までをルーティングすることができます。合わせて、AVAudioSourceNodeのフォーマットを指定したり、AVAudioOutputNodeのボリュームを設定します。

準備ができたら、AVAudioEngineのstartメソッドで再生開始です。

```swift:AudioEngeneWaveGenerator.swift
# class AudioEngeneWaveGeneratorの一部
func start() {
    refData.frame = 0
    let inputFormat = AVAudioFormat(commonFormat: format!.commonFormat, sampleRate: Double(sampleRate), channels: 1, interleaved: format!.isInterleaved)
    audioEngine.attach(sourceNode)
    audioEngine.connect(sourceNode, to: mainMixer!, format: inputFormat!)
    audioEngine.connect(mainMixer!, to: outputNode!, format: nil)
    mainMixer?.outputVolume = 0
    
    do {
        try audioEngine.start()
    } catch {
        fatalError("Coud not start engine: \(error.localizedDescription)")
    }
}
```

これで音が鳴るようになりました。


**4. 音の再生を止める**

音の再生を止めるメソッドも作っておきます。

```swift:AudioEngeneWaveGenerator.swift
# class AudioEngeneWaveGeneratorの一部
func stop() {
    audioEngine.stop()
}
```

**5. 音を差し替えられるようにする**

ここからは、サンプル[「Synthesizer」](https://github.com/TokyoYoshida/CoreAudioExamples/tree/main/CoreAudioExamples/Samples/Synthesizer)の説明になります。

ここまではAudioEngeneWaveGeneratorクラスを実装していましたが、ここからはSynthesizerクラスになります。実装はほぼ同じなので、追加したところだけを説明します。

まず、音を差し替えたり、エフェクターをつなげたりできるようにします。

これは、波形の生成をする関数をprotocolで抽象化し、それぞれの部品をクラスにすることで実現できます。
今回は次のような構成で作ってみました。

**<波形生成関連クラスの関係図>**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/88f02a6f-c793-2fe0-27fd-3a81d435eb58.png)

protocol AudioSourceは、音程(tone)と、時間を与えるとその時点の波形を出力するメソッド（signal)を持ちます。

```swift:Synthesizer.swift
protocol AudioSource {
    var tone: Float {get set}
    func signal(time: Float) -> Float
}
```

そして先程説明したレンダー関数の波形生成部分は、AudioSourceに委譲するようにします。

```swift:Synthesizer.swift
// class Synthesizerの一部
private var audioSource: AudioSource?

lazy var sourceNode = AVAudioSourceNode { [self] (_, _, frameCount, audioBufferList) -> OSStatus in
    let abl = UnsafeMutableAudioBufferListPointer(audioBufferList)
    guard let oscillator = self.audioSource else {fatalError("Oscillator is nil")}
    for frame in 0..<Int(frameCount) {
        let sampleVal: Float = oscillator.signal(time: self.time)　// この部分
        self.time += self.deltaTime
        for buffer in abl {
            let buf: UnsafeMutableBufferPointer<Float> = UnsafeMutableBufferPointer(buffer)
            buf[frame] = sampleVal
        }
    }
    return noErr
}
```

**6. オシレーターを作る**

サイン波を発生させるオシレーターを作ります。先ほど紹介したサイン波の数式をclassでくるんでいるだけです。

updateToneメソッドの詳細は割愛しますが、音色を急に変化させてもゆっくりなめらかにcurrentToneを変化させるための処理です。これがなくても動作しますが、音色を変化させたときにプチプチとノイズが入ります。

```swift:Synthesizer.swift
class SinOscillator: ToneAdjuster { // ToneAdjusterはupdateToneの実体を定義するためのもので、それ以外はAudioSourceプロトコルと同じ
    override func signal(time: Float) -> Float {
        updateTone()
        return sin(currentTone * 2.0 * Float(Double.pi) * time)
    }
    
}
```

**7. ミキサーを作る**

ミキサーは、AudioSouceを継承しつつ、エフェクターを加えることができるプロトコルとして宣言します。

```swift:Synthesizer.swift
protocol Mixer: AudioSource {
    func addEffector(index: Int, effector: Effector)
    func removeEffector(at index: Int)
}
```

ミキサーの実装であるAudioMixerは、オシレータを1つ、エフェクターを複数保持しています。

```swift:Synthesizer.swift
class AudioMixer: Mixer {
    private var oscillator: Oscillator
    private var effectors: [Int:Effector] = [:]
// 〜略〜
```

ミキサーのsignalメソッドが、実際にレンダー関数から呼ばれるところです。

オシレーターに時間を渡して波形を生成させ、エフェクターに波形を渡し、その出力をさらに別のエフェクターに渡すということを繰り返しています。

セマフォによる排他制御をしているのは、波形の処理中に画面側でオシレーターやエフェクターを変更したときの競合を避けるためです。

```swift:Synthesizer.swift
// class AudioMixerの一部
let semaphore = DispatchSemaphore(value: 1)

func signal(time: Float) -> Float {
    semaphore.wait()

    var waveValue = oscillator.signal(time: time)
    for (_,effector) in effectors {
        waveValue = effector.signal(waveValue: waveValue, time: time)
    }

    semaphore.signal()

    return waveValue
}
```

**8. エフェクターを作る**

エフェクターを作ります。これが一番楽しい作業でした。

エフェクターは、入力した波形に変化を加えて、出力します。

例えばディレイエフェクターは、入力した波形に、バッファにためた過去の波形を少し音量を下げて加えます。そしてその結果をバッファにためます。このようにすると残響効果が現れて、面白い音が作り出せます。

```swift:Synthesizer.swift
class DelayEffector: Effector {
    var delayCount = 22_100
    lazy var buffer = RingBuffer<Float>(delayCount + 1)
    var index: Int = 0

    func signal(waveValue: Float, time: Float) -> Float {
        func enqueue(_ value: Float) {
            if !buffer.enqueue(value) {
                fatalError("Cannot enqueue buffer.")
            }
        }
        if delayCount > 0 {
            delayCount -= 1
            enqueue(waveValue)
            return waveValue
        }
        if let delayValue = buffer.dequeue() {
            let ret = waveValue + delayValue*0.4
            enqueue(ret)
            return ret
        }
        fatalError("Cannot dequeue buffer.")
    }
}
```

なお、各エフェクターの厳密な言葉の定義は違っているかもしれないのでご了承下さい。（ある程度のイメージは合っていると思います）

**9. 画面を作る**

最後に画面を作ります。鍵盤を付けるのもいいですが、アナログシンセっぽく使えるといいかなと思い、音程はスライダーにしてみました。

<img width="200px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116010/2c76101b-b3e5-c2a5-5362-59a0dd145d46.png">

### 参考資料

Building a Synthesizer in Swift
https://betterprogramming.pub/building-a-synthesizer-in-swift-866cd15b731

### 最後に

iOSを使ったARやML、音声処理などの作品やサンプル、技術情報を発信しています。
作品ができたらTwitterで発信していきますのでフォローをお願いします🙏

Twitter
https://twitter.com/jugemjugemjugem

Qiitaは、iOS開発、とくにARや機械学習、グラフィックス処理、音声処理について発信しています。
https://qiita.com/TokyoYoshida

Noteでは、連載記事を書いています。
https://note.com/tokyoyoshida

Zennは機械学習が多めです。
https://zenn.dev/tokyoyoshida