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

2,696 阅读10分钟

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

参考资料