iOS音频播放(七)AudioFileStream介绍与实战

AudioFileStream介绍

AudioFileStreamer的做用是用来读取采样率、码率、时长等基本信息以及分离音频帧。git

数据的相关内容都和它相关,因此仍是很重要的,其实AudioQueue使用起来比较简单,复杂的部分都在这个数据的处理上了。。。github

根据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示例。bash

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个回调函数比较重要,下面详说。markdown

OSStatus返回值用来判断是否成功初始化(OSStatus == noErr)。网络

解析数据

在初始化完成以后,调用该方法解析文件数据。解析时调用方法:app

extern OSStatus AudioFileStreamParseBytes(AudioFileStreamID inAudioFileStream,
                                          UInt32 inDataByteSize,
                                          const void* inData,
                                          UInt32 inFlags);
复制代码

参数的说明以下:函数

  • inAudioFileStream:AudioFileStreamID实例,由AudioFileStreamOpen打开
  • inDataByteSize:这次解析的数据字节大小
  • inData:这次解析的数据大小
  • inFlags:数据解析标志,其中只有一个值kAudioFileStreamParseFlag_Discontinuity = 1,表示解析的数据是不是不连续的,目前咱们能够传0。

OSStatus的值不是noErr则表示解析不成功,对应的错误码:oop

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/…)是代码

参考资料

相关文章
相关标签/搜索