分享:iOS音頻播放系列之AudioFileStream

本來說好是要在第三篇中講AudioFileStreamAudioQueue,但寫著寫著發(fā)現(xiàn)光AudioFileStream就好多內(nèi)容枷颊,最后還是決定分篇介紹,這篇先來說一下AudioFileStream该面,下一篇計(jì)劃說一下和AudioFileStream類似的AudioFile夭苗,下下篇再來說AudioQueue

在本篇那種將會(huì)提到計(jì)算音頻時(shí)長duration和音頻seek的方法隔缀,這些方法對(duì)于CBR編碼形式的音頻文件可以做到比較精確而對(duì)于VBR編碼形式的會(huì)存在較大的誤差(關(guān)于CBR和VBR题造,請(qǐng)看本系列的[第一篇](http://www.reibang.com/p/7980f466674e),具體講到duration和seek時(shí)會(huì)再進(jìn)行說明猾瘸。原文|地址


AudioFileStream介紹

第一篇中說到AudioFileStreamer時(shí)提到它的作用是用來讀取采樣率界赔、碼率丢习、時(shí)長等基本信息以及分離音頻幀。那么在官方文檔中Apple是這樣描述的:

To play streamed audio content, such as from a network connection, use Audio File Stream Services in concert with Audio Queue Services. Audio File Stream Services parses audio packets and metadata from common audio file container formats in a network bitstream. You can also use it to parse packets and metadata from on-disk files

根據(jù)Apple的描述AudioFileStreamer用在流播放中淮悼,當(dāng)然不僅限于網(wǎng)絡(luò)流咐低,本地文件同樣可以用它來讀取信息和分離音頻幀。AudioFileStreamer的主要數(shù)據(jù)是文件數(shù)據(jù)而不是文件路徑袜腥,所以數(shù)據(jù)的讀取需要使用者自行實(shí)現(xiàn)见擦,

支持的文件格式有:

  • 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

上述格式是iOS、MacOSX所支持的音頻格式羹令,這類格式可以被系統(tǒng)提供的API解碼鲤屡,如果想要解碼其他的音頻格式(如OGG、APE福侈、FLAC)就需要自己實(shí)現(xiàn)解碼器了酒来。


初始化AudioFileStream

第一步,自然是要生成一個(gè)AudioFileStream的實(shí)例:

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

第一個(gè)參數(shù)和之前的AudioSession的初始化方法一樣是一個(gè)上下文對(duì)象癌刽;

第二個(gè)參數(shù)AudioFileStream_PropertyListenerProc是歌曲信息解析的回調(diào)役首,每解析出一個(gè)歌曲信息都會(huì)進(jìn)行一次回調(diào);

第三個(gè)參數(shù)AudioFileStream_PacketsProc是分離幀的回調(diào)显拜,每解析出一部分幀就會(huì)進(jìn)行一次回調(diào)衡奥;

第四個(gè)參數(shù)AudioFileTypeID是文件類型的提示,這個(gè)參數(shù)來幫助AudioFileStream對(duì)文件格式進(jìn)行解析远荠。這個(gè)參數(shù)在文件信息不完整(例如信息有缺陷)時(shí)尤其有用矮固,它可以給與AudioFileStream一定的提示,幫助其繞過文件中的錯(cuò)誤或者缺失從而成功解析文件譬淳。所以在確定文件類型的情況下建議各位還是填上這個(gè)參數(shù)档址,如果無法確定可以傳入0 ;

//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'        
};

第五個(gè)參數(shù)是返回的AudioFileStream實(shí)例對(duì)應(yīng)的AudioFileStreamID邻梆,這個(gè)ID需要保存起來作為后續(xù)一些方法的參數(shù)使用守伸;

返回值用來判斷是否成功初始化(OSStatus == noErr)。


解析數(shù)據(jù)

在初始化完成之后浦妄,只要拿到文件數(shù)據(jù)就可以進(jìn)行解析了尼摹。解析時(shí)調(diào)用方法

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

第一個(gè)參數(shù)AudioFileStreamID,即初始化時(shí)返回的ID剂娄;

第二個(gè)參數(shù)inDataByteSize蠢涝,本次解析的數(shù)據(jù)長度;

第三個(gè)參數(shù)inData阅懦,本次解析的數(shù)據(jù)和二;

第四個(gè)參數(shù)是說本次的解析和上一次解析是否是連續(xù)的關(guān)系,如果是連續(xù)的傳入0耳胎,否則傳入kAudioFileStreamParseFlag_Discontinuity惯吕。

這里需要插入解釋一下何謂“連續(xù)”惕它。在第一篇中我們提到過形如MP3的數(shù)據(jù)都以幀的形式存在的,解析時(shí)也需要以幀為單位解析废登。但在解碼之前我們不可能知道每個(gè)幀的邊界在第幾個(gè)字節(jié)怠缸,所以就會(huì)出現(xiàn)這樣的情況:我們傳給AudioFileStreamParseBytes的數(shù)據(jù)在解析完成之后會(huì)有一部分?jǐn)?shù)據(jù)余下來,這部分?jǐn)?shù)據(jù)是接下去那一幀的前半部分钳宪,如果再次有數(shù)據(jù)輸入需要繼續(xù)解析時(shí)就必須要用到前一次解析余下來的數(shù)據(jù)才能保證幀數(shù)據(jù)完整,所以在正常播放的情況下傳入0即可扳炬。目前知道的需要傳入kAudioFileStreamParseFlag_Discontinuity的情況有兩個(gè)吏颖,一個(gè)是在seek完畢之后顯然seek后的數(shù)據(jù)和之前的數(shù)據(jù)完全無關(guān);另一個(gè)是開源播放器AudioStreamer的作者@Matt Gallagher曾在自己的blog中提到過的:

the Audio File Stream Services hit me with a nasty bug: AudioFileStreamParseBytes will crash when trying to parse a streaming MP3.

In this case, if we pass the kAudioFileStreamParseFlag_Discontinuity flag to AudioFileStreamParseBytes on every invocation between receiving kAudioFileStreamProperty_ReadyToProducePackets and the first successful call to MyPacketsProc, then AudioFileStreamParseBytes will be extra cautious in its approach and won't crash.

Matt發(fā)布這篇blog是在2008年恨樟,這個(gè)Bug年代相當(dāng)久遠(yuǎn)了半醉,而且原因未知,究竟是否修復(fù)也不得而知劝术,而且由于環(huán)境不同(比如測(cè)試用的mp3文件和所處的iOS系統(tǒng))無法重現(xiàn)這個(gè)問題缩多,所以我個(gè)人覺得還是按照Matt的work around在回調(diào)得到kAudioFileStreamProperty_ReadyToProducePackets之后,在正常解析第一幀之前都傳入kAudioFileStreamParseFlag_Discontinuity比較好养晋。

回到之前的內(nèi)容衬吆,AudioFileStreamParseBytes方法的返回值表示當(dāng)前的數(shù)據(jù)是否被正常解析,如果OSStatus的值不是noErr則表示解析不成功绳泉,其中錯(cuò)誤碼包括:

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!'
};

大多數(shù)都可以從字面上理解逊抡,需要提一下的是kAudioFileStreamError_NotOptimized,文檔上是這么說的:

It is not possible to produce output packets because the file's packet table or other defining info is either not present or is after the audio data.

它的含義是說這個(gè)音頻文件的文件頭不存在或者說文件頭可能在文件的末尾零酪,當(dāng)前無法正常Parse冒嫡,換句話說就是這個(gè)文件需要全部下載完才能播放,無法流播四苇。

注意AudioFileStreamParseBytes方法每一次調(diào)用都應(yīng)該注意返回值孝凌,一旦出現(xiàn)錯(cuò)誤就可以不必繼續(xù)Parse了。


解析文件格式信息

在調(diào)用AudioFileStreamParseBytes方法進(jìn)行解析時(shí)會(huì)首先讀取格式信息月腋,并同步的進(jìn)入AudioFileStream_PropertyListenerProc回調(diào)方法

image.png

來看一下這個(gè)回調(diào)方法的定義

typedef void (*AudioFileStream_PropertyListenerProc)(void * inClientData,
                                                     AudioFileStreamID inAudioFileStream,
                                                     AudioFileStreamPropertyID inPropertyID,
                                                     UInt32 * ioFlags);

回調(diào)的第一個(gè)參數(shù)是Open方法中的上下文對(duì)象蟀架;

第二個(gè)參數(shù)inAudioFileStream是和Open方法中第四個(gè)返回參數(shù)AudioFileStreamID一樣,表示當(dāng)前FileStream的ID罗售;

第三個(gè)參數(shù)是此次回調(diào)解析的信息ID辜窑。表示當(dāng)前PropertyID對(duì)應(yīng)的信息已經(jīng)解析完成信息(例如數(shù)據(jù)格式、音頻數(shù)據(jù)的偏移量等等)寨躁,使用者可以通過AudioFileStreamGetProperty接口獲取PropertyID對(duì)應(yīng)的值或者數(shù)據(jù)結(jié)構(gòu)穆碎;

extern OSStatus AudioFileStreamGetProperty(AudioFileStreamID inAudioFileStream,
                                           AudioFileStreamPropertyID inPropertyID,
                                           UInt32 * ioPropertyDataSize,
                                           void * outPropertyData);

第四個(gè)參數(shù)ioFlags是一個(gè)返回參數(shù),表示這個(gè)property是否需要被緩存职恳,如果需要賦值kAudioFileStreamPropertyFlag_PropertyIsCached否則不賦值(這個(gè)參數(shù)我也不知道應(yīng)該在啥場(chǎng)景下使用所禀。方面。一直都沒去理他);

這個(gè)回調(diào)會(huì)進(jìn)來多次色徘,但并不是每一次都需要進(jìn)行處理恭金,可以根據(jù)需求處理需要的PropertyID進(jìn)行處理(PropertyID列表如下)。

//AudioFileStreamProperty枚舉
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'
};

這里列幾個(gè)我認(rèn)為比較重要的PropertyID:

  1. kAudioFileStreamProperty_BitRate

表示音頻數(shù)據(jù)的碼率褂策,獲取這個(gè)Property是為了計(jì)算音頻的總時(shí)長Duration(因?yàn)锳udioFileStream沒有這樣的接口横腿。。)斤寂。

UInt32 bitRate;
UInt32 bitRateSize = sizeof(bitRate);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_BitRate, &bitRateSize, &bitRate);
if (status != noErr)
{
    //錯(cuò)誤處理
}

補(bǔ)充:發(fā)現(xiàn)在流播放的情況下耿焊,有時(shí)數(shù)據(jù)流量比較小時(shí)會(huì)出現(xiàn)ReadyToProducePackets還是沒有獲取到bitRate的情況,這時(shí)就需要分離一些拼音幀然后計(jì)算平均bitRate遍搞,計(jì)算公式如下:

UInt32 averageBitRate = totalPackectByteCount / totalPacketCout;
  1. kAudioFileStreamProperty_DataOffset
    表示音頻數(shù)據(jù)在整個(gè)音頻文件中的offset(因?yàn)榇蠖鄶?shù)音頻文件都會(huì)有一個(gè)文件頭之后才使真正的音頻數(shù)據(jù))罗侯,這個(gè)值在seek時(shí)會(huì)發(fā)揮比較大的作用,音頻的seek并不是直接seek文件位置而seek時(shí)間(比如seek到2分10秒的位置)溪猿,seek時(shí)會(huì)根據(jù)時(shí)間計(jì)算出音頻數(shù)據(jù)的字節(jié)offset然后需要再加上音頻數(shù)據(jù)的offset才能得到在文件中的真正offset钩杰。
SInt64 dataOffset;
UInt32 offsetSize = sizeof(dataOffset);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataOffset, &offsetSize, &dataOffset);
if (status != noErr)
{
    //錯(cuò)誤處理
}
  1. kAudioFileStreamProperty_DataFormat

表示音頻文件結(jié)構(gòu)信息,是一個(gè)AudioStreamBasicDescription的結(jié)構(gòu)

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)
{
    //錯(cuò)誤處理
}
  1. kAudioFileStreamProperty_FormatList

作用和kAudioFileStreamProperty_DataFormat是一樣的诊县,區(qū)別在于用這個(gè)PropertyID獲取到是一個(gè)AudioStreamBasicDescription的數(shù)組讲弄,這個(gè)參數(shù)是用來支持AAC SBR這樣的包含多個(gè)文件類型的音頻格式。由于到底有多少個(gè)format我們并不知曉依痊,所以需要先獲取一下總數(shù)據(jù)大写共恰:

//獲取數(shù)據(jù)大小
Boolean outWriteable;
UInt32 formatListSize;
OSStatus status = AudioFileStreamGetPropertyInfo(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, &outWriteable);
if (status != noErr)
{
    //錯(cuò)誤處理
}

//獲取formatlist
AudioFormatListItem *formatList = malloc(formatListSize);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, formatList);
if (status != noErr)
{
    //錯(cuò)誤處理
}

//選擇需要的格式
for (int i = 0; i * sizeof(AudioFormatListItem) < formatListSize; i++)
{
    AudioStreamBasicDescription pasbd = formatList[i].mASBD;
    //選擇需要的格式。抗悍。                             
}
free(formatList);
  1. kAudioFileStreamProperty_AudioDataByteCount

顧名思義驹饺,音頻文件中音頻數(shù)據(jù)的總量。這個(gè)Property的作用一是用來計(jì)算音頻的總時(shí)長缴渊,二是可以在seek時(shí)用來計(jì)算時(shí)間對(duì)應(yīng)的字節(jié)offset赏壹。

UInt64 audioDataByteCount;
UInt32 byteCountSize = sizeof(audioDataByteCount);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_AudioDataByteCount, &byteCountSize, &audioDataByteCount);
if (status != noErr)
{
    //錯(cuò)誤處理
}

補(bǔ)充: 發(fā)現(xiàn)在流播放的情況下,有時(shí)數(shù)據(jù)流量比較小時(shí)會(huì)出現(xiàn)ReadyToProducePackets還是沒有獲取到audioDataByteCount的情況衔沼,這時(shí)就需要近似計(jì)算audioDataByteCount蝌借。一般來說音頻文件的總大小一定是可以得到的(利用文件系統(tǒng)或者Http請(qǐng)求中的contentLength),那么計(jì)算方法如下:

UInt32 dataOffset = ...; //kAudioFileStreamProperty_DataOffset
UInt32 fileLength = ...; //音頻文件大小
UInt32 audioDataByteCount = fileLength - dataOffset;
  1. kAudioFileStreamProperty_ReadyToProducePackets

這個(gè)PropertyID可以不必獲取對(duì)應(yīng)的值指蚁,一旦回調(diào)中這個(gè)PropertyID出現(xiàn)就代表解析完成菩佑,接下來可以對(duì)音頻數(shù)據(jù)進(jìn)行幀分離了。


計(jì)算時(shí)長Duration

獲取時(shí)長的最佳方法是從ID3信息中去讀取凝化,那樣是最準(zhǔn)確的稍坯。如果ID3信息中沒有存,那就依賴于文件頭中的信息去計(jì)算了。

計(jì)算duration的公式如下:

double duration = (audioDataByteCount * 8) / bitRate

音頻數(shù)據(jù)的字節(jié)總量audioDataByteCount可以通過kAudioFileStreamProperty_AudioDataByteCount獲取瞧哟,碼率bitRate可以通過kAudioFileStreamProperty_BitRate獲取也可以通過Parse一部分?jǐn)?shù)據(jù)后計(jì)算平均碼率來得到混巧。

對(duì)于CBR數(shù)據(jù)來說用這樣的計(jì)算方法的duration會(huì)比較準(zhǔn)確,對(duì)于VBR數(shù)據(jù)就不好說了勤揩。所以對(duì)于VBR數(shù)據(jù)來說咧党,最好是能夠從ID3信息中獲取到duration,獲取不到再想辦法通過計(jì)算平均碼率的途徑來計(jì)算duration陨亡。


分離音頻幀

讀取格式信息完成之后繼續(xù)調(diào)用AudioFileStreamParseBytes方法可以對(duì)幀進(jìn)行分離傍衡,并同步的進(jìn)入AudioFileStream_PacketsProc回調(diào)方法。

image.png

回調(diào)的定義:

typedef void (*AudioFileStream_PacketsProc)(void * inClientData,
                                            UInt32 numberOfBytes,
                                            UInt32 numberOfPackets,
                                            const void * inInputData,
                                            AudioStreamPacketDescription * inPacketDescriptions);

第一個(gè)參數(shù)负蠕,一如既往的上下文對(duì)象聪舒;

第二個(gè)參數(shù),本次處理的數(shù)據(jù)大信凹薄;

第三個(gè)參數(shù)滔迈,本次總共處理了多少幀(即代碼里的Packet)止吁;

第四個(gè)參數(shù),本次處理的所有數(shù)據(jù)燎悍;

第五個(gè)參數(shù)敬惦,AudioStreamPacketDescription數(shù)組,存儲(chǔ)了每一幀數(shù)據(jù)是從第幾個(gè)字節(jié)開始的谈山,這一幀總共多少字節(jié)俄删。

//AudioStreamPacketDescription結(jié)構(gòu)
//這里的mVariableFramesInPacket是指實(shí)際的數(shù)據(jù)幀只有VBR的數(shù)據(jù)才能用到(像MP3這樣的壓縮數(shù)據(jù)一個(gè)幀里會(huì)有好幾個(gè)數(shù)據(jù)幀)
struct  AudioStreamPacketDescription
{
    SInt64  mStartOffset;
    UInt32  mVariableFramesInPacket;
    UInt32  mDataByteSize;
};

下面是我按照自己的理解實(shí)現(xiàn)的回調(diào)方法片段:

static void MyAudioFileStreamPacketsCallBack(void *inClientData,
                                             UInt32 numberOfBytes,
                                             UInt32 numberOfPackets,
                                             const void *inInputData,
                                             AudioStreamPacketDescription  *inPacketDescriptions)
{
    //處理discontinuous..

    if (numberOfBytes == 0 || numberOfPackets == 0)
    {
        return;
    }

    BOOL deletePackDesc = NO;
    if (packetDescriptioins == NULL)
    {
        //如果packetDescriptioins不存在,就按照CBR處理奏路,平均每一幀的數(shù)據(jù)后生成packetDescriptioins
        deletePackDesc = YES;
        UInt32 packetSize = numberOfBytes / numberOfPackets;
        packetDescriptioins = (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)
            {
                packetDescriptioins[i].mDataByteSize = numberOfBytes - packetOffset;
            }
            else
            {
                packetDescriptioins[i].mDataByteSize = packetSize;
            }
        }
    }

    for (int i = 0; i < numberOfPackets; ++i)
    {
        SInt64 packetOffset = packetDescriptioins[i].mStartOffset;
        UInt32 packetSize   = packetDescriptioins[i].mDataByteSize;

        //把解析出來的幀數(shù)據(jù)放進(jìn)自己的buffer中
        ...
    }

    if (deletePackDesc)
    {
        free(packetDescriptioins);
    }
}

inPacketDescriptions這個(gè)字段為空時(shí)需要按CBR的數(shù)據(jù)處理畴椰。但其實(shí)在解析CBR數(shù)據(jù)時(shí)inPacketDescriptions一般也會(huì)有返回,因?yàn)榧词故荂BR數(shù)據(jù)幀的大小也不是恒定不變的鸽粉,例如CBR的MP3會(huì)在每一幀的數(shù)據(jù)后放1 byte的填充位斜脂,這個(gè)填充位也并非時(shí)時(shí)刻刻存在,所以幀的大小會(huì)有1 byte的浮動(dòng)触机。(比如采樣率44.1KHZ帚戳,碼率160kbps的CBR MP3文件每一幀的大小在522字節(jié)和523字節(jié)浮動(dòng)。所以不能因?yàn)橛衖nPacketDescriptions沒有返回NULL而判定音頻數(shù)據(jù)就是VBR編碼的)儡首。


Seek

就音頻的角度來seek功能描述為“我要拖到xx分xx秒”片任,而實(shí)際操作時(shí)我們需要操作的是文件,所以我們需要知道的是“我要拖到xx分xx秒”這個(gè)操作對(duì)應(yīng)到文件上是要從第幾個(gè)字節(jié)開始讀取音頻數(shù)據(jù)蔬胯。

對(duì)于原始的PCM數(shù)據(jù)來說每一個(gè)PCM幀都是固定長度的对供,對(duì)應(yīng)的播放時(shí)長也是固定的,但一旦轉(zhuǎn)換成壓縮后的音頻數(shù)據(jù)就會(huì)因?yàn)榫幋a形式的不同而不同了氛濒。對(duì)于CBR而言每個(gè)幀中所包含的PCM數(shù)據(jù)幀是恒定的犁钟,所以每一幀對(duì)應(yīng)的播放時(shí)長也是恒定的棱诱;而VBR則不同,為了保證數(shù)據(jù)最優(yōu)并且文件大小最小涝动,VBR的每一幀中所包含的PCM數(shù)據(jù)幀是不固定的迈勋,這就導(dǎo)致在流播放的情況下VBR的數(shù)據(jù)想要做seek并不容易。這里我們也只討論CBR下的seek醋粟。

CBR數(shù)據(jù)的seek一般是這樣實(shí)現(xiàn)的(參考并修改自matt的blog):

  1. 近似地計(jì)算應(yīng)該seek到哪個(gè)字節(jié)
double seekToTime = ...; //需要seek到哪個(gè)時(shí)間靡菇,秒為單位
UInt64 audioDataByteCount = ...; //通過kAudioFileStreamProperty_AudioDataByteCount獲取的值
SInt64 dataOffset = ...; //通過kAudioFileStreamProperty_DataOffset獲取的值
double durtion = ...; //通過公式(AudioDataByteCount * 8) / BitRate計(jì)算得到的時(shí)長

//近似seekOffset = 數(shù)據(jù)偏移 + seekToTime對(duì)應(yīng)的近似字節(jié)數(shù)
SInt64 approximateSeekOffset = dataOffset + (seekToTime / duration) * audioDataByteCount;
  1. 計(jì)算seekToTime對(duì)應(yīng)的是第幾個(gè)幀(Packet)

我們可以利用之前Parse得到的音頻格式信息來計(jì)算PacketDuration。

audioItem.fileFormat.mFramesPerPacket
audioItem.fileFormat.mSampleRate;

//首先需要計(jì)算每個(gè)packet對(duì)應(yīng)的時(shí)長
AudioStreamBasicDescription asbd = ...; ////通過kAudioFileStreamProperty_DataFormat或者kAudioFileStreamProperty_FormatList獲取的值
double packetDuration = asbd.mFramesPerPacket / asbd.mSampleRate

//然后計(jì)算packet位置
SInt64 seekToPacket = floor(seekToTime / packetDuration);
  1. 使用AudioFileStreamSeek計(jì)算精確的字節(jié)偏移和時(shí)間

AudioFileStreamSeek可以用來尋找某一個(gè)幀(Packet)對(duì)應(yīng)的字節(jié)偏移(byte offset):

  • 如果ioFlags里有kAudioFileStreamSeekFlag_OffsetIsEstimated說明給出的outDataByteOffset是估算的米愿,并不準(zhǔn)確厦凤,那么還是應(yīng)該用第1步計(jì)算出來的approximateSeekOffset來做seek;
  • 如果ioFlags里沒有kAudioFileStreamSeekFlag_OffsetIsEstimated說明給出了準(zhǔn)確的outDataByteOffset育苟,就是輸入的seekToPacket對(duì)應(yīng)的字節(jié)偏移量较鼓,我們可以根據(jù)outDataByteOffset來計(jì)算出精確的seekOffset和seekToTime;
SInt64 seekByteOffset;
UInt32 ioFlags = 0;
SInt64 outDataByteOffset;
OSStatus status = AudioFileStreamSeek(audioFileStreamID, seekToPacket, &outDataByteOffset, &ioFlags);
if (status == noErr && !(ioFlags & kAudioFileStreamSeekFlag_OffsetIsEstimated))
{
  //如果AudioFileStreamSeek方法找到了準(zhǔn)確的幀字節(jié)偏移违柏,需要修正一下時(shí)間
  seekToTime -= ((approximateSeekOffset - dataOffset) - outDataByteOffset) * 8.0 / bitRate;
  seekByteOffset = outDataByteOffset + dataOffset;
}
else
{
  seekByteOffset = approximateSeekOffset;
}
  1. 按照seekByteOffset讀取對(duì)應(yīng)的數(shù)據(jù)繼續(xù)使用AudioFileStreamParseByte進(jìn)行解析

如果是網(wǎng)絡(luò)流可以通過設(shè)置range頭來獲取字節(jié)博烂,本地文件的話直接seek就好了。調(diào)用AudioFileStreamParseByte時(shí)注意剛seek完第一次Parse數(shù)據(jù)需要加參數(shù)kAudioFileStreamParseFlag_Discontinuity漱竖。


關(guān)閉AudioFileStream

AudioFileStream使用完畢后需要調(diào)用AudioFileStreamClose進(jìn)行關(guān)閉禽篱,沒啥特別需要注意的。

extern OSStatus AudioFileStreamClose(AudioFileStreamID inAudioFileStream);

小結(jié)

本篇關(guān)于AudioFileStream做了詳細(xì)介紹馍惹,小結(jié)一下:

  • 使用AudioFileStream首先需要調(diào)用AudioFileStreamOpen躺率,需要注意的是盡量提供inFileTypeHint參數(shù)幫助AudioFileStream解析數(shù)據(jù),調(diào)用完成后記錄AudioFileStreamID万矾;

  • 當(dāng)有數(shù)據(jù)時(shí)調(diào)用AudioFileStreamParseBytes進(jìn)行解析悼吱,每一次解析都需要注意返回值,返回值一旦出現(xiàn)noErr以外的值就代表Parse出錯(cuò)良狈,其中kAudioFileStreamError_NotOptimized代表該文件缺少頭信息或者其頭信息在文件尾部不適合流播放舆绎;

  • 使用AudioFileStreamParseBytes需要注意第四個(gè)參數(shù)在需要合適的時(shí)候傳入kAudioFileStreamParseFlag_Discontinuity

  • 調(diào)用AudioFileStreamParseBytes后會(huì)首先同步進(jìn)入AudioFileStream_PropertyListenerProc回調(diào)來解析文件格式信息们颜,如果回調(diào)得到kAudioFileStreamProperty_ReadyToProducePackets表示解析格式信息完成吕朵;

  • 解析格式信息完成后繼續(xù)調(diào)用AudioFileStreamParseBytes會(huì)進(jìn)入MyAudioFileStreamPacketsCallBack回調(diào)來分離音頻幀,在回調(diào)中應(yīng)該將分離出來的幀信息保存到自己的buffer中

  • seek時(shí)需要先近似的計(jì)算seekTime對(duì)應(yīng)的seekByteOffset窥突,然后利用AudioFileStreamSeek計(jì)算精確的offset努溃,如果能得到精確的offset就修正一下seektime,如果無法得到精確的offset就用之前的近似結(jié)果

  • AudioFileStream使用完畢后需要調(diào)用AudioFileStreamClose進(jìn)行關(guān)閉阻问;


下篇預(yù)告

下一篇將講述如何使用AudioFile梧税。


參考資料

Using Audio

Streaming and playing an MP3 stream

Streaming MP3/AAC audio again

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子第队,更是在濱河造成了極大的恐慌哮塞,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蝉仇,警方通過查閱死者的電腦和手機(jī)芯咧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門营密,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事“砘澹” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵褪贵,是天一觀的道長掂之。 經(jīng)常有香客問我,道長脆丁,這世上最難降的妖魔是什么世舰? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮偎快,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘洽胶。我一直安慰自己晒夹,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布姊氓。 她就那樣靜靜地躺著丐怯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪翔横。 梳的紋絲不亂的頭發(fā)上读跷,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音禾唁,去河邊找鬼效览。 笑死,一個(gè)胖子當(dāng)著我的面吹牛荡短,可吹牛的內(nèi)容都是我干的丐枉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼掘托,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼瘦锹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤弯院,失蹤者是張志新(化名)和其女友劉穎辱士,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體听绳,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡颂碘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了辫红。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凭涂。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖贴妻,靈堂內(nèi)的尸體忽然破棺而出切油,到底是詐尸還是另有隱情,我是刑警寧澤名惩,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布澎胡,位于F島的核電站,受9級(jí)特大地震影響娩鹉,放射性物質(zhì)發(fā)生泄漏攻谁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一弯予、第九天 我趴在偏房一處隱蔽的房頂上張望戚宦。 院中可真熱鬧,春花似錦锈嫩、人聲如沸受楼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽艳汽。三九已至,卻和暖如春对雪,著一層夾襖步出監(jiān)牢的瞬間河狐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國打工瑟捣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留馋艺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓迈套,卻偏偏與公主長得像丈钙,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子交汤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容