LoginSignup
28
24

More than 3 years have passed since last update.

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

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

できたもの

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

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

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

作り方の概要

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

<処理の流れ>
image.png

作り方

本稿は「Synthesizer」の解説記事ですが、波形の作り方は「AudioEngeneGenerateWave」の方がシンプルで分かりやすいので、まずはこれをベースに解説していきます。

1. AVAudioEngineを生成する

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

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)になります。

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メソッドで再生開始です。

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. 音の再生を止める

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

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

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

ここからは、サンプル「Synthesizer」の説明になります。

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

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

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

<波形生成関連クラスの関係図>
image.png

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

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

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

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を変化させるための処理です。これがなくても動作しますが、音色を変化させたときにプチプチとノイズが入ります。

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を継承しつつ、エフェクターを加えることができるプロトコルとして宣言します。

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

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

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

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

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

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

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. エフェクターを作る

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

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

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

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. 画面を作る

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

参考資料

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

28
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
24