AudioKit iOS: how can an input node be dynamically connected to mixer in active chain?
Asked Answered
E

1

6

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()
    }

}
Escapee answered 14/6, 2018 at 17:35 Comment(4)
Hey, did you solve this issue?Schilt
@DimaGimburg No resolution yet. I was force updated to new betas of macOS, Xcode, iOS (long story), encountered major breakage and need to recompile or newly link AudioKit. It will be at least a week until I can look at it again.Escapee
Thank you very much. Please let me know if you got something working, I've posted yesterday a question in the same domain of connecting node dynamically to an already connected to output mixer. Hope to get an answer and hope to get it solved somehow because this SDK is much more easier and intuitive to work with than native kits.Schilt
I'd also interested to hear about solutions to this problem. I'm having similar issues in connecting nodes dynamically, whereby connecting seems to be okay, with no errors or warnings, but the connected node fails to produce any audio.Liberalism
E
3

In disconnect, detach will detach the node from the underlying AVAudioEngine. AKMicrophone's underlying node it's a property of AVAudioEngine, so it's probably better to just disconnect it.

let disconnect = node is AKMicrophone ? disconnectOutput : detach
node.disconnect()

But muting it is easier.

mic.volume = 0
Extremism answered 14/6, 2018 at 18:19 Comment(1)
That is a great tip and I've put it in my code. However it doesn't solve the issue which occurs upon connect() of the AKMicrophone node to a running AKMixer.Escapee

© 2022 - 2024 — McMap. All rights reserved.