AudioKit:AKNodeOutputPlot和AKMicrophone无法正常运行,可能是由于生命周期或MVVM体系结构决定

时间:2019-01-15 23:07:17

标签: ios audiokit

在学习AudioKit并在更大的应用程序中进行扩展的早期,我接受了标准建议,即AudioKit should be effectively be a global singleton.我设法构建了一个非常复杂的原型,并且在全世界都很好。

一旦我开始扩大规模并接近实际发行版。我们决定将MVVM用于我们的体系结构,并尝试不使用庞大的AudioKit Singelton来处理应用程序中音频需求的各个方面。简而言之,MVVM是如此的优雅,并已证明清理了我们的代码库。

与我们的AudioKit结构直接相关,它是这样的:

AudioKit和AKMixer驻留在Singelton实例中,并具有公共功能,该功能允许各种视图模型和我们的其他音频模型连接和分离各个节点(AKPlayerAKSampler,等等...)。在完成的最低限度测试中,我可以确认它与AKPlayer模块一起尝试时可以正常工作,并且效果很好。

我遇到了一个问题,尽管实际的代码实现与我的工作原型完全相同,但我一生无法让AKNodeOutputPlotAKMicrophone相互配合。

我担心的是我做错了什么,认为我可以模块化AudioKit以及需要连接到它的各个节点和组件,还是AKNodeOutputPlot有我不知道的特殊要求。

以下是我可以提供的最简短的代码片段,而不会压倒这个问题:

AudioKit Singelton(在AppDelegate中调用):

import Foundation
import AudioKit

class AudioKitConfigurator
{
    static let shared: AudioKitConfigurator = AudioKitConfigurator()

    private let mainMixer: AKMixer = AKMixer()

    private init()
    {
        makeMainMixer()
        configureAudioKitSettings()
        startAudioEngine()
    }

    deinit
    {
        stopAudioEngine()
    }

    private func makeMainMixer()
    {
        AudioKit.output = mainMixer
    }

    func mainMixer(add node: AKNode)
    {
        mainMixer.connect(input: node)
    }

    func mainMixer(remove node: AKNode)
    {
        node.detach()
    }

    private func configureAudioKitSettings()
    {
        AKAudioFile.cleanTempDirectory()
        AKSettings.defaultToSpeaker = true
        AKSettings.playbackWhileMuted = true
        AKSettings.bufferLength = .medium

        do
        {
            try AKSettings.setSession(category: .playAndRecord, with: .allowBluetoothA2DP)
        }

        catch
        {
            AKLog("Could not set session category.")
        }

    }

    private func startAudioEngine()
    {
        do
        {
            try AudioKit.start()
        }
        catch
        {
            AKLog("Fatal Error: AudioKit did not start!")
        }
    }

    private func stopAudioEngine()
    {
        do
        {
            try AudioKit.stop()
        }
        catch
        {
            AKLog("Fatal Error: AudioKit did not stop!")
        }
    }
}

麦克风组件:

import Foundation
import AudioKit
import AudioKitUI

enum MicErrorsToThrow: String, Error
{
    case recordingTooShort          = "The recording was too short, just silently failing"
    case audioFileFailedToUnwrap    = "The Audio File failed to Unwrap from the recorder"
    case recorderError              = "The Recorder was unable to start recording."
    case recorderCantReset          = "In attempt to reset the recorder, it was unable to"
}

class Microphone
{
    private var mic:            AKMicrophone    = AKMicrophone()
    private var micMixer:       AKMixer         = AKMixer()
    private var micBooster:     AKBooster       = AKBooster()
    private var recorder:       AKNodeRecorder!
    private var recordingTimer: Timer

    init()
    {
        micMixer = AKMixer(mic)
        micBooster = AKBooster(micMixer)
        micBooster.gain = 0
        recorder = try? AKNodeRecorder(node: micMixer)

        //TODO: Need to finish the recording timer implementation, leaving blank for now
        recordingTimer = Timer(timeInterval: 120, repeats: false, block: { (timer) in

        })

        AudioKitConfigurator.shared.mainMixer(add: micBooster)
    }

    deinit {
//      removeComponent()
    }

    public func removeComponent()
    {
        AudioKitConfigurator.shared.mainMixer(remove: micBooster)
    }

    public func reset() throws
    {
        if recorder.isRecording
        {
            recorder.stop()
        }
        do
        {
            try recorder.reset()
        }
        catch
        {
            AKLog("Recorder can't reset!")
            throw MicErrorsToThrow.recorderCantReset
        }
    }

    public func setHeadphoneMonitoring()
    {
        // microphone will be monitored while recording
        // only if headphones are plugged
        if AKSettings.headPhonesPlugged {
            micBooster.gain = 1
        }
    }

    /// Start recording from mic, call this function when using in conjunction with a AKNodeOutputPlot so that it can display the waveform in realtime while recording
    ///
    /// - Parameter waveformPlot: AKNodeOutputPlot view object which displays waveform from recording
    /// - Throws: Only error to throw is from recorder property can't start recording, something wrong with microphone. Enum is MicErrorsToThrow.recorderError
    public func record(waveformPlot: AKNodeOutputPlot) throws
    {
        waveformPlot.node = mic
        do
        {
            try recorder.record()
//          self.recordingTimer.fire()
        }
        catch
        {
            print("Error recording!")
            throw MicErrorsToThrow.recorderError
        }
    }

    /// Stop the recorder, and get the recording as an AKAudioFile, necessary to call if you are using AKNodeOutputPlot
    ///
    /// - Parameter waveformPlot: AKNodeOutputPlot view object which displays waveform from recording
    /// - Returns: AKAudioFile
    /// - Throws: Two possible errors, recording was too short (right now is 0.0, but should probably be like 0.5 secs), or could not retrieve audio file from recorder, MicErrorsToThrow.audioFileFailedToUnwrap, MicErrorsToThrow.recordingTooShort
    public func stopRecording(waveformPlot: AKNodeOutputPlot) throws -> AKAudioFile
    {
        waveformPlot.pause()
        waveformPlot.node = nil

        recordingTimer.invalidate()
        if let tape = recorder.audioFile
        {
            if tape.duration > 0.0
            {
                recorder.stop()
                AKLog("Printing tape: CountOfFloatChannelData:\(tape.floatChannelData?.first?.count) | maxLevel:\(tape.maxLevel)")
                return tape
            }
            else
            {
                //TODO: This should be more gentle than an NSError, it's just that they managed to tap the buttona and tap again to record nothing, honestly duration should probbaly be like 0.5, or 1.0 even. But let's return some sort of "safe" error that doesn't require UI
                throw MicErrorsToThrow.recordingTooShort
            }
        }
        else
        {
            //TODO: need to return error here, could not recover audioFile from recorder
            AKLog("Can't retrieve or unwrap audioFile from recorder!")
            throw MicErrorsToThrow.audioFileFailedToUnwrap
        }
    }
}

现在,在我的VC中,AKNodeOutputPlot是Storybard上的视图,并通过IBOutlet进行了连接。它呈现在屏幕上,按照我的喜好进行了样式化,并且确实可以连接并正常工作。在VC / VM中,也是我的Microphone组件的实例属性。我的想法是,在录制时,我们将nodeOutput对象传递给ViewModel,然后将其调用record(waveformPlot: AKNodeOutputPlot)的{​​{1}}函数,然后Microphone就足以将它们连接起来。不幸的是,事实并非如此。

查看:

waveformPlot.node = mic

ViewModel:

class ComposerVC: UIViewController, Storyboarded
{
    var coordinator: MainCoordinator?
    let viewModel: ComposerViewModel = ComposerViewModel()

    @IBOutlet weak var recordButton: RecordButton!
    @IBOutlet weak var waveformPlot: AKNodeOutputPlot! // Here is our waveformPlot object, again confirmed rendering and styled

    // MARK:- VC Lifecycle Methods
    override func viewDidLoad()
    {
        super.viewDidLoad()

        setupNavigationBar()
        setupConductorButton()
        setupRecordButton()
    }

    func setupWaveformPlot() {
        waveformPlot.plotType = .rolling
        waveformPlot.gain = 1.0
        waveformPlot.shouldFill = true
    }

    override func viewDidAppear(_ animated: Bool)
    {
        super.viewDidAppear(animated)

        setupWaveformPlot()

        self.didDismissComposerDetailToRootController()
    }

    // Upon touching the Record Button, it in turn will talk to ViewModel which will then call Microphone module to record and hookup waveformPlot.node = mic
    @IBAction func tappedRecordView(_ sender: Any)
    {
        self.recordButton.recording.toggle()
        self.recordButton.animateToggle()
        self.viewModel.tappedRecord(waveformPlot: waveformPlot)
        { (waveformViewModel, error) in
            if let waveformViewModel = waveformViewModel
            {
                self.segueToEditWaveForm()
                self.performSegue(withIdentifier: "composerToEditWaveForm", sender: waveformViewModel)
                //self.performSegue(withIdentifier: "composerToDetailSegue", sender: self)
            }
        }
    }

我很乐意提供更多代码,但是我只是不想让他们的时间不知所措。逻辑似乎很合理,我只是觉得我缺少明显的东西,或者是AudioKit + AKNodeOutputPlot + AKMicrohone的完全误解。

任何想法都非常受欢迎,谢谢!

1 个答案:

答案 0 :(得分:1)

编辑 AudioKit 4.6修复了所有问题!高度鼓励您的项目使用MVVM / AudioKit模块化!

====

因此,经过大量实验。我得出了一些结论:

  1. 在一个单独的项目中,我带来了AudioKitConfiguratorMicrophone类,对其进行了初始化,并将它们钩接到AKNodeOutputPlot上,并且运行正常。

    < / li>
  2. 在我的大型项目中,无论做什么,我都无法使用相同的类。

现在,我将恢复到旧版本,慢慢添加组件直到它再次崩溃,然后将逐个更新体系结构,因为这个问题太复杂了,可能正在与其他一些库交互。我还从AudioKit 4.5.6降级到AudioKit 4.5.3。

这不是解决方案,而是目前唯一可行的解​​决方案。好消息是,完全有可能格式化AudioKit以使其与MVVM体系结构一起使用。