はじめに
この記事では、マイクから音声を入力して一旦ファイル化し、その音をスピーカーから出力するまでの処理をSwift4で書いてみることにします。
Swiftのアップデートが早くてSwift4もすぐにオワコンになってしまうかもしれないですが、自分用の備忘も兼ねて記録しておきます
使ったもの
- Xcode 10.1
- Swift 4.2
- AVFoundation
共通で必要な処理の実装
AVFoundationのインスタンスを生成する
let session = AVAudioSession.sharedInstance()
再生と録音をすることを宣言してからアクティブ化する
try session.setCategory(.playAndRecord, mode: .default)
try session.setActive(true)
音声入力の実装
まず録音で使用するAVAudioRecorderを宣言する
var audioRecorder: AVAudioRecorder!
録音フォーマットの設定をする
let settings = [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 44100,
AVNumberOfChannelsKey: 2,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]
AVAudioRecorderインスタンスを生成する
audioRecorder = try AVAudioRecorder(url: getAudioFileUrl(), settings: settings)
audioRecorder.delegate = self as? AVAudioRecorderDelegate
func getAudioFileUrl() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let docsDirect = paths[0]
let audioUrl = docsDirect.appendingPathComponent("recording.m4a")
return audioUrl
}
ボタンを押したら録音開始(録音中に押したら録音停止)
@IBAction func record(_ sender: Any) {
if !audioRecorder.isRecording {
audioRecorder.record()
} else {
audioRecorder.stop()
}
}
音声出力の実装
まず再生で使用するAVAudioEngine、AVAudioFile、AVAudioPlayerNodeを宣言する
var audioEngine: AVAudioEngine!
var audioFile: AVAudioFile!
var audioPlayerNode: AVAudioPlayerNode!
AVAudioEngine、AVAudioFile、AVAudioPlayerNode各々のインスタンスを生成する
audioEngine = AVAudioEngine()
audioFile = try AVAudioFile(forReading: getAudioFileUrl())
audioPlayerNode = AVAudioPlayerNode()
//上で書いた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のつなぎ合わせをする
audioEngine.attach(audioPlayerNode)
audioEngine.connect(audioPlayerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)
ボタンを押したら再生開始(再生中に押したら初めから再生)
@IBAction func play(_ sender: Any) {
do {
//中略(インスタンス生成やNodeのつなぎ合わせはここでやる)
audioPlayerNode.stop()
audioPlayerNode.scheduleFile(audioFile, at: nil)
try audioEngine.start()
audioPlayerNode.play()
} catch let error {
print(error)
}
}
通しで書くとこんな感じ
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
}
}
こんなクラスファイルがあると実装が楽
この辺のユーティリティと言うほどではないけれど、いろんな画面から呼び出しそうな処理はこんな感じで一箇所にまとめて外出ししてしまった方がコードが冗長にならなくていいなあと思います
あくまで一例ですが…
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をつなぎ合わせるという考え方が、楽器→エフェクター→アンプの接続を想起させるような作りになっていて面白いと思いました