天天看點

iOS音頻播放 (四):AudioFile前言AudioFile介紹AudioFile的打開“姿勢”讀取音頻格式資訊讀取音頻資料Seek關閉AudioFile小結示例代碼下篇預告參考資料

Audio Playback in iOS (Part 4) : AudioFile

前言

接着第三篇的

AudioStreamFile

這一篇要來聊一下

AudioFile

。和

AudioStreamFile

一樣

AudioFile

AudioToolBox

 framework中的一員,它也能夠完成第一篇所述的第2步,讀取音頻格式資訊和進行幀分離,但事實上它的功能遠不止如此。

AudioFile介紹

按照官方文檔的描述:

a C programming interface that enables you to read or write a wide variety of audio data to or from disk or a memory buffer.With Audio File Services you can:

  • Create, initialize, open, and close audio files
  • Read and write audio files
  • Optimize audio files
  • Work with user data and global information

這個類可以用來建立、初始化音頻檔案;讀寫音頻資料;對音頻檔案進行優化;讀取和寫入音頻格式資訊等等,功能十分強大,可見它不但可以用來支援音頻播放,甚至可以用來生成音頻檔案。當然,在本篇文章中隻會涉及一些和音頻播放相關的内容(打開音頻檔案、讀取格式資訊、讀取音頻資料,其實我也隻對這些方法有一點了解,其餘的功能沒用過。。>_<).

AudioFile的打開“姿勢”

AudioFile

提供了兩個打開檔案的方法:

1、 

AudioFileOpenURL

1
2
3
4
5
6
7
8
9
10
      
enum {
  kAudioFileReadPermission      = 0x01,
  kAudioFileWritePermission     = 0x02,
  kAudioFileReadWritePermission = 0x03
};

extern OSStatus AudioFileOpenURL (CFURLRef inFileRef,
                                  SInt8 inPermissions,
                                  AudioFileTypeID inFileTypeHint,
                                  AudioFileID * outAudioFile);

           

從方法的定義上來看是用來讀取本地檔案的:

第一個參數,檔案路徑;

第二個參數,檔案的允許使用方式,是讀、寫還是讀寫,如果打開檔案後進行了允許使用方式以外的操作,就得到

kAudioFilePermissionsError

錯誤碼(比如Open時聲明是

kAudioFileReadPermission

但卻調用了

AudioFileWriteBytes

);

第三個參數,和

AudioFileStream

的open方法中一樣是一個幫助

AudioFile

解析檔案的類型提示,如果檔案類型确定的話應當傳入;

第四個參數,傳回AudioFile執行個體對應的

AudioFileID

,這個ID需要儲存起來作為後續一些方法的參數使用;

傳回值用來判斷是否成功打開檔案(OSSStatus == noErr)。

2、 

AudioFileOpenWithCallbacks

1
2
3
4
5
6
7
      
extern OSStatus AudioFileOpenWithCallbacks (void * inClientData,
                                            AudioFile_ReadProc inReadFunc,
                                            AudioFile_WriteProc inWriteFunc,
                                            AudioFile_GetSizeProc inGetSizeFunc,
                                            AudioFile_SetSizeProc inSetSizeFunc,
                                            AudioFileTypeID inFileTypeHint,
                                            AudioFileID * outAudioFile);

           

看過第一個Open方法後,這個方法乍看上去讓人有點迷茫,沒有URL的參數如何告訴AudioFile該打開哪個檔案?還是先來看一下參數的說明吧:

第一個參數,上下文資訊,不再多做解釋;

第二個參數,當

AudioFile

需要讀音頻資料時進行的回調(調用Open和Read方式後

同步

回調);

第三個參數,當

AudioFile

需要寫音頻資料時進行的回調(寫音頻檔案功能時使用,暫不讨論);

第四個參數,當

AudioFile

需要用到檔案的總大小時回調(調用Open和Read方式後

同步

回調);

第五個參數,當

AudioFile

需要設定檔案的大小時回調(寫音頻檔案功能時使用,暫不讨論);

第六、七個參數和傳回值同

AudioFileOpenURL

方法;

這個方法的重點在于

AudioFile_ReadProc

這個回調。換一個角度了解,這個方法相比于第一個方法自由度更高,AudioFile需要的隻是一個資料源,無論是磁盤上的檔案、記憶體裡的資料甚至是網絡流隻要能在

AudioFile

需要資料時(Open和Read時)通過

AudioFile_ReadProc

回調為AudioFile提供合适的資料就可以了,也就是說使用方法不僅僅可以讀取本地檔案也可以如

AudioFileStream

一樣以流的形式讀取資料。

下面來看一下

AudioFile_GetSizeProc

AudioFile_ReadProc

這兩個讀取功能相關的回調

1
2
3
4
5
6
7
      
typedef SInt64 (*AudioFile_GetSizeProc)(void * inClientData);

typedef OSStatus (*AudioFile_ReadProc)(void * inClientData,
                                       SInt64 inPosition,
                                       UInt32 requestCount,
                                       void * buffer,
                                       UInt32 * actualCount);

           

首先是

AudioFile_GetSizeProc

回調,這個回調很好了解,傳回檔案總長度即可,總長度的擷取途徑自然是檔案系統或者httpResponse等等。

接下來是

AudioFile_ReadProc

回調:

第一個參數,上下文對象,不再贅述;

第二個參數,需要讀取第幾個位元組開始的資料;

第三個參數,需要讀取的資料長度;

第四個參數,傳回參數,是一個資料指針并且其空間已經被配置設定,我們需要做的是把資料memcpy到buffer中;

第五個參數,實際提供的資料長度,即memcpy到buffer中的資料長度;

傳回值,如果沒有任何異常産生就傳回noErr,如果有異常可以根據異常類型選擇需要的error常量傳回(一般用不到其他傳回值,傳回noErr就足夠了);

這裡需要解釋一下這個回調方法的工作方式。

AudioFile

需要資料時會調用回調方法,需要資料的時間點有兩個:

  1. Open方法調用時,由于

    AudioFile

    的Open方法調用過程中就會對音頻格式資訊進行解析,隻有符合要求的音頻格式才能被成功打開否則Open方法就會傳回錯誤碼(換句話說,Open方法一旦調用成功就相當于

    AudioStreamFile

    在Parse後傳回

    ReadyToProducePackets

    一樣,隻要Open成功就可以開始讀取音頻資料,詳見第三篇),是以在Open方法調用的過程中就需要提供一部分音頻資料來進行解析;
  2. Read相關方法調用時,這個不需要多說很好了解;

通過回調提供資料時需要注意inPosition和requestCount參數,這兩個參數指明了本次回調需要提供的資料範圍是從inPosition開始requestCount個位元組的資料。這裡又可以分為兩種情況:

  1. 有充足的資料:那麼我們需要把這個範圍内的資料拷貝到buffer中,并且給actualCount指派requestCount,最後傳回noError;
  2. 資料不足:沒有充足資料的話就隻能把手頭有的資料拷貝到buffer中,需要注意的是這部分被拷貝的資料必須是從inPosition開始的

    連續資料

    ,拷貝完成後給actualCount指派實際拷貝進buffer中的資料長度後傳回noErr,這個過程可以用下面的代碼來表示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
      
static OSStatus MyAudioFileReadCallBack(void *inClientData,
                                        SInt64 inPosition,
                                        UInt32 requestCount,
                                        void *buffer,
                                        UInt32 *actualCount)
{
    __unsafe_unretained MyContext *context = (__bridge MyContext *)inClientData;

    *actualCount = [context availableDataLengthAtOffset:inPosition maxLength:requestCount];
    if (*actualCount > 0)
    {
        NSData *data = [context dataAtOffset:inPosition length:*actualCount];
        memcpy(buffer, [data bytes], [data length]);
    }

    return noErr;
}

           

說到這裡又需要分兩種情況:

2.1. Open方法調用時的回調資料不足:AudioFile的Open方法會根據檔案格式類型分幾步進行資料讀取以解析确定是否是一個合法的檔案格式,其中每一步的inPosition和requestCount都不一樣,如果某一步不成功就會直接進行下一步,如果幾部下來都失敗了,那麼Open方法就會失敗。簡單的說就是在調用Open之前首先需要保證音頻檔案的格式資訊完整,這就意味着

AudioFile

并不能獨立用于音頻流的讀取,在流播放時首先需要使用

AudioStreamFile

來得到

ReadyToProducePackets

标志位來保證資訊完整;

2.2. Read方法調用時的回調資料不足:這種情況下inPosition和requestCount的數值與Read方法調用時傳入的參數有關,資料不足對于Read方法本身沒有影響,隻要回調傳回noErr,Read就成功,隻是實際交給Read方法的調用方的資料會不足,那麼就把這個問題的處理交給了Read的調用方;

讀取音頻格式資訊

成功打開音頻檔案後就可以讀取其中的格式資訊了,讀取用到的方法如下:

1
2
3
4
5
6
7
8
9
      
extern OSStatus AudioFileGetPropertyInfo(AudioFileID inAudioFile,
                                         AudioFilePropertyID inPropertyID,
                                         UInt32 * outDataSize,
                                         UInt32 * isWritable);
                                      
extern OSStatus AudioFileGetProperty(AudioFileID inAudioFile,
                                     AudioFilePropertyID inPropertyID,
                                     UInt32 * ioDataSize,
                                     void * outPropertyData);    

           

AudioFileGetPropertyInfo

方法用來擷取某個屬性對應的資料的大小(outDataSize)以及該屬性是否可以被write(isWritable),而

AudioFileGetProperty

則用來擷取屬性對應的資料。對于一些大小可變的屬性需要先使用

AudioFileGetPropertyInfo

擷取資料大小才能取擷取資料(例如formatList),而有些确定類型單個屬性則不必先調用

AudioFileGetPropertyInfo

直接調用

AudioFileGetProperty

即可(比如BitRate),例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
      
AudioFileID fileID; //Open方法傳回的AudioFileID

//擷取格式資訊
UInt32 formatListSize = 0;
OSStatus status = AudioFileGetPropertyInfo(_fileID, kAudioFilePropertyFormatList, &formatListSize, NULL);
if (status == noErr)
{
    AudioFormatListItem *formatList = (AudioFormatListItem *)malloc(formatListSize);
    status = AudioFileGetProperty(fileID, kAudioFilePropertyFormatList, &formatListSize, formatList);
    if (status == noErr)
    {
        for (int i = 0; i * sizeof(AudioFormatListItem) < formatListSize; i += sizeof(AudioFormatListItem))
        {
            AudioStreamBasicDescription pasbd = formatList[i].mASBD;
            //選擇需要的格式。。                             
        }
    }
    free(formatList);
}

//擷取碼率
UInt32 bitRate;
UInt32 bitRateSize = sizeof(bitRate);
status = AudioFileGetProperty(fileID, kAudioFilePropertyBitRate, &size, &bitRate);
if (status != noErr)
{
    //錯誤處理
}

           

可以擷取的屬性有下面這些,大家可以參考文檔來擷取自己需要的資訊(注意到這裡有EstimatedDuration,可以得到Duration了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
      
enum
{
  kAudioFilePropertyFileFormat             =    'ffmt',
  kAudioFilePropertyDataFormat             =    'dfmt',
  kAudioFilePropertyIsOptimized            =    'optm',
  kAudioFilePropertyMagicCookieData        =    'mgic',
  kAudioFilePropertyAudioDataByteCount     =    'bcnt',
  kAudioFilePropertyAudioDataPacketCount   =    'pcnt',
  kAudioFilePropertyMaximumPacketSize      =    'psze',
  kAudioFilePropertyDataOffset             =    'doff',
  kAudioFilePropertyChannelLayout          =    'cmap',
  kAudioFilePropertyDeferSizeUpdates       =    'dszu',
  kAudioFilePropertyMarkerList             =    'mkls',
  kAudioFilePropertyRegionList             =    'rgls',
  kAudioFilePropertyChunkIDs               =    'chid',
  kAudioFilePropertyInfoDictionary         =    'info',
  kAudioFilePropertyPacketTableInfo        =    'pnfo',
  kAudioFilePropertyFormatList             =    'flst',
  kAudioFilePropertyPacketSizeUpperBound   =    'pkub',
  kAudioFilePropertyReserveDuration        =    'rsrv',
  kAudioFilePropertyEstimatedDuration      =    'edur',
  kAudioFilePropertyBitRate                =    'brat',
  kAudioFilePropertyID3Tag                 =    'id3t',
  kAudioFilePropertySourceBitDepth         =    'sbtd',
  kAudioFilePropertyAlbumArtwork           =    'aart',
  kAudioFilePropertyAudioTrackCount        =    'atct',
  kAudioFilePropertyUseAudioTrack          =    'uatk'
}; 

           

讀取音頻資料

讀取音頻資料的方法分為兩類:

1、直接讀取音頻資料:

1
2
3
4
5
      
extern OSStatus AudioFileReadBytes (AudioFileID inAudioFile,
                                    Boolean inUseCache,
                                    SInt64 inStartingByte,
                                    UInt32 * ioNumBytes,
                                    void * outBuffer);

           

第一個參數,FileID;

第二個參數,是否需要cache,一般來說傳false;

第三個參數,從第幾個byte開始讀取資料

第四個參數,這個參數在調用時作為輸入參數表示需要讀取讀取多少資料,調用完成後作為輸出參數表示實際讀取了多少資料(即Read回調中的requestCount和actualCount);

第五個參數,buffer指針,需要事先配置設定好足夠大的記憶體(ioNumBytes大,即Read回調中的buffer,是以Read回調中不需要再配置設定記憶體);

傳回值表示是否讀取成功,EOF時會傳回

kAudioFileEndOfFileError

使用這個方法得到的資料都是沒有進行過幀分離的資料,如果想要用來播放或者解碼還必須通過

AudioFileStream

進行幀分離;

2、按幀(Packet)讀取音頻資料:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
      
extern OSStatus AudioFileReadPacketData (AudioFileID inAudioFile,
                                         Boolean inUseCache,
                                         UInt32 * ioNumBytes,
                                         AudioStreamPacketDescription * outPacketDescriptions,
                                         SInt64 inStartingPacket,
                                         UInt32 * ioNumPackets,
                                         void * outBuffer);
                                      

extern OSStatus AudioFileReadPackets (AudioFileID inAudioFile,
                                      Boolean inUseCache,
                                      UInt32 * outNumBytes,
                                      AudioStreamPacketDescription * outPacketDescriptions,
                                      SInt64 inStartingPacket,
                                      UInt32 * ioNumPackets,
                                      void * outBuffer);

           

按幀讀取的方法有兩個,這兩個方法看上去差不多,就連參數也幾乎相同,但使用場景和效率上卻有所不同,官方文檔中如此描述這兩個方法:

  • AudioFileReadPacketData

     is memory efficient when reading variable bit-rate (VBR) audio data;
  • AudioFileReadPacketData

     is more efficient than 

    AudioFileReadPackets

     when reading compressed file formats that do not have packet tables, such as MP3 or ADTS. This function is a good choice for reading either CBR (constant bit-rate) or VBR data if you do not need to read a fixed duration of audio.
  • Use 

    AudioFileReadPackets

     only when you need to read a fixed duration of audio data, or when you are reading only uncompressed audio.

隻有當需要讀取固定時長音頻或者非壓縮音頻時才會用到

AudioFileReadPackets

,其餘時候使用

AudioFileReadPacketData

會有更高的效率并且更省記憶體;

下面來看看這些參數:

第一、二個參數,同

AudioFileReadBytes

第三個參數,對于

AudioFileReadPacketData

來說ioNumBytes這個參數在輸入輸出時都要用到,在輸入時表示outBuffer的size,輸出時表示實際讀取了多少size的資料。而對

AudioFileReadPackets

來說outNumBytes隻在輸出時使用,表示實際讀取了多少size的資料;

第四個參數,幀資訊數組指針,在輸入前需要配置設定記憶體,大小必須足夠存在ioNumPackets個幀資訊(ioNumPackets * sizeof(AudioStreamPacketDescription));

第五個參數,在輸入時表示需要讀取多少個幀,在輸出時表示實際讀取了多少幀;

第六個參數,outBuffer資料指針,在輸入前就需要配置設定好空間,這個參數看上去兩個方法一樣但其實并非如此。對于

AudioFileReadPacketData

來說隻要配置設定

近似幀大小 * 幀數

的記憶體空間即可,方法本身會針對給定的記憶體空間大小來決定最後輸出多少個幀,如果空間不夠會适當減少出的幀數;而對于

AudioFileReadPackets

來說則需要配置設定

最大幀大小(或幀大小上界) * 幀數

的記憶體空間才行(最大幀大小和幀大小上界的差別等下會說);這也就是為何第三個參數一個是輸入輸出雙向使用的,而另一個隻是輸出時使用的原因。就這點來說兩個方法中前者在使用的過程中要比後者更省記憶體;

傳回值,同

AudioFileReadBytes

這兩個方法讀取後的資料為幀分離後的資料,可以直接用來播放或者解碼。

下面給出兩個方法的使用代碼(以MP3為例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
      
AudioFileID fileID; //Open方法傳回的AudioFileID
UInt32 ioNumPackets = ...; //要讀取多少個packet
SInt64 inStartingPacket = ...; //從第幾個Packet開始讀取

UInt32 bitRate = ...; //AudioFileGetProperty讀取kAudioFilePropertyBitRate
UInt32 sampleRate = ...; //AudioFileGetProperty讀取kAudioFilePropertyDataFormat或kAudioFilePropertyFormatList
UInt32 byteCountPerPacket = 144 * bitRate / sampleRate; //MP3資料每個Packet的近似大小

UInt32 descSize = sizeof(AudioStreamPacketDescription) * ioNumPackets;
AudioStreamPacketDescription * outPacketDescriptions = (AudioStreamPacketDescription *)malloc(descSize);

UInt32 ioNumBytes = byteCountPerPacket * ioNumPackets;
void * outBuffer = (void *)malloc(ioNumBytes);

OSStatus status = AudioFileReadPacketData(fileID,
                                          false,
                                          &ioNumBytes,
                                          outPacketDescriptions,
                                          inStartingPacket,
                                          &ioNumPackets,
                                          outBuffer);

           
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
      
AudioFileID fileID; //Open方法傳回的AudioFileID
UInt32 ioNumPackets = ...; //要讀取多少個packet
SInt64 inStartingPacket = ...; //從第幾個Packet開始讀取

UInt32 maxByteCountPerPacket = ...; //AudioFileGetProperty讀取kAudioFilePropertyMaximumPacketSize,最大的packet大小
//也可以用:
//UInt32 byteCountUpperBoundPerPacket = ...; //AudioFileGetProperty讀取kAudioFilePropertyPacketSizeUpperBound,目前packet大小上界(未掃描全檔案的情況下)

UInt32 descSize = sizeof(AudioStreamPacketDescription) * ioNumPackets;
AudioStreamPacketDescription * outPacketDescriptions = (AudioStreamPacketDescription *)malloc(descSize);

UInt32 outNumBytes = 0;
UInt32 ioNumBytes = maxByteCountPerPacket * ioNumPackets;
void * outBuffer = (void *)malloc(ioNumBytes);

OSStatus status = AudioFileReadPackets(fileID,
                                       false,
                                       &outNumBytes,
                                       outPacketDescriptions,
                                       inStartingPacket,
                                       &ioNumPackets,
                                       outBuffer);

           

Seek

seek的思路和之前講

AudioFileStream

時講到的是一樣的,差別在于AudioFile沒有方法來幫助修正seek的offset和seek的時間:

  • 使用

    AudioFileReadBytes

    時需要計算出approximateSeekOffset
  • 使用

    AudioFileReadPacketData

    或者

    AudioFileReadPackets

    時需要計算出seekToPacket

approximateSeekOffset和seekToPacket的計算方法參見第三篇。

關閉AudioFile

AudioFile

使用完畢後需要調用

AudioFileClose

進行關閉,沒啥特别需要注意的。

小結

本篇針對

AudioFile

的音頻讀取功能做了介紹,小結一下:

  • AudioFile

    有兩個Open方法,需要針對自身的使用場景選擇不同的方法;
  • AudioFileOpenURL

    用來讀取本地檔案
  • AudioFileOpenWithCallbacks

    的使用場景比前者要廣泛,使用時需要注意

    AudioFile_ReadProc

    ,這個回調方法在Open方法本身和Read方法被調用時會被

    同步

    調用
  • 必須保證音頻檔案格式資訊可讀時才能使用

    AudioFile

    的Open方法,AudioFile并不能獨立用于音頻流的讀取,需要配合

    AudioStreamFile

    使用才能讀取流(需要用

    AudioStreamFile

    來判斷檔案格式資訊可讀之後再調用Open方法);
  • 使用

    AudioFileGetProperty

    讀取格式資訊時需要判斷所讀取的資訊是否需要先調用

    AudioFileGetPropertyInfo

    獲得資料大小後再進行讀取;
  • 讀取音頻資料應該根據使用的場景選擇不同的音頻讀取方法,對于不同的讀取方法seek時需要計算的變量也不相同;
  • AudioFile

    使用完畢後需要調用

    AudioFileClose

    進行關閉;

示例代碼

對于本地檔案用AudioFile讀取比較簡單就不在這裡提供demo了,對于流播放中的AudioFile使用推薦大家閱讀豆瓣的開源播放器代碼DOUAudioStreamer。

下篇預告

下一篇将講述如何使用

AudioQueue

參考資料

Audio File Services Reference

原創文章,版權聲明:自由轉載-非商用-非衍生-保持署名 |  Creative Commons BY-NC-ND 3.0

繼續閱讀