2

I'm writing a first in first out recording app that buffers up to 2.5 mins of audio using AudioQueue. I've got most of it figured out but I'm at a roadblock trying to crop audio data.

I've seen people do it with AVAssetExportSession but it seems like it wouldn't be performant to export a new track every time the AudioQueueInputCallback is called.

I'm not married to using AVAssestExportSession by any means if anyone has a better idea.

Here's where I'm doing my write and was hoping to execute the crop.

 var beforeSeconds = TimeInterval() // find the current estimated duration (not reliable)
    var propertySize = UInt32(MemoryLayout.size(ofValue: beforeSeconds))
    var osStatus = AudioFileGetProperty(audioRecorder.recordFile!, kAudioFilePropertyEstimatedDuration, &propertySize, &beforeSeconds)

    if numPackets > 0 {
      AudioFileWritePackets(audioRecorder.recordFile!, // write to disk
                                       false,
                                       buffer.mAudioDataByteSize,
                                       packetDescriptions,
                                       audioRecorder.recordPacket,
                                       &numPackets,
                                       buffer.mAudioData)
      audioRecorder.recordPacket += Int64(numPackets) // up the packet index

      var afterSeconds = TimeInterval() // find the after write estimated duration (not reliable)
      var propertySize = UInt32(MemoryLayout.size(ofValue: afterSeconds))
      var osStatus = AudioFileGetProperty(audioRecorder.recordFile!, kAudioFilePropertyEstimatedDuration, &propertySize, &afterSeconds)
      assert(osStatus == noErr, "couldn't get record time")

      if afterSeconds >= 150.0 {
        print("hit max buffer!")
        audioRecorder.onBufferMax?(afterSeconds - beforeSeconds)
      }
    }

Here's where the callback is executed

func onBufferMax(_ difference: Double){
    let asset = AVAsset(url: tempFilePath)
    let duration = CMTimeGetSeconds(asset.duration)
    guard duration >= 150.0 else { return }

    guard let exporter = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetAppleM4A) else {
      print("exporter init failed")
      return }


    exporter.outputURL = getDocumentsDirectory().appendingPathComponent("buffered.caf") // helper function that calls the FileManager
    exporter.outputFileType = AVFileTypeAppleM4A

    let startTime = CMTimeMake(Int64(difference), 1)
    let endTime = CMTimeMake(Int64(WYNDRConstants.maxTimeInterval + difference), 1)

    exporter.timeRange = CMTimeRangeFromTimeToTime(startTime, endTime)
    exporter.exportAsynchronously(completionHandler: {
      switch exporter.status {
      case .failed:
        print("failed to export")
      case .cancelled:
        print("canceled export")
      default:
        print("export successful")
      }
    })
  }
aBikis
  • 323
  • 4
  • 16
  • 1
    So whenever you stop recording, you want to have the last 2.5 minutes of audio, providing you've been recording for at least 2.5 minutes? – Rhythmic Fistman Jun 01 '17 at 23:22
  • Yep! And if it's been recording less than 2.5 minutes I want whatever amount of time that is. – aBikis Jun 02 '17 at 00:41

1 Answers1

4

A ring buffer is a useful structure for storing, either in memory or on disk, the most recent n seconds of audio. Here is a simple solution that stores the audio in memory, presented in the traditional UIViewController format.

N.B 2.5 minutes of 44.1kHz audio stored as floats requires about 26MB of RAM, which is on the heavy side for a mobile device.

import AVFoundation

class ViewController: UIViewController {
    let engine = AVAudioEngine()

    var requiredSamples: AVAudioFrameCount = 0
    var ringBuffer: [AVAudioPCMBuffer] = []
    var ringBufferSizeInSamples: AVAudioFrameCount = 0

    func startRecording() {
        let input = engine.inputNode!

        let bus = 0
        let inputFormat = input.inputFormat(forBus: bus)

        requiredSamples = AVAudioFrameCount(inputFormat.sampleRate * 2.5 * 60)

        input.installTap(onBus: bus, bufferSize: 512, format: inputFormat) { (buffer, time) -> Void in
            self.appendAudioBuffer(buffer)
        }

        try! engine.start()
    }

    func appendAudioBuffer(_ buffer: AVAudioPCMBuffer) {
        ringBuffer.append(buffer)
        ringBufferSizeInSamples += buffer.frameLength

        // throw away old buffers if ring buffer gets too large
        if let firstBuffer = ringBuffer.first {
            if ringBufferSizeInSamples - firstBuffer.frameLength >= requiredSamples {
                ringBuffer.remove(at: 0)
                ringBufferSizeInSamples -= firstBuffer.frameLength
            }
        }
    }

    func stopRecording() {
        engine.stop()

        let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("foo.m4a")
        let settings: [String : Any] = [AVFormatIDKey: Int(kAudioFormatMPEG4AAC)]

        // write ring buffer to file.
        let file = try! AVAudioFile(forWriting: url, settings: settings)
        for buffer in ringBuffer {
            try! file.write(from: buffer)
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // example usage
        startRecording()

        DispatchQueue.main.asyncAfter(deadline: .now() + 4*60) {
            print("stopping")
            self.stopRecording()
        }
    }
}
Rhythmic Fistman
  • 34,352
  • 5
  • 87
  • 159
  • 1
    And there it is! MY MAN! Thank you so much. I can't tell you how long I looked for this. – aBikis Jun 03 '17 at 03:24
  • 1
    this is the main function of the app so I'm not entirely concerned with memory although that is a good thing to look out for. – aBikis Jun 03 '17 at 03:26
  • This is super helpful, thanks. Just curious; is there any particular reason why the buffer on the tap is only 512, when so much audio is being recorded? – jbm Oct 04 '18 at 23:40