I'm trying to dynamically modify a graph of nodes in attached to an AVAudioEngine
, but the graph outputs silence after every reconnection.
In the below sample, I'd like to dynamically connect player
to engine.mainMixerNode
, but whenever I call toggleBypass
I get silence.
Is it possible to carry out this rewiring without pausing playback of the AVAudioPlayerNode
?
class Sample: UIViewController {
let engine = AVAudioEngine()
let player = AVAudioPlayerNode()
let effectNode = AVAudioUnitDelay()
@objc func toggleBypass() {
if effectNode.numberOfInputs == 0 {
engine.connect(player, to: effectNode, format: file.processingFormat)
engine.connect(effectNode, to: engine.mainMixerNode, format: file.processingFormat)
} else {
engine.connect(player, to: engine.mainMixerNode, format: file.processingFormat)
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(toggleBypass)))
try! AVAudioSession.sharedInstance().setCategory(.playback)
try! AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.005)
try! AVAudioSession.sharedInstance().setActive(true, options: [])
do {
engine.attach(player)
engine.attach(effectNode)
engine.connect(player, to: effectNode, format: file.processingFormat)
engine.connect(effectNode, to: engine.mainMixerNode, format: file.processingFormat)
player.scheduleBuffer(buffer, at: nil, options: .loops, completionHandler: nil)
engine.prepare()
try engine.start()
player.play()
} catch {
assertionFailure(String(describing: error))
}
}
lazy var file: AVAudioFile = {
let fileURL = Bundle.main.url(forResource: "beber", withExtension: "mp3")!
return try! AVAudioFile(forReading: fileURL)
}()
lazy var buffer: AVAudioPCMBuffer = {
let buffer = AVAudioPCMBuffer(pcmFormat: file.processingFormat, frameCapacity: UInt32(file.length))!
try! file.read(into: buffer)
return buffer
}()
}
(Yes, I know I can update the effect's bypass
property, but I'm interested knowing what the steps are to dynamically rewire a graph)
After further research, I realise that (as of iOS 13) it is not possible to carry out this rewiring without pausing playback of the AVAudioPlayerNode
.
Re-connecting a player node uses the connect(_:to:fromBus:toBus:format:)
method, which first disconnects the node from the node it was previously connected to, thus stopping playback.
However, you can use a different connect method (connect(_:to:fromBus:format:)
to connect the player to several effects as well as to a mixer that plays through:
let engine = AVAudioEngine()
let player = AVAudioPlayerNode()
let effectOne = AVAudioUnitTimePitch()
let effectTwo = AVAudioUnitReverb()
let mixerThru = AVAudioMixerNode()
let mixerOne = AVAudioMixerNode()
let mixerTwo = AVAudioMixerNode()
engine.attach(player)
engine.attach(effectOne)
engine.attach(effectTwo)
engine.attach(mixerThru)
engine.attach(mixerOne)
engine.attach(mixerTwo)
player.scheduleBuffer(buffer, at: nil, options: .loops, completionHandler: nil)
let effectThruConnectionPoint = AVAudioConnectionPoint(node: mixerThru, bus: 0)
let effectOneConnectionPoint = AVAudioConnectionPoint(node: effectOne, bus: 0)
let effectTwoConnectionPoint = AVAudioConnectionPoint(node: effectTwo, bus: 0)
engine.connect(player, to: [effectThruConnectionPoint, effectOneConnectionPoint, effectTwoConnectionPoint], fromBus: 0, format: nil)
engine.connect(effectOne, to: mixerOne, format: file.processingFormat)
engine.connect(effectTwo, to: mixerTwo, format: file.processingFormat)
engine.connect(mixerThru, to: engine.mainMixerNode, format: file.processingFormat)
engine.connect(mixerOne, to: engine.mainMixerNode, format: file.processingFormat)
engine.connect(mixerTwo, to: engine.mainMixerNode, format: file.processingFormat)
mixerTwo.outputVolume = 0
mixerThru.outputVolume = 0
(Props to RH for the idea and code)