AVAudioEngine Real-Time Effects
May 31, 2020
Thanks to
AVPlayer
, playing back audio files isn't complicated on iOS and macOS. But what
if you want to add real-time effects to audio?
AVAudioEngine
makes that very easy as well.
Let's see how to do so:
Audio Playback
Before adding any effects, we'll need to set up a basic audio player.
First, we'll create the AudioPlayer
class:
import AVFoundation
class AudioPlayer {
var engine: AVAudioEngine!
var player: AVAudioPlayerNode!
func setup(url: URL) {
// TODO: Setup
}
}
These variables will contain the audio engine and the player node later on.
AVAudioEngine
is based on the concept of nodes. A node
can provide, modify, or output audio. You can connect these nodes to
create custom audio players, for example. In this case, an
AVAudioPlayerNode
will provide the audio.
Let's start creating the setup
function. First, we'll need
to initialize the audio engine and player:
engine = AVAudioEngine()
player = AVAudioPlayerNode()
Next, we'll set the app's audio session category to "playback":
try? AVAudioSession.sharedInstance().setCategory(.playback)
Let's open the audio file as an AVAudioFile
with the url
passed into the setup function:
guard let file = try? AVAudioFile(forReading: url) else {
fatalError("Couldn't read file")
}
After having opened the file, we can create an audio buffer with the file's length and format…
guard let buffer = AVAudioPCMBuffer(pcmFormat: file.processingFormat, frameCapacity: AVAudioFrameCount(file.length)) {
fatalError("Couldn't create buffer")
}
…and read the audio file into the buffer:
try? file.read(into: buffer)
// TODO: Add effects
For this example, we'll let the player loop the audio file:
player.scheduleBuffer(buffer, at: nil, options: AVAudioPlayerNodeBufferOptions.loops, completionHandler: nil)
In the (very basic) explanation of the node-based architecture of
AVAudioEngine
, I mentioned that different nodes have to be
connected. We'll now attach the player node to the audio engine, and
connect the player to the engine's mixer node:
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: buffer.format)
Finally, we'll start the audio engine and begin playing the audio file:
engine.prepare()
try? engine.start()
player.play()
Adding Effects
Since we've now set up the player, we're ready to add effects. The
following code will need to be added to where we previously inserted the
todo (// TODO: Add effects
). I'll demonstrate how to add an
effect node with the
AVAudioUnitDistortion
audio unit node.
Let's create the distortion effect and load a factory preset:
let distortion = AVAudioUnitDistortion()
distortion.loadFactoryPreset(.drumsBitBrush)
That's basically it! Now we can attach and connect the effect node. To
do so, replace the previously added engine.connect(…)
with
these new connections:
engine.attach(distortion)
engine.connect(player, to: distortion, format: buffer.format)
engine.connect(distortion, to: engine.mainMixerNode, format: buffer.format)
Here we connect the player to the distortion effect and the distortion effect to the main mixer node. This way the player audio will be routed through the effect audio unit node before being played back.
More Effects
Here are some other effects, you can try out:
You can even chain different effects together by connecting them (example: player → eq → distortion → main mixer).
Usage
Our finished player class can be used like this:
let player = AudioPlayer()
player.setup(url: /* some URL */)
All you need to do is replace /* some URL */
with a URL
from an audio file.
Conclusion
The node-based architecture of AVAudioEngine
allows you to
create custom audio players with very little effort. If you just need to
play audio, AVPlayer
is the API to use. But for more
complicated use cases AVAudioEngine
is awesome! You can
even
build synthesizers with it.