iOS音频(3):Audio File Stream(一)

2018-05-08  本文已影响0人  李李李大胖子

Audio File Stream:它的作用是用来读取采样率、码率、时长等基本信息以及分离音频帧。

音频文件流服务支持以下音频数据类型:
  • AIFF
  • AFIC
  • WAVE
  • CAF
  • NeXT
  • ADTS
  • MPEG Audio Layer 3
  • AAC

一、初始化AudioFileStream

OSStatus AudioFileStreamOpen(void *inClientData, 
AudioFileStream_PropertyListenerProc inPropertyListenerProc, 
AudioFileStream_PacketsProc inPacketsProc, AudioFileTypeID inFileTypeHint, 
AudioFileStreamID  _Nullable *outAudioFileStream);

参数:
  • inClientData:指向要传递给回调函数的值或结构的指针。
  • inPropertyListenerProc:您的属性侦听器回调。只要解析器在数据流中找到属性的值,就会使用属性ID调用属性侦听器。然后您可以调用和函数来获取该属性的值。AudioFileStreamGetPropertyInfoAudioFileStreamGetProperty
  • inPacketsProc:您的音频数据回调。只要解析器在数据流中找到音频数据包,它就会将数据传递给您的音频数据回调。
  • inFileTypeHint:音频文件类型提示。如果您打算传递给解析器的音频文件流的类型是解析器无法轻松或唯一地根据数据(例如ADTS或AC3)确定的类型,则可以使用此参数指示类型。音频文件服务中的枚举中列出了可能的值。AudioFileTypeID
    无法确定音频文件类型,请通过0
  • outAudioFileStream:在输出上,表示音频文件流解析器的不透明对象。该对象在本文档中被称为音频文件流解析器ID。您需要将此ID传递给音频文件流API中的其他功能。

AudioFileTypeID:

CF_ENUM(AudioFileTypeID) {
        kAudioFileAIFFType              = 'AIFF',
        kAudioFileAIFCType              = 'AIFC',
        kAudioFileWAVEType              = 'WAVE',
        kAudioFileRF64Type              = 'RF64',
        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',
        kAudioFileFLACType              = 'flac'
};

二、解析数据

获取一些流式数据。当有数据传递给解析器时调用该函数。按顺序将数据发送到解析器,如果可能的话,没有间隙。

OSStatus AudioFileStreamParseBytes(
AudioFileStreamID inAudioFileStream, 
UInt32 inDataByteSize, 
const void *inData, 
AudioFileStreamParseFlags inFlags);
参数
  • inAudioFileStream:传递数据的解析器的ID。解析器ID由初始化时返回。
  • inDataByteSize:本次解析的数据字节
  • inData:需要解析的数据
  • inFlags:本次的解析和上一次解析是否是连续的关系,如果是连续的传入0,不连续传kAudioFileStreamParseFlag_Discontinuity

这里需要插入解释一下何谓“连续”。在第一篇中我们提到过形如MP3的数据都以帧的形式存在的,解析时也需要以帧为单位解析。但在解码之前我们不可能知道每个帧的边界在第几个字节,所以就会出现这样的情况:我们传给AudioFileStreamParseBytes的数据在解析完成之后会有一部分数据余下来,这部分数据是接下去那一帧的前半部分,如果再次有数据输入需要继续解析时就必须要用到前一次解析余下来的数据才能保证帧数据完整,所以在正常播放的情况下传入0即可。目前知道的需要传入kAudioFileStreamParseFlag_Discontinuity的情况有两个,一个是在seek完毕之后显然seek后的数据和之前的数据完全无关;
AudioFileStreamParseBytes方法的返回值表示当前的数据是否被正常解析,如果OSStatus的值不是noErr则表示解析不成功,其中错误码包括:

enum
{
//指定的文件类型不受支持。
  kAudioFileStreamError_UnsupportedFileType        = 'typ?',
//数据格式不受指定文件类型的支持
  kAudioFileStreamError_UnsupportedDataFormat      = 'fmt?',
//该属性不受支持
  kAudioFileStreamError_UnsupportedProperty        = 'pty?',
//为属性数据提供的缓冲区大小不正确
  kAudioFileStreamError_BadPropertySize            = '!siz',
//由于流式音频文件的数据包表或其他定义信息不存在或出现在音频数据之后,因此不可能产生输出数据包。
  kAudioFileStreamError_NotOptimized               = 'optm',
//数据包偏移小于0或超过文件末尾,或者在构建数据包表时读取损坏的数据包大小。
  kAudioFileStreamError_InvalidPacketOffset        = 'pck?',
//该文件格式不正确,不是其类型的音频文件的有效实例,或者未被识别为音频文件。
  kAudioFileStreamError_InvalidFile                = 'dta?',
//音频数据之前,此文件中不存在属性值。
  kAudioFileStreamError_ValueUnknown               = 'unk?',
//提供给解析器的数据量不足以产生任何结果。
  kAudioFileStreamError_DataUnavailable            = 'more',
//企图进行非法行动。
  kAudioFileStreamError_IllegalOperation           = 'nope',
//发生未指定的错误。
  kAudioFileStreamError_UnspecifiedError           = 'wht?',
//音频数据中出现不连续性,音频文件流服务无法恢复。
  kAudioFileStreamError_DiscontinuityCantRecover   = 'dsc!'
};

kAudioFileStreamError_NotOptimized:它的含义是说这个音频文件的文件头不存在或者说文件头可能在文件的末尾,当前无法正常Parse,换句话说就是这个文件需要全部下载完才能播放,无法流播。

注意:AudioFileStreamParseBytes方法每一次调用都应该注意返回值,一旦出现错误就可以不必继续Parse了。

解析文件格式信息

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

typedef void (*AudioFileStream_PropertyListenerProc)(void *inClientData,
                                                     AudioFileStreamID inAudioFileStream, 
                                                     AudioFileStreamPropertyID inPropertyID, 
                                                     AudioFileStreamPropertyFlags *ioFlags);
参数
  • inClientData:Open方法中的上下文对象
  • inAudioFileStream:inAudioFileStream是和Open方法中第四个返回参数AudioFileStreamID一样,表示当前FileStream的ID
  • inPropertyID:此次回调解析的信息ID。表示当前PropertyID对应的信息已经解析完成信息(例如数据格式、音频数据的偏移量等等),使用者可以通过AudioFileStreamGetProperty接口获取PropertyID对应的值或者数据结构;
extern OSStatus AudioFileStreamGetProperty(AudioFileStreamID inAudioFileStream,
                                           AudioFileStreamPropertyID inPropertyID,
                                           UInt32 * ioPropertyDataSize,
                                           void * outPropertyData);
  • ioFlags:ioFlags是一个返回参数,表示这个property是否需要被缓存,如果需要赋值kAudioFileStreamPropertyFlag_PropertyIsCached否则不赋值(这个参数我也不知道应该在啥场景下使用。。。。);

这个回调会进来多次,但并不是每一次都需要进行处理,可以根据需求处理需要的PropertyID进行处理(PropertyID列表如下)。

enum
{
  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'
};

1、kAudioFileStreamProperty_BitRate:表示音频数据的码率,获取这个Property是为了计算音频的总时长Duration(因为AudioFileStream没有这样的接口。。)

UInt32 bitRate;
UInt32 bitRateSize = sizeof(bitRate);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_BitRate, &bitRateSize, &bitRate);
if (status != noErr)
{
    //错误处理
}

//在流播放的情况下,有时数据流量比较小时会出现ReadyToProducePackets还是没有获取到bitRate的情况,这时就需要分离一些拼音帧然后计算平均bitRate,计算公式如下:
UInt32 averageBitRate = totalPackectByteCount / totalPacketCout;

2、kAudioFileStreamProperty_DataOffset:表示音频数据在整个音频文件中的offset(因为大多数音频文件都会有一个文件头之后才使真正的音频数据),这个值在seek时会发挥比较大的作用,音频的seek并不是直接seek文件位置而seek时间(比如seek到2分10秒的位置),seek时会根据时间计算出音频数据的字节offset然后需要再加上音频数据的offset才能得到在文件中的真正offset。

SInt64 dataOffset;
UInt32 offsetSize = sizeof(dataOffset);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataOffset, &offsetSize, &dataOffset);
if (status != noErr)
{
    //错误处理
}

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

struct AudioStreamBasicDescription
{
    Float64 mSampleRate;
    UInt32  mFormatID;
    UInt32  mFormatFlags;
    UInt32  mBytesPerPacket;
    UInt32  mFramesPerPacket;
    UInt32  mBytesPerFrame;
    UInt32  mChannelsPerFrame;
    UInt32  mBitsPerChannel;
    UInt32  mReserved;
};

AudioStreamBasicDescription asbd;
UInt32 asbdSize = sizeof(asbd);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataFormat, &asbdSize, &asbd);
if (status != noErr)
{
    //错误处理
}

4、kAudioFileStreamProperty_FormatList:作用和kAudioFileStreamProperty_DataFormat是一样的,区别在于用这个PropertyID获取到是一个AudioStreamBasicDescription的数组,这个参数是用来支持AAC SBR这样的包含多个文件类型的音频格式。由于到底有多少个format我们并不知晓,所以需要先获取一下总数据大小

//获取数据大小
Boolean outWriteable;
UInt32 formatListSize;
OSStatus status = AudioFileStreamGetPropertyInfo(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, &outWriteable);
if (status != noErr)
{
    //错误处理
}

//获取formatlist
AudioFormatListItem *formatList = malloc(formatListSize);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, formatList);
if (status != noErr)
{
    //错误处理
}

//选择需要的格式
for (int i = 0; i * sizeof(AudioFormatListItem) < formatListSize; i++)
{
    AudioStreamBasicDescription pasbd = formatList[i].mASBD;
    //选择需要的格式。。                             
}
free(formatList);

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

UInt64 audioDataByteCount;
UInt32 byteCountSize = sizeof(audioDataByteCount);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_AudioDataByteCount, &byteCountSize, &audioDataByteCount);
if (status != noErr)
{
    //错误处理
}
//有时数据流量比较小时会出现ReadyToProducePackets还是没有获取到audioDataByteCount的情况,这时就需要近似计算audioDataByteCount。一般来说音频文件的总大小一定是可以得到的(利用文件系统或者Http请求中的contentLength),那么计算方法如下:
UInt32 dataOffset = ...; //kAudioFileStreamProperty_DataOffset
UInt32 fileLength = ...; //音频文件大小
UInt32 audioDataByteCount = fileLength - dataOffset;

6、kAudioFileStreamProperty_ReadyToProducePackets:这个PropertyID可以不必获取对应的值,一旦回调中这个PropertyID出现就代表解析完成,接下来可以对音频数据进行帧分离了。

计算时长Duration

获取时长的最佳方法是从ID3信息中去读取,那样是最准确的。如果ID3信息中没有存,那就依赖于文件头中的信息去计算了。
double duration = (audioDataByteCount * 8) / bitRate
音频数据的字节总量audioDataByteCount可以通过kAudioFileStreamProperty_AudioDataByteCount获取,码率bitRate可以通过kAudioFileStreamProperty_BitRate获取也可以通过Parse一部分数据后计算平均码率来得到。
对于CBR数据来说用这样的计算方法的duration会比较准确,对于VBR数据就不好说了。所以对于VBR数据来说,最好是能够从ID3信息中获取到duration,获取不到再想办法通过计算平均码率的途径来计算duration。

分离音频帧和关闭AudioFileStream下一篇再写,这篇写的内容有点多,消化消化~
上一篇 下一篇

猜你喜欢

热点阅读