iOS/swift音频播放(三)— AudioFileStream

  • AudioFileStream
  • 初始化AudioFileStream
  • 数据解析AudioFileStreamParseBytes
  • 解析文件格式信息AudioFileStream_PropertyListenerProc
  • 根据获PropertyID读取音频格式信息
  • AudioFileStreamGetPropertyInfo
  • AudioFileStreamGetProperty
  • 计算时长Duration
  • 分离音频帧
  • AudioFileStream_PacketsProc
  • Seek
  • 计算应该seek到哪个字节
  • 计算seekToTime对应的是第几个帧
  • 使用AudioFileStreamSeek计算精确的字节偏移和时间
  • 按照seekByteOffset读取对应的数据继续使用AudioFileStreamParseByte进行解析
  • 关闭AudioFileStream
  • AudioFileStream使用流程总结


AudioFileStream

iOS mp3边下边播 ios播放mp3文件_swift


根据官方的描述AudioFileStreamer用在流播放中。而且它的使用不仅限于网络流,本地文件同样可以用它来读取信息和分离音频帧。AudioFileStreamer的主要数据是文件数据而不是文件路径,所以数据的读取需要使用者自行实现,

支持的文件格式如下所示,如果想要解码其他音频格式(如OGG、FLAC等)就需要自己来实现解码器了。

MPEG-1 Audio Layer 3, used for .mp3 files
MPEG-2 ADTS, used for the .aac audio data format
AIFC
AIFF
CAF
MPEG-4, used for .m4a, .mp4, and .3gp files
NeXT
WAVE

初始化AudioFileStream

public func AudioFileStreamOpen(_ inClientData: UnsafeMutableRawPointer?, _ inPropertyListenerProc: AudioFileStream_PropertyListenerProc, _ inPacketsProc: AudioFileStream_PacketsProc, _ inFileTypeHint: AudioFileTypeID, _ outAudioFileStream: UnsafeMutablePointer<AudioFileStreamID?>) -> OSStatus

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

第二个参数 inPropertyListenerProc: 是音频信息解析的回调,每解析出一个音频信息都会进行一次回调。

第三个参数 inPacketsProc: 分离帧的回调,每解析出一部分帧就会进行一次回调。

第四个参数 inFileTypeHint: 和AudioFile的open方法相同,是一个帮助AudioFileStream解析文件的类型提示,如果文件类型确定的话应当传入。

第五个参数 outAudioFileStream:返回的AudioFileStream实例对应的AudioFileStreamID,这个ID需要保存起来作为后续一些方法的参数使用;

返回值为noErr时表示成功初始化

数据解析AudioFileStreamParseBytes

当我们初始化完成后,只要拿到文件数据就可以进行解析了。解析时调用方法AudioFileStreamParseBytes

public func AudioFileStreamParseBytes(_ inAudioFileStream: AudioFileStreamID, _ inDataByteSize: UInt32, _ inData: UnsafeRawPointer?, _ inFlags: AudioFileStreamParseFlags) -> OSStatus

第一个参数 inAudioFileStream:初始化时返回的ID。

第二个参数 inDataByteSize:本次解析的数据长度。

第三个参数 inData:本次解析的数据。

第四个参数 inFlags:本次的解析是否和上一次是连续的关系,如果是连续的传入0,否则传入kAudioFileStreamParseFlag_Discontinuity。

什么是连续?:MP3的数据都以帧的形式存在的,解析时也需要以帧为单位解析。但在解码之前我们不可能知道每个帧的边界在第几个字节,所以就会出现这样的情况:我们传给AudioFileStreamParseBytes的数据在解析完成之后会有一部分数据余下来,这部分数据是接下去那一帧的前半部分,如果再次有数据输入需要继续解析时就必须要用到前一次解析余下来的数据才能保证帧数据完整,所以在正常播放的情况下传入0即可。

返回值表示成功当前的数据是否被正常解析,如果返回值不是noErr则表示解析不成功。AudioFileStreamParseBytes方法每一次调用都应该注意返回值,一旦出现错误就可以不必继续解析了。
其中错误码如下所示:

public var kAudioFileStreamError_UnsupportedFileType: OSStatus { get }
指定的文件类型不支持

public var kAudioFileStreamError_UnsupportedDataFormat: OSStatus { get }
指定的文件类型不支持该数据格式。

public var kAudioFileStreamError_UnsupportedProperty: OSStatus { get }
不支持该属性

public var kAudioFileStreamError_BadPropertySize: OSStatus { get }
您为属性数据提供的缓冲区大小不正确

public var kAudioFileStreamError_NotOptimized: OSStatus { get }
无法生成输出数据包,因为流音频文件的数据包或其他定义信息不存在或出现在音频数据之后,无法正常解析。换句话说就是这个文件需要全部下载完才能播放,无法流播。

public var kAudioFileStreamError_InvalidPacketOffset: OSStatus { get }
数据包偏移量小于0,或者超过了文件末尾,或者在构建数据包表时读取了损坏的数据包大小。

public var kAudioFileStreamError_InvalidFile: OSStatus { get }
该文件格式错误,不是该类型音频文件的有效实例,或者未被识别为音频文件。

public var kAudioFileStreamError_ValueUnknown: OSStatus { get }
该属性值在音频数据之前不存在于此文件中。

public var kAudioFileStreamError_DataUnavailable: OSStatus { get }
提供给解析器的数据量不足以产生任何结果。

public var kAudioFileStreamError_IllegalOperation: OSStatus { get }
试图进行非法操作。

public var kAudioFileStreamError_UnspecifiedError: OSStatus { get }
发生了未指定的错误。

public var kAudioFileStreamError_DiscontinuityCantRecover: OSStatus { get }
音频数据中出现了间断,并且音频文件流服务无法恢复。

解析文件格式信息AudioFileStream_PropertyListenerProc

在调用AudioFileStreamParseBytes方法进行解析时会首先读取格式信息,并同步的进入AudioFileStream_PropertyListenerProc回调方法

public typealias AudioFileStream_PropertyListenerProc = @convention(c) (UnsafeMutableRawPointer, AudioFileStreamID, AudioFileStreamPropertyID, UnsafeMutablePointer<AudioFileStreamPropertyFlags>) -> Void

let listenerProc: AudioFileStream_PropertyListenerProc = {
	inClientData,
	inAudioFileStream,
	inPropertyID,
	ioFlags in
}

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

第二个参数 inAudioFileStream: 表示当前audioFileStream的ID

第三个参数 inPropertyID: 此次回调解析的信息ID。表示当前PropertyID对应的信息已经解析完成信息(例如数据格式、音频数据的偏移量等等),可以通过AudioFileStreamGetProperty接口获取PropertyID对应的值或者数据结构。

第四个参数 ioFlags:是一个返回参数,表示这个property是否需要被缓存,如果需要赋值kAudioFileStreamPropertyFlag_PropertyIsCached,否则不赋值

PropertyID类型如下:

public var kAudioFileStreamProperty_ReadyToProducePackets: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_FileFormat: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_DataFormat: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_FormatList: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_MagicCookieData: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_AudioDataByteCount: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_AudioDataPacketCount: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_MaximumPacketSize: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_DataOffset: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_ChannelLayout: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_PacketToFrame: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_FrameToPacket: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_RestrictsRandomAccess: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_PacketToRollDistance: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_PreviousIndependentPacket: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_NextIndependentPacket: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_PacketToDependencyInfo: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_PacketToByte: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_ByteToPacket: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_PacketTableInfo: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_PacketSizeUpperBound: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_AverageBytesPerPacket: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_BitRate: AudioFileStreamPropertyID { get }
public var kAudioFileStreamProperty_InfoDictionary: AudioFileStreamPropertyID { get }

列举几个常用的ID的含义
kAudioFileStreamProperty_ReadyToProducePackets这个 PropertyID 不必获取相应的值,一旦回调中这个 PropertyID 出现就代表解析完毕,接下来能够对音频数据进行帧分离了。

kAudioFileStreamProperty_DataFormat表示音频文件结构信息,是一个AudioStreamBasicDescription的结构

public struct AudioStreamBasicDescription {
    public var mSampleRate: Float64// 音频格式的采样率,单位为HZ
    public var mFormatID: AudioFormatID//编码格式
    public var mFormatFlags: AudioFormatFlags//数据格式
    public var mBytesPerPacket: UInt32//每个Packet的Bytes数
    public var mFramesPerPacket: UInt32//每个Packet的帧数
    public var mBytesPerFrame: UInt32//每帧的Byte数
    public var mChannelsPerFrame: UInt32//每帧的声道数
    public var mBitsPerChannel: UInt32//每个声道的采样深度
    public var mReserved: UInt32
}

kAudioFileStreamProperty_FormatList作用和kAudioFileStreamProperty_DataFormat是一样的,差别在于用这个PropertyID获取到是一个AudioStreamBasicDescription的数组,这个參数是用来支持AAC SBR这种包括多个文件类型的音频格式。

kAudioFileStreamProperty_BitRate表示音频数据的码率,获取这个Property是为了计算音频的总时长Duration

kAudioFileStreamProperty_DataOffset表示音频数据在整个音频文件里的offset(由于大多数音频文件都会有一个文件头之后才使真正的音频数据),这个值在seek时会发挥比較大的作用。音频的seek并非直接seek文件位置而是seek时间,seek时会依据时间计算出音频数据的字节offset然后须要再加上音频数据的offset才干得到在文件里的真正offset。

kAudioFileStreamProperty_AudioDataByteCount音频文件中音频数据的总量。这个Property的作用一是用来计算音频的总时长,二是可以在seek时用来计算时间对应的字节offset。

根据获PropertyID读取音频格式信息

AudioFileStreamGetPropertyInfo

获取某个PropertyID对应数据的大小以及是否可以被write

public func AudioFileStreamGetPropertyInfo(_ inAudioFileStream: AudioFileStreamID, _ inPropertyID: AudioFileStreamPropertyID, _ outPropertyDataSize: UnsafeMutablePointer<UInt32>?, _ outWritable: UnsafeMutablePointer<DarwinBoolean>?) -> OSStatus

第一个参数inAudioFileStream: AudioFileStream实例所对应的ID

第二个参数inPropertyID: 要读取的PropertyID

第三个参数outPropertyDataSize: 返回某个PropertyID对应数据的大小

第四个参数outWritable: 返回该属性是否可以被write

AudioFileStreamGetProperty

获取某个PropertyID对应数据

public func AudioFileStreamGetProperty(_ inAudioFileStream: AudioFileStreamID, _ inPropertyID: AudioFileStreamPropertyID, _ ioPropertyDataSize: UnsafeMutablePointer<UInt32>, _ outPropertyData: UnsafeMutableRawPointer) -> OSStatus

第一个参数inAudioFileStream: AudioFileStream实例所对应的ID

第二个参数inPropertyID: 表示想获取哪个PropertyID

第三个参数ioPropertyDataSize: 获取的property所表示的数据结构大小,对于大小不定的propertyID,需要先调用AudioFileStreamGetPropertyInfo函数先获取一下大小。

第四个参数outPropertyData: 返回参数,会返回获取的property的值。

计算时长Duration

ID3:一般是位于一个mp3文件的开头或末尾的若干字节内,附加了关于该mp3的歌手,标题,专辑名称,年代,风格等信息,该信息就被称为ID3信息

获取时长的最佳方法是从ID3信息中去读取,那样是最准确的。如果ID3信息中没有存,那就依赖于文件头中的信息去计算了。

let audioDataByteCount = ... //音频数据的总量,使用kAudioFileStreamProperty_AudioDataByteCount获取
let bitRate = ... //音频数据的码率,使用kAudioFileStreamProperty_BitRate获取
let duration = (audioDataByteCount * 8) / bitRate

对于CBR数据来说用这样的计算方法的duration会比较准确,对于VBR数据就不好说了。所以对于VBR数据来说,最好是能够从ID3信息中获取到duration,获取不到再想办法通过计算平均码率的途径来计算duration。

分离音频帧

读取音频格式信息完成之后继续调用AudioFileStreamParseBytes方法可以对帧进行分离,并同步的进入AudioFileStream_PacketsProc回调方法。

AudioFileStream_PacketsProc

public typealias AudioFileStream_PacketsProc = @convention(c) (UnsafeMutableRawPointer, UInt32, UInt32, UnsafeRawPointer, UnsafeMutablePointer<AudioStreamPacketDescription>?) -> Void

let packetsProc: AudioFileStream_PacketsProc = {
	inClientData,
	numberOfBytes,
	numberOfPackets,
	inInputData,
	inPacketDescriptions in
	}

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

第二个参数 numberOfBytes: 本次处理的数据大小

第三个参数 numberOfPackets:本次总共处理了多少帧(即代码里的Packet)

第四个参数 inInputData:本次处理的所有数据

第五个参数 inPacketDescriptions:AudioStreamPacketDescription数组,存储了每一帧数据是从第几个字节开始的,这一帧总共多少字节

public struct AudioStreamPacketDescription {
    public var mStartOffset: Int64//第几个字节开始
    public var mVariableFramesInPacket: UInt32//数据包中数据的样本帧数。对于每个数据包具有恒定帧数的格式,此字段设置为0。
    public var mDataByteSize: UInt32//这一帧总共多少字节
}

Seek

之前AudioFile的介绍了音频数据就会因为编码形式的不同导致每个帧中的数据的不同,CBR编码每个帧中所包含的PCM数据帧是恒定的,VBR编码的每一帧中所包含的PCM数据帧是不固定的。这里我们同样展示CBR编码下的几种seek方式。

计算应该seek到哪个字节

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

计算seekToTime对应的是第几个帧

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

使用AudioFileStreamSeek计算精确的字节偏移和时间

AudioFileStreamSeek可以用来寻找某一个帧(Packet)对应的字节偏移

public func AudioFileStreamSeek(_ inAudioFileStream: AudioFileStreamID, _ inPacketOffset: Int64, _ outDataByteOffset: UnsafeMutablePointer<Int64>, _ ioFlags: UnsafeMutablePointer<AudioFileStreamSeekFlags>) -> OSStatus

第一个参数 inAudioFileStream:初始化时返回的ID。

第二个参数 inPacketOffset:寻找的帧数

第三个参数 outDataByteOffset:对应的字节偏移量

第四个参数 ioFlags

public struct AudioFileStreamSeekFlags : OptionSet {
    public init(rawValue: UInt32)
    public static var offsetIsEstimated: AudioFileStreamSeekFlags { get }
}

如果ioFlags.rawValue == AudioFileStreamSeekFlags.offsetIsEstimated.rawValue说明给出的outDataByteOffset是估算的,并不准确,那么还是应该用第1种方法(计算应该seek到哪个字节)来做seek。
如果ioFlags.rawValue != AudioFileStreamSeekFlags.offsetIsEstimated.rawValue说明给出了准确的outDataByteOffset,就是输入的seekToPacket对应的字节偏移量,我们可以根据outDataByteOffset来计算出精确的seekOffsetseekToTime

var seekToPacket = ...//寻找的帧数
var outDataByteOffset: Int64 = ...//对应的字节偏移量
var ioFlags: AudioFileStreamSeekFlags = AudioFileStreamSeekFlags(rawValue: 0)
let status = AudioFileStreamSeek(audioFileStreamID, seekToPacket, &outDataByteOffset, &ioFlags)
if status == noErr && ioFlags.rawValue != AudioFileStreamSeekFlags.offsetIsEstimated.rawValue {
	//如果AudioFileStreamSeek方法找到了准确的帧字节偏移,需要修正一下时间
	seekToTime -= ((seekOffset - dataOffset) - outDataByteOffset) * 8.0 / bitRate;
	seekByteOffset = outDataByteOffset + dataOffset
} else {
	seekByteOffset = seekOffset
}

按照seekByteOffset读取对应的数据继续使用AudioFileStreamParseByte进行解析

如果是网络流可以通过设置range头来获取字节,本地文件的话直接seek就好了。调用AudioFileStreamParseByte时注意刚seek完第一次Parse数据需要填加参数kAudioFileStreamParseFlag_Discontinuity表示数据中断。

关闭AudioFileStream

AudioFileStreamClose(AudioFileStreamID inAudioFileStream)

AudioFileStream使用流程总结

  1. 首先调用AudioFileStreamOpen,尽量提供inFileTypeHint参数帮助AudioFileStream解析数据,调用完成后取得AudioFileStreamID
  2. 当有数据时调用AudioFileStreamParseBytes进行解析,每一次解析都需要注意返回值,一旦出现noErr以外的返回值就代表Parse出错。使用AudioFileStreamParseBytes需要注意第四个参数在需要合适的时候传入kAudioFileStreamParseFlag_Discontinuity
  3. 调用AudioFileStreamParseBytes后会首先同步进入AudioFileStream_PropertyListenerProc回调来解析文件格式信息,如果回调得到kAudioFileStreamProperty_ReadyToProducePackets表示解析格式信息完成。
  4. 解析格式信息完成后继续调用AudioFileStreamParseBytes会进入AudioFileStream_PacketsProc回调来分离音频帧,在回调中应该将分离出来的帧信息保存到自己的buffer中。
  5. seek时需要先大概的估算seekTime对应的seekByteOffset,然后利用AudioFileStreamSeek计算精确的offset,如果能得到精确的offset就修正一下seektime,如果无法得到精确的offset就用之前的估算结果。
  6. AudioFileStream使用完毕后需要调用AudioFileStreamClose进行关闭;