[iOS] AVAudioEngineでエフェクトをかけてみる
こんにちは。きんくまです。
iOSで音をならしてみたかったです。それで、鳴らした後にエフェクトもかけてみました。
つくったもの
音量注意!!
機能
- 音声ファイルの再生/停止
- ディレイエフェクト
- ディストーションエフェクト
- 再生速度変更
ソースコード全部
最初に全部載せてしまいます
import UIKit import AVFoundation class ViewController: UIViewController { @IBOutlet weak var playStopButton: ToggleButton! @IBOutlet weak var delayButton: ToggleButton! @IBOutlet weak var distortionButton: ToggleButton! @IBOutlet weak var speedButon: ToggleButton! @IBOutlet weak var delaySlider: UISlider! @IBOutlet weak var speedSlider: UISlider! var audioFile: AVAudioFile? var audioEngine: AVAudioEngine? var audioPlayer: AVAudioPlayerNode? var needsFileScheduled = true var varispeedNode: AVAudioUnitVarispeed? var delayNode: AVAudioUnitDelay? var distortionNode: AVAudioUnitDistortion? override func viewDidLoad() { super.viewDidLoad() playStopButton.setTitle("play", for: .normal) delaySlider.minimumValue = 0 delaySlider.maximumValue = 1 delaySlider.setValue(0.1, animated: false) speedSlider.minimumValue = 0.2 speedSlider.maximumValue = 2 speedSlider.setValue(0.5, animated: false) setupAudioSession() setupAudioFile() guard let format = audioFile?.processingFormat else { return } configureEngine(with: format) } func setupAudioSession() { do { let session = AVAudioSession.sharedInstance() try session.setCategory(.playback) try session.overrideOutputAudioPort(.speaker) try session.setActive(true, options: []) } catch let error { print("dbg session error \(error.localizedDescription)") } } func setupAudioFile() { do { guard let bundleUrl = Bundle.main.url(forResource: "bensound-jazzyfrenchy.mp3", withExtension: nil) else { return } let file = try AVAudioFile(forReading: bundleUrl) audioFile = file } catch let error { print("error \(error.localizedDescription)") } } func configureEngine(with format: AVAudioFormat) { let engine = AVAudioEngine() audioEngine = engine let player: AVAudioPlayerNode player = AVAudioPlayerNode() audioPlayer = player engine.attach(player) let varispeed = AVAudioUnitVarispeed() varispeed.rate = speedSlider.value engine.attach(varispeed) varispeedNode = varispeed let delay = AVAudioUnitDelay() delay.delayTime = TimeInterval(delaySlider.value) engine.attach(delay) delayNode = delay let dist = AVAudioUnitDistortion() let distPreset = AVAudioUnitDistortionPreset.drumsBufferBeats dist.loadFactoryPreset(distPreset) engine.attach(dist) distortionNode = dist } func scheduleAudioFile() { guard let engine = audioEngine, let file = audioFile, let player = audioPlayer else { return } engine.reset() let mainMixer = engine.mainMixerNode let format = file.processingFormat var effects: [AVAudioNode] = [] if delayButton.isOn, let delay = delayNode { effects.append(delay) } if distortionButton.isOn, let distortion = distortionNode { effects.append(distortion) } if speedButon.isOn, let speed = varispeedNode { effects.append(speed) } if effects.count == 0 { engine.connect(player, to: mainMixer, format: format) } else { var currentNode: AVAudioNode = player effects.forEach { (newNode) in engine.connect(currentNode, to: newNode, format: format) currentNode = newNode } engine.connect(currentNode, to: mainMixer, format: format) } let output = engine.outputNode engine.connect(mainMixer, to: output, format: format) player.scheduleFile(file, at: nil) { [weak self] in self?.needsFileScheduled = true } } func playSound() { if needsFileScheduled { scheduleAudioFile() } if let running = audioEngine?.isRunning, !running { do { try audioEngine?.start() } catch let error { print("error \(error.localizedDescription)") } } audioPlayer?.play() playStopButton.setTitle("stop", for: .normal) playStopButton.setStateOn() } func stopSound() { audioPlayer?.stop() playStopButton.setTitle("play", for: .normal) playStopButton.setStateOff() } func playOrStop() { if let player = audioPlayer, player.isPlaying { stopSound() } else { playSound() } } @IBAction func didTapPlayButton() { playOrStop() } @IBAction func didTapDelayButton() { delayButton.toggleState() if let isPlaying = audioPlayer?.isPlaying, isPlaying { playSound() } } @IBAction func didTapDistortionButton() { distortionButton.toggleState() if let isPlaying = audioPlayer?.isPlaying, isPlaying { playSound() } } @IBAction func didTapSpeedButton() { speedButon.toggleState() if let isPlaying = audioPlayer?.isPlaying, isPlaying { playSound() } } @IBAction func didChangeDelaySlider() { delayNode?.delayTime = TimeInterval(delaySlider.value) } @IBAction func didChangeSpeedSlider() { varispeedNode?.rate = speedSlider.value } }
こっちは本編に関係ないのですが、上のソースに入っているので載せます
class ToggleButton: UIButton { var isOn: Bool = false override func awakeFromNib() { super.awakeFromNib() if isOn { setStateOn() } else { setStateOff() } } func setStateOn() { isOn = true // 298ED6 let highlight = UIColor(hue: 205.0 / 360.0, saturation: 0.81, brightness: 0.84, alpha: 1) layer.cornerRadius = 10 layer.borderWidth = 1 layer.borderColor = highlight.cgColor backgroundColor = highlight setTitleColor(.white, for: .normal) } func setStateOff() { isOn = false layer.cornerRadius = 10 layer.borderWidth = 1 layer.borderColor = UIColor.gray.cgColor backgroundColor = .white setTitleColor(.black, for: .normal) } func toggleState() { if isOn { setStateOff() } else { setStateOn() } } }
AVAudioEngineって?
AVFoundationは、音声と描画を管理します。
音声処理はCやC++を使いそうですが、swiftから操作できるのがポイントなのかと。
AVFoundationの中で音声処理ができるのがAVAudioEngineです。
AVAudioEngine
AVAudioNodeと呼ばれるノードを入力から出力までつなげてあげれば音がなります。
入力(ファイルや動的生成された波形データなど)-> AVAudioPlayerNode -> エフェクトNode(効果) -> ミキサーNode(音量調整) -> 出力(スピーカーやヘッドフォン)
一番簡単な構成だと以下になります。
AVAudioPlayerNode -> AVAudioEngineのmainMixerNode(AVAudioMixerNode)
初期化処理
func setupAudioSession() { do { let session = AVAudioSession.sharedInstance() try session.setCategory(.playback) try session.overrideOutputAudioPort(.speaker) try session.setActive(true, options: []) } catch let error { print("dbg session error \(error.localizedDescription)") } } func setupAudioFile() { do { guard let bundleUrl = Bundle.main.url(forResource: "bensound-jazzyfrenchy.mp3", withExtension: nil) else { return } let file = try AVAudioFile(forReading: bundleUrl) audioFile = file } catch let error { print("error \(error.localizedDescription)") } }
setupAudioSessionは処理をやらなくても音自体はなるのですが、再生と停止を繰り返したときの1回目の音量がおかしなことになりました。
なので必要なのかと。
setupAudioFileでmp3のファイルからAVAudioEngineで扱える形式に変えています。
内部では難しいこと(ファイル形式ごとのデコード処理をしてるとか)をしてそうですが、開発者的には数行ですんでいます。
EngineとNode初期化
func configureEngine(with format: AVAudioFormat) { let engine = AVAudioEngine() audioEngine = engine let player: AVAudioPlayerNode player = AVAudioPlayerNode() audioPlayer = player engine.attach(player) let varispeed = AVAudioUnitVarispeed() varispeed.rate = speedSlider.value engine.attach(varispeed) varispeedNode = varispeed let delay = AVAudioUnitDelay() delay.delayTime = TimeInterval(delaySlider.value) engine.attach(delay) delayNode = delay let dist = AVAudioUnitDistortion() let distPreset = AVAudioUnitDistortionPreset.drumsBufferBeats dist.loadFactoryPreset(distPreset) engine.attach(dist) distortionNode = dist }
Engineと必要なNodeをインスタンス化しています。AVAudioPlayerNodeはわざわざ変数制限をしています。
これは以前にバグがあったのをネットの記事で見たので、それの回避処理に合わせてやっています。最近だと必要ないのかも
let player: AVAudioPlayerNode player = AVAudioPlayerNode() //本当だったらこれでもよさそうだけど、そうしていない let player = AVAudioPlayerNode()
Nodeをつなぐ
scheduleAudioFileの中
if effects.count == 0 { engine.connect(player, to: mainMixer, format: format) } else { var currentNode: AVAudioNode = player effects.forEach { (newNode) in engine.connect(currentNode, to: newNode, format: format) currentNode = newNode } engine.connect(currentNode, to: mainMixer, format: format) } let output = engine.outputNode engine.connect(mainMixer, to: output, format: format)
UIでEffectボタンのスイッチがOnになっていたら、Nodeを入力と出力の間にはさむ処理をしています。
Playerにfileをセット
player.scheduleFile(file, at: nil) { [weak self] in self?.needsFileScheduled = true }
再生
func playSound() { if needsFileScheduled { scheduleAudioFile() } if let running = audioEngine?.isRunning, !running { do { try audioEngine?.start() } catch let error { print("error \(error.localizedDescription)") } } audioPlayer?.play() }
Engineは一度startすれば、Onにしっぱなしで問題ないと思うので、条件分岐で1回だけするようにしています。
最後にplayerをplayすれば再生開始です。
エフェクトのスライダー
@IBAction func didChangeDelaySlider() { delayNode?.delayTime = TimeInterval(delaySlider.value) }
Nodeの設定をUIのSliderの値を使って調整しています。これは、リアルタイムに変更しても適用されました。
感想
面白かったです
参考リンク
AVAudioEngine Tutorial for iOS: Getting Started
[iOS 8] AVFoundationのAVAudioPlayerNodeで音楽ファイルを再生してみる
iOSアプリ開発 入門 (4) - AVAudioEngine