音频播放2
学习地址:iOS音频播放三 以下纯属个人笔记
Audio File Stream Services#
解析采样率、码率、时长等信息,分离音频帧 —— 音频文件解析器
一、初始化AudioFileStream
extern OSStatus
AudioFileStreamOpen (
void * __nullable inClientData,
AudioFileStream_PropertyListenerProc inPropertyListenerProc,
AudioFileStream_PacketsProc inPacketsProc,
AudioFileTypeID inFileTypeHint,
AudioFileStreamID __nullable * __nonnull outAudioFileStream)
-
inClientData :上下文信息,生命周期长
-
inPropertyListenerProc :采样率、码率、时长采集回调函数(需要自己创建)
-
inPacketsProc :文件包解析回调函数(需要自己创建)
-
inFileTypeHint :文件格式提示
-
outAudioFileStream :返回的是AudioFileStream实例对应的AudioFileStreamID,这个ID需存储作为后续方法的参数
-
OSStatus :判断是否初始化成功(OSStatus == noErr)
二、解析数据
extern OSStatus
AudioFileStreamParseBytes(
AudioFileStreamID inAudioFileStream,
UInt32 inDataByteSize,
const void * inData,
AudioFileStreamParseFlags inFlags)
-
inAudioFileStream :AudioFileStreamID,即初始化时返回的ID
-
inDataByteSize :本次解析的数据长度
-
inData :本次解析的数据
-
inFlags :这个参数说明本次解析和上一次解析是否是连续的关系,如果是不连续传入
kAudioFileStreamParseFlag_Discontinuity
,反之传入0 -
OSStatus :解析返回值,如果解析成功(OSStatus == noErr)
网易音乐的博主是如何解释连续的:
因为在第一篇中提到过形如MP3的数据都是以帧的形式存在的,解析时也需要以帧为单位解析。但在解码之前我们不可能知道每个帧的边界在第几个字节,所以就会出现这样的情况:我们传给AudioFileStreamParseBytes的数据在解析完成之后会有一部分数据余下来,这部分数据世界下去那一帧的前半部分,如果再次有数据输入需要继续解析时就必须要用前一次解析余下来的数据才能保证帧数据完整,所以在正常播放的时候传入0即可,目前知道需要传入‘1’的情况有两种:
- 在seek完毕之后显然seek后的数据和之前的数据完全无关;
- 在work around 在回调得到
kAudioFileStreamProperty_ReadyToProducePackets
之后,在正常解析第一帧之前都传入kAudioFileStreamParseFlag_Discontinuity
比较好。
OSStatus 错误枚举展示:
CF_ENUM(OSStatus)
{
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!'
};
需要提一下是kAudioFileStreamError_NotOptimized
,文档描述:
It is not possible to produce output packets because the streamed audio file's packet table or other defining information is not present or appears after the audio data.
不能播放这个输出包,因为这个流音频文件包表头或者其他定义的信息没有显示或展示在音频数据之后。换句话说:文件需要全部下载完成才能进行播放,无法流播。
注意AudioFileStreamParseBytes
方法每一次调用都应该注意返回值,一旦出现错误就不必继续Parse了。
三、解析文件格式信息##
在调用AudioFileStreamParseBytes
方法进行解析时会首先读取格式信息,并同步的进入AudioFileStream_PropertyListenerProc
回调方法:
typedef void (*AudioFileStream_PropertyListenerProc)(
void * inClientData,
AudioFileStreamID inAudioFileStream,
AudioFileStreamPropertyID inPropertyID,
AudioFileStreamPropertyFlags * ioFlags);
- inPropertyID :这个参数是此次回调解析的信息ID。表示当前PropertyID对应的信息已经解析完成信息(例如数据格式、音频数据的偏移量等等),使用者可以通过
AudioFileStreamGetProperty
接口获取PropertyID对应的值或者数据结构:
extern OSStatus
AudioFileStreamGetPropertyInfo(
AudioFileStreamID inAudioFileStream,
AudioFileStreamPropertyID inPropertyID,
UInt32 * __nullable outPropertyDataSize,
Boolean * __nullable outWritable)
- ioFlags :这个参数是一个返回参数,表示这个Property是否需要被缓存,如果需要赋值
kAudioFileStreamPropertyFlag_PropertyIsCached
,反之不赋值?
这个回调会进去多次,但并不是每次都需要进行处理,可以根据需求处理需要的PropertyID进行处理(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'
};
这里列出几个比较重要的PropertID
1.kAudioFileStreamProperty_BitRate
表示音频数据的码率,获取这个property是为了计算音频的总时长duration(因为AudioFileStream没有这样的接口?)
四、分离音频帧##
读取格式信息之后继续调用AudioFileStreamParseBytes
方法可以对帧进行分离,并同步进入AudioFileStream_PacketsProc
回调方法。
typedef void (*AudioFileStream_PacketsProc)(
void * inClientData,
UInt32 inNumberBytes,
UInt32 inNumberPackets,
const void * inInputData,
AudioStreamPacketDescription *inPacketDescriptions);
- inNumberBytes :本次处理的数据大小
- inNumberPackets :本次总共处理了多少帧(即代码里的Packet)
- inInputData :本次处理的所有的数据
- inPacketDescriptions :一个数组,存储了每一帧数据是从第几个
// mVariableFramesInPacket是指实际的数据帧只有VBR的数据才能用到(像MP3这样压缩数据一个帧里面会有好几个数据帧)
struct AudioStreamPacketDescription
{
SInt64 mStartOffset;
UInt32 mVariableFramesInPacket;
UInt32 mDataByteSize;
};
下面是网易云音乐工程师写的毁掉方法片段 我重新整理了一下:
#pragma mark -
#pragma mark - Packet Call Back
static void MyAudioFileStreamPacketsCallBack(void * inClinetData,
UInt32 inNumberBytes,
UInt32 inNumberPackets,
const void * inInputData,
AudioStreamPacketDescription * inPacketDescriptions)
{
LVPlayer * audioFileStream = (__bridge LVPlayer *)inClinetData;
[audioFileStream handleAudioFileStreamPackets:inClinetData
numberOfBytes:inNumberBytes
numberOfPackets:inNumberPackets
packetDescriptions:inPacketDescriptions];
};
- (void)handleAudioFileStreamPackets:(const void *)packets
numberOfBytes:(UInt32)numberOfBytes
numberOfPackets:(UInt32)numberOfPackets
packetDescriptions:(AudioStreamPacketDescription *)packetDescription
{
// 1.处理discontinue..
if (_discontinuous) {
_discontinuous = NO;
}
// 2.空返回
if (numberOfBytes == 0 || numberOfPackets == 0) {
return;
}
// 3.
BOOL deletePackDesc = NO;
if (packetDescription == 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 = numberOfBytes - packetOffset;
}
else
{
descriptions[i].mDataByteSize = packetSize;
}
}
packetDescription = descriptions;
}
NSMutableArray * parsedDataArray = [NSMutableArray array];
for (int i = 0; i < numberOfPackets; ++ i)
{
SInt64 packetOffset = packetDescription[i].mStartOffset;
SInt32 packetSize = packetDescription[i].mDataByteSize;
// 解析出来的帧数据放进自己的buffer中
// ...
}
if (deletePackDesc) {
free(packetDescription);
}
}
五、Seek
就音频角度来说Seek功能描述为“我要拖到XX分XX秒”,而实际操作时我们需要操作的是文件,所以我们需要知道的是“我要拖到XX分XX秒”这个操作对应文件上是要从第几个字节开始读取音频数据。
对于原始的PCM数据来说每一个PCM帧都是固定长度的,对应的播放时长也是固定的,但一旦转换成压缩后的音频数据就会因为编码形式的不同而不同了。
对于CBR(固定码率)而言每个帧所包含的PCM数据帧都是恒定的,所以每一帧对应的播放时长也是恒定的;
对于VBR(可变码率)则是不同的,为了保证数据最优并且文件最小,VBR的每一帧所包含的PCM数据帧是不固定的,这就导致在流播放的情况下VBR的数据想要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对应的是第几帧(Packet)
我们可以利用之前Parse得到的音频格式信息来计算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
进行解析。
如果是网络流可以通过设置range头来获取字节,本地文件的话直接seek就好了。调用AudioFileStreamParseByte
时注意刚seek完第一次Parse数据需要加参数kAudioFileStreamParseFlag_Discontinuity
。
六、关闭AudioFileStream
AudioFileStream
使用完毕后需要调用AudioFileStreamClose
进行关闭;
extern OSStatus AudioFileStreamClose(AudioFileStreamID inAudioFileStream);
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步