AudioKit: AKNodeOutputPlot и AKMicrophone не работают, возможно, из-за решений по жизненному циклу или архитектуре MVVM - PullRequest
0 голосов
/ 16 января 2019

В начале моего обучения с AudioKit и масштабирования в более крупном приложении я принял стандартный совет, что AudioKit должен быть по сути глобальным синглтоном. Мне удалось создать действительно сложный прототип, и все было хорошо в мире.

Как только я начал расширяться и приближаться к реальному релизу. Мы решили использовать MVVM для нашей архитектуры и постараться не иметь чудовищно большого AudioKit Singelton, чтобы удовлетворить все аспекты наших аудио потребностей в приложении. Короче говоря, MVVM был невероятно элегантным и наглядно очистил нашу кодовую базу.

В прямой связи с нашей структурой AudioKit это выглядит примерно так:

AudioKit и AKMixer находятся в экземпляре Singelton и имеют общедоступные функции, которые позволяют различным моделям представления и нашим другим моделям Audio подключать и отключать различные узлы (AKPlayer, AKSampler и т. Д.). В минимальном тестировании, которое я провел, я могу подтвердить, что это работает, поскольку я попробовал это с моим модулем AKPlayer, и это прекрасно работает.

Я сталкиваюсь с проблемой, когда я не могу, на всю жизнь, заставить AKNodeOutputPlot и AKMicrophone работать друг с другом, несмотря на то, что фактическая реализация кода идентична моим рабочим прототипам.

Меня беспокоит, сделал ли я неправильную вещь, думая, что мог бы модульно установить 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
        }
    }
}

Теперь, в моем ВК, AKNodeOutputPlot - это вид на Сторибарда, подключенный через IBOutlet. Он визуализируется на экране, он стилизован по моему вкусу, и он определенно подключен и работает. Также в VC / VM есть свойство экземпляра моего Microphone компонента. Я думал, что при записи мы передадим объект nodeOutput в ViewModel, который затем вызовет функцию record(waveformPlot: AKNodeOutputPlot) из Microphone, которой тогда будет waveformPlot.node = mic достаточно, чтобы подключить их. К сожалению, это не так.

Вид:

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

ViewModel:

import Foundation
import AudioKit
import AudioKitUI

class ComposerViewModel: ViewModelProtocol
{

//MARK:- Instance Variables
var recordingState: RecordingState

var mic:            Microphone                      = Microphone()

init()
{
    self.recordingState = .readyToRecord
}


func resetViewModel()
{
    self.resetRecorder()
}

func resetRecorder()
{
    do
    {
        try mic.reset()
    }
    catch let error as MicErrorsToThrow
    {
        switch error {
        case .audioFileFailedToUnwrap:
            print(error)
        case .recorderCantReset:
            print(error)
        case .recorderError:
            print(error)
        case .recordingTooShort:
            print(error)
        }
    }
    catch {
        print("Secondary catch in start recording?!")
    }
    recordingState = .readyToRecord
}

func tappedRecord(waveformPlot: AKNodeOutputPlot, completion: ((EditWaveFormViewModel?, Error?) -> ())? = nil)
{
    switch recordingState
    {
    case .readyToRecord:
        self.startRecording(waveformPlot: waveformPlot)

    case .recording:
        self.stopRecording(waveformPlot: waveformPlot, completion: completion)

    case .finishedRecording: break
    }
}

func startRecording(waveformPlot: AKNodeOutputPlot)
{

    recordingState = .recording
    mic.setHeadphoneMonitoring()
    do
    {
        try mic.record(waveformPlot: waveformPlot)
    }

    catch let error as MicErrorsToThrow
    {
        switch error {
        case .audioFileFailedToUnwrap:
            print(error)
        case .recorderCantReset:
            print(error)
        case .recorderError:
            print(error)
        case .recordingTooShort:
            print(error)
        }
    }
    catch {
        print("Secondary catch in start recording?!")
    }
}

Я рад предоставить больше кода, но я просто не хочу перегружать их временем. Логика кажется здравой, я просто чувствую, что упускаю что-то очевидное и / или полное недопонимание AudioKit + AKNodeOutputPlot + AKMicrohone.

Любые идеи приветствуются, спасибо!

1 Ответ

0 голосов
/ 18 января 2019

EDIT AudioKit 4.6 исправил все проблемы! Настоятельно поощряйте MVVM / Модуляризацию AudioKit для ваших проектов!

====

Так что после многих экспериментов. Я пришел к нескольким выводам:

  1. В отдельном проекте я перенес свои классы AudioKitConfigurator и Microphone, инициализировал их, подключил их к AKNodeOutputPlot, и он работал безупречно.

  2. В моем очень большом проекте, независимо от того, что я делаю, я не могу заставить работать одни и те же классы.

Сейчас я возвращаюсь к старой сборке, медленно добавляя компоненты, пока она снова не сломается, и буду обновлять архитектуру один за другим, поскольку эта проблема слишком сложна и может взаимодействовать с некоторыми другими библиотеками. Я также опустил версию с AudioKit 4.5.6 до AudioKit 4.5.3.

Это не решение, а единственное, которое работает сейчас. Хорошей новостью является то, что AudioKit вполне можно отформатировать для работы с архитектурой MVVM.

...