Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

}
like image 817
Steve D. Avatar asked Jun 14 '18 17:06

Steve D.


1 Answers

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
like image 140
dave234 Avatar answered Oct 07 '22 00:10

dave234