LuAndre LuAndre - 26 days ago 27
iOS Question

Swift 3 LPCM Audio Recorder | Error: kAudioFileInvalidPacketOffsetError

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

Answer

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.