iOS 音頻流播(三)

本篇我們介紹AudioFile和AudioFileStream。在第一篇技術(shù)棧的分析里,我們提到過AudioFile和AudioFileStream都可以用來解析采樣率、碼率、時長等信息,分離原始音頻數(shù)據(jù)中的音頻幀。這兩個都可以使用在流式播放中,當然,不僅限于流播,本地音頻也一樣可以使用。
??可能有的小伙伴會有疑問,既然它們倆功能相似,選擇其中一個不就可以了嗎?其實不然,AudioFile的功能遠比AudioFileStream強大,除了共同的解析音頻數(shù)據(jù)分離音頻幀之外,它還可以讀取音頻數(shù)據(jù),甚至可以寫音頻(生成音頻文件),而AudioFileStream本身沒有讀取音頻數(shù)據(jù)的功能??雌饋磉x擇AudioFile就OK了,不料AudioFile卻需要AudioFileStream來保證數(shù)據(jù)的完整性,否則會大大增加出錯的可能性(別急別急,且看下文一一道來)。
??下面我們就先認識一下Mr.AudioFileStream。

初始化AudioFileStream

首先當然是調(diào)用AudioFileStreamOpen()生成一個AudioFileStream實例,函數(shù)聲明如下:

OSStatus    
AudioFileStreamOpen (
                            void * __nullable                       inClientData,
                            AudioFileStream_PropertyListenerProc    inPropertyListenerProc,
                            AudioFileStream_PacketsProc             inPacketsProc,
                            AudioFileTypeID                         inFileTypeHint,
                            AudioFileStreamID __nullable * __nonnull outAudioFileStream)
  • 第一個參數(shù),inClientData是一個上下文對象,會回傳給回調(diào)函數(shù)。必須保證inClientData生命周期足夠長,否則在回調(diào)函數(shù)里使用的時候就是一個野指針了。一般我們會傳自身(self),在回調(diào)函數(shù)取出后再調(diào)用自身的實例方法(C和OC雖然可以混編,但是C函數(shù)并不是作為OC類的實例方法存在的,所以C函數(shù)里的self并不指代OC對象)。
  • 第二個參數(shù),inPropertyListenerProc是歌曲信息解析的回調(diào),每解析出一個就會進行一次回調(diào)。
  • 第三個參數(shù),inPacketsProc是分離音頻幀的回調(diào),每解析出一部分音頻幀就會進行一次回調(diào)。
  • 第四個參數(shù),inFileTypeHint是音頻文件格式的描述信息,這個參數(shù)來幫助AudioFileStream對文件格式進行解析。這個參數(shù)在文件信息不完整(例如信息有缺陷)時尤其有用,它可以給與AudioFileStream一定的提示,幫助其繞過文件中的錯誤或者缺失從而成功解析文件,如果無法確定可以傳入0。
// AudioToolBox定義的AudioFileTypeID
CF_ENUM(AudioFileTypeID) {
        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'        
};
  • 第五個參數(shù),outAudioFileStream代表生成的AudioFileStream實例,這個參數(shù)必須保存起來作為后續(xù)一些方法的參數(shù)使用。
  • 返回值表示是否調(diào)用成功(status == noErr),關(guān)于OSStatus的解釋,可以參閱這里

注意:在播放網(wǎng)絡(luò)音頻時,很多鏈接并沒有指明音頻格式,此時可以根據(jù)MIME type來確定音頻格式,而本地音頻可以根據(jù)文件擴展名確定。MIME type與擴展名有關(guān),用于確定文件的類型。 在HTTP請求中,MIME type通過請求頭中的 Content-Type 表示。iOS中可通過 <MobileCoreServices/UTType.h> 中定義的相關(guān)方法可以實現(xiàn) fileExtension <--> UTType <--> mimeType 的互轉(zhuǎn)。具體轉(zhuǎn)換方法可以看我之前的一篇博文

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

上文AudioFileStream并沒有提供讀取音頻數(shù)據(jù)的接口,所以音頻數(shù)據(jù)的讀取需要自行實現(xiàn)。本地播放可以通過NSFileHandle提供的接口,流播時通過HTTP請求獲得。在得到音頻數(shù)據(jù)之后,調(diào)用AudioFileStreamParseBytes()就可以進行解析了。

OSStatus
AudioFileStreamParseBytes(  
                                AudioFileStreamID               inAudioFileStream,
                                UInt32                          inDataByteSize,
                                const void *                    inData,
                                AudioFileStreamParseFlags       inFlags)
  • 第一個參數(shù),inAudioFileStream是初始化時得到的AudioFileStreamID。
  • 第二個參數(shù),inDataByteSize是本次解析的數(shù)據(jù)長度。
  • 第三個參數(shù),inData是本次解析的音頻數(shù)據(jù)。
  • 第四個參數(shù),inFlags表示本次解析與上一次是否是連續(xù)關(guān)系。在第一篇中我們提到過形如MP3的數(shù)據(jù)都以幀的形式存在的,解析時也需要以幀為單位解析。但在解碼之前我們不可能知道每個幀的邊界在第幾個字節(jié),所以就會出現(xiàn)這樣的情況:我們傳給AudioFileStreamParseBytes的數(shù)據(jù)在解析完成之后會有一部分數(shù)據(jù)余下來,這部分數(shù)據(jù)是接下去那一幀的前半部分,如果再次有數(shù)據(jù)輸入需要繼續(xù)解析時就必須要用到前一次解析余下來的數(shù)據(jù)才能保證幀數(shù)據(jù)完整,所以在正常播放的情況下傳入0即可。需要傳入kAudioFileStreamParseFlag_Discontinuity的情況有兩個,一個是在seek完畢之后,顯然seek后的數(shù)據(jù)和之前的數(shù)據(jù)完全無關(guān);另一個和AudioFileStream的bug有關(guān),在回調(diào)得到kAudioFileStreamProperty_ReadyToProducePackets之后,在正常解析第一包之前最好都傳入kAudioFileStreamParseFlag_Discontinuity。
  • 返回值表示本次解析是否成功(同樣status == noErr)。

注意:AudioFileStreamParseBytes()函數(shù)每次調(diào)用都必須檢查返回值,一旦出錯就沒有必要繼續(xù)解析了。注意一下若是返回這個kAudioFileStreamError_NotOptimized,說明這個音頻文件無法流播,只能下載完所有數(shù)據(jù)才能播放。

解析歌曲信息

調(diào)用AudioFileStreamParseBytes()之后首先會解析歌曲信息,每解析出一個,同步回調(diào)AudioFileStream_PropertyListenerProc。

typedef void (*AudioFileStream_PropertyListenerProc)(
                                            void *                          inClientData,
                                            AudioFileStreamID               inAudioFileStream,
                                            AudioFileStreamPropertyID       inPropertyID,
                                            AudioFileStreamPropertyFlags *  ioFlags)
  • 第一個參數(shù),inClientData是初始化時指定的上下文信息。
  • 第二個參數(shù),inAudioFileStream指代AudioFileStream對象。
  • 第三個參數(shù),inPropertyID表示音頻的信息,可以通過AudioFileStreamGetProperty()函數(shù)取值。
  • 第四個參數(shù),ioFlags表示這個property是否需要被緩存。

來看一下AudioFileStreamGetProperty()函數(shù)。

OSStatus
AudioFileStreamGetProperty( 
                            AudioFileStreamID                   inAudioFileStream,
                            AudioFileStreamPropertyID           inPropertyID,
                            UInt32 *                            ioPropertyDataSize,
                            void *                              outPropertyData)
  • 第一個參數(shù),inAudioFileStream指代AudioFileStream對象。
  • 第二個參數(shù),inPropertyID表示想獲取哪個property。
  • 第三個參數(shù),想要獲取的property所表示的數(shù)據(jù)結(jié)構(gòu)大小,對于大小不定的propertyID,需要先調(diào)用AudioFileStreamGetPropertyInfo()函數(shù)先獲取一下大小,比如kAudioFileStreamProperty_FormatList。
  • 第四個參數(shù),outPropertyData是一個返回參數(shù),會返回獲取的property的值。
// AudioFileStream 定義的所有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

  • kAudioFileStreamProperty_DataOffset:第一篇提到MP3文件有一個頭信息,之后才是真正的音頻數(shù)據(jù),這個屬性表示的就是頭信息的大小,在seek操作時有比較大的作用。對于用戶來講,seek操作操作的是時間,但對于編碼來講,我們seek的是文件位置,seek時會根據(jù)時間計算出音頻數(shù)據(jù)的字節(jié)offset然后需要再加上音頻數(shù)據(jù)的offset才能得到在文件中的真正offset。。
// 注意數(shù)據(jù)類型一定不能錯,否則會獲取不到想要的結(jié)果。
SInt64 dataOffset;
UInt32 offsetSize = sizeof(dataOffset);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataOffset, &offsetSize, &dataOffset);
if (status != noErr)
{
    //錯誤處理
}
  • kAudioFileStreamProperty_AudioDataByteCount:表示真正可播放的音頻數(shù)據(jù)大?。ǔヮ^信息),很明顯也可以這么計算audioDataByteCount = fileSize - dataOffset。
UInt64 audioDataByteCount;
UInt32 byteCountSize = sizeof(audioDataByteCount);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_AudioDataByteCount, &byteCountSize, &audioDataByteCount);
if (status != noErr)
{
    //錯誤處理
}
  • kAudioFileStreamProperty_BitRate:獲取碼率可以用來計算音頻時長(AudioFileStream沒有提供直接獲取音頻時長的接口)。文件大小與碼率的關(guān)系在第一篇提到過,從而可得 duration = ((fileSize - dataOffset) * 8) / bitRate。
UInt32 bitRate;
UInt32 bitRateSize = sizeof(bitRate);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_BitRate, &bitRateSize, &bitRate);
if (status != noErr)
{
    //錯誤處理
}
  • kAudioFileStreamProperty_DataFormat:表示音頻文件結(jié)構(gòu)信息,是一個AudioStreamBasicDescription的結(jié)構(gòu)體。
struct AudioStreamBasicDescription
{
    // 采樣率
    Float64             mSampleRate;
    // 音頻的類型,MP3 or WAV
    AudioFormatID       mFormatID;
    // 隨mFormatID而定
    AudioFormatFlags    mFormatFlags;
   //  每個數(shù)據(jù)包中的字節(jié)數(shù)
    UInt32              mBytesPerPacket;
   // 每個數(shù)據(jù)包的幀數(shù)(第一篇提到過,原始數(shù)據(jù)如PCM一包一幀,壓縮格式如MP3一包多幀)
    UInt32              mFramesPerPacket;
   // 每幀的字節(jié)數(shù)
    UInt32              mBytesPerFrame;
   // 每幀的聲道數(shù)
    UInt32              mChannelsPerFrame;
   // 每個聲道的采樣位數(shù)
    UInt32              mBitsPerChannel;
   // 與內(nèi)存對齊有關(guān)
    UInt32              mReserved;
};
// 獲取format
AudioStreamBasicDescription asbd;
UInt32 asbdSize = sizeof(asbd);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataFormat, &asbdSize, &asbd);
if (status != noErr)
{
    //錯誤處理
} 
  • kAudioFileStreamProperty_FormatList:作用和kAudioFileStreamProperty_DataFormat是一樣的,區(qū)別在于用這個PropertyID獲取到是一個AudioStreamBasicDescription的數(shù)組,這個參數(shù)是用來支持AAC,SBR這樣的包含多個文件類型的音頻格式。由于到底有多少個format我們并不知曉,所以需要先獲取一下總數(shù)據(jù)大?。?/li>
//獲取數(shù)據(jù)大小
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);
  • kAudioFileStreamProperty_ReadyToProducePackets:這個PropertyID可以不必獲取對應(yīng)的值,一旦回調(diào)中這個PropertyID出現(xiàn)就代表解析完成,接下來可以對音頻數(shù)據(jù)進行幀分離了。
分離音頻幀

歌曲信息讀取完整后,繼續(xù)調(diào)用AudioFileStreamParseBytes()方法可以對幀進行分離,并同步的進入AudioFileStream_PacketsProc回調(diào)方法。

typedef void (*AudioFileStream_PacketsProc)(void * inClientData,
                                            UInt32 numberOfBytes,
                                            UInt32 numberOfPackets,
                                            const void * inInputData,
                                            AudioStreamPacketDescription * inPacketDescriptions);
  • 第一個參數(shù),inClientData是初始化時傳入的上下文對象。
  • 第二個參數(shù),numberOfBytes表示本次處理的音頻數(shù)據(jù)總量。
  • 第三個參數(shù),numberOfPackets表示本次處理的數(shù)據(jù)包數(shù)。
  • 第四個參數(shù),inInputData表示本次處理的所有數(shù)據(jù)。
  • 第五個參數(shù),AudioStreamPacketDescription數(shù)組,存儲了每一幀數(shù)據(jù)是從第幾個字節(jié)開始的,這一幀總共多少字節(jié)。
//這里的mVariableFramesInPacket是指實際的數(shù)據(jù)幀
//只有VBR的數(shù)據(jù)才能用到(像MP3這樣的壓縮數(shù)據(jù)一個幀里會有好幾個數(shù)據(jù)幀)
struct  AudioStreamPacketDescription
{
    // 音頻數(shù)據(jù)從哪里開始
    SInt64  mStartOffset;
    UInt32  mVariableFramesInPacket;
    // 這個數(shù)據(jù)包的大小
    UInt32  mDataByteSize;
};

AudioFileStream的具體用法可以戳這里。在Xcode-->Edit Scheme中添加啟動參數(shù)為本地音頻文件的路徑即可。

關(guān)閉AudioFileStream

AudioFileStream使用完畢后需要調(diào)用AudioFileStreamClose()進行關(guān)閉。

extern OSStatus AudioFileStreamClose(AudioFileStreamID inAudioFileStream); 

關(guān)于Mr.AudioFileStream的介紹到這里就差不多了,我們接著說它的大兄弟Mr.AudioFile。當然我們討論的是音頻播放相關(guān)的內(nèi)容,對于AudioFile僅會使用到它解析分離音頻幀的部分,對于寫音頻,這里就不討論啦。

初始化AudioFile

AudioFile提供了兩種讀取音頻文件的方法。第一種是通過文件路徑(因此僅能處理本地音頻,略過略過)。第二種是在AudioFile解析分離音頻幀的時候提供音頻數(shù)據(jù)給它。在流播的時候,音頻數(shù)據(jù)正好是一點一點通過HTTP請求返回的,所以我們把返回的數(shù)據(jù)一一提供給AudioFile就可以解析了;對于本地文件,可以用NSFIleHandle讀取數(shù)據(jù)提供給它,效果相同。

OSStatus AudioFileOpenWithCallbacks (void * inClientData,
                                            AudioFile_ReadProc inReadFunc,
                                            AudioFile_WriteProc inWriteFunc,
                                            AudioFile_GetSizeProc inGetSizeFunc,
                                            AudioFile_SetSizeProc inSetSizeFunc,
                                            AudioFileTypeID inFileTypeHint,
                                            AudioFileID * outAudioFile);
  • 第一個參數(shù),inClientData是上下文對象。
  • 第二個參數(shù),在AudioFile需要數(shù)據(jù)時由inReadFunc回調(diào)提供。
  • 第三個參數(shù),inWriteFunc與寫音頻有關(guān),略過。
  • 第四個參數(shù),通過inGetSizeFunc回調(diào)告訴AudioFile要解析的音頻文件大小,流播中通過請求頭的Content-Length獲得。
  • 第五個參數(shù),inSetSizeFunc與寫音頻有關(guān),略過。
  • 第六個參數(shù),inFileTypeHint同AudioFileStream,是文件格式的提示信息,同AudioFileStream。
  • 第七個參數(shù),outAudioFile是生成的AudioFile實例,保存起來留給其它函數(shù)當參數(shù)使用。

注意:初始化AudioFile時就需要提供音頻數(shù)據(jù)給它,除此之外在調(diào)用AudioFileReadXXX()相關(guān)方法時也需要提供合適的音頻數(shù)據(jù)給它。

上面說到AudioFile在解析分離音頻幀時需要通過兩個回調(diào)函數(shù)通知它。先來看一下提供音頻數(shù)據(jù)的回調(diào) —— AudioFile_ReadProc。

typedef OSStatus (*AudioFile_ReadProc)(void * inClientData,
                                       SInt64 inPosition,
                                       UInt32 requestCount,
                                       void * buffer,
                                       UInt32 * actualCount);
  • 第一個參數(shù),inClientData是初始化時傳遞的上下文對象。
  • 第二個參數(shù),inPosition指明了AudioFile需要從什么位置開始讀取音頻數(shù)據(jù),也就是說從第幾個字節(jié)開始。
  • 第三個參數(shù),requestCount指明AudioFile請求讀取的數(shù)據(jù)量,只是請求,不代表最后讀取的數(shù)據(jù)量。
  • 第四個參數(shù),buffer是一個數(shù)據(jù)指針并且其空間已經(jīng)被分配,我們需要做的是把數(shù)據(jù)memcpy到buffer中。
  • 第五個參數(shù),actualCount是實際提供的數(shù)據(jù)長度,即memcpy到buffer中的數(shù)據(jù)長度。
  • 如果沒有出錯,返回noErr即可。

這里需要解釋一下這個回調(diào)方法的工作方式。AudioFile需要數(shù)據(jù)時會調(diào)用回調(diào)方法,需要數(shù)據(jù)的時間點有兩個:

  • AudioFileOpenWithCallbacks()方法調(diào)用時,由于AudioFile的open方法調(diào)用過程中就會對音頻格式信息進行解析,只有符合要求的音頻格式才能被成功打開否則open方法就會返回錯誤碼(換句話說,open方法一旦調(diào)用成功就相當于AudioFileStream調(diào)用AudioFileStreamParseBytes()后返回ReadyToProducePackets
    一樣,只要open成功就可以開始讀取音頻數(shù)據(jù),所以在open方法調(diào)用的過程中就需要提供一部分音頻數(shù)據(jù)來進行解析。
  • AudioFileReadXXX()相關(guān)方法調(diào)用時,讀取數(shù)據(jù)時當然需要提供數(shù)據(jù)了。

通過回調(diào)提供數(shù)據(jù)時需要注意inPosition和requestCount參數(shù),這兩個參數(shù)指明了本次回調(diào)需要提供的數(shù)據(jù)范圍是從inPosition開始的 requestCount個連續(xù)字節(jié)的數(shù)據(jù)。這里又可以分為兩種情況:

  • 有充足的數(shù)據(jù):那么我們需要把這個范圍內(nèi)的數(shù)據(jù)拷貝到buffer中,并且給actualCount賦值requestCount,最后返回noError;
  • 數(shù)據(jù)不足:沒有充足數(shù)據(jù)的話就只能把手頭有的數(shù)據(jù)拷貝到buffer中,需要注意的是這部分被拷貝的數(shù)據(jù)必須是從inPosition開始的連續(xù)數(shù)據(jù),拷貝完成后給actualCount賦值實際拷貝進buffer中的數(shù)據(jù)長度后返回noErr,這個過程可以用下面的代碼來表示:
// totalData表示當前擁有的所有音頻數(shù)據(jù),NSData類型
static OSStatus mAudioFile_ReadProc(
                                     void *     inClientData,
                                     SInt64     inPosition,
                                     UInt32     requestCount,
                                     void *     buffer,
                                     UInt32 *   actualCount)
{
    // 如果需要讀取的長度超過擁有的數(shù)據(jù)長度
    if (inPosition + requestCount > [totalData length]) {
        // 如果讀取起點的位置已經(jīng)超過或等于擁有的數(shù)據(jù)長度了
        if (inPosition >= [totalData length]) {
            // 此時真正讀取長度就沒有了
            *actualCount = 0;
        }else{
            // 否則總共擁有的數(shù)據(jù)長度減去起點就是能讀到的所有數(shù)據(jù)了
            *actualCount = (UInt32)([totalData length] - inPosition);
        }
    }else{
        // 若是不比擁有的數(shù)據(jù)長度大
        // 真正讀取的就是請求的長度
        *actualCount = requestCount;
    }
    
    // EOF 整個文件讀取結(jié)束
    if (*actualCount == 0) return noErr;
    
    // 最后將從inPosition開始,長度為actualCount的數(shù)據(jù)拷貝到buffer中
    memcpy(buffer, (uint8_t *)[totalData bytes] + inPosition, *actualCount);
    // 返回noErr
    return noErr;
}

說到這里又需要分兩種情況(oh-oh,媽媽再也不用擔心我不會分類了):

  • AudioFileOpenWithCallbacks()方法調(diào)用時的回調(diào)數(shù)據(jù)不足:AudioFile的Open方法會根據(jù)音頻文件格式分幾步進行數(shù)據(jù)讀取,接著解析以確定是否是一個合法的文件格式,其中每一步的inPosition和requestCount都不一樣,如果某一步不成功就會直接進行下一步,如果幾部下來都失敗了,那么open方法就會失敗。簡單的說就是在調(diào)用open之前首先需要保證音頻文件的格式信息完整,這就意味著AudioFile并不能獨立用于音頻流的讀取,在流播放時首先需要使用AudioStreamFile來得到ReadyToProducePackets標志位來保證信息完整;
  • AudioFileReadXXX()方法調(diào)用時的回調(diào)數(shù)據(jù)不足:這種情況下inPosition和requestCount的數(shù)值與AudioFileReadXXX()方法調(diào)用時傳入的參數(shù)有關(guān),數(shù)據(jù)不足對于Read方法本身沒有影響,只要回調(diào)返回noErr,AudioFileReadXXX()就成功,只是實際交給AudioFileReadXXX()方法的調(diào)用方的數(shù)據(jù)會不足,那么就把這個問題的處理交給了AudioFileReadXXX()的調(diào)用方,對應(yīng)播放器的狀態(tài)就是buffering;
解析音頻信息

讀數(shù)據(jù)時AudioFile和AudioFileStream差不多,成功打開AudioFile之后就可以獲取歌曲信息了,包括比特率,音頻時長等。

OSStatus AudioFileGetPropertyInfo(AudioFileID inAudioFile,
                                         AudioFilePropertyID inPropertyID,
                                         UInt32 * outDataSize,
                                         UInt32 * isWritable);
                                      
OSStatus AudioFileGetProperty(AudioFileID inAudioFile,
                                     AudioFilePropertyID inPropertyID,
                                     UInt32 * ioDataSize,
                                     void * outPropertyData); 

AudioFileGetPropertyInfo方法用來獲取某個屬性對應(yīng)的數(shù)據(jù)的大?。╫utDataSize)以及該屬性是否可以被write(isWritable),而AudioFileGetProperty則用來獲取屬性對應(yīng)的數(shù)據(jù)。對于一些大小可變的屬性需要先使用AudioFileGetPropertyInfo獲取數(shù)據(jù)大小才能取獲取數(shù)據(jù)(例如formatList),而有些確定類型單個屬性則不必先調(diào)用AudioFileGetPropertyInfo直接調(diào)用AudioFileGetProperty即可。

- (BOOL)_fillFileFormat
{
    UInt32 size;
    OSStatus status;
    
    // 支持AAC SBR類型的文件
    // kAudioFilePropertyFormatList返回的是AudioFormatListItem數(shù)組
    status = AudioFileGetPropertyInfo(_fileID, kAudioFilePropertyFormatList, &size, NULL);
    if (status != noErr) {
        return NO;
    }
    
    // 求出有多少個
    UInt32 numFormats = size / sizeof(AudioFormatListItem);
    // 分配好內(nèi)存
    AudioFormatListItem *formatList = (AudioFormatListItem *)malloc(size);
    
    // 獲取值
    status = AudioFileGetProperty(_fileID, kAudioFilePropertyFormatList, &size, formatList);
    if (status != noErr) {
        free(formatList);
        return NO;
    }
    
    // 只有一個的話直接取出來
    if (numFormats == 1) {
        _fileFormat = formatList[0].mASBD;
    }
    else {
        
        status = AudioFormatGetPropertyInfo(kAudioFormatProperty_DecodeFormatIDs, 0, NULL, &size);
        if (status != noErr) {
            free(formatList);
            return NO;
        }
        
        UInt32 numDecoders = size / sizeof(OSType);
        OSType *decoderIDS = (OSType *)malloc(size);
        
        status = AudioFormatGetProperty(kAudioFormatProperty_DecodeFormatIDs, 0, NULL, &size, decoderIDS);
        if (status != noErr) {
            free(formatList);
            free(decoderIDS);
            return NO;
        }
        
        UInt32 i;
        for (i = 0; i < numFormats; ++i) {
            OSType decoderID = formatList[i].mASBD.mFormatID;
            
            BOOL found = NO;
            for (UInt32 j = 0; j < numDecoders; ++j) {
                if (decoderID == decoderIDS[j]) {
                    found = YES;
                    break;
                }
            }
            
            if (found) {
                break;
            }
        }
        
        free(decoderIDS);
        
        if (i >= numFormats) {
            free(formatList);
            return NO;
        }
        
        _fileFormat = formatList[i].mASBD;
    }
    
    free(formatList);
    return YES;
}

- (BOOL)_fillMiscProperties
{
    UInt32 size;
    OSStatus status;
    
    UInt32 bitRate = 0;
    size = sizeof(bitRate);
    status = AudioFileGetProperty(_fileID, kAudioFilePropertyBitRate, &size, &bitRate);
    if (status != noErr) {
        return NO;
    }
    _bitRate = bitRate;
    
    SInt64 dataOffset = 0;
    size = sizeof(dataOffset);
    status = AudioFileGetProperty(_fileID, kAudioFilePropertyDataOffset, &size, &dataOffset);
    if (status != noErr) {
        return NO;
    }
    _dataOffset = (NSUInteger)dataOffset;
    
    Float64 estimatedDuration = 0.0;
    size = sizeof(estimatedDuration);
    status = AudioFileGetProperty(_fileID, kAudioFilePropertyEstimatedDuration, &size, &estimatedDuration);
    if (status != noErr) {
        return NO;
    }
    _estimatedDuration = estimatedDuration;
    
    return YES;
}

讀取音頻數(shù)據(jù)

讀取音頻數(shù)據(jù)的方法分為兩類:

  • 直接讀取音頻數(shù)據(jù):
OSStatus AudioFileReadBytes (AudioFileID inAudioFile,
                                    Boolean inUseCache,
                                    SInt64 inStartingByte,
                                    UInt32 * ioNumBytes,
                                    void * outBuffer);
  • 第一個參數(shù),F(xiàn)ileID。
  • 第二個參數(shù),是否需要cache,一般來說傳false。
  • 第三個參數(shù),從第幾個byte開始讀取數(shù)據(jù)。
  • 第四個參數(shù),這個參數(shù)在調(diào)用時作為輸入?yún)?shù)表示需要讀取讀取多少數(shù)據(jù),調(diào)用完成后作為輸出參數(shù)表示實際讀取了多少數(shù)據(jù)(即Read回調(diào)中的requestCount和actualCount)。
  • 第五個參數(shù),buffer指針,需要事先分配好足夠大的內(nèi)存(ioNumBytes大,即Read回調(diào)中的buffer,所以Read回調(diào)中不需要再分配內(nèi)存)。
  • 返回值表示是否讀取成功,EOF時會返回kAudioFileEndOfFileError。

注意:使用這個方法得到的數(shù)據(jù)都是沒有進行過幀分離的數(shù)據(jù),如果想要用來播放或者解碼還必須通過AudioFileStream進行幀分離。

  • 按包(Packet)讀取音頻數(shù)據(jù):
OSStatus AudioFileReadPacketData (AudioFileID inAudioFile,
                                         Boolean inUseCache,
                                         UInt32 * ioNumBytes,
                                         AudioStreamPacketDescription * outPacketDescriptions,
                                         SInt64 inStartingPacket,
                                         UInt32 * ioNumPackets,
                                         void * outBuffer);          
OSStatus AudioFileReadPackets (AudioFileID inAudioFile,
                                      Boolean inUseCache,
                                      UInt32 * outNumBytes,
                                      AudioStreamPacketDescription * outPacketDescriptions,
                                      SInt64 inStartingPacket,
                                      UInt32 * ioNumPackets,
                                      void * outBuffer);

按包讀取的方法有兩個,這兩個方法看上去差不多,就連參數(shù)也幾乎相同,但使用場景和效率上卻有所不同。只有當需要讀取固定時長音頻或者非壓縮音頻時才會用到AudioFileReadPackets(),其余時候使用AudioFileReadPacketData()會有更高的效率并且更省內(nèi)存(所以AudioFileReadPackets()已經(jīng)被標記為deprecated~~);
下面來看看這些參數(shù):

  • 第一、二個參數(shù),同AudioFileReadBytes。
  • 第三個參數(shù),對于AudioFileReadPacketData()來說ioNumBytes這個參數(shù)在輸入輸出時都要用到,在輸入時表示outBuffer的size,輸出時表示實際讀取了多少size的數(shù)據(jù)。而對AudioFileReadPackets()來說outNumBytes只在輸出時使用,表示實際讀取了多少size的數(shù)據(jù);
  • 第四個參數(shù),幀信息數(shù)組指針,在輸入前需要分配內(nèi)存,大小必須足夠存儲ioNumPackets個幀信息(ioNumPackets * sizeof(AudioStreamPacketDescription))。
  • 第五個參數(shù),從第幾幀開始讀取數(shù)據(jù)。
  • 第六個參數(shù),在輸入時表示需要讀取多少個幀,在輸出時表示實際讀取了多少幀。
  • 第七個參數(shù),outBuffer數(shù)據(jù)指針,在輸入前就需要分配好空間,這個參數(shù)看上去兩個方法一樣但其實并非如此。對于AudioFileReadPacketData()來說只要分配近似幀大小 * 幀數(shù)的內(nèi)存空間即可,方法本身會針對給定的內(nèi)存空間大小來決定最后輸出多少個幀,如果空間不夠會適當減少出的幀數(shù);而對于AudioFileReadPackets()來說則需要分配最大幀大小(或幀大小上界) * 幀數(shù)的內(nèi)存空間才行;這也就是為何第三個參數(shù)一個是輸入輸出雙向使用的,而另一個只是輸出時使用的原因。就這點來說兩個方法中前者在使用的過程中要比后者更省內(nèi)存。
  • 返回值,同AudioFileReadBytes。

這兩個方法讀取后的數(shù)據(jù)為幀分離后的數(shù)據(jù),可以直接用來播放或者解碼。具體使用可以參考這里;

關(guān)閉AudioFile

AudioFile使用完畢后需要調(diào)用AudioFileClose進行關(guān)閉。

extern OSStatus AudioFileClose (AudioFileID inAudioFile);  

下一篇會介紹AudioConverter。

說明:很多話我是直接從這里直接拿過來的,作者總結(jié)的非常好,可能我表述來表述去也就那么個意思,所以就直接拿來用了。有些地方,包括我自己遇到的坑,會做一些補充說明,大家知道就好。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,480評論 2 379

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