この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは。きんくまです。
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