ernewston
ernewston

Reputation: 1015

AudioKit: sync recorder, players and metronome

I'm working on a proof of concept for AudioKit to evaluate if it fits our needs.

We are currently using AVAudioRecorder, AVPlayer and AVMutableComposition for a multi-track recorder and it works great, but I want to try to do the same with AudioKit or AVAudioEngine.

I'm new with AudioKit so I need some guidance:

  1. I'm using AKClipRecorder as the recorder and calling start(at:) to sync with the players, but I think something is not set properly, because there is always a delay in the recording. Why could this be happening? For the players, I'm using an array of AKPlayers and calling play(at:) to sync them, and from my tests, the players seem to sync ok.

  2. I'm having a hard time to find how to sync AKMetronome with the recorder and players, there is no start(at:) API. How can I achieve this? And is it possible to start the recorder and players in a certain beat?

  3. Lastly, is there a way to set the file format of the AKClipRecorder?

I add my code below.

import Foundation
import AudioKit

class MultiTrackRecorder {

    // MARK: - Constants & Vars

    private var mic: AKMicrophone!
    private var micBooster: AKBooster!
    private var mainMixer: AKMixer!
    private var recorder: AKClipRecorder!
    private var players: [AKPlayer] = []
    private var metronome: AKMetronome!

    // MARK: - Public Methods

    func startEngine() throws {
        AKSettings.bufferLength = .medium
        AKSettings.defaultToSpeaker = true
        try AKSettings.setSession(category: .playAndRecord)

        mic = AKMicrophone()

        recorder = AKClipRecorder(node: mic)

        micBooster = AKBooster(mic)
        micBooster.gain = 0

        metronome = AKMetronome()
        metronome.tempo = 120

        mainMixer = AKMixer()
        mainMixer.connect(input: micBooster)
        mainMixer.connect(input: metronome)

        AudioKit.output = mainMixer
        try AudioKit.start()
    }

    func startRecording() throws {
        try recorder.recordClip(completion: recordingEnded)
        let startTime = AVAudioTime.now() + 0.25
        recorder.start(at: startTime)
        players.forEach { $0.play(at: startTime) }
        //metronome.start()
    }

    func stopRecording() {
        recorder.stopRecording()
        recorder.stop()
        players.forEach { $0.stop() }
        metronome.stop()
    }

    func play() {
        let startTime = AVAudioTime.now() + 0.25
        players.forEach { $0.play(at: startTime) }
    }

    func stop() {
        players.forEach { $0.stop() }
    }

    // MARK: - Private Methods

    private func addPlayer(withFileURL url: URL) {
        guard let player = AKPlayer(url: url) else { return }
        players.append(player)
        mainMixer.connect(input: player)
    }

    private func getDocumentsDirectory() -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let documentsDirectory = paths[0]
        return documentsDirectory
    }

    private func getDocsPathWithRandomFileName() -> URL {
        let docsDirectory = getDocumentsDirectory()
        let fileName = "\(UUID().uuidString).caf"
        return docsDirectory.appendingPathComponent(fileName, isDirectory: false)
    }

    private func recordingEnded(url: URL?, somethimg: Double, error: Error?) {
        guard let savedFile = url else {
            print("TODO: Handle this error")
            return
        }
        do {
            let moveToDirectory = getDocsPathWithRandomFileName()
            try FileManager.default.moveItem(at: savedFile, to: moveToDirectory)
            addPlayer(withFileURL: moveToDirectory)
            print("New track has been saved in: \(moveToDirectory.absoluteString)")
        } catch {
            print("TODO: Handle this error")
        }
    }
}

Upvotes: 2

Views: 1015

Answers (1)

dave234
dave234

Reputation: 4955

Try AKSamplerMetronome instead of AKMetronome, it has a play(at:) function.

As for getting the timing right, there are a few things at play. AKMicrophone won't provide correct timestamps, use AudioKit.engine.inputNode instead. If you really want it to be accurate, you will need to compensate for io latency.

let startTime = AVAudioTime.now() + 0.25
let audioSession = AVAudioSession.sharedInstance()

// start players in advance to compensate for hardware output latency
let playbackStart = startTime - audioSession.outputLatency

//start recorders late to compensate for hardware input latency.
let recordingStart = startTime + audioSession.inputLatency

I just posted an example where loopback audio is compared for accuracy in the repo yesterday.

Upvotes: 3

Related Questions