[iOS]Swift4で音声の入力・出力をする方法

# はじめに
この記事では、マイクから音声を入力して一旦ファイル化し、その音をスピーカーから出力するまでの処理をSwift4で書いてみることにします。
Swiftのアップデートが早くてSwift4もすぐにオワコンになってしまうかもしれないですが、自分用の備忘も兼ねて記録しておきます:grinning:

# 使ったもの
* Xcode 10.1
* Swift 4.2
* AVFoundation 

# 共通で必要な処理の実装
##### AVFoundationのインスタンスを生成する
```swift
let session = AVAudioSession.sharedInstance()
```
##### 再生と録音をすることを宣言してからアクティブ化する
```swift
try session.setCategory(.playAndRecord, mode: .default)
try session.setActive(true)
```
# 音声入力の実装
##### まず録音で使用するAVAudioRecorderを宣言する
```swift
var audioRecorder: AVAudioRecorder!
```
##### 録音フォーマットの設定をする
```swift
let settings = [
    AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
    AVSampleRateKey: 44100,
    AVNumberOfChannelsKey: 2,
    AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]
```
##### AVAudioRecorderインスタンスを生成する
```swift
audioRecorder = try AVAudioRecorder(url: getAudioFileUrl(), settings: settings)
audioRecorder.delegate = self as? AVAudioRecorderDelegate
```
```swift
func getAudioFileUrl() -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    let docsDirect = paths[0]
    let audioUrl = docsDirect.appendingPathComponent("recording.m4a")
    
    return audioUrl
}
```
##### ボタンを押したら録音開始（録音中に押したら録音停止）
```swift
@IBAction func record(_ sender: Any) {
    if !audioRecorder.isRecording {
        audioRecorder.record()
    } else {
        audioRecorder.stop()
    }
}
```

# 音声出力の実装
##### まず再生で使用するAVAudioEngine、AVAudioFile、AVAudioPlayerNodeを宣言する
```swift
var audioEngine: AVAudioEngine!
var audioFile: AVAudioFile!
var audioPlayerNode: AVAudioPlayerNode!
```
##### AVAudioEngine、AVAudioFile、AVAudioPlayerNode各々のインスタンスを生成する
```swift
audioEngine = AVAudioEngine()
audioFile = try AVAudioFile(forReading: getAudioFileUrl())
audioPlayerNode = AVAudioPlayerNode()
```
```swift
//上で書いたfuncと同じ
func getAudioFileUrl() -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    let docsDirect = paths[0]
    let audioUrl = docsDirect.appendingPathComponent("recording.m4a")
    
    return audioUrl
}
```
##### Nodeのつなぎ合わせをする
```swift
audioEngine.attach(audioPlayerNode)
audioEngine.connect(audioPlayerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)
```
##### ボタンを押したら再生開始（再生中に押したら初めから再生）
```swift
@IBAction func play(_ sender: Any) {
    do {
    //中略（インスタンス生成やNodeのつなぎ合わせはここでやる）
        audioPlayerNode.stop()
        audioPlayerNode.scheduleFile(audioFile, at: nil)
            
        try audioEngine.start()
        audioPlayerNode.play()
    } catch let error {
        print(error)
    }
}
```
# 通しで書くとこんな感じ
```swift
import UIKit
import AVFoundation

class ViewController: UIViewController {

    var audioRecorder: AVAudioRecorder!
    var audioEngine: AVAudioEngine!
    var audioFile: AVAudioFile!
    var audioPlayerNode: AVAudioPlayerNode!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        let session = AVAudioSession.sharedInstance()

        do {
            try session.setCategory(.playAndRecord, mode: .default)
            try session.setActive(true)

            let settings = [
                AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
                AVSampleRateKey: 44100,
                AVNumberOfChannelsKey: 2,
                AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
            ]

            audioRecorder = try AVAudioRecorder(url: getAudioFileUrl(), settings: settings)
            audioRecorder.delegate = self as? AVAudioRecorderDelegate
        } catch let error {
            print(error)
        }

    }

    @IBAction func record(_ sender: Any) {
        if !audioRecorder.isRecording {
            audioRecorder.record()
        } else {
            audioRecorder.stop()
        }
    }

    
    @IBAction func play(_ sender: Any) {
        audioEngine = AVAudioEngine()
        do {
            audioFile = try AVAudioFile(forReading: getAudioFileUrl())
            
            audioPlayerNode = AVAudioPlayerNode()
            audioEngine.attach(audioPlayerNode)
            audioEngine.connect(audioPlayerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)
            
            audioPlayerNode.stop()
            audioPlayerNode.scheduleFile(audioFile, at: nil)
            
            try audioEngine.start()
            audioPlayerNode.play()
        } catch let error {
            print(error)
        }
    }
    
    func getAudioFileUrl() -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let docsDirect = paths[0]
        let audioUrl = docsDirect.appendingPathComponent("recording.m4a")

        return audioUrl
    }

}
```

# こんなクラスファイルがあると実装が楽
この辺のユーティリティと言うほどではないけれど、いろんな画面から呼び出しそうな処理はこんな感じで一箇所にまとめて外出ししてしまった方がコードが冗長にならなくていいなあと思います:ok_woman:
あくまで一例ですが…

```AudioManager.swift
import UIKit
import AVFoundation

final class AudioManager {
    
    var audioRecorder: AVAudioRecorder!
    var audioEngine: AVAudioEngine!
    var audioFile: AVAudioFile!
    var audioPlayerNode: AVAudioPlayerNode!
    var audioUnitTimePitch: AVAudioUnitTimePitch!
    
    init() {}
    
    func setUpAudioRecorder() {
        let session = AVAudioSession.sharedInstance()
        
        do {
            try session.setCategory(.playAndRecord, mode: .default)
            try session.setActive(true)
            
            let settings = [
                AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
                AVSampleRateKey: 44100,
                AVNumberOfChannelsKey: 2,
                AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
            ]
            
            audioRecorder = try AVAudioRecorder(url: getAudioFilrUrl(), settings: settings)
            audioRecorder.delegate = self as? AVAudioRecorderDelegate
        } catch let error {
            print(error)
        }
    }
    
    func playSound(speed: Float, pitch: Float, echo: Bool, reverb: Bool) {
        audioEngine = AVAudioEngine()
        
        let url = getAudioFilrUrl()
        
        do {
            audioFile = try AVAudioFile(forReading: url)
            
            audioPlayerNode = AVAudioPlayerNode()
            audioEngine.attach(audioPlayerNode)
            
            audioUnitTimePitch = AVAudioUnitTimePitch()
            audioUnitTimePitch.rate = speed
            audioUnitTimePitch.pitch = pitch
            audioEngine.attach(audioUnitTimePitch)
            
            let echoNode = AVAudioUnitDistortion()
            echoNode.loadFactoryPreset(.multiEcho1)
            audioEngine.attach(echoNode)
            
            let reverbNode = AVAudioUnitReverb()
            reverbNode.loadFactoryPreset(.cathedral)
            reverbNode.wetDryMix = 50
            audioEngine.attach(reverbNode)
            
            if echo && reverb {
                connectAudioNodes(audioPlayerNode, audioUnitTimePitch, echoNode, reverbNode, audioEngine.outputNode)
            } else if echo {
                connectAudioNodes(audioPlayerNode, audioUnitTimePitch, echoNode, audioEngine.outputNode)
            } else if reverb {
                connectAudioNodes(audioPlayerNode, audioUnitTimePitch, reverbNode, audioEngine.outputNode)
            } else {
                connectAudioNodes(audioPlayerNode, audioUnitTimePitch, audioEngine.outputNode)
            }
            
            audioPlayerNode.stop()
            audioPlayerNode.scheduleFile(audioFile, at: nil)
            
            try audioEngine.start()
            audioPlayerNode.play()
        } catch let error {
            print(error)
        }
    }
    
    private func connectAudioNodes(_ nodes: AVAudioNode...) {
        for x in 0..<nodes.count - 1 {
            audioEngine.connect(nodes[x], to: nodes[x+1], format: audioFile.processingFormat)
        }
    }
    
    func getAudioFilrUrl() -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let docsDirect = paths[0]
        let audioUrl = docsDirect.appendingPathComponent("recording.m4a")
        
        return audioUrl
    }
}
```

# おわりに
単に音声の入出力だけでも結構色々やらなきゃいけないことが多いみたいでなれるまでは大変かもしれませんね。
個人的にはNodeをつなぎ合わせるという考え方が、楽器→エフェクター→アンプの接続を想起させるような作りになっていて面白いと思いました:open_mouth: