AudioFileStream介绍
AudioFileStreamer的作用是用来读取采样率、码率、时长等基本信息以及分离音频帧。
数据的相关内容都和它相关,所以还是很重要的,其实AudioQueue使用起来比较简单,复杂的部分都在这个数据的处理上了。。。
根据Apple的描述AudioFileStreamer用在流播放中,当然不仅限于网络流,本地文件同样可以用它来读取信息和分离音频帧。AudioFileStreamer的主要数据是文件数据而不是文件路径,所以数据的读取需要使用者自行实现。
AudioFileStreamer的主要数据是文件数据,支持的文件格式有:
- 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
初始化AudioFileStream,创建一个音频流解析器,生成一个AudioFileStream示例。
extern OSStatus
AudioFileStreamOpen (
void * __nullable inClientData,
AudioFileStream_PropertyListenerProc inPropertyListenerProc,
AudioFileStream_PacketsProc inPacketsProc,
AudioFileTypeID inFileTypeHint,
AudioFileStreamID __nullable * __nonnull outAudioFileStream) __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);
- inClientData:用户指定的数据,用于传递给回调函数,这里我们指定(__bridge LocalAudioPlayer*)self
- inPropertyListenerProc:是歌曲信息解析的回调,每解析出一个歌曲信息都会进行一次回调
- inPacketsProc:是分离帧的回调,当解析到一个音频帧时,将回调该方法
- inFileTypeHint:指明音频数据的格式,如果你不知道音频数据的格式,可以传0
- outAudioFileStream:AudioFileStreamID实例,需保存供后续使用
//AudioFileTypeID枚举
enum {
kAudioFileAIFFType = 'AIFF',
kAudioFileAIFCType = 'AIFC',
kAudioFileWAVEType = 'WAVE',
kAudioFileSoundDesigner2Type = 'Sd2f',
kAudioFileNextType = 'NeXT',
kAudioFileMP3Type = 'MPG3', // mpeg layer 3
kAudioFileMP2Type = 'MPG2', // mpeg layer 2
kAudioFileMP1Type = 'MPG1', // mpeg layer 1
kAudioFileAC3Type = 'ac-3',
kAudioFileAAC_ADTSType = 'adts',
kAudioFileMPEG4Type = 'mp4f',
kAudioFileM4AType = 'm4af',
kAudioFileM4BType = 'm4bf',
kAudioFileCAFType = 'caff',
kAudioFile3GPType = '3gpp',
kAudioFile3GP2Type = '3gp2',
kAudioFileAMRType = 'amrf'
};
这个函数会创建一个AudioFileStreamID,之后所有的操作都是基于这个ID来的,然后还是创建2个回调 inPropertyListenerProc 和 inPacketsProc,这2个回调函数比较重要,下面详说。
OSStatus返回值用来判断是否成功初始化(OSStatus == noErr)。
解析数据
在初始化完成之后,调用该方法解析文件数据。解析时调用方法:
extern OSStatus AudioFileStreamParseBytes(AudioFileStreamID inAudioFileStream,
UInt32 inDataByteSize,
const void* inData,
UInt32 inFlags);
参数的说明如下:
- inAudioFileStream:AudioFileStreamID实例,由AudioFileStreamOpen打开
- inDataByteSize:此次解析的数据字节大小
- inData:此次解析的数据大小
- inFlags:数据解析标志,其中只有一个值kAudioFileStreamParseFlag_Discontinuity = 1,表示解析的数据是否是不连续的,目前我们可以传0。
OSStatus的值不是noErr则表示解析不成功,对应的错误码:
enum
{
kAudioFileStreamError_UnsupportedFileType = 'typ?',
kAudioFileStreamError_UnsupportedDataFormat = 'fmt?',
kAudioFileStreamError_UnsupportedProperty = 'pty?',
kAudioFileStreamError_BadPropertySize = '!siz',
kAudioFileStreamError_NotOptimized = 'optm',
kAudioFileStreamError_InvalidPacketOffset = 'pck?',
kAudioFileStreamError_InvalidFile = 'dta?',
kAudioFileStreamError_ValueUnknown = 'unk?',
kAudioFileStreamError_DataUnavailable = 'more',
kAudioFileStreamError_IllegalOperation = 'nope',
kAudioFileStreamError_UnspecifiedError = 'wht?',
kAudioFileStreamError_DiscontinuityCantRecover = 'dsc!'
};
每次调用成功后应该注意返回值,一旦出现错误就不必要进行后续的解析。
回调介绍
解析文件格式信息
AudioFileStream_PropertyListenerProc,解析文件格式信息的回调,在调用AudioFileStreamParseBytes方法进行解析时会首先读取格式信息,并同步的进入AudioFileStream_PropertyListenerProc回调方法。
在这个回调中,你可以拿到你想要的音频相关信息,比如音频结构(AudioStreamBasicDescription),码率(BitRate),MagicCookie等等,通过这些信息,你还可以计算其他数据,比如音频总时长。
进入这个方法看一下:
typedef void (*AudioFileStream_PropertyListenerProc)(
void * inClientData,
AudioFileStreamID inAudioFileStream,
AudioFileStreamPropertyID inPropertyID,
AudioFileStreamPropertyFlags * ioFlags);
第一个参数是我们初始化实例的上下文对象
第二个参数是实例的ID
第三个参数是此次回调解析的信息ID,表示当前PropertyID对应的信息已经解析完成(例如数据格式,音频信息的偏移量),可以通过AudioFileStreamGetProperty来获取这个propertyID里面对应的值
extern OSStatus
AudioFileStreamGetPropertyInfo(
AudioFileStreamID inAudioFileStream,
AudioFileStreamPropertyID inPropertyID,
UInt32 * __nullable outPropertyDataSize,
Boolean * __nullable outWritable)
__OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);
第四个参数ioFlags是一个返回的参数,表示这个property是否需要缓存,如果需要的话就可以赋值kAudioFileStreamPropertyFlag_PropertyIsCached
这个回调会进行多次,但不是每一次都需要进行处理,propertyID的列表如下:
CF_ENUM(AudioFileStreamPropertyID)
{
kAudioFileStreamProperty_ReadyToProducePackets = 'redy',
kAudioFileStreamProperty_FileFormat = 'ffmt',
kAudioFileStreamProperty_DataFormat = 'dfmt',
kAudioFileStreamProperty_FormatList = 'flst',
kAudioFileStreamProperty_MagicCookieData = 'mgic',
kAudioFileStreamProperty_AudioDataByteCount = 'bcnt',
kAudioFileStreamProperty_AudioDataPacketCount = 'pcnt',
kAudioFileStreamProperty_MaximumPacketSize = 'psze',
kAudioFileStreamProperty_DataOffset = 'doff',
kAudioFileStreamProperty_ChannelLayout = 'cmap',
kAudioFileStreamProperty_PacketToFrame = 'pkfr',
kAudioFileStreamProperty_FrameToPacket = 'frpk',
kAudioFileStreamProperty_PacketToByte = 'pkby',
kAudioFileStreamProperty_ByteToPacket = 'bypk',
kAudioFileStreamProperty_PacketTableInfo = 'pnfo',
kAudioFileStreamProperty_PacketSizeUpperBound = 'pkub',
kAudioFileStreamProperty_AverageBytesPerPacket = 'abpp',
kAudioFileStreamProperty_BitRate = 'brat',
kAudioFileStreamProperty_InfoDictionary = 'info'
};
这里解释几个propertyID
1.kAudioFileStreamProperty_ReadyToProducePackets 表示解析完成,可以对音频数据开始进行帧的分离
2.kAudioFileStreamProperty_BitRate 表示音频数据的码率,获取这个property是为了计算音频的总时长duration,而且在数据量比较小时出现ReadyToProducePackets还是没有获取到bitRate,这时需要分离一些帧,然后计算平均bitRate UInt32 averageBitRate = totalPackectByteCount / totalPacketCout;
3.kAudioFileStreamProperty_DataOffset 表示音频数据在整个音频文件的offset,因为大多数音频文件都会有一个文件头。个值在seek时会发挥比较大的作用,音频的seek并不是直接seek文件位置而seek时间(比如seek到2分10秒的位置),seek时会根据时间计算出音频数据的字节offset然后需要再加上音频数据的offset才能得到在文件中的真正offset。
4.kAudioFileStreamProperty_DataFormat 表示音频文件结构信息,是一个AudioStreamBasicDescription
struct AudioStreamBasicDescription
{
Float64 mSampleRate;
AudioFormatID mFormatID;
AudioFormatFlags mFormatFlags;
UInt32 mBytesPerPacket;
UInt32 mFramesPerPacket;
UInt32 mBytesPerFrame;
UInt32 mChannelsPerFrame;
UInt32 mBitsPerChannel;
UInt32 mReserved;
};
5.kAudioFileStreamProperty_FormatList 作用和kAudioFileStreamProperty_DataFormat一样,不过这个获取到的是一个AudioStreamBasicDescription的数组,这个参数用来支持AAC SBR这样包含多个文件类型的音频格式。但是我们不知道有多少个format,所以要先获取总数据大小
AudioFormatListItem *formatList = malloc(formatListSize);
OSStatus status = AudioFileStreamGetProperty(_audioFileStreamID, kAudioFileStreamProperty_FormatList, &formatListSize, formatList);
if (status == noErr) {
UInt32 supportedFormatsSize;
status = AudioFormatGetPropertyInfo(kAudioFormatProperty_DecodeFormatIDs, 0, NULL, &supportedFormatsSize);
if (status != noErr) {
free(formatList);
return;
}
UInt32 supportedFormatCount = supportedFormatsSize / sizeof(OSType);
OSType *supportedFormats = (OSType *)malloc(supportedFormatsSize);
status = AudioFormatGetProperty(kAudioFormatProperty_DecodeFormatIDs, 0, NULL, &supportedFormatsSize, supportedFormats);
if (status != noErr) {
free(formatList);
free(supportedFormats);
return;
}
for (int i = 0; i * sizeof(AudioFormatListItem) < formatListSize; i++) {
AudioStreamBasicDescription format = formatList[i].mASBD;
for (UInt32 j = 0; j < supportedFormatCount; j++) {
if (format.mFormatID == supportedFormats[j]) {
format = format;
[self calculatePacketDuration];
break;
}
}
}
free(supportedFormats);
};
free(formatList);
6.kAudioFileStreamProperty_AudioDataByteCount 表示音频文件音频数据的总量。这个是用来计算音频的总时长并且可以在seek的时候计算时间对应的字节offset
UInt32 audioDataByteCount;
UInt32 byteCountSize = sizeof(audioDataByteCount);
OSStatus status = AudioFileStreamGetProperty(_audioFileStreamID, kAudioFileStreamProperty_AudioDataByteCount, &byteCountSize, &audioDataByteCount);
if (status == noErr) {
NSLog(@"audioDataByteCount : %u, byteCountSize: %u",audioDataByteCount,byteCountSize);
}
跟bitRate一样,在数据量比较小的时候可能获取不到audioDataByteCount,这时就需要近似计算
UInt32 dataOffset = ...; //kAudioFileStreamProperty_DataOffset
UInt32 fileLength = ...; //音频文件大小
UInt32 audioDataByteCount = fileLength - dataOffset;
这里分享下音频时长的2种计算方式:
-
总时长 = 总帧数*单帧的时长
单帧的时长 = 单帧的采样个数*每帧的时长
每帧的时长 = 1/采样率
采样率:单位时间内的采样个数
-
总时长 = 文件总的字节数/码率
码率:单位时间内的文件字节数
解析完音频帧之后,我们来分离音频帧。
分离音频帧
读取完格式信息完成后,我们来继续调用AudioFileStreamParseBytes方法对帧进行分离,并进入AudioFileStream_PacketsProc回调方法。
typedef void (*AudioFileStream_PacketsProc)(
void * inClientData,
UInt32 inNumberBytes,
UInt32 inNumberPackets,
const void * inInputData,
AudioStreamPacketDescription *inPacketDescriptions);
第一个参数同样是上下文对象
第二个参数,本次处理的数据大小
第三个参数,本次共处理了多少帧,
第四个参数,处理的所有数据
第五个参数,AudioStreamPacketDescription数组,存储了每一帧数据是从第几个字节开始的,这一帧总共多少字节
struct AudioStreamPacketDescription
{
SInt64 mStartOffset;
UInt32 mVariableFramesInPacket;
UInt32 mDataByteSize;
};
处理分离音频帧
if (_discontinuous) {
_discontinuous = NO;
}
if (numberOfBytes == 0 || numberOfPackets == 0) {
return;
}
BOOL deletePackDesc = NO;
if (packetDescriptions == NULL) {
//如果packetDescriptions不存在,就按照CBR处理,平均每一帧数据的数据后生成packetDescriptions
deletePackDesc = YES;
UInt32 packetSize = numberOfBytes / numberOfPackets;
AudioStreamPacketDescription *descriptions = (AudioStreamPacketDescription *)malloc(sizeof(AudioStreamPacketDescription)*numberOfPackets);
for (int i = 0; i < numberOfPackets; i++) {
UInt32 packetOffset = packetSize * i;
descriptions[i].mStartOffset = packetOffset;
descriptions[i].mVariableFramesInPacket = 0;
if (i == numberOfPackets-1) {
descriptions[i].mDataByteSize = numberOfPackets-packetOffset;
}else{
descriptions[i].mDataByteSize = packetSize;
}
}
packetDescriptions = descriptions;
}
NSMutableArray *parseDataArray = [NSMutableArray array];
for (int i = 0; i < numberOfPackets; i++) {
SInt64 packetOffset = packetDescriptions[i].mStartOffset;
//把解析出来的帧数据放进自己的buffer中
NParseAudioData *parsedData = [NParseAudioData parsedAudioDataWithBytes:packets+packetOffset packetDescription:packetDescriptions[i]];
[parseDataArray addObject:parsedData];
if (_processedPacketsCount < BitRateEstimationMaxPackets) {
_processedPacketsSizeTotal += parsedData.packetDescription.mDataByteSize;
_processedPacketsCount += 1;
[self calculateBitRate];
[self calculateDuration];
}
}
...
if (deletePackDesc) {
free(packetDescriptions);
}
inPacketDescriptions这个字段为空时需要按CBR的数据处理。但其实在解析CBR数据时inPacketDescriptions一般也有返回,因为即使是CBR数据帧的大小也不是恒定不变的,例如CBR的MP3会在每一帧的数据后放1byte的填充位,这个填充位也不一定一直存在,所以帧会有1byte的浮动
Seek
这个其实就是我们拖动进度条,需要到几分几秒,而我们实际上操作的是文件,即寻址到第几个字节开始播放音频数据
对于原始的PCM数据来说每一个PCM帧都是固定长度的,对应的播放时长也是固定的,但一旦转换成压缩后的音频数据就会因为编码形式的不同而不同了。对于CBR而言每个帧中所包含的PCM数据帧是恒定的,所以每一帧对应的播放时长也是恒定的;而VBR则不同,为了保证数据最优并且文件大小最小,VBR的每一帧中所包含的PCM数据帧是不固定的,这就导致在流播放的情况下VBR的数据想要做seek并不容易。这里我们也只讨论CBR下的seek。
我们一般是这样实现CBR的seek
1.近似地计算seek到哪个字节
double seekToTime = ...; //需要seek到哪个时间,秒为单位
UInt64 audioDataByteCount = ...; //通过kAudioFileStreamProperty_AudioDataByteCount获取的值
SInt64 dataOffset = ...; //通过kAudioFileStreamProperty_DataOffset获取的值
double durtion = ...; //通过公式(AudioDataByteCount * 8) / BitRate计算得到的时长
//近似seekOffset = 数据偏移 + seekToTime对应的近似字节数
SInt64 approximateSeekOffset = dataOffset + (seekToTime / duration) * audioDataByteCount;
2.计算seekToTime对应的是第几个帧 利用之前的解析得到的音频格式信息计算packetDuration
//首先需要计算每个packet对应的时长
AudioStreamBasicDescription asbd = ...; ////通过kAudioFileStreamProperty_DataFormat或者kAudioFileStreamProperty_FormatList获取的值
double packetDuration = asbd.mFramesPerPacket / asbd.mSampleRate
//然后计算packet位置
SInt64 seekToPacket = floor(seekToTime / packetDuration);
3.使用AudioFileStreamSeek计算精确的字节偏移时间 AudioFileStreamSeek可以用来寻找某一个帧(Packet)对应的字节偏移(byte offset):
-
如果ioFlags里有kAudioFileStreamSeekFlag_OffsetIsEstimated说明给出的outDataByteOffset是估算的,并不准确,那么还是应该用第1步计算出来的approximateSeekOffset来做seek;
-
如果ioFlags里没有kAudioFileStreamSeekFlag_OffsetIsEstimated说明给出了准确的outDataByteOffset,就是输入的seekToPacket对应的字节偏移量,我们可以根据outDataByteOffset来计算出精确的seekOffset和seekToTime;
4.按照seekByteOffset读取对应的数据继续使用AudioFileStreamParseByte进行解析
计算duration
获取时长的最佳方法是从ID3信息中去读取,那样是最准确的。如果ID3信息中没有存,那就依赖于文件头中的信息去计算了。
计算duration的公式如下:
double duration = (audioDataByteCount * 8) / bitRate
音频数据的字节总量audioDataByteCount可以通过kAudioFileStreamProperty_AudioDataByteCount获取,码率bitRate可以通过kAudioFileStreamProperty_BitRate获取也可以通过Parse一部分数据后计算平均码率来得到。
对于CBR数据来说用这样的计算方法的duration会比较准确,对于VBR数据就不好说了。所以对于VBR数据来说,最好是能够从ID3信息中获取到duration,获取不到再想办法通过计算平均码率的途径来计算duration。
最后需要关闭AudioFileStream
extern OSStatus AudioFileStreamClose(AudioFileStreamID inAudioFileStream);
小结
- 使用AudioFileStream需要先调用AudioFileStreamOpen,最好提供文件类型帮助解析
- 当有数据时调用AudioFileStreamParseBytes进行解析,当出现noErr以外的值则代表解析出错,kAudioFileStreamError_NotOptimized则代表文件缺少头信息或者在文件尾部不适合流播放
- 在调用AudioFileStreamParseBytes之后会先进入AudioFileStream_PropertyListenerProc,当回调得到kAudioFileStreamProperty_ReadyToProducePackets则再进入MyAudioFileStreamPacketsCallBack分离帧信息。
- 使用后需关闭AudioFileStream
这里(github.com/Nicholas86/…)是代码