0

安裝 ffmpeg

| 7/17/2011
最近發現電腦裡面太多有的沒有的影片格式,想說一口氣就把這疑大堆亂七八糟的東西解決掉。因為網路上面有太多不同的 codec。就算說是同一種 codec,也有可能因為實作上面的不同,即便你是使用支援該格式的播放器去觀看,恐怕也是會面臨一個頭兩個大的窘境。

這裡我把目標放在 ffmpeg 上面。不用說,大家都知道,這套軟體是集合絕大多格式的編碼工具,許多人都會將這套工具搭配 mencoding 來使用。(這裡因為我不是很喜歡 mencoding,所以在碰到問題之前就乾脆先放棄了)

要在 Mac 下面編譯 ffmpeg 很簡單,就跟一般的 tarball 應用程式安裝的方式相同。至於在 Windows 下面得依靠 MinGW,因為又麻煩,編譯速度又慢,這裡就不說了。

首先得去 ffmpeg.org 取得最新的 source,然後解壓縮:

tar xf ffmpeg-HEAD.tar.gz
cd /path/to/ffmpeg/package/dictionary

接下來是快樂的 config 時間,可以使用 ./configure --help 看看有哪些東西是需要、不需要的,把參數記下來,然後:

./configure --prefix=/path/to/install/dictionary --disable-doc --disable-yasm

因為 Xcode 不是用 yasm 編譯組合語言的,這裡要把它關掉。
然後就是快樂的編譯時間:make !!

經過大約五到十分鐘左右就應該會編譯完成,之後在 make install 就完成了!
後續的就是一些 path 設定的小部分,首先編輯 ~/.bash_profile ,在裡面加上:
export PATH=$PATH:/path/to/install/dictionary/bin

之後就可以開始研究 ffmpeg 去了!
2

Self assign IP address

| 2/08/2011
解了一大堆 Windows 的怪問題,今天換個口味,來解一下 Mac 上面的怪問題。這個問題看起來似乎是從 Leopard 開始的,當然我不知道確定的時間,但是就是有這個問題。
當我們使用 AirPort 直接連結無線基地台的時候,在絕大多數的情況下都是使用 DHCP 來取得 IP Address。取得 IP 這個動作在 AirPort 是有設定一個 Timeout 的時間在的。當 Timeout 過了之後,系統將會自動設定一個 168.* 的 IP 給你,因為被分配到一個不屬於該 AP 的網域之下,所以你的電腦將沒有辦法連上網路。

仔細去分析發生這個問題的癥結:Timeout,可以發現,發生這種事情可能的理由就是該 AP 的網路連線上面有問題,而另外一個最重要的理由就是防火牆!或許大家會覺得不可思議,為什麼防火牆會把 DHCP 給擋在門外?或許沒有人可以給我們一個正確的答案,甚至有的時候你防火牆沒有打開,卻也會發生這種現象。解決的辦法其實也很簡單,只要你在安全性設定裡面把防火牆先「打開」再「關閉」,這個問題便會迎刃而解!
0

繼續解決 Windows 的問題

| 11/16/2010
- 刪除沒有辦法刪除的檔案
有的時候你會發現某些檔案沒有辦法刪除,你會得到存取被拒的結果。這些檔案有可能是你電腦在重灌之前留下來的檔案,因為使用者資訊改變的關係,那些本來被設定成特殊權限的系統檔案一時之間找不到主人,自然你就缺少對該檔案控制的權力。使用下面的指令可以幫你重新取回檔案的所有權:

takeown /F filename /R

這個指令會將檔案的所有者轉移到現在登入的使用者身上,然後再改變該檔案的存取權限,讓管理者擁有最高控制權:

icacls filename /grant Administrators:F

接下來,你就可以快樂的砍掉他了!
這一連串的指令當然可以寫成 batch 檔案,之後好存取:

@echo off
takeown /F %1 /R
icacls %1 /grant Administrators:F
del %1

然後將檔案存成 forcedel.bat 就大功告成啦!只要把檔案拖到這個 batch file,檔案自然會被摧毀!

- 開啟檔案位置
這是一個從 Vista 開始很好用的功能。你可以經由這個右鍵指令直接找到捷徑本來的檔案位置。不知道從何時開始,你會發現當你按下去之後,這個指令一點反應都沒有!解決的辦法:

找出 Internet Explorer 9 的更新,然後反安裝他吧…老師說過,Beta 版的東西少碰為妙…
0

wchar_t 的長度…

| 7/02/2010
wchar_t 是 ANCI C 提供支援 Unicode 的方案。使用方式跟一般 char * 字串很像,整個介面都是差不多的。

char * anciStr = "測試"
wchar_t * uniStr = L"測試" // 注意這裡需要加上一個 micro

而我們常用的字串函式 strlen, strcpy 等,自然就會有對應的東西出現:wcslen, wcscpy。這些東西則是定義再 wchar.h 裡面。

雖然支援了 Unicode 是很好,但是並不是所有的作業系統對這些東西的編碼定義都相同。今天如果要做跨平台的系統程式設計,就得要不斷的面對每個系統對字元符號的編碼定義不同的問題。好比如說,在 Windows 上面, char 字元預設是 ASCII +1 的 8bit 字,wchar_t 則是 UTF-16 Little Endian。這看起來像是很合情合理,但是像是 Linux、Mac OS X 這種全面支援 Unicode 的系統而言,char 早就已經是 Unicode 了。同樣是 Unicode ,卻依然有大大的不同!在這兩的系統上面,char 是被定義成 UTF-8(今天 Windows 在 UTF-8 上面是有 BOM 的,而這裡沒有。其實 BOM 對 UTF-8 而言很沒意義),而 wchar_t 則是 UTF-32(Big Endian 跟 Little Endian 都有可能,看 CPU)。所以說,我們用函式測試的時候就會出現這樣的結果:

strlen(anciStr) // Windows 會出現 4,Linux、Mac 則是 6
wcslen(uniStr) // 這裡統一會出現 2
看的懂差異的來源嗎?

今天 Cocoa Foundation 提供 NSString 的 Class,這個 Class 是以 UTF-16 Big Endian 儲存的。但是該 Class 本身具備好了許多編碼轉換的函式,可以讓你在 不同的編碼之間轉換!所以說,NSString 跟 wchar_t 之間的變化,應該是:

wchar_t * string = malloc([nsString length] * sizeof(wchar_t) + 1);
wcscpy(string, (const wchar_t *)[nsString cStringUsingEncoding:NSUTF32LittleEndianStringEncoding]);
0

Play Music using AudioUnit

| 6/29/2010
AudioUnit playback - Introtuction of CoreAudio Programming
AudioUnit 是 Mac OS X 在 Audio Programming 上面使用,代表一段音樂資料的基本單位。一般來說,如果只是要讓應用程式具有播放音樂的能力,並不需要使用到這種東西。連同蘋果 10.2 開始提供的 CoreAudio API 在內,都屬於低階程式編輯的東西。一般來說,如果只是要讓你的程式播放音樂,還有 QTKit 之類高階的 API 讓你簡單的達到播放音樂的功能。但是如果你希望做到一些更加進階的功能:編輯、Codec 的開發,你就必須要瞭解這個東西。這邊只有簡單記錄一下要如何使用 AudioUnit 來播放音樂。

我們會使用到一些 Framework,必須要把他們連結進來:
  1. AudioUnit.framework
  2. AudioToolbox.framework
  3. CoreService.framework

首先,我們使用 Component 來取得使用者設定的 Default Output Device Unit。AudioUnit 分成許多不同的種類,他們可以是 Outputs, Mixers, or DSP。閱讀 AUComponent.h 可以得到其他資訊。我們可以簡單的使用 ComponentDescription 來取得 Output Audio Unit。

Code:
ComponentDescription desc;
Component comp;
AudioUnit outputUnit;
OSStatus err;

// 設定 Component 型態
desc.componentType = kAudioUnitType_Output;
// 每一種型態都有一個 sub type,用來深入描述該型態
desc.componentSubType = kAudioUnitSubType_DefaultOutput;
// 下面的部份,所有的 AudioUint 都是一樣的,依樣畫葫蘆就可以了
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
desc.componentFlags = 0;
desc.componentFlagsMask = 0;

// 取得與描述相同的 Component
comp = FindNextComponent(NULL, &desc);
if (comp == NULL) exit(-1);
// 取得 Output AudioUnit
err = OpenAComponent(comp, &outputUnit);

接下來,我們要設定 AudioUnit 的參數。
AudioUnit 有許多不同的參數可以設定,其中最常使用到的兩個函數就屬於 AudioUnitGetProperty 以及 AudioUnitSetProperty 兩個。其中,我們也經常使用 AudioUnitGetPropertyInfo 來取得一些資訊用以避免設定上的錯誤。

在 AudioUnit 的眾多參數裡面,最重要的一項就是 AudioStreamBasicDescription (ASBD)。這項變數裡面存放了關於 Audio Stream 的描述,包含 simple rate、Package information 以及 Stream 格式。我們可以思考 AudioUnit 包含了兩個最大的部份:input 跟 output。所以說我們可以為這兩個部分設定 ASBD。(注意:AudioUnit 型態本身就是個 Reference)

Code:
UInt32 size;
Boolean isWritable;
AudioStreamBasicDescription outputDesc;
AudioStreamBasicDescription inputDesc;

// 取得 Stream format 的 size,以及是否可以寫入。
err = AudioUnitGetPropertyInfo(outputUnit,
                               kAudioUnitPorperity_StreamFormat,
                               kAudioUnitScope_Output,
                               0, &size, &isWritable);

// 取得 Stream Format
err = AudioUnitGetProperty(outputUnit,
                           kAudioUnitProperty_StreamFormat,
                           kAudioUnitScope_Output,
                           0, &outputDesc, &size);

// 將 input 跟 output 的 stream format 設定相同
err = AudioUnitSetProperty(outputUnit,
                           kAudioUnitProperty_StreamFormat,
                           kAudioUnitScope_Input,
                           0, &outputDesc, &size);

// 注意,在所有 Property 設定結束之後才使用 AudioUnitInitialize。這個函式的消費很大。
err = AudioUintInitialize(outputUint);
這裡需要注意的地方還有一個,所有的 err 都可以作為結束的判斷式。

我們得到的 ASBD 資料格式應該如下面的範例:
接下來我們要設定一個 Render Callback 來做為 AudioUnit 取得播放資料的緩衝區的地方。我們使用 kAudioUnitProperty_SetRenderCallback 以及 AURenderCallbackStruct。被設定的函式只有在 Audio Converter 需要資料的時候才會被呼叫。在這裡我們假設被呼叫的函式名稱為 MyFileRenderProc。AURenderCallbackStruct 有兩個 Member:inputProc 以及 inputProcRefCon。前者為我們要設定的 callback function 名稱,後者則是該函式的參數。

Code:
AURenderCallbackStruct renderCallback;
memset(&renderCallback, 0, sizeof(AURenderCallbackStruct));

// 設定 Render Callback。因為播放的時候不需要傳入參數,所以 RefCon 設定為 0
renderCallback.inputProc = MyFileRenderProc;
renderCallback.inputProcRefCon = 0;

// 設定到 AudioUnit 裡面
err = AudioUnitSetProperty(outputUnit,
                           kAudioUnitProperty_SetRenderCallback,
                           kAudioUnitScope_Input,
                           0, &renderCallback,
                           sizeof(AURenderCallbackStruct));


這些設定都設定完成之後,我們就來讀取音樂檔案,並且放置到緩衝區裡面。當 AudioUnit 在播放的時候他會自動取得緩衝區裡面的資料使用。如果是要用來播放其他格式的檔案,那麼我們就必須要擁有其他格式的 Codec 來將檔案解碼成系統所支援的格式。這裡,我們就使用蘋果提供的 Audio File API 來讀取檔案。記住,在這裡我們只有假設我們讀取的是很小的音樂檔案,所以說我們將整個檔案都放到了緩衝區中。事實上,當你的音樂比較龐大的時候,最好是將你解碼的部份分段放入緩衝區中,解碼的動作可能必須要開一個新的執行緒以避免和主程序互衝。

Code:
UInt64 totalPacketCount;
UInt64 fileByteCount;
UInt32 packetSize;
...
OSStatus
OpenAudioFile(AudioFileID * fileID,
                       AudioStreamBasicDescription * fileASBD,
                       const char * filename)

{
OSStatus err = noErr;
Uint32 size;

// 產生路徑,記住檔案路徑是以 UTF8 編碼的
CFURLRef fileURL = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, (const UInt8*)filename, strlen(filename), NO);

// AudioFileOpen 已經不再被支援了
err = AudioFileOpenURL(fileURL, kAudioFileReadPermission, 0, fileID);

// 讀取 Packet 資訊,這個資訊會在 AudioConverter 中使用
// 我們已經確定了 Format Size 了,這裡不再使用 AudioUnitGetPropertyInfo
size = sizeof(AudioStreamBasicDescription);
memset(fileASBD, 0, size);
err = AudioFileGetProperty(*fileID, kAudioFilePropertyDataFormat, &size, fileASBD);
  
size = sizeof(totalPacketCount);
err = AudioFileGetProperty(*fileID, kAudioFilePropertyAudioDataPacketCount, &size, &totalPacketCount);
  
size = sizeof(fileByteCount);
err = AudioFileGetProperty(*fileID, kAudioFilePropertyAudioDataByteCount, &size, &fileByteCount);
  
size = sizeof(packetSize);
err = AudioFileGetProperty(*fileID, kAudioFilePropertyMaximumPacketSize, &size, &packetSize);

return err;
}

int main(int argc, char ** argv)
{
AudioFileID fileID;
UInt32 bytesReturned = 0;
UInt32 packets = totalPacketCount;
char * audioBuffer = malloc(fileByteCount);

...
// 將整個音樂檔案放到記憶體中
err = OpenAudioFile(&fileID, &inputDesc, argv[1]);
err = AudioFileReadPackets(fileID, NO, &butesReturned, NULL, 0, &packets, audioBuffer);
}

接下來,我們就來撰寫確實的 Render Callback。這個 Callback 會在 AudioUnit 需要 Audio Frame 的時候被呼叫。我們在這裡使用 AudioConverter 將讀近來的檔案轉換成系統支援的格式。這裡我們不多寫程式碼,所以我們就只有在 PCM 之間轉換。也就是說,這個程式碼只能夠播放 PCM 的檔案格式:WAV、AIF。

Code:
AudioConverter converter;
AudioConverterNew(&inputDesc, &outputDesc, &converter);

OSStatus MyFileRenderProc(void * inRefCon,
                              AudioUnitRenderActionFlags *inActionFlag,
                              const AudioTimeStamp *timeStamp, UInt32 inBusNumber,
                              UInt32 inNumFrames, AudioBufferList *ioData)
{
    OSStatus err = noErr;
  
    AudioConverterFillComplexBuffer(audioConverter,
                                    MyACComplexInputProc,
                                    0, &inNumFrames, ioData, 0);
  
    return err;
}

OSStatus MyACComplexInputProc(AudioConverterRef inAudioConverter,
                              UInt32 * ioNumberDataPackets,
                              AudioBufferList * ioData,
                              AudioStreamPacketDescription ** ioDataPacketDescription,
                              void * inUserData)
{
    OSStatus err = noErr;
    UInt32 bytesCopied = 0;
  
    ioData->mBuffers[0].mData = NULL;
    ioData->mBuffers[0].mDataByteSize = 0;
  
    if (playedPacketOffset + *ioNumberDataPackets > totalPacketCount)
        *ioNumberDataPackets = totalPacketCount - playedPacketOffset;
  
    if (*ioNumberDataPackets) {
        if (sourceBuffer != NULL) {
            free(sourceBuffer);
            sourceBuffer = NULL;
        }
      
        bytesCopied = *ioNumberDataPackets * packetSize;
        sourceBuffer = calloc(1, bytesCopied);
        memcpy(sourceBuffer, audioBuffer + playedByteOffset, bytesCopied);
      
        playedByteOffset += bytesCopied;
        playedPacketOffset += *ioNumberDataPackets;
      
        ioData->mBuffers[0].mData = sourceBuffer;
        ioData->mBuffers[0].mDataByteSize = bytesCopied;
    }
    else {
        ioData->mBuffers[0].mData = NULL;
        ioData->mBuffers[0].mDataByteSize = 0;
        isPlaying = NO;
        err = noErr;
    }

    return err;
}

以上,是使用 AudioUnit 來播放音樂檔案的簡單程式。如果有可能,我會繼續將學習 CoreAudio 時所學到的東西繼續發佈上來。也請發現有問題的前輩多多指教。

Download Source Code Here.
How to compile: gcc auplaysample.m -framework AudioUnit -framework AudioToolbox -framework CoreService -o AudioPlay

P.S. 話說回來,如果我用 NSSound 的話,上面一大串就剩下兩行了!高階 API 跟低階 API 差距好大啊…