1. 程式人生 > >iOS CoreAudio學習筆記(一)—— Overview of CoreAudio

iOS CoreAudio學習筆記(一)—— Overview of CoreAudio

既然是Overview,那麼這一章文字的內容會佔絕大部分比例。

The Core Audio Frameworks

CoreAudio是一堆框架的集合,他們通常被分為兩組:audio engines,用來處理音訊流;helper APIs,提供了便捷的方法:從engine中獲取音訊資料或者將音訊資料寫入engine或用音訊資料做一些其他的事。

iPhone和Mac都有3類引擎APIs:

Audio Units(音訊單元)

每一個單元從某處(硬體、另一個音訊單元、回撥函式等等)接收一個音訊資料的buffer,對它執行某些工作(比如施加特效),然後將buffer傳遞給另一個單元。一個單元能夠潛在地擁有許多輸入和輸出,這樣它就可以將多條音訊流混縮成一條輸出

Audio Queues(音隊)
一個在音訊單元之上的抽象類,使用AQ能輕易的播放和錄製音訊,並且不需要擔心當直接操作時間約束的I/O的音訊單元時會出現的執行緒程式設計挑戰(因為AQ都已經幫你搞定了:P)。使用音隊進行錄音需要設定一個回撥函式,每當來自輸入裝置的新資料可用的時候重複地接收這些資料的buffer;要使用音隊播放音訊,則需要使用音訊資料來填充buffer然後將這些buffer傳遞給音隊
OpenAL
用來建立3D音訊(環繞音)的具有工業標準的API,並且它的設計和OpenGL圖形標準很相似。當你使用它的時候你會覺得它非常的酷炫
為了從這些引擎當中獲取資料,CoreAudio提供了許多的輔助APIs,它們到處都是!

Audio File Services

這個API抽象出了各種音訊檔案的容器格式的詳細內容,也就是說,你不需要通過寫程式碼具體的指明你需要訪問哪種音訊格式的特性(比如AIFF,WAV,MP3),這個API可以讓你直接獲取或者設定該音訊資料包含的格式,然後對它們進行讀寫

Audio File Stream Services
如果你的音訊來自網路,這個API能幫助你找出這個音訊在網路流中的格式。所以你可以將音訊流提供給其中一個播放引擎或者用其他感興趣的方式處理它。
Audio Converter Services
音訊可以以多種格式存在。而當一個音訊被送達音訊引擎(audio engine)的時候,它應該是未經壓縮的可播放的格式(LPCM)。這個API能夠幫助你在編碼後的音訊格式(比如AAC、MP3)和無損原始樣本(直接通過音訊單元獲取的)之間進行轉換
Extened Audio File Services
Audio Converter Services和Audio File Stream Services的結合,它能讓你在對音訊檔案進行讀取或者寫入的同時能進行格式轉換。舉個栗子:從一個檔案中讀取AAC格式的資料然後在記憶體中將它轉換成非壓縮的PCM格式,這樣的操作在Extended Audio File Services中可以只用一次呼叫完成。
Core MIDI
大多數CoreAudio框架都涉及處理你從其他資源獲取或者從輸入裝置中抓取的音訊取樣。通過Core MIDI框架,你可以通過描述音符以及它們是如何被播放出來的(比如,它們聽起來像鋼琴還是夏威夷四絃琴)來迅速地合成音訊。
還有一少部分是指定平臺的

Audio Session Services

這個僅iOS支援的框架允許你的App使用其他系統整合它用的音訊資源。舉個栗子,你使用這個API聲明瞭一個音訊“類目”,它決定了iPod音訊是否能夠在你的app播放時繼續播放,以及鈴聲/靜音開關是否能夠使你的app變成靜音。

當你開發應用程式的時候,你會用一些有趣的方式將這些API聯合起來。舉個栗子,你可以使用Audio File Stream Services來從一個網路無線電流獲取音訊資料然後使用OpenAL把這段音訊放到3D環境中一個指定的位置。

Core Audio 的約束規範

我們通過呼叫C函式來呼叫CA框架,所以得時刻準備好處理C程式設計問題和事件,比如指標、手動記憶體管理、結構體、列舉。

在C語言中沒有類,物件,封裝等主要的特性,然而和蘋果的其他基於C語言的框架一樣,CA提供了大量的這些現今的特性,甚至用C語言的語法呈現了這些特性。

蘋果的模型C框架是Core Foundation,它是Foundation框架的底層,是最基礎的OC框架,幾乎所有的Mac和iPhone應用都會用到。
我們通過類來認識Foundation框架,比如NSString, NSURL, NSDate,NSObject。

而在許多情況下,OC類通過呼叫CF來組裝它們的功能,CF提供了不透明型別(指向資料結構的指標,而這些資料結構真正的成員被隱藏了)以及作用於這些物件之上的函式。比如,一個NSString在字面上是等同於CFStringRef的(你可以在他們之間任意轉換),並且它的length方法也和CFStringGetLength()函式是等價的,這個函式會將一個CFStringRef作為它的物件。通過一致的函式命名規範將這些不透明型別聯結起來,CF就這樣提供了高可管理的C API,它們和你在Cocoa中使用的異常相似。

CA的設計是(和CF)非常相似的,CA的許多重要物件(比如audio queues 和audio files)被當做不透明物件,傳遞給比如AudioQueueStart(),AudioFileOpenURL()這樣的意料之中命名的函式。CA並沒有明確地構建在CF之上——AudioQueueRef嚴格上來說並不是一個CF不透明型別。然而,它確實用到了CF的重要的物件,比如CFStringRef和CFURLRef,它們可以隨意在你的Cocoa編碼中與NSString和NSURL相互轉換。

咱們的第一個CoreAudio應用程式

好,現在讓咱們實際寫點東西來感受下Core Audio的程式碼。Audio engine APIs有大量的移動部件,因此會更加的複雜。所以我們將會以使用一個簡單的helper APIs作為開始。在這個示例中,我們將會去設法從一個音訊檔案中獲取元資料(關於這個音訊的資訊)。

注意:Core Audio並沒有預設被包含到Xcode的命令列可執行工程中,所以我們需要手動在Build phase中新增framework:AudioToolbox.framework。然後將AudioToolbox.h import到程式碼中。我們將手動將音訊檔案路徑作為工程的引數新增進工程中,通過argv引數獲取。

開啟Edit Scheme

開啟Edit Scheme

選擇Run下的Arguments欄,點選加號輸入路徑。如果路徑中有空格,要用雙引號將整個路徑引起來

選擇Run下的Arguments欄,點選加號輸入路徑。如果路徑中有空格,要用雙引號將整個路徑引起來
#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>

int main(int argc, const char * argv[]) {

    // argv表示傳遞給這個程式的引數陣列,是一系列字串
    // argc表示argv中元素的個數
    // 預設情況下argv只有一個元素:該程式本身的路徑。而我們可以手動為其新增引數
    @autoreleasepool {
        if (argc < 2) {

            printf("Usage: no file !\n");
            return -1;

        }

        printf("%s",argv[0]);   // 1

        // 如果提供了路徑,需要將它從c字串轉換成NSString或者CFStringRef這種蘋果大多數框架都回用的。
        // stringByExpandingTildeInPath表示字串會自動在前面加上~來代表根目錄
        NSString * audioFilePath = [[NSString stringWithUTF8String:argv[1]] stringByExpandingTildeInPath];  // 2

        // AudioFile API使用的是URL來代表檔案路徑,所以把字串再轉換成URL
        NSURL * audioURL = [NSURL fileURLWithPath:audioFilePath];   // 3

        // CoreAuido使用AudioFileID型別來指代音訊檔案物件
        AudioFileID audioFile;  // 4

        // 大部分CoreAuido呼叫函式成功或失敗的訊號通過一個OSStatus型別的返回值來確認。
        // 除了noErr訊號外的訊號都代表發生了錯誤。
        // 我們應該在【所有的】CoreAuido函式呼叫後檢查這個返回值因為前面已經發生錯誤了,後續的呼叫就顯得毫無意義。
        // 比如,如果我們不能成功建立一個AuidoFileID物件,那麼我們想從這個物件代表的那個檔案中獲取音訊屬性就完全是徒勞的。
        OSStatus theErr = noErr;    // 5

        // 來到了我們第一次呼叫的CoreAudio函式:AuidoFileOpenURL。它有4個引數:CFURLRef,檔案許可權的flag,檔案型別提示和一個接收建立的AudioFileID物件的指標。
        // 第一個引數:我們可以直接通過強制型別轉換把一個NSURL轉換成CFURLRef(當然我們需要加上關鍵字__bridge。)
        // 第二個引數:檔案操作許可權,我們這裡只需要讀取資料的許可權,所以傳遞一個列舉值(蘋果一貫的命名規範)。
        // 第三個引數:我們不需要提供任何檔案型別提示,所以傳0,這樣CoreAudio就會自己來解決(其實這個引數我都沒搞懂是什麼意思,反正一般傳0就對了)
        // 第四個引數:傳一個接收AudioFileID物件的指標,傳入我們之前宣告的AudioFileID型別的變數地址就行了。
        theErr = AudioFileOpenURL((__bridge CFURLRef)audioURL, kAudioFileReadPermission, 0, &audioFile); // 6

        // 如果上面的呼叫失敗了,那麼直接終止程式,因為以後的所有操作都沒意義了。
        assert(theErr == noErr);    // 7

        // 為了拿到這個檔案的元資料,我們將會被要求提供一個元資料屬性,kAudioFilePropertyInfoDictionary。但是這個呼叫需要為返回的元資料分配記憶體。所以我們宣告這麼一個變數來接收我們需要分配的記憶體的size。
        UInt32 dictionarySize = 0;  // 8

        // 為了得到我們需要分配多少記憶體,我們呼叫AudioFileGetPropertyInfo函式,傳入你想要拿到資料的那個檔案的AudioFileID、你想要啥子資訊、一個用來接收結果的指標、以及一個指向一個標識變數的指標用來指示這個屬性是否是可寫的(我們對此毫不在乎,所以傳0)。
        theErr = AudioFileGetPropertyInfo(audioFile, kAudioFilePropertyInfoDictionary, &dictionarySize, 0); // 9
        assert(theErr == noErr); // 10

        // 為了從一個音訊檔案獲取屬性(這裡我們要的是這個音訊檔案的元資料)的呼叫,需要基於這個屬性本身填充各種型別(這裡是一個字典)。有的屬性是數字,有的是字串。文件和CoreAudio標頭檔案描述了這些值。我們在第二個引數傳入kAudioFilePropertyInfoDictionary就可以得到一個字典。所以我們宣告這麼一個變數它是CFDictionaryRef型別的物件(它可以隨意轉換成NSDictionary)。
        CFDictionaryRef dictionary; // 11

        // 啊,我們終於到了最終的時刻了,終於要開始獲取屬性了。呼叫AudioFileGetProperty函式,傳入AudioFileID、一個常量(列舉型別,表示屬性型別)、一個指向你準備好用來接收的size的指標、一個用來接收最終結果的指標(就是一個字典了)
        theErr = AudioFileGetProperty(audioFile, kAudioFilePropertyInfoDictionary, &dictionarySize, &dictionary); // 12

        assert(theErr == noErr); // 13

        // 我們來看看得到了什麼。對任意的CoreFoundation或者Cocoa 物件都可以使用"%@"在格式化字串裡面來獲取一個字典的字串表示。
        NSLog(@"dictionary : %@", dictionary); // 14

        // Core Foundation沒有提供自動記憶體釋放,所以CFDictionaryRef物件在傳入AudioFileGetProperty函式後它的retain count是1。我們用CFRelease函式來釋放我們對這個物件的興趣。
        CFRelease(dictionary); // 15

        // AudioFileID同樣需要被清空。但是它本身並不是一個CoreFoundation物件,因此它不能通過呼叫CFRelease釋放。取而代之的,它有自己的自殺方法:AudioFileClose()。
        theErr = AudioFileClose(audioFile); // 16
        assert(theErr == noErr); // 17

        // 結束了。我們用了二十多行程式碼,但是實際上都是為了調那麼三個函式:開啟一個檔案、為元資料分配容器、獲取元資料。
    }
    return 0;
}

來執行一下,看看我們打印出了什麼:

/Users/dreamhack/Library/Developer/Xcode/DerivedData/MyFirstCoreAudioProj-ckhojjygpvgkcgcwmohckworckis/Build/Products/Debug/MyFirstCoreAudioProj2015-04-03 11:21:38.851 MyFirstCoreAudioProj[636:303] dictionary : {
album = “If I Didn’t Have You (Bernadette’s Song - From the Big Bang Theory) [MusiCares\U00ae Version] - Single”;
“approximate duration in seconds” = “139.572”;
artist = “Simon Helberg, Johnny Galecki, Jim Parsons, Kaley Cuoco, Kunal Nayyar & Mayim Bialik”;
comments = “(MusiCares\U00ae Version)”;
“encoding application” = “iTunes 11.1.2”;
genre = Soundtrack;
title = “If I Didn’t Have You (Bernadette’s Song - From the Big Bang Theory) [MusiCares\U00ae Version]”;
“track number” = “1/1”;
year = 2013;
}

經驗之談

你要隨時做好準備應對未知結果,比如對MP3和AAC檔案的不同級別的元資料支援。掌握CoreAudio並不僅僅是理解這些API,還包括提高實現的意識,這個庫究竟是怎樣工作的,它好在哪裡,它的短處在哪。
CoreAudio並不止是你呼叫它的語法,也包括它的語義。在某些情況下,語法正確的程式碼可能會在實踐中出錯,因為它違反了隱式協議、在不同的硬體中表現得不一樣、又或者它在一個時間約束的回撥中佔用了太多的CPU時間。成功的CoreAudio程式猿在當事情並沒有像他們預期的那樣或者第一次執行時並不足夠好的時候並不會魯莽的繼續下去。你必須嘗試找出究竟發生了什麼並想一個更好的方法。

CoreAudio屬性

在上面的例子中,CoreAudio的呼叫全部都是關於從音訊檔案物件中獲取屬性。隨時準備和執行屬性獲取器和設定器的呼叫在CoreAudio中是日常規範,是非常必要的。
因為CoreAudio是一個屬性驅動的API。屬性是鍵值對,而鍵是列舉整型數。值可以是API定義的任何型別。每一個CoreAudio的API都通過它的屬性列表來交流它的功能和狀態。比如,如果你檢視AudioFileGetProperty()函式,你會在文件中找到一個音訊檔案屬性列表的連結:

kAudioFilePropertyFileFormat                =   ‘ffmt’,
kAudioFilePropertyDataFormat            =   ‘dfmt’,
kAudioFilePropertyIsOptimized           =   ‘optm’,
kAudioFilePropertyMagicCookieData       =   ‘mgic’,
kAudioFilePropertyAudioDataByteCount    =   ‘bcnt’
…

這些鍵是32位整數值,你可以在文件和標頭檔案中讀到。你可以看到,這4位字元編碼被單引號引了起來代表C字元。假設fmt 是“format”的簡寫,你會發現ffmt 是”file format”的編碼而dfmt 則意為“data format”。這樣的編碼貫穿於整個CoreAudio作為屬性的鍵有時也作為錯誤狀態碼。如果你企圖寫入一個CoreAudio不認識的檔案格式,你將會得到fmt? 響應,也就是kAudioFileUnsupportedDataFormatError。

你通過這些API獲取或者設定的值取決於屬性的設定。通過kAudioFilePropertyInfoDictionary屬性將得到一個CFDictionaryRef的指標,但是如果傳入的是kAudioFilePropertyEstimatedDuration,你需要準備接收一個NSTimeInterval(實際上就是一個double)的指標。這是及其強大的,因為只需要少量的函式就可以支援潛在無限多的屬性。然而,建立這樣的呼叫需要涉及額外的工作,代表性地,你不得不呼叫“get property info”來為接收屬性值分配記憶體或者檢查屬性是否是可寫的。

另一個需要注意的是CoreAudio函式引數的命名規範。我們來看看AudioFileGetProperty()的定義:

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

注意到這些引數的命名:使用in,out或者io指示一個引數被這個函式是否只用作輸入(前兩個引數,指定了要使用哪個檔案和你想要的屬性),是否只用作輸出(第四個引數,outPropertyData 用屬性的值填充一個指標指向的內容),是否既用作輸入也用作輸出(第三個引數,ioDataSize,接收你分配給outPropertyData的記憶體緩衝區的大小,然後寫回實際上被寫入緩衝區的位元組數)。這種命名模式貫穿整個CoreAudio,特別是一個引數用指標來填充其值的時候。

總結

這一節總覽了Core Audio許多不同的部分,嘗試了使用Audio File Services來程式設計獲取一個本地磁碟的音訊檔案的元資料屬性。我們看到了CoreAudio如何將屬性作為一個關鍵的語義來和它不同的API協同工作。我們同樣看到了CoreAudio如何使用4字元編碼來指定屬性鍵和錯誤訊號。

當然你現在還不需要真正的去處理音訊本身。如果你想那樣的話,你首先需要理解聲音在數字形態下是如何被表示以及處理的。接下來為了與音訊資料工作,準備刻苦鑽研CoreAudio的API吧。