Swift 3 LPCM Audio Recorder | Error: kAudioFileInv

2019-07-15 01:12发布

问题:

The below recorder works only the first time, if you tried recording a second time it gives the error 'kAudioFileInvalidPacketOffsetError' when trying to AudioFileWritePackets.

Any idea why this is happening?

Thank you in advance

Repository located here

Recorder

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?

    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) -> 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
    func setOutputFile(path: String)
    {
        setOutputFile(url: URL(fileURLWithPath: path))
    }

    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)

        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
                    {
                        // This is where the error occurs when recording after the first time
                        //<----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
                    {
                     // This is where the error occurs when recording after the first time
                    //<----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
    {
        print("Error setting audio input.")
        handler?(.error)
    }
}//eom

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

        handler?(.ready)
    }
}//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 {
        print("Unable to get audio queue property.")
    }
    }//eom
  }

ViewController

import UIKit

import AudioToolbox

class ViewController: UIViewController {

    //MARK: - Properties
    var recorder:SpeechRecorder?

    @IBOutlet weak var startStopRecordingButton: UIButton!



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

       //having same recorder gives error
       recorder = SpeechRecorder()
    }


    //MARK: - Start / End Recording

    func startRecording()
    {
        //alloc/init recorder everytime we start recording gives no error
        //recorder = SpeechRecorder()


        //settings
        recorder?.format = kAudioFormatLinearPCM
        recorder?.sampleRate = 16000;
        recorder?.channelsPerFrame = 1
        recorder?.bitsPerChannel = 16
        recorder?.framesPerPacket = 1
        recorder?.bytesPerFrame = ((recorder!.channelsPerFrame * recorder!.bitsPerChannel) / 8)
        recorder?.bytesPerPacket = recorder!.bytesPerFrame * recorder!.framesPerPacket
        recorder?.formatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked

        //outputfile
        let outputfilePath:String = MyFileManager().createTempFilePathWithUniqueName("recorderAudio", andExtension: "wav")
        print("temp filepath: ", outputfilePath)
        recorder?.setOutputFile(path: outputfilePath)


        //handler
        recorder?.handler = { [weak self] status in
            switch status
            {
                case .busy:
                    print("started Recording\n\n")
                    break
                case .ready:
                    print("finish recorder, ready to start recording\n\n")
                    break
                case .error:
                    print("error occur with recorder\n\n")

                    DispatchQueue.main.async
                    {
                        self?.startStopRecordingButton.isSelected = false
                        self?.view.backgroundColor = UIColor.white
                    }

                    break
                }
        }//


        recorder?.start()
    }//eom


    func stopRecording()
    {
      recorder?.stop()
    }//eom

    //MARK: - Actions
    @IBAction func startStopRecording()
    {
        if startStopRecordingButton.isSelected
        {
            startStopRecordingButton.isSelected = false
            self.view.backgroundColor = UIColor.white
            startStopRecordingButton.setTitle("Start Recording", for: UIControlState.normal)

            self.stopRecording()
        }
        else
        {
            startStopRecordingButton.isSelected = true
            self.view.backgroundColor = UIColor.green
            startStopRecordingButton.setTitle("Stop Recording", for: UIControlState.normal)

            self.startRecording()
        }
    }//eom



    //MARK: - Memory
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}

FileManager (Creates Temp filepath)

import Foundation

@objc class MyFileManager:NSObject
{
     private let unique_debug = true
     private var _temporyDirectory:String = ""

    //MARK: - Properties
    var directory:String {
        return _temporyDirectory
    }

    //MARK: - Init
    override init() {
        super.init()

        _temporyDirectory = NSTemporaryDirectory()
    }//eom

    func createHomeDirFileUniqueWithName(_ myFileName:String, andExtension fileExtension:String)->URL
    {
        //filename
        let time:Date = Date.init()
        let dateformatter:DateFormatter = DateFormatter()
        dateformatter .dateFormat = "ddMMyyyy-hh-mm-ss-a"
        let tempDate:String = dateformatter .string(from: time)
        let tempFileName = "\(myFileName)-\(tempDate).\(fileExtension)"

        //directory
        var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]

        documentsDirectory.appendPathComponent(tempFileName)

        if unique_debug {  print("\(documentsDirectory)") }

        return documentsDirectory
    }//eom

    //MARK: - Names
    func createGlobalUniqueFileName(_ myFileName:String)->String
    {
        let guid = ProcessInfo.processInfo.globallyUniqueString
        let uniqueFileName = ("\(myFileName)_\(guid)")

        if unique_debug {  print("\(uniqueFileName)") }

        return uniqueFileName
    }//eom

    func createUniqueNameWithFilename(_ myFileName:String, andExtension fileExtension:String)->String
    {
        //filename
        let time:Date = Date.init()
        let dateformatter:DateFormatter = DateFormatter()
        dateformatter .dateFormat = "ddMMyyyy-hh-mm-ss-a"
        let currentDateString = dateformatter .string(from: time)

        let finalName = myFileName + currentDateString + "." + fileExtension

        if unique_debug {  print("\(finalName)") }

        return finalName
    }//eom

    //MARK: - Paths
    func createTempFilePathWithUniqueName(_ myFileName:String, andExtension fileExtension:String)->String
    {
        let tempFileName = self.createUniqueNameWithFilename(myFileName, andExtension: fileExtension)

        let tempFile = _temporyDirectory + tempFileName

        if unique_debug {  print("\(tempFile)") }

        return tempFile
    }//eom

    //MARK: - Helpers
    func enumerateDirectory(directory:String)
    {
        do
        {
            let filesInDir:[String] = try FileManager.default.contentsOfDirectory(atPath: directory)
            for currFile in filesInDir {
                print(currFile)
            }//eofl
        }
        catch let error
        {
            print("error: \(error.localizedDescription)")
        }
    }//eom

    func doesFileExistInDirectory(filename:String) -> Bool {
        do
        {
            let filesInDir:[String] = try FileManager.default.contentsOfDirectory(atPath: _temporyDirectory)
            for currFile in filesInDir
            {
                print(currFile)
                if currFile == filename {
                    return true
                }
            }//eofl
        }
        catch let error
        {
            print("error: \(error.localizedDescription)")
        }

        return false
    }//eom

}//eoc

回答1:

You're not resetting your currentPacket count to zero, so on subsequent recordings, you're asking AudioFileWritePackets to start writing to its new file at a nonzero starting packet, which it refuses to do.

The correct solution is (probably) to re-create RecordState each time you start a recording, although hackily setting

recordState!.currentPacket = 0

before calling AudioQueueNewInput seems to work too.