AudioToolBox Recorder gets affect by AVFoundation AudioPlayer

273 views Asked by At

So, I have the following classes:

  1. audio recorder (using AudioToolbox and CoreAudio) that records audio.
  2. audio player (using AVFoundation)

The recorder captures audio, sends it to a server who then replies with another audio, then the player plays the received audio.

When I tried to call the recorder again to record audio, its doesn't records the audio properly.

Any idea on how I can reset the recorder so that it can properly recorder after the audio player finishes playing?

I tried initialize the recorder again (right before recording) but that doesnt work.

the line that affects the recorder is the 2 lines below, which unfortunate are needed in order to play audio with AVFoundation.

let sharedSession = AVAudioSession.sharedInstance()
try sharedSession.setCategory(AVAudioSessionCategoryPlayback)

Flow of execution

  • Recorder Audio
  • Send and Received audio to Server
  • Play Audio from server Recorder Audio at this point the recorder Does NOT record properly
  • Send and Received audio to Server FAILS
  • Play Audio FAILS

Thank you in advance! Lu

Link to recorder project

AudioRecorder:

import UIKit
import CoreAudio
import AudioToolbox
class SpeechRecorder: NSObject {

    static let sharedInstance = SpeechRecorder()

    // MARK:- properties
    @objc enum Status: Int {
        case ready
        case busy
        case error
    }

    internal struct RecordState {
        var format: AudioStreamBasicDescription
        var queue: UnsafeMutablePointer<AudioQueueRef?>
        var buffers: [AudioQueueBufferRef?]
        var file: AudioFileID?
        var currentPacket: Int64
        var recording: Bool
    };

    private var _recordState: RecordState?
    private var _audioURL:URL?

    var format: AudioFormatID {
        get { return _recordState!.format.mFormatID }
        set {  _recordState!.format.mFormatID = newValue }
    }

    var sampleRate: Float64 {
        get { return _recordState!.format.mSampleRate }
        set {  _recordState!.format.mSampleRate = newValue  }
    }

    var formatFlags: AudioFormatFlags {
        get {  return _recordState!.format.mFormatFlags }
        set {   _recordState!.format.mFormatFlags = newValue  }
    }

    var channelsPerFrame: UInt32 {
        get {   return _recordState!.format.mChannelsPerFrame }
        set {   _recordState!.format.mChannelsPerFrame = newValue }
    }

    var bitsPerChannel: UInt32 {
        get {   return _recordState!.format.mBitsPerChannel }
        set {   _recordState!.format.mBitsPerChannel = newValue  }
    }

    var framesPerPacket: UInt32 {
        get {  return _recordState!.format.mFramesPerPacket }
        set {   _recordState!.format.mFramesPerPacket = newValue }
    }

    var bytesPerFrame: UInt32 {
        get {  return _recordState!.format.mBytesPerFrame }
        set {   _recordState!.format.mBytesPerFrame = newValue }
    }

    var bytesPerPacket: UInt32 {
        get { return _recordState!.format.mBytesPerPacket  }
        set {  _recordState!.format.mBytesPerPacket = newValue }
    }

    //MARK: - Handlers
    public var handler: ((_ status:Status, _ data:NSData?, _ errorDesc:String?) -> Void)?

    // MARK:- Init
    override init()
    {
        super.init()
        self._recordState = RecordState(format: AudioStreamBasicDescription(),
                                       queue: UnsafeMutablePointer<AudioQueueRef?>.allocate(capacity: 1),
                                       buffers: [AudioQueueBufferRef?](repeating: nil, count: 1),
                                       file: nil,
                                       currentPacket: 0,
                                       recording: false)
    }//eom



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

    func setOutputFileNameWithDocumentsDirectory(nameDesired:String)
    {
        _audioURL = getDocumentsPath().appendingPathComponent(nameDesired)
        setOutputFile(url: _audioURL!)
    }//eom

    func setOutputFileNameWithTempDirectory(nameDesired:String)
    {
        let tempDir = NSTemporaryDirectory()
        let tempURLdir = URL(fileURLWithPath: tempDir)
        _audioURL = tempURLdir.appendingPathComponent(nameDesired)
        setOutputFile(url: _audioURL!)
    }//eom

    private func setOutputFile(path: String)
    {
        setOutputFile(url: URL(fileURLWithPath: path))
    }//eom

    private func setOutputFile(url: URL)
    {
        AudioFileCreateWithURL(url as CFURL,
                               kAudioFileWAVEType,
                               &_recordState!.format,
                               AudioFileFlags.dontPageAlignAudioData.union(.eraseFile),
                               &_recordState!.file)
    }

    // MARK:- Start / Stop Recording
    func start()
    {
        handler?(.busy, nil, nil)

        self._recordState?.currentPacket = 0

        let inputAudioQueue: AudioQueueInputCallback =
            { (userData: UnsafeMutableRawPointer?,
                audioQueue: AudioQueueRef,
                bufferQueue: AudioQueueBufferRef,
                startTime: UnsafePointer<AudioTimeStamp>,
                packets: UInt32,
                packetDescription: UnsafePointer<AudioStreamPacketDescription>?) in

                let internalRSP = unsafeBitCast(userData, to: UnsafeMutablePointer<RecordState>.self)
                if packets > 0
                {
                    var packetsReceived = packets
                    let outputStream:OSStatus = AudioFileWritePackets(internalRSP.pointee.file!,
                                                                      false,
                                                                      bufferQueue.pointee.mAudioDataByteSize,
                                                                      packetDescription,
                                                                      internalRSP.pointee.currentPacket,
                                                                      &packetsReceived,
                                                                      bufferQueue.pointee.mAudioData)
                    if outputStream != 0
                    {
                        if verbose
                        {

                            print("Error with AudioFileWritePackets")
                            //<----DEBUG
                            switch outputStream
                            {
                            case kAudioFilePermissionsError:
                                print("kAudioFilePermissionsError")
                                break
                            case kAudioFileNotOptimizedError:
                                print("kAudioFileNotOptimizedError")
                                break
                            case kAudioFileInvalidChunkError:
                                print("kAudioFileInvalidChunkError")
                                break
                            case kAudioFileDoesNotAllow64BitDataSizeError:
                                print("kAudioFileDoesNotAllow64BitDataSizeError")
                                break
                            case kAudioFileInvalidPacketOffsetError:
                                print("kAudioFileInvalidPacketOffsetError")
                                break
                            case kAudioFileInvalidFileError:
                                print("kAudioFileInvalidFileError")
                                break
                            case kAudioFileOperationNotSupportedError:
                                print("kAudioFileOperationNotSupportedError")
                                break
                            case kAudioFileNotOpenError:
                                print("kAudioFileNotOpenError")
                                break
                            case kAudioFileEndOfFileError:
                                print("kAudioFileEndOfFileError")
                                break
                            case kAudioFilePositionError:
                                print("kAudioFilePositionError")
                                break
                            case kAudioFileFileNotFoundError:
                                print("kAudioFileFileNotFoundError")
                                break
                            case kAudioFileUnspecifiedError:
                                print("kAudioFileUnspecifiedError")
                                break
                            case kAudioFileUnsupportedFileTypeError:
                                print("kAudioFileUnsupportedFileTypeError")
                                break
                            case kAudioFileUnsupportedDataFormatError:
                                print("kAudioFileUnsupportedDataFormatError")
                                break
                            case kAudioFileUnsupportedPropertyError:
                                print("kAudioFileUnsupportedPropertyError")
                                break
                            case kAudioFileBadPropertySizeError:
                                print("kAudioFileBadPropertySizeError")
                                break
                            default:
                                print("unknown error")
                                break
                            }
                            //<----DEBUG
                        }
                    }
                    internalRSP.pointee.currentPacket += Int64(packetsReceived)
                }

                if internalRSP.pointee.recording
                {
                    let outputStream:OSStatus = AudioQueueEnqueueBuffer(audioQueue, bufferQueue, 0, nil)
                    if outputStream != 0
                    {
                        if verbose
                        {
                            print("Error with AudioQueueEnqueueBuffer")
                            //<----DEBUG
                            switch outputStream
                            {
                            case kAudioFilePermissionsError:
                                print("kAudioFilePermissionsError")
                                break
                            case kAudioFileNotOptimizedError:
                                print("kAudioFileNotOptimizedError")
                                break
                            case kAudioFileInvalidChunkError:
                                print("kAudioFileInvalidChunkError")
                                break
                            case kAudioFileDoesNotAllow64BitDataSizeError:
                                print("kAudioFileDoesNotAllow64BitDataSizeError")
                                break
                            case kAudioFileInvalidPacketOffsetError:
                                print("kAudioFileInvalidPacketOffsetError")
                                break
                            case kAudioFileInvalidFileError:
                                print("kAudioFileInvalidFileError")
                                break
                            case kAudioFileOperationNotSupportedError:
                                print("kAudioFileOperationNotSupportedError")
                                break
                            case kAudioFileNotOpenError:
                                print("kAudioFileNotOpenError")
                                break
                            case kAudioFileEndOfFileError:
                                print("kAudioFileEndOfFileError")
                                break
                            case kAudioFilePositionError:
                                print("kAudioFilePositionError")
                                break
                            case kAudioFileFileNotFoundError:
                                print("kAudioFileFileNotFoundError")
                                break
                            case kAudioFileUnspecifiedError:
                                print("kAudioFileUnspecifiedError")
                                break
                            case kAudioFileUnsupportedFileTypeError:
                                print("kAudioFileUnsupportedFileTypeError")
                                break
                            case kAudioFileUnsupportedDataFormatError:
                                print("kAudioFileUnsupportedDataFormatError")
                                break
                            case kAudioFileUnsupportedPropertyError:
                                print("kAudioFileUnsupportedPropertyError")
                                break
                            case kAudioFileBadPropertySizeError:
                                print("kAudioFileBadPropertySizeError")
                                break
                            default:
                                print("unknown error")
                                break
                                 //<----DEBUG
                            }
                        }
                    }
                }
        }

        let queueResults = AudioQueueNewInput(&_recordState!.format, inputAudioQueue, &_recordState, nil, nil, 0, _recordState!.queue)
        if queueResults == 0
        {
            let bufferByteSize: Int = calculate(format: _recordState!.format, seconds: 0.5)
            for index in (0..<_recordState!.buffers.count)
            {
                AudioQueueAllocateBuffer(_recordState!.queue.pointee!, UInt32(bufferByteSize), &_recordState!.buffers[index])
                AudioQueueEnqueueBuffer(_recordState!.queue.pointee!, _recordState!.buffers[index]!, 0, nil)
            }

            AudioQueueStart(_recordState!.queue.pointee!, nil)
            _recordState?.recording = true
        }
        else
        {
            handler?(.error, nil, "Error setting audio input.")
        }
    }//eom

    func stop()
    {
        _recordState?.recording = false
        if let recordingState: RecordState = _recordState
        {
            AudioQueueStop(recordingState.queue.pointee!, true)
            AudioQueueDispose(recordingState.queue.pointee!, true)
            AudioFileClose(recordingState.file!)

            let audioData:NSData? = NSData(contentsOf: _audioURL!)
            handler?(.ready, audioData, nil)
        }
    }//eom

    // MARK:- Helper methods
    func calculate(format: AudioStreamBasicDescription, seconds: Double) -> Int
    {
        let framesRequiredForBufferTime = Int(ceil(seconds * format.mSampleRate))
        if framesRequiredForBufferTime > 0

        {
            return (framesRequiredForBufferTime * Int(format.mBytesPerFrame))
        }
        else
        {
            var maximumPacketSize = UInt32(0)
            if format.mBytesPerPacket > 0
            {
                maximumPacketSize = format.mBytesPerPacket
            }
            else
            {
                audioQueueProperty(propertyId: kAudioQueueProperty_MaximumOutputPacketSize, value: &maximumPacketSize)
            }

            var packets = 0
            if format.mFramesPerPacket > 0
            {
                packets = (framesRequiredForBufferTime / Int(format.mFramesPerPacket))
            } else
            {
                packets = framesRequiredForBufferTime
            }

            if packets == 0
            {
                packets = 1
            }

            return (packets * Int(maximumPacketSize))
        }
    }//eom

    func audioQueueProperty<T>(propertyId: AudioQueuePropertyID, value: inout T)
    {
        let propertySize = UnsafeMutablePointer<UInt32>.allocate(capacity: 1)
        propertySize.pointee = UInt32(MemoryLayout<T>.size)

        let queueResults = AudioQueueGetProperty(_recordState!.queue.pointee!, propertyId, &value, propertySize)
        propertySize.deallocate(capacity: 1)

        if queueResults != 0 {
            handler?(.error, nil, "Unable to get audio queue property.")
        }
    }//eom
}

Player:

import UIKit
import AVFoundation


protocol AudioPlayerDelegate {
    func audioPlayer_playbackError(playerItemID:String, error:String)
    func audioPlayer_playbackSuccess(playerItemID:String)
}

class AudioPlayer: NSObject, AVAudioPlayerDelegate
{
    //properties
    private var _audioPlayer:AVAudioPlayer?
    var delegate:AudioPlayerDelegate?
    var playerItemID:String = ""
    var volume:Float?

    //MARK: - Play Audio
    func playAudioFromData(_ playerItemID:String, dataToPlay:Data)
    {
        do {
            let sharedSession = AVAudioSession.sharedInstance()
            try sharedSession.setCategory(AVAudioSessionCategoryPlayback)
            try sharedSession.setActive(true)

            _audioPlayer = try AVAudioPlayer(data: dataToPlay)

            _audioPlayer?.numberOfLoops         = 0
            _audioPlayer?.isMeteringEnabled     = true
            _audioPlayer?.delegate              = self

            //volume
            if volume != nil {
                _audioPlayer?.volume = volume!
            }

            //id
            self.playerItemID = playerItemID

            _audioPlayer?.play()
        }
        catch let error {
            self.delegate?.audioPlayer_playbackError(playerItemID: self.playerItemID, error: error.localizedDescription)
        }
    }//eom

    func playAudioFromUrl(_ url:URL)
    {
        do {
            let sharedSession = AVAudioSession.sharedInstance()
            try sharedSession.setCategory(AVAudioSessionCategoryPlayback)
            try sharedSession.setActive(true)

            if FileManager.default.fileExists(atPath: url.path) {
                _audioPlayer = try AVAudioPlayer(contentsOf: url)

                _audioPlayer?.numberOfLoops         = 0
                _audioPlayer?.isMeteringEnabled     = true
                _audioPlayer?.delegate              = self

                //volume
                if volume != nil {
                    _audioPlayer?.volume = volume!
                }

                //id
                self.playerItemID = url.absoluteString

                _audioPlayer?.play()
            }
            else {
                self.delegate?.audioPlayer_playbackError(playerItemID: self.playerItemID, error: "audio file does not exist")
            }
        }
        catch let error  {
            self.delegate?.audioPlayer_playbackError(playerItemID: self.playerItemID, error: error.localizedDescription)
        }
    }//eom

    //MARK: - Player Options
    func pausePlay()
    {
        _audioPlayer?.pause()
    }//eom

    func stopPlay()
    {
        _audioPlayer?.stop()

        do {
            let sharedSession = AVAudioSession.sharedInstance()
            try sharedSession.setActive(false)
        }
        catch let error {
            if verbose { print("un-able to set session to inactive, error: \(error)") }
        }
    }//eom

    //MARK: - Delegates
    func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
        //inactive session
        do {
            let sharedSession = AVAudioSession.sharedInstance()
            try sharedSession.setActive(false)
        }
        catch let error {
            if verbose { print("un-able to set session to inactive, error: \(error)") }
        }

        //report status
        if error != nil {
            self.delegate?.audioPlayer_playbackError(playerItemID: self.playerItemID, error: error!.localizedDescription)
        }
        else {
            self.delegate?.audioPlayer_playbackError(playerItemID: self.playerItemID, error: "decode error did occurred")
        }

        //reset
        self._audioPlayer?.delegate = nil
        self._audioPlayer = nil
        self.playerItemID = ""
    }//eom

    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {

        //inactive session
        do {
            let sharedSession = AVAudioSession.sharedInstance()
            try sharedSession.setActive(false)
        }
        catch let error {
            if verbose { print("un-able to set session to inactive, error: \(error)") }
        }

        //report status
        if flag {
            delegate?.audioPlayer_playbackSuccess(playerItemID: self.playerItemID)
        }
        else {
            delegate?.audioPlayer_playbackError(playerItemID: self.playerItemID, error: "player finish playing with error")
        }

        //reset
        self._audioPlayer?.delegate = nil
        self._audioPlayer = nil
        self.playerItemID = ""
    }//eom

}//eoc
1

There are 1 answers

1
mcfresh On BEST ANSWER

If you are going to be working with both AudioToolBox and AVFoundation, you may want to be careful with the AudioSession. AVFoundation does a lot of updates to the AudioSession on the backend.

A quick fix for your Player would be to removed any audio session calls like the below:

let sharedSession = AVAudioSession.sharedInstance()
try sharedSession.setCategory(AVAudioSessionCategoryPlayback)
try sharedSession.setActive(true)

_audioPlayer?.numberOfLoops         = 0
_audioPlayer?.isMeteringEnabled     = true

For more advance audio manipulation, Check out the book Learning Core Audio by Chris Adamson, Kevin Avila