iOS音頻學習二之AudioFile

上一篇我介紹了AudioFileStream,這一篇我來介紹一下AudioFile。
AudioFile跟AudioFileStream一樣,也能讀取音頻格式信息和進行幀的分離,但是功能比AudioFileStream強大

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

extern OSStatus 
AudioFileOpenURL (  CFURLRef                            inFileRef,
                    AudioFilePermissions                inPermissions,
                    AudioFileTypeID                     inFileTypeHint,
                    AudioFileID __nullable * __nonnull  outAudioFile)                   __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);

第一個參數是文件路徑
第二個參數是文件的允許使用方式,一共三種,讀、寫和讀寫,如果打開文件后使用了權限外的操作,就會報錯

typedef CF_ENUM(SInt8, AudioFilePermissions) {
    kAudioFileReadPermission      = 0x01,
    kAudioFileWritePermission     = 0x02,
    kAudioFileReadWritePermission = 0x03
};

第三個參數跟AudioFileStream一樣,傳入一個參數幫助解析文件
第四個參數返回AudioFile實例對應的一個ID,需要保存起來作為一些參數的查詢
返回值返回noErr則成功

2.AudioFileOpenWithCallbacks

extern OSStatus 
AudioFileOpenWithCallbacks (
                void *                              inClientData,
                AudioFile_ReadProc                  inReadFunc,
                AudioFile_WriteProc __nullable      inWriteFunc,
                AudioFile_GetSizeProc               inGetSizeFunc,
                AudioFile_SetSizeProc __nullable    inSetSizeFunc,
                AudioFileTypeID                     inFileTypeHint,
                AudioFileID __nullable * __nonnull  outAudioFile)               __OSX_AVAILABLE_STARTING(__MAC_10_3,__IPHONE_2_0);

第一個參數,上下文對象,一般為AudioFile實例
第二個參數,當AudioFile需要讀音頻數據時進行的回調(同步回調)
第三個參數,當AudioFile需要寫音頻數據時進行的回調(暫不討論,傳Null)
第四個參數,當AudioFile需要用到文件的總大小時的回調(同步回調)
第五個參數,當AudioFile需要設置文件大小時的回調(寫音頻文件功能時使用,傳Null,暫不討論)
第六,第七同AudioFileOpenURL方法
這個方法的重點在于AudioFile_ReadProc這個回調。這個方法比第一個自由度更高,AudioFile只需要一個數據源,無論是磁盤或者內存的數據甚至是網絡流只要能在AudioFile需要時即open和read時,通過AudioFile_ReadProc回調給AudioFile提供合適的數據就可以了,即可以讀取網絡流和本地文件
我們來看這兩個回調

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

AudioFile_GetSizeProc這個回調比較簡單,就是返回文件總長度
AudioFile_ReadProc
第一個參數,上下文對象,一般為AudioFile實例
第二個參數,需要讀取第幾個字節開始的數據
第三個參數,需要讀取的數據長度
第四個參數,返回參數,是一個數據指針且其空間已經被分配了,我們需要做的是把數據memcpy到buffer中
第五個參數,實際提供的數據長度,即memcpy到buffer的數據長度
返回值,如果沒有異常的話就直接返回noErr。
AudioFile需要數據時會調用回調方法,需要數據的時間點有兩個

  • Open方法調用時,由于AudioFile的Open方法調用過程中就會對音頻格式信息進行解析,只有復合要求的音頻格式才能被成功打開否則Open方法就會返回碼
  • Read相關方法調用時;
    這個回調函數需要注意inPosition和requestCount參數,這兩個參數指明了本次回調需要提供的數據范圍是從inPosition和requestCount個字節的數據。這里就有兩種情況
  • 有充足的數據:那么我們需要把這個范圍內的數據拷貝到buffer中,并且給actualCount賦值給requestCount,最后返回noErr
  • 數據不足: 沒有充足數據的話就只能把手頭的數據拷貝到buffer中,拷貝的數據必須是從inPosition開始的連續數據,拷貝完成后給actualCount賦值實際拷貝進buffer中的數據長度,然后返回noErr,如下代碼
static OSStatus ZJAudioFileReadCallBack(void *inclientData,SInt64 inPosition, UInt32 requestCount, void *buffer, UInt32 *actualCount){
    ZJAudioFile *audioFile = (__bridge ZJAudioFile *)inclientData;
    *actualCount = [audioFile availableDataLengthAtOffset:inPosition maxLength:requestCount];
    if (*actualCount>0) {
        NSData *data = [audioFile dataAtOffset:inPosition length:*actualCount];
        memcpy(buffer, [data bytes], [data length]);
        
    }
    return noErr;
}
  • 當Open方法調用時的回調數據不足:AudioFile的Open方法會根據文件格式類型分幾步進行數據讀取以解析確定是否是一個合法的文件格式,其中每一步的inPosition和requestCount都不一樣,如果某一步不成會直接進行下一步,如果幾步下來都失敗了,那么Open方法就會失敗。即在調用Open方法之前需要保證音頻文件的格式信息完整,所以AudioFile并不能獨立應用于音頻流的讀取,在流播放的時候需要使用AudioFileStream得到ReadyToProducePackets標志位來保證信息完整;
  • Read方法調用時回調數據不足:這種情況下inPosition和requestCount的數值與Read方法調用時傳入的參數有關,數據不足對于Read方法本身沒有影響,只要回調返回noErr,Read就成功,只是實際交給Read方法的調用方的數據會不足,所以需要去處理;
讀取音頻格式信息

像AudioFileStream一樣,AudioFile會有兩個getProperty的方法去獲取音頻格式信息

extern OSStatus
AudioFileGetPropertyInfo(       AudioFileID             inAudioFile,
                                AudioFilePropertyID     inPropertyID,
                                UInt32 * __nullable     outDataSize,
                                UInt32 * __nullable     isWritable)         __OSX_AVAILABLE_STARTING(__MAC_10_2,__IPHONE_2_0);
                                      
extern OSStatus
AudioFileGetProperty(   AudioFileID             inAudioFile,
                        AudioFilePropertyID     inPropertyID,
                        UInt32                  *ioDataSize,
                        void                    *outPropertyData)           __OSX_AVAILABLE_STARTING(__MAC_10_2,__IPHONE_2_0);

AudioFileGetPropertyInfo方法用來獲取某個屬性對應的數據大小(outDataSize)以及該屬性能否被write(isWritable),而AudioFileGetProperty則用來獲取屬性對應的數據。對于一些大小可變的屬性需要先使用AudioFileGetPropertyInfo獲取數據大小才能獲取數據(例如formatList),而有些確定類型單個則不必先調用AudioFileGetPropertyInfo直接調用AudioFileGetProperty則可(比如bitRate)

  UInt32 formatListSize;
    OSStatus status = AudioFileGetPropertyInfo(_audioFileID, kAudioFilePropertyFormatList, &formatListSize, NULL);
    if (status == noErr) {
        BOOL found = NO;
        AudioFormatListItem *formatList = malloc(formatListSize);
        OSStatus status = AudioFileGetProperty(_audioFileID, kAudioFilePropertyFormatList, &formatListSize, formatList);
        if (status == noErr) {
            UInt32 supportedFormatsSize;
            status = AudioFormatGetPropertyInfo(kAudioFormatProperty_DecodeFormatIDs, 0, NULL, &supportedFormatsSize);
            if (status != noErr) {
                free(formatList);
                [self _closeAudioFile];
                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);
                [self _closeAudioFile];
                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]) {
                        NSLog(@"i -- %u j -- %u",i,j);
                        _format = format;
                        found = YES;
                        break;
                    }
                }
            }
            free(supportedFormats);
        }
        free(formatList);
        
        if (!found) {
            [self _closeAudioFile];
            return;
        }else{
            [self _calculatePacketsDuration];
        }
    }
    
    UInt32 size = sizeof(_bitRate);
    status = AudioFileGetProperty(_audioFileID, kAudioFilePropertyBitRate, &size, &_bitRate);
    
    if (status != noErr) {
        [self _closeAudioFile];
        return;
    }
    
    size = sizeof(_dataOffset);
    status = AudioFileGetProperty(_audioFileID, kAudioFilePropertyDataOffset, &size, &_dataOffset);
    if (status != noErr) {
        [self _closeAudioFile];
        return;
    }
    
    _audioDataByteCount = _fileSize-_dataOffset;
    size = sizeof(_duration);
    status = AudioFileGetProperty(_audioFileID, kAudioFilePropertyEstimatedDuration, &size, &_duration);
    if (status != noErr) {
        [self _calculateDuration];
    }
    
    size = sizeof(_maxPacketSize);
    status = AudioFileGetProperty(_audioFileID, kAudioFilePropertyPacketSizeUpperBound, &size, &_maxPacketSize);
    if (status != noErr || _maxPacketSize == 0) {
        status = AudioFileGetProperty(_audioFileID, kAudioFilePropertyMaximumPacketSize, &size, &_maxPacketSize);
        if (status != noErr) {
            [self _closeAudioFile];
            return;
        }
    }

以下是可以獲取到的屬性

CF_ENUM(AudioFilePropertyID)
{
    kAudioFilePropertyFileFormat            =   'ffmt',
    kAudioFilePropertyDataFormat            =   'dfmt',
    kAudioFilePropertyIsOptimized           =   'optm',
    kAudioFilePropertyMagicCookieData       =   'mgic',
    kAudioFilePropertyAudioDataByteCount    =   'bcnt',
    kAudioFilePropertyAudioDataPacketCount  =   'pcnt',
    kAudioFilePropertyMaximumPacketSize     =   'psze',
    kAudioFilePropertyDataOffset            =   'doff',
    kAudioFilePropertyChannelLayout         =   'cmap',
    kAudioFilePropertyDeferSizeUpdates      =   'dszu',
    kAudioFilePropertyDataFormatName        =   'fnme',
    kAudioFilePropertyMarkerList            =   'mkls',
    kAudioFilePropertyRegionList            =   'rgls',
    kAudioFilePropertyPacketToFrame         =   'pkfr',
    kAudioFilePropertyFrameToPacket         =   'frpk',
    kAudioFilePropertyPacketToByte          =   'pkby',
    kAudioFilePropertyByteToPacket          =   'bypk',
    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'
};

里面的有EstimatedDuration和bitRate,可以直接獲取duration和bitRate了

讀取音頻數據

讀取音頻數據分為直接讀取音頻數據和按幀(packet)讀取音頻數據
先來看直接讀取音頻數據

extern OSStatus AudioFileReadBytes (AudioFileID inAudioFile,
                                    Boolean inUseCache,
                                    SInt64 inStartingByte,
                                    UInt32 * ioNumBytes,
                                    void * outBuffer);

第一個參數fileID,即Open方法里面得到的實例id;
第二個參數,是否需要cache,一般都傳false;
第三個參數,從第幾個bytes開始讀取數據;
第四個參數,這個參數在調用時作為輸入參數表示需要讀取多少數據,調用完成后作為輸出參數表示實際讀取了多少數據(Read回調中的requestCount和actualCount);
第五個參數,buffer指針,需要事先分配好足夠大的內存(ioNumBytes大,即Read回調中的buffer,所以Read回調不需要再分配內存);
返回值表示是否讀取成功,如果失敗則返回kAudioFileEndOfFileError;
這個方法得到的數據都是沒有幀分離的數據,如果想要用來播放或者解碼還必須通過AudioFileStream進行幀分離;

我們繼續看按幀(packet)讀取音頻數據

extern OSStatus 
AudioFileReadPackets (  AudioFileID                     inAudioFile, 
                        Boolean                         inUseCache,
                        UInt32 *                        outNumBytes,
                        AudioStreamPacketDescription * __nullable outPacketDescriptions,
                        SInt64                          inStartingPacket, 
                        UInt32 *                        ioNumPackets,
                        void * __nullable               outBuffer)          __OSX_AVAILABLE_BUT_DEPRECATED(__MAC_10_2,__MAC_10_10, __IPHONE_2_0,__IPHONE_8_0);//iOS8已經deprecated

extern OSStatus 
AudioFileReadPacketData (   AudioFileID                     inAudioFile, 
                            Boolean                         inUseCache,
                            UInt32 *                        ioNumBytes,
                            AudioStreamPacketDescription * __nullable outPacketDescriptions,
                            SInt64                          inStartingPacket, 
                            UInt32 *                        ioNumPackets,
                            void * __nullable               outBuffer)          __OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_2_2);

第一個回調函數在iOS8以后已經deprecated了,其實兩個函數差別不大,只是前者之前更多用來讀取固定時長音頻或者非壓縮音頻時,現在一般采用第二種,且效率更高并且省內存
我們來看參數
第一,二個參數跟AudioFileReadBytes一致
第三個參數,AudioFileReadPacketData中,ioNumBytes這個參數在輸入輸出中都要用到,在輸入表示outBuffer的size,輸出時表示實際讀取了多少size的數據。
第四個參數,幀信息數組指針,在輸入前需要分配內存,大小必須足夠存在ioNumPackets個幀(sizeof(AudioStreamPacketDescription)*ioNumPackets);
第五個參數,從第幾幀開始讀取數據
第六個參數,在輸入時表示需要讀取多少個幀,在輸出時表示實際讀取了多少幀
第七個參數,outBuffer數據指針,在輸入前就需要分配好空間,這個參數看上去兩個方法一樣但其實并非這樣。對于AudioFileReadPacketData來說只要分配近似幀*幀數的內存空間即可,方法本身會對給定的內存空間大小來決定最后輸出多少個幀,如果空間不夠會適當減少出的幀數;而對于AudioFileReadPackets來說則需分配最大幀大小(或幀大小上界)*幀數的內存空間才行;第三個參數后者是輸入輸出雙向使用的,而前者只是作為輸出使用,這也是后者省內存的原因
返回值,同AudioFileReadBytes
這兩個方法讀取后的數據為幀分離后的數據,可以用來直接播放或者解碼

- (NSArray *)parseData:(BOOL *)isErr
{
    UInt32 ioNumPackets = packetPerRead;//要讀取多少個packet
    UInt32 ioNumBytes = ioNumPackets * _maxPacketSize;獲取輸出輸入的size
    void *outBuffer = (void *)malloc(ioNumBytes);
    
    AudioStreamPacketDescription *outPacketDescriptions = NULL;
    OSStatus status = noErr;
    
    UInt32 descSize = sizeof(AudioStreamPacketDescription) *ioNumPackets;
    outPacketDescriptions = (AudioStreamPacketDescription *)malloc(descSize);
    status = AudioFileReadPacketData(_audioFileID, false, &ioNumBytes, outPacketDescriptions, _packetOffset, &ioNumPackets, outBuffer);
    
    if (status != noErr) {
        *isErr = status == kAudioFileEndOfFileError;
        free(outBuffer);
        return nil;
    }
    
    if (ioNumBytes == 0) {
        *isErr = YES;
    }
    
    _packetOffset += ioNumPackets;
    
    if (ioNumPackets > 0) {
        
        NSMutableArray *parsedDataArray = [[NSMutableArray alloc]init];
        for (int i = 0; i < ioNumPackets; i++) {
            AudioStreamPacketDescription packetDescription;
            if (outPacketDescriptions) {
                packetDescription = outPacketDescriptions[i];
            }else{
                packetDescription.mStartOffset = i*_format.mBytesPerPacket;
                packetDescription.mDataByteSize = _format.mBytesPerPacket;
                packetDescription.mVariableFramesInPacket = _format.mFramesPerPacket;
            }
            
            ZJParsedAudioData *parsedData = [ZJParsedAudioData parsedAudioDataWithBytes:outBuffer+packetDescription.mStartOffset packetDescription:packetDescription];
            
            if (parsedData) {
                [parsedDataArray addObject:parsedData];
            }
        }
        return parsedDataArray;
    }
    return nil;
}

同樣AudioFile也需要關閉

extern OSStatus AudioFileClose (AudioFileID inAudioFile);  

到此基本完結了,小結

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

推薦閱讀更多精彩內容