How can an input node be dynamically connected to a mixer in the active chain in AudioKit iOS?
Environment: AudioKit 4.3, Swift 4.1, Xcode 9.4.1, iOS 11.4.
Problem
I am building an app with dynamic modules consisting of a chain of AKNode objects. These modules are connected to and detached from a dedicated AKMixer node of the running AudioKit engine dynamically as requested. This works well, except when trying to connect any module containing an input node such as AKMicrophone or AKStereoInput, which results in a crash:
2018-06-14 10:13:33.696384-0700 MyApp[3440:2578936] [mcmx] 338: input bus 0 sample rate is 0 2018-06-14 10:13:33.696749-0700 MyApp[3440:2578936] [avae] AVAEInternal.h:103:_AVAE_CheckNoErr: [AVAudioEngineGraph.mm:3632:UpdateGraphAfterReconfig: (AUGraphParser::InitializeActiveNodesInOutputChain(ThisGraph, kOutputChainFullTraversal, *conn.srcNode, isChainActive)): error -10875 2018-06-14 10:13:33.700474-0700 DynamicMic[3440:2578936] *** Terminating app due to uncaught exception 'com.apple.coreaudio.avfaudio', reason: 'error -10875'
Alternatively, calling AudioKit.stop()
, then performing the problematic connect, and then calling AudioKit.start()
fails to start up AudioKit, but it avoids the crash:
AKMicrophone.swift:init():45:Mixer inputs 8 2018-06-14 10:16:09.532277-0700 MyApp[3443:2580588] [mcmx] 338: input bus 0 sample rate is 0 2018-06-14 10:16:09.532603-0700 MyApp[3443:2580588] [avae] AVAEInternal.h:103:_AVAE_CheckNoErr: [AVAudioEngineGraph.mm:1265:Initialize: (err = AUGraphParser::InitializeActiveNodesInOutputChain(ThisGraph, kOutputChainOptimizedTraversal, *GetOutputNode(), isOutputChainActive)): error -10875 2018-06-14 10:16:09.532654-0700 MyApp[3443:2580588] [avae] AVAudioEngine.mm:149:-[AVAudioEngine prepare]: Engine@0x1c0008010: could not initialize, error = -10875 2018-06-14 10:16:09.651495-0700 MyApp[3443:2580588] [mcmx] 338: input bus 0 sample rate is 0 2018-06-14 10:16:09.651549-0700 MyApp[3443:2580588] [avae] AVAEInternal.h:103:_AVAE_CheckNoErr: [AVAudioEngineGraph.mm:1265:Initialize: (err = AUGraphParser::InitializeActiveNodesInOutputChain(ThisGraph, kOutputChainOptimizedTraversal, *GetOutputNode(), isOutputChainActive)): error -10875
The only approach that works is crafting the entire audio node graph in a static fashion, including the AKMicrophone node, setting the output node, then starting AudioKit one time only. However this approach fails to meet the requirement of a dynamic audio node graph required by my app.
Code
Here's a trimmed down version of the managed AudioKit class. Ideally AudioEngine.start()
is called at an entry point such as the AppDelegate didFinishLaunchingWithOptions
method.
import Foundation
import AudioKit
class AudioEngine {
private static var _mainMixer: AKMixer = AKMixer()
// Connected main mixer input nodes.
private static var _mainMixerNodes = [AKNode]()
private static var _isInited = false
private static var _isStarted = false
static func start() {
if !_isInited {
// Clean tempFiles !
AKAudioFile.cleanTempDirectory()
// Session settings
AKSettings.bufferLength = .medium
do {
try AKSettings.setSession(category: .playAndRecord, with: .allowBluetoothA2DP)
} catch {
AKLog("Could not set session category.")
}
AKSettings.defaultToSpeaker = true
_isInited = true
}
if !_isStarted {
AudioKit.output = _mainMixer
print("AudioEngine start: just set output to global mixer")
do {
try AudioKit.start()
AKLog("AudioEngine: AudioKit started")
_isStarted = true
} catch {
AKLog("AudioEngine: AudioKit could not start")
}
}
}
static func stop() {
if _isStarted {
AudioKit.output = nil
do {
try AudioKit.stop()
AKLog("AudioEngine: AudioKit stopped")
_isStarted = false
} catch {
AKLog("AudioEngine: AudioKit could not stop")
}
}
}
static func connect(_ node: AKNode) {
if !_mainMixerNodes.contains(node) {
_mainMixer.connect(input: node)
_mainMixerNodes.append(node)
}
}
static func disconnect(_ node: AKNode) {
if let nodeIndex = _mainMixerNodes.index(of: node) {
node.detach()
_mainMixerNodes.remove(at: nodeIndex)
}
}
}
Later in the app flow, a custom view is opened that uses the microphone via an input node (AKMicrophone). This is where the problem occurs. Here's a thinned version of that:
import UIKit
import AudioKit
class MicViewController: UIViewController {
let mic = AKMicrophone()
override func viewDidLoad() {
super.viewDidLoad()
// Approach 1: Causes a crash.
AudioEngine.connect(mic)
// Approach 2: Stop engine, connect, start engine again. Does not work.
// AudioEngine.stop()
// AudioEngine.connect(mic)
// AudioEngine.start()
}
}