iOS/swift音频播放(二)— AudioFile

AudioFile

AudiFile这个类是用来做什么的呢?
首先我们看一下苹果官方介绍,如下图所示:
在这里插入图片描述
根据官方文档,可以得知这个类可以用来创建、初始化音频文件;读写音频数据,对音频进行优化;读取和写入音频信息。

所以我们可以使用它来支持音频播放 以及生成音频文件

初始化AudioFile

1. AudioFileOpenURL

public func AudioFileOpenURL(_ inFileRef: CFURL, _ inPermissions: AudioFilePermissions, _ inFileTypeHint: AudioFileTypeID, _ outAudioFile: UnsafeMutablePointer<AudioFileID?>) -> OSStatus

第一个参数 inFileRef: 文件路径

第二个参数 inPermissions: 文件的使用方式,一共包含读、写、读写这三种模式(如下图所示),如果文件打开后进行了权限以外的操作,就会报错。

public enum AudioFilePermissions : Int8 {

    case readPermission

    case writePermission

    case readWritePermission
}

第三个参数inFileTypeHint:文件的类型,如果无法确认可以传入0。
这个参数在文件信息不完整的时候尤其有用,可以给AudioFile一些提示去解析我们的音频文件。
AudioFileTypeID会有以下几种类型

var kAudioFileAIFFType: AudioFileTypeID
//An Audio Interchange File Format (AIFF) file.
var kAudioFileAIFCType: AudioFileTypeID
//An Audio Interchange File Format Compressed (AIFF-C) file.
var kAudioFileWAVEType: AudioFileTypeID
//A Microsoft WAVE file.
var kAudioFileSoundDesigner2Type: AudioFileTypeID
//A Sound Designer II file.
var kAudioFileNextType: AudioFileTypeID
//A NeXT or Sun Microsystems file.
var kAudioFileMP3Type: AudioFileTypeID
//An MPEG Audio Layer 3 (.mp3) file.
var kAudioFileMP2Type: AudioFileTypeID
//An MPEG Audio Layer 2 (.mp2) file.
var kAudioFileMP1Type: AudioFileTypeID
//An MPEG Audio Layer 1 (.mp1) file.
var kAudioFileAC3Type: AudioFileTypeID
//An AC-3 file.
var kAudioFileAAC_ADTSType: AudioFileTypeID
//An Advanced Audio Coding (AAC) Audio Data Transport Stream (ADTS) file.
var kAudioFileMPEG4Type: AudioFileTypeID
//An MPEG 4 file.
var kAudioFileM4AType: AudioFileTypeID
//An M4A file.
var kAudioFileCAFType: AudioFileTypeID
//A Core Audio File Format file.
var kAudioFile3GPType: AudioFileTypeID
//A 3GPP file, suitable for video content on GSM mobile phones.
var kAudioFile3GP2Type: AudioFileTypeID
//A 3GPP2 file, suitable for video content on CDMA mobile phones.
var kAudioFileAMRType: AudioFileTypeID
//An AMR (Adaptive Multi-Rate) file suitable for compressed speech.

第四个参数outAudioFile:返回AudioFile实例所对应的ID。

返回值OSStatus : 返回noErr则成功

2. AudioFileOpenWithCallbacks

AudioFileOpenWithCallbacks (
				void *								inClientData,
				AudioFile_ReadProc					inReadFunc,
				AudioFile_WriteProc __nullable		inWriteFunc,
				AudioFile_GetSizeProc				inGetSizeFunc,
				AudioFile_SetSizeProc __nullable	inSetSizeFunc,
                AudioFileTypeID						inFileTypeHint,
                AudioFileID	__nullable * __nonnull	outAudioFile)
                

第一个参数 inClientData:上下文对象

第二个参数 inReadFunc:当AudioFile需要读音频数据时进行的回调

第三个参数 inWriteFunc: 当AudioFile需要写音频数据时进行的回调

第四个参数 inGetSizeFunc: 当AudioFile需要用到文件的总大小时的回调

第五个参数 inSetSizeFunc:当AudioFile需要设置文件大小时的回调

第六个参数 inFileTypeHint: 文件的类型。这个参数帮助AudioFile对文件格式进行解析。这个参数在文件信息不完整(例如信息有缺陷)时尤其有用,它可以给与AudioFileStream一定的提示,帮助其绕过文件中的错误或者缺失从而成功解析文件。所以在确定文件类型的情况下尽量填上这个参数,如果无法确定可以传入0。

第七个参数 outAudioFile: 返回AudioFile实例所对应的ID。

AudioFile_ReadProc
public typealias AudioFile_ReadProc = @convention(c) (UnsafeMutableRawPointer, Int64, UInt32, UnsafeMutableRawPointer, UnsafeMutablePointer<UInt32>) -> OSStatus

let readProc: AudioFile_ReadProc = { (
        inClientData,
        inPosition,
        requestCount,
        buffer,
        actualCount) -> OSStatus in

        return noErr
    }
    

第一个参数inClientData: 上下文对象

第二个参数inPosition: 需要读取第几个字节开始的数据

第三个参数requestCount: 需要读取的数据长度

第四个参数buffer: 返回参数,是一个数据指针且其空间已经被分配,我们需要做的是把数据memcpy到buffer中

第五个参数actualCount: 实际提供的数据长度,即memcpy到buffer中的数据长度

AudioFile需要数据时会调用回调方法,需要数据的时机有以下两个:
Open方法调用时:

由于AudioFile的Open方法调用过程中会对音频格式信息进行解析,只有符合要求的音频格式才能被成功打开,否则Open方法就会返回错误码。所以在Open方法调用的过程中就需要提供一部分音频数据来进行解析。

Read相关方法调用时:

在这个回调函数inPosition和requestCount这两个参数表示本次回调需要提供从inPosition开始requestCount个字节长度的数据。这时候会分为一下两种情况:
当我们有充足的数据时: 我们需要把这个范围内的数据拷贝到buffer中,并且给actualCount赋值requestCount,最后返回noErr

当我们数据不足时: 我们就只能把手头的数据拷贝到buffer中,拷贝的数据必须是从inPosition开始的连续数据,拷贝完成后给actualCount赋值实际拷贝进buffer中的数据长度,然后返回noErr。数据不足时又分为两种情况:

  1. Open方法调用时的回调数据不足: AudioFile的Open方法会根据文件格式类型分几步进行数据读取以解析确定是否是一个合法的文件格式,其中每一步的inPosition和requestCount都不一样,如果某一步不成功就会直接进行下一步,如果几部下来都失败了,那么Open方法就会失败。简单的说就是在调用Open之前首先需要保证音频文件的格式信息完整,这就意味着AudioFile并不能独立用于音频流的读取,在流播放时首先需要使用AudioStreamFile来得到ReadyToProducePackets标志位来保证信息完整

  2. Read方法调用时的回调数据不足: 这种情况下inPosition和requestCount的数值与Read方法调用时传入的参数有关,数据不足对于Read方法本身没有影响,只要回调返回noErr,Read就成功,只是实际交给Read方法的调用方的数据会不足,那么就把这个问题的处理交给了Read的调用方;

AudioFile_WriteProc
public typealias AudioFile_WriteProc = @convention(c) (UnsafeMutableRawPointer, Int64, UInt32, UnsafeRawPointer, UnsafeMutablePointer<UInt32>) -> OSStatus

let writeProc: AudioFile_WriteProc = { (
        inClientData,
        inPosition,
        requestCount,
        buffer,
        actualCount) -> OSStatus in

        return noErr
    }
    

第一个参数inClientData: 上下文对象

第二个参数inPosition: 需要写入第几个字节开始的数据

第三个参数requestCount: 需要写入的数据长度

第四个参数buffer: 要写入的数据的缓冲区

第五个参数actualCount: 写入的数据长度

AudioFile_GetSizeProc
public typealias AudioFile_GetSizeProc = @convention(c) (UnsafeMutableRawPointer) -> Int64

这个回调表示返回文件总长度,总长度的获取途径自然是文件系统或者httpResponse等等。

AudioFile_SetSizeProc
public typealias AudioFile_SetSizeProc = (UnsafeMutableRawPointer, Int64) -> OSStatus

这个回调表示设置文件大小。

读取音频格式信息

成功打开音频文件后,就要开始读取文件格式信息

AudioFileGetPropertyInfo

用来获取某个属性对应数据的大小,以及该属性是否可以被write

public func AudioFileGetPropertyInfo(_ inAudioFile: AudioFileID, _ inPropertyID: AudioFilePropertyID, _ outDataSize: UnsafeMutablePointer<UInt32>?, _ isWritable: UnsafeMutablePointer<UInt32>?) -> OSStatus

第一个参数inAudioFile: AudioFile实例所对应的ID

第二个参数inPropertyID: 要读取的属性ID

第三个参数outDataSize: 某个属性对应数据的大小

第四个参数isWritable: 该属性是否可以被write

AudioFileGetProperty

用来获取属性对应的数据

public func AudioFileGetProperty(_ inAudioFile: AudioFileID, _ inPropertyID: AudioFilePropertyID, _ ioDataSize: UnsafeMutablePointer<UInt32>, _ outPropertyData: UnsafeMutableRawPointer) -> OSStatus

第一个参数inAudioFile: AudioFile实例所对应的ID

第二个参数inPropertyID: 要读取的属性ID

第三个参数ioDataSize: 要读取属性的数据大小

第四个参数outPropertyData: 属性对应的数据

使用说明: 对于一些大小可变的属性需要先使用AudioFileGetPropertyInfo获取数据大小才能取获取数据,而有些确定类型单个属性则不必先调用AudioFileGetPropertyInfo,直接调用AudioFileGetProperty即可
例子如下

var formatListSize: UInt32 = 0
if AudioFileGetPropertyInfo(fileID, kAudioFilePropertyFileFormat, &formatListSize, nil) == noErr {
	let numFormats: UInt32 = formatListSize / UInt32(MemoryLayout<AudioFormatListItem>.size)
	var formatList = UnsafeMutablePointer<AudioFormatListItem>.allocate(capacity:Int(numFormats))
	AudioFileGetProperty(fileID, kAudioFilePropertyFileFormat, &formatListSize, &formatList)
}

可以获取的属性ID如下:

//音频文件的格式   char *
public var kAudioFilePropertyFileFormat: AudioFilePropertyID { get }
//音频数据的格式   AudioStreamPacketDescription
public var kAudioFilePropertyDataFormat: AudioFilePropertyID { get }
//是否可以优化  0/1
public var kAudioFilePropertyIsOptimized: AudioFilePropertyID { get }
//Magic Cookie文件头 char *
public var kAudioFilePropertyMagicCookieData: AudioFilePropertyID { get }
//文件长度    Uint64
public var kAudioFilePropertyAudioDataByteCount: AudioFilePropertyID { get }
//Packet的数目   Uint64
public var kAudioFilePropertyAudioDataPacketCount: AudioFilePropertyID { get }
//最大的Packet大小 Uint32
public var kAudioFilePropertyMaximumPacketSize: AudioFilePropertyID { get }
//数据的偏移量  SInt64
public var kAudioFilePropertyDataOffset: AudioFilePropertyID { get }
//声道结构    AudioFormatListItem
public var kAudioFilePropertyChannelLayout: AudioFilePropertyID { get }
//是否更新文件头信息   1/0
public var kAudioFilePropertyDeferSizeUpdates: AudioFilePropertyID { get }
public var kAudioFilePropertyDataFormatName: AudioFilePropertyID { get }
//音频中所有markers    CFStringRef表示的Markers列表
public var kAudioFilePropertyMarkerList: AudioFilePropertyID { get }
//音频中所有Region CFStringRef表示的Region列表
public var kAudioFilePropertyRegionList: AudioFilePropertyID { get }
//将包数转换成帧数    AudioFramePacketTranslation中mPacket做输入,mFrame做输出
public var kAudioFilePropertyPacketToFrame: AudioFilePropertyID { get }
//将帧数转换成包数    AudioFramePacketTranslation中mFrame做输入,mFrameOffsetInPacket,mPacket做输出
public var kAudioFilePropertyFrameToPacket: AudioFilePropertyID { get }
//
public var kAudioFilePropertyRestrictsRandomAccess: AudioFilePropertyID { get }
public var kAudioFilePropertyPacketToRollDistance: AudioFilePropertyID { get }
public var kAudioFilePropertyPreviousIndependentPacket: AudioFilePropertyID { get }
public var kAudioFilePropertyNextIndependentPacket: AudioFilePropertyID { get }
public var kAudioFilePropertyPacketToDependencyInfo: AudioFilePropertyID { get }
//将包数转换成字节数   AudioFramePacketTranslation中mPacket做输入,mByte做输出
public var kAudioFilePropertyPacketToByte: AudioFilePropertyID { get }
// 将字节数转换成包数  AudioFramePacketTranslation中mByte做输入,mPacket和mByteOffsetInPacket做输出
public var kAudioFilePropertyByteToPacket: AudioFilePropertyID { get }
//文件中的chunk编码格式   4字符编码格式数组 
public var kAudioFilePropertyChunkIDs: AudioFilePropertyID { get }
//字典表示的Info   CFDictionary
public var kAudioFilePropertyInfoDictionary: AudioFilePropertyID { get }
//设置PacketTableInfo   PacketTableInfo
public var kAudioFilePropertyPacketTableInfo: AudioFilePropertyID { get }
//支持的格式列表 编码格式list
public var kAudioFilePropertyFormatList: AudioFilePropertyID { get }
//理论上的最大Packet大小  Uint64
public var kAudioFilePropertyPacketSizeUpperBound: AudioFilePropertyID { get }
public var kAudioFilePropertyPacketRangeByteCountUpperBound: AudioFilePropertyID { get }
//设置写保护区大小,单位为秒   Uint32
public var kAudioFilePropertyReserveDuration: AudioFilePropertyID { get }
//估算的音频时长 , 单位秒   Uint32
public var kAudioFilePropertyEstimatedDuration: AudioFilePropertyID { get }
//码率  Uint32
public var kAudioFilePropertyBitRate: AudioFilePropertyID { get }
public var kAudioFilePropertyID3Tag: AudioFilePropertyID { get }
public var kAudioFilePropertyID3TagOffset: AudioFilePropertyID { get }
// 位深度 Uint32
public var kAudioFilePropertySourceBitDepth: AudioFilePropertyID { get }
//专辑名    CFDataRef
public var kAudioFilePropertyAlbumArtwork: AudioFilePropertyID { get }
public var kAudioFilePropertyAudioTrackCount: AudioFilePropertyID { get }
public var kAudioFilePropertyUseAudioTrack: AudioFilePropertyID { get }

读取音频数据

接下来我们进行音频数据的读取,读取方法有两类

直接读取音频数据:

AudioFileReadBytes
public func AudioFileReadBytes(_ inAudioFile: AudioFileID, _ inUseCache: Bool, _ inStartingByte: Int64, _ ioNumBytes: UnsafeMutablePointer<UInt32>, _ outBuffer: UnsafeMutableRawPointer) -> OSStatus

第一个参数inAudioFile: AudioFile实例所对应的ID

第二个参数inUseCache: 是否需要缓存

第三个参数inStartingByte: 从第几个byte开始读取数据

第四个参数ioNumBytes: 这个参数在调用时作为输入参数,表示需要读取多少数据,调用完成后作为输出参数表示实际读取了多少数据(即AudioFile_ReadProc回调中的requestCount和actualCount)

第五个参数outBuffer: buffer指针,需要事先分配好足够大的内存( ioNumBytes大小 ,即AudioFile_ReadProc回调中的buffer,所以在AudioFile_ReadProc回调中不需要再分配内存)。

返回值表示是否读取成功,EOF时会返回kAudioFileEndOfFileError;

使用AudioFileReadBytes得到的数据都是没有进行过帧分离的数据,如果想要用来播放或者解码还必须通过AudioFileStream进行帧分离;

按帧(Packet)读取音频数据:

按帧读取的方法有两个,这两个方法看上去差不多,就连参数也几乎相同,但使用场景和效率上却有所不同

AudioFileReadBytes :
public func AudioFileReadPacketData(_ inAudioFile: AudioFileID, _ inUseCache: Bool, _ ioNumBytes: UnsafeMutablePointer<UInt32>, _ outPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?, _ inStartingPacket: Int64, _ ioNumPackets: UnsafeMutablePointer<UInt32>, _ outBuffer: UnsafeMutableRawPointer?) -> OSStatus

AudioFileReadPackets
public func AudioFileReadPackets(_ inAudioFile: AudioFileID, _ inUseCache: Bool, _ outNumBytes: UnsafeMutablePointer<UInt32>, _ outPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?, _ inStartingPacket: Int64, _ ioNumPackets: UnsafeMutablePointer<UInt32>, _ outBuffer: UnsafeMutableRawPointer?) -> OSStatus

第一个参数inAudioFile: AudioFile实例所对应的ID

第二个参数inUseCache: 是否需要缓存

第三个参数ioNumBytes: 对于AudioFileReadPacketData方法ioNumBytes在输入时表示outBuffer的size,输出时表示实际读取了多少size的数据,而对于AudioFileReadPackets方法ioNumBytes只在输出时使用,表示实际读取了多少size的数据。

第四个参数outPacketDescriptions: 帧信息数组指针,在输入前需要分配内存,大小必须足够存在ioNumPackets个帧信息(ioNumPackets * sizeof(AudioStreamPacketDescription));

第五个参数inStartingPacket: 从第几帧开始读取数据

第六个参数ioNumPackets: 在输入时表示需要读取多少个帧,在输出时表示实际读取了多少帧

第七个参数outBuffer: 数据指针,在输入前就需要分配好空间,对于AudioFileReadPacketData方法只要分配近似帧大小 * 帧数的内存空间即可,方法本身会针对给定的内存空间大小来决定最后输出多少个帧,如果空间不够会适当减少输出的帧数。而对于AudioFileReadPackets则需要分配最大帧大小 * 帧数的内存空间才行。这也就是为何第三个参数一个是输入输出双向使用的,而另一个只是输出时使用的原因。就这点来说两个方法中前者在使用的过程中要比后者更省内存;

返回值表示是否读取成功,EOF时会返回kAudioFileEndOfFileError;

一般当需要读取固定时长音频或者非压缩音频时才会用到AudioFileReadPackets,其余时候使用AudioFileReadPacketData会有更高的效率并且更省内存;这两个方法读取后的数据为帧分离后的数据,可以直接用来播放或者解码。

Seek

对于原始的PCM数据来说每一个PCM帧都是固定长度的,对应的播放时长也是固定的,但一旦转换成压缩后的音频数据就会因为编码形式的不同而不同了。

对于CBR编码而言每个帧中所包含的PCM数据帧是恒定的,所以每一帧对应的播放时长也是恒定的;

而VBR编码则不同,为了保证数据最优并且文件大小最小,VBR编码的每一帧中所包含的PCM数据帧是不固定的,这就导致在流播放的情况下VBR的数据想要做seek并不容易。这里我们展示CBR编码下的seek。

由于AudioFile没有方法来帮助修正seek的offset和seek的时间,所以我们只能用下两种seek到预估的位置,并不十分准确。

使用AudioFileReadBytes时计算应该seek到哪个字节

var seekToTime = ...//需要seek到哪个时间,秒为单位
var audioDataByteCount = ... //文件长度,通过kAudioFilePropertyAudioDataByteCount获取的值
var dataOffset = ... //数据偏移,通过kAudioFilePropertyDataOffset获取的值
var durtion = ... //通过公式(AudioDataByteCount * 8) / BitRate计算得到的时长
var seekOffset = dataOffset + (seekToTime / duration) * audioDataByteCount//预估seekOffset = 数据偏移 + seekToTime对应的近似字节数

使用AudioFileReadPacketData或者AudioFileReadPackets时计算seekToTime对应的是第几个帧

var seekToTime = ...//需要seek到哪个时间,秒为单位
var description: AudioStreamBasicDescription = ...//通过kAudioFilePropertyDataFormat获取的值
var packetDuration = description.mFramesPerPacket / description.mSampleRate//计算每个packet对应的时长
var seekToPacket = floor(seekToTime / packetDuration)//计算packet位置    
     

关闭AudioFile

AudioFile使用完毕后需要调用AudioFileClose进行关闭

extern OSStatus AudioFileClose(AudioFileID inAudioFile)	

AudioFile使用流程总结

  1. AudioFile有两个Open方法,需要针对自身的使用场景选择不同的方法;
    AudioFileOpenURL用来读取本地文件,AudioFileOpenWithCallbacks的使用场景比前者要广泛,使用时需要注意AudioFile_ReadProc,这个回调方法在Open方法本身和Read方法被调用时会被同步调用。
  2. 必须保证音频文件格式信息可读时才能使用AudioFile的Open方法,AudioFile并不能独立用于音频流的读取,需要配合AudioStreamFile使用才能读取流(需要用AudioStreamFile来判断文件格式信息可读之后再调用Open方法)。
  3. 使用AudioFileGetProperty读取格式信息时需要判断所读取的信息是否需要先调用AudioFileGetPropertyInfo获得数据大小后再进行读取。
  4. 读取音频数据应该根据使用的场景选择不同的音频读取方法,对于不同的读取方法seek时需要计算的变量也不相同。
  5. AudioFile使用完毕后需要调用AudioFileClose进行关闭。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值