linux音訊程式設計指南

helloxchen發表於2010-10-21

Linux音訊程式設計指南

肖文鵬 (), 自由軟體愛好者
本文作者肖文鵬是一名自由軟體愛好者,主要從事作業系統和分散式計算環境的研究,喜愛Linux和Python。你可以透過 與他取得聯絡。

簡介: 雖然目前Linux的優勢主要體現在網路服務方面,但事實上同樣也有著非常豐富的媒體功能,本文就是以多媒體應用中最基本的聲音為物件,介紹如何在Linux平臺下開發實際的音訊應用程式,同時還給出了一些常用的音訊程式設計框架。

--&gt

音訊訊號是一種連續變化的模擬訊號,但計算機只能處理和記錄二進位制的數字訊號,由自然音源得到的音訊訊號必須經過一定的變換,成為數字音訊訊號之後,才能送到計算機中作進一步的處理。

數字音訊系統透過將聲波的波型轉換成一系列二進位制資料,來實現對原始聲音的重現,實現這一步驟的裝置常被稱為模/數轉換器(A/D)。A/D轉換器以每秒鐘上萬次的速率對聲波進行取樣,每個取樣點都記錄下了原始模擬聲波在某一時刻的狀態,通常稱之為樣本(sample),而每一秒鐘所取樣的數目則稱為取樣頻率,透過將一串連續的樣本連線起來,就可以在計算機中描述一段聲音了。對於取樣過程中的每一個樣本來說,數字音訊系統會分配一定儲存位來記錄聲波的振幅,一般稱之為取樣分辯率或者取樣精度,取樣精度越高,聲音還原時就會越細膩。

數字音訊涉及到的概念非常多,對於在Linux下進行音訊程式設計的程式設計師來說,最重要的是理解聲音數字化的兩個關鍵步驟:取樣和量化。取樣就是每隔一定時間就讀一次聲音訊號的幅度,而量化則是將取樣得到的聲音訊號幅度轉換為數字值,從本質上講,取樣是時間上的數字化,而量化則是幅度上的數字化。下面介紹幾個在進行音訊程式設計時經常需要用到的技術指標:

  1. 取樣頻率
    取樣頻率是指將模擬聲音波形進行數字化時,每秒鐘抽取聲波幅度樣本的次數。取樣頻率的選擇應該遵循奈奎斯特(Harry Nyquist)取樣理論:如果對某一模擬訊號進行取樣,則取樣後可還原的最高訊號頻率只有取樣頻率的一半,或者說只要取樣頻率高於輸入訊號最高頻率的兩倍,就能從取樣訊號系列重構原始訊號。正常人聽覺的頻率範圍大約在20Hz~20kHz之間,根據奈奎斯特取樣理論,為了保證聲音不失真,取樣頻率應該在40kHz左右。常用的音訊取樣頻率有8kHz、11.025kHz、22.05kHz、16kHz、37.8kHz、44.1kHz、48kHz等,如果採用更高的取樣頻率,還可以達到DVD的音質。
  2. 量化位數
    量化位數是對模擬音訊訊號的幅度進行數字化,它決定了模擬訊號數字化以後的動態範圍,常用的有8位、12位和16位。量化位越高,訊號的動態範圍越大,數字化後的音訊訊號就越可能接近原始訊號,但所需要的存貯空間也越大。
  3. 聲道數
    聲道數是反映音訊數字化質量的另一個重要因素,它有單聲道和雙聲道之分。雙聲道又稱為立體聲,在硬體中有兩條線路,音質和音色都要優於單聲道,但數字化後佔據的儲存空間的大小要比單聲道多一倍。

出於對安全性方面的考慮,Linux下的應用程式無法直接對音效卡這類硬體裝置進行操作,而是必須透過核心提供的驅動程式才能完成。在Linux上進行音訊程式設計的本質就是要藉助於驅動程式,來完成對音效卡的各種操作。

對硬體的控制涉及到暫存器中各個位元位的操作,通常這是與裝置直接相關並且對時序的要求非常嚴格,如果這些工作都交由應用程式設計師來負責,那麼對音效卡的程式設計將變得異常複雜而困難起來,驅動程式的作用正是要遮蔽硬體的這些底層細節,從而簡化應用程式的編寫。目前Linux下常用的音效卡驅動程式主要有兩種:OSS和ALSA。

最早出現在Linux上的音訊程式設計介面是OSS(Open Sound System),它由一套完整的核心驅動程式模組組成,可以為絕大多數音效卡提供統一的程式設計介面。OSS出現的歷史相對較長,這些核心模組中的一部分(OSS/Free)是與Linux核心原始碼共同免費釋出的,另外一些則以二進位制的形式由4Front Technologies公司提供。由於得到了商業公司的鼎力支援,OSS已經成為在Linux下進行音訊程式設計的事實標準,支援OSS的應用程式能夠在絕大多數音效卡上工作良好。

雖然OSS已經非常成熟,但它畢竟是一個沒有完全開放原始碼的商業產品,ALSA(Advanced Linux Sound Architecture)恰好彌補了這一空白,它是在Linux下進行音訊程式設計時另一個可供選擇的音效卡驅動程式。ALSA除了像OSS那樣提供了一組核心驅動程式模組之外,還專門為簡化應用程式的編寫提供了相應的函式庫,與OSS提供的基於ioctl的原始程式設計介面相比,ALSA函式庫使用起來要更加方便一些。ALSA的主要特點有:

  • 支援多種音效卡裝置
  • 模組化的核心驅動程式
  • 支援SMP和多執行緒
  • 提供應用開發函式庫
  • 相容OSS應用程式

ALSA和OSS最大的不同之處在於ALSA是由志願者維護的自由專案,而OSS則是由公司提供的商業產品,因此在對硬體的適應程度上OSS要優於ALSA,它能夠支援的音效卡種類更多。ALSA雖然不及OSS運用得廣泛,但卻具有更加友好的程式設計介面,並且完全相容於OSS,對應用程式設計師來講無疑是一個更佳的選擇。

如何對各種音訊裝置進行操作是在Linux上進行音訊程式設計的關鍵,透過核心提供的一組系統呼叫,應用程式能夠訪問音效卡驅動程式提供的各種音訊裝置介面,這是在Linux下進行音訊程式設計最簡單也是最直接的方法。

無論是OSS還是ALSA,都是以核心驅動程式的形式執行在Linux核心空間中的,應用程式要想訪問音效卡這一硬體裝置,必須藉助於Linux核心所提供的系統呼叫(system call)。從程式設計師的角度來說,對音效卡的操作在很大程度上等同於對磁碟檔案的操作:首先使用open系統呼叫建立起與硬體間的聯絡,此時返回的檔案描述符將作為隨後操作的標識;接著使用read系統呼叫從裝置接收資料,或者使用write系統呼叫向裝置寫入資料,而其它所有不符合讀/寫這一基本模式的操作都可以由ioctl系統呼叫來完成;最後,使用close系統呼叫告訴Linux核心不會再對該裝置做進一步的處理。

  • open系統呼叫
    系統呼叫open可以獲得對音效卡的訪問權,同時還能為隨後的系統呼叫做好準備,其函式原型如下所示:
    int open(const char *pathname, int flags, int mode);
    

    引數pathname是將要被開啟的裝置檔案的名稱,對於音效卡來講一般是/dev/dsp。引數flags用來指明應該以什麼方式開啟裝置檔案,它可以是O_RDONLY、O_WRONLY或者O_RDWR,分別表示以只讀、只寫或者讀寫的方式開啟裝置檔案;引數mode通常是可選的,它只有在指定的裝置檔案不存在時才會用到,指明新建立的檔案應該具有怎樣的許可權。
    如果open系統呼叫能夠成功完成,它將返回一個正整數作為檔案識別符號,在隨後的系統呼叫中需要用到該識別符號。如果open系統呼叫失敗,它將返回-1,同時還會設定全域性變數errno,指明是什麼原因導致了錯誤的發生。
  • read系統呼叫
    系統呼叫read用來從音效卡讀取資料,其函式原型如下所示:
    int read(int fd, char *buf, size_t count);
    

    引數fd是裝置檔案的識別符號,它是透過之前的open系統呼叫獲得的;引數buf是指向緩衝區的字元指標,它用來儲存從音效卡獲得的資料;引數count則用來限定從音效卡獲得的最大位元組數。如果read系統呼叫成功完成,它將返回從音效卡實際讀取的位元組數,通常情況會比count的值要小一些;如果read系統呼叫失敗,它將返回-1,同時還會設定全域性變數errno,來指明是什麼原因導致了錯誤的發生。
  • write系統呼叫
    系統呼叫write用來向音效卡寫入資料,其函式原型如下所示:
    size_t write(int fd, const char *buf, size_t count);
    

    系統呼叫write和系統呼叫read在很大程度是類似的,差別只在於write是向音效卡寫入資料,而read則是從音效卡讀入資料。引數fd同樣是裝置檔案的識別符號,它也是透過之前的open系統呼叫獲得的;引數buf是指向緩衝區的字元指標,它儲存著即將向音效卡寫入的資料;引數count則用來限定向音效卡寫入的最大位元組數。
    如果write系統呼叫成功完成,它將返回向音效卡實際寫入的位元組數;如果read系統呼叫失敗,它將返回-1,同時還會設定全域性變數errno,來指明是什麼原因導致了錯誤的發生。無論是read還是write,一旦呼叫之後Linux核心就會阻塞當前應用程式,直到資料成功地從音效卡讀出或者寫入為止。
  • ioctl系統呼叫
    系統呼叫ioctl可以對音效卡進行控制,凡是對裝置檔案的操作不符合讀/寫基本模式的,都是透過ioctl來完成的,它可以影響裝置的行為,或者返回裝置的狀態,其函式原型如下所示:
    int ioctl(int fd, int request, ...);
    

    引數fd是裝置檔案的識別符號,它是在裝置開啟時獲得的;如果裝置比較複雜,那麼對它的控制請求相應地也會有很多種,引數request的目的就是用來區分不同的控制請求;通常說來,在對裝置進行控制時還需要有其它引數,這要根據不同的控制請求才能確定,並且可能是與硬體裝置直接相關的。
  • close系統呼叫
    當應用程式使用完音效卡之後,需要用close系統呼叫將其關閉,以便及時釋放佔用的硬體資源,其函式原型如下所示:
    int close(int fd);
    

    引數fd是裝置檔案的識別符號,它是在裝置開啟時獲得的。一旦應用程式呼叫了close系統呼叫,Linux核心就會釋放與之相關的各種資源,因此建議在不需要的時候儘量及時關閉已經開啟的裝置。

對於Linux應用程式設計師來講,音訊程式設計介面實際上就是一組音訊裝置檔案,透過它們可以從音效卡讀取資料,或者向音效卡寫入資料,並且能夠對音效卡進行控制,設定取樣頻率和聲道數目等等。

  • /dev/sndstat
    裝置檔案/dev/sndstat是音效卡驅動程式提供的最簡單的介面,通常它是一個只讀檔案,作用也僅僅只限於彙報音效卡的當前狀態。一般說來,/dev/sndstat是提供給終端使用者來檢測音效卡的,不宜用於程式當中,因為所有的資訊都可以透過ioctl系統呼叫來獲得。 Linux提供的cat命令可以很方便地從/dev/sndstat獲得音效卡的當前狀態: [xiaowp@linuxgam sound]$ cat /dev/sndstat
  • /dev/dsp

    音效卡驅動程式提供的/dev/dsp是用於數字取樣(sampling)和數字錄音(recording)的裝置檔案,它對於Linux下的音訊程式設計來講非常重要:向該裝置寫資料即意味著啟用音效卡上的D/A轉換器進行放音,而向該裝置讀資料則意味著啟用音效卡上的A/D轉換器進行錄音。目前許多音效卡都提供有多個數字取樣裝置,它們在Linux下可以透過/dev/dsp1等裝置檔案進行訪問。

    DSP是數字訊號處理器(Digital Signal Processor)的簡稱,它是用來進行數字訊號處理的特殊晶片,音效卡使用它來實現模擬訊號和數字訊號的轉換。音效卡中的DSP裝置實際上包含兩個組成部分:在以只讀方式開啟時,能夠使用A/D轉換器進行聲音的輸入;而在以只寫方式開啟時,則能夠使用D/A轉換器進行聲音的輸出。嚴格說來,Linux下的應用程式要麼以只讀方式開啟/dev/dsp輸入聲音,要麼以只寫方式開啟/dev/dsp輸出聲音,但事實上某些音效卡驅動程式仍允許以讀寫的方式開啟/dev/dsp,以便同時進行聲音的輸入和輸出,這對於某些應用場合(如IP電話)來講是非常關鍵的。

    在從DSP裝置讀取資料時,從音效卡輸入的模擬訊號經過A/D轉換器變成數字取樣後的樣本(sample),儲存在音效卡驅動程式的核心緩衝區中,當應用程式透過read系統呼叫從音效卡讀取資料時,儲存在核心緩衝區中的數字取樣結果將被複制到應用程式所指定的使用者緩衝區中。需要指出的是,音效卡取樣頻率是由核心中的驅動程式所決定的,而不取決於應用程式從音效卡讀取資料的速度。如果應用程式讀取資料的速度過慢,以致低於音效卡的取樣頻率,那麼多餘的資料將會被丟棄;如果讀取資料的速度過快,以致高於音效卡的取樣頻率,那麼音效卡驅動程式將會阻塞那些請求資料的應用程式,直到新的資料到來為止。

    在向DSP裝置寫入資料時,數字訊號會經過D/A轉換器變成模擬訊號,然後產生出聲音。應用程式寫入資料的速度同樣應該與音效卡的取樣頻率相匹配,否則過慢的話會產生聲音暫停或者停頓的現象,過快的話又會被核心中的音效卡驅動程式阻塞,直到硬體有能力處理新的資料為止。與其它裝置有所不同,音效卡通常不會支援非阻塞(non-blocking)的I/O操作。

    無論是從音效卡讀取資料,或是向音效卡寫入資料,事實上都具有特定的格式(format),預設為8位無符號資料、單聲道、8KHz取樣率,如果預設值無法達到要求,可以透過ioctl系統呼叫來改變它們。通常說來,在應用程式中開啟裝置檔案/dev/dsp之後,接下去就應該為其設定恰當的格式,然後才能從音效卡讀取或者寫入資料。

  • /dev/audio
    /dev/audio類似於/dev/dsp,它相容於Sun工作站上的音訊裝置,使用的是mu-law編碼方式。如果音效卡驅動程式提供了對/dev/audio的支援,那麼在Linux上就可以透過cat命令,來播放在Sun工作站上用mu-law進行編碼的音訊檔案:
    [xiaowp@linuxgam sound]$ cat audio.au > /dev/audio
    

    由於裝置檔案/dev/audio主要出於對相容性的考慮,所以在新開發的應用程式中最好不要嘗試用它,而應該以/dev/dsp進行替代。對於應用程式來說,同一時刻只能使用/dev/audio或者/dev/dsp其中之一,因為它們是相同硬體的不同軟體介面。
  • /dev/mixer
    在音效卡的硬體電路中,混音器(mixer)是一個很重要的組成部分,它的作用是將多個訊號組合或者疊加在一起,對於不同的音效卡來說,其混音器的作用可能各不相同。執行在Linux核心中的音效卡驅動程式一般都會提供/dev/mixer這一裝置檔案,它是應用程式對混音器進行操作的軟體介面。混音器電路通常由兩個部分組成:輸入混音器(input mixer)和輸出混音器(output mixer)。
    輸入混音器負責從多個不同的訊號源接收模擬訊號,這些訊號源有時也被稱為混音通道或者混音裝置。模擬訊號透過增益控制器和由軟體控制的音量調節器後,在不同的混音通道中進行級別(level)調製,然後被送到輸入混音器中進行聲音的合成。混音器上的電子開關可以控制哪些通道中有訊號與混音器相連,有些音效卡只允許連線一個混音通道作為錄音的音源,而有些音效卡則允許對混音通道做任意的連線。經過輸入混音器處理後的訊號仍然為模擬訊號,它們將被送到A/D轉換器進行數字化處理。
    輸出混音器的工作原理與輸入混音器類似,同樣也有多個訊號源與混音器相連,並且事先都經過了增益調節。當輸出混音器對所有的模擬訊號進行了混合之後,通常還會有一個總控增益調節器來控制輸出聲音的大小,此外還有一些音調控制器來調節輸出聲音的音調。經過輸出混音器處理後的訊號也是模擬訊號,它們最終會被送給喇叭或者其它的模擬輸出裝置。對混音器的程式設計包括如何設定增益控制器的級別,以及怎樣在不同的音源間進行切換,這些操作通常來講是不連續的,而且不會像錄音或者放音那樣需要佔用大量的計算機資源。由於混音器的操作不符合典型的讀/寫操作模式,因此除了open和close兩個系統呼叫之外,大部分的操作都是透過ioctl系統呼叫來完成的。與/dev/dsp不同,/dev/mixer允許多個應用程式同時訪問,並且混音器的設定值會一直保持到對應的裝置檔案被關閉為止。
    為了簡化應用程式的設計,Linux上的音效卡驅動程式大多都支援將混音器的ioctl操作直接應用到聲音裝置上,也就是說如果已經開啟了/dev/dsp,那麼就不用再開啟/dev/mixer來對混音器進行操作,而是可以直接用開啟/dev/dsp時得到的檔案識別符號來設定混音器。
  • /dev/sequencer
    目前大多數音效卡驅動程式還會提供/dev/sequencer這一裝置檔案,用來對音效卡內建的波表合成器進行操作,或者對MIDI匯流排上的樂器進行控制,一般只用於計算機音樂軟體中。
  • 在Linux下進行音訊程式設計時,重點在於如何正確地操作音效卡驅動程式所提供的各種裝置檔案,由於涉及到的概念和因素比較多,所以遵循一個通用的框架無疑將有助於簡化應用程式的設計。

    對音效卡進行程式設計時首先要做的是開啟與之對應的硬體裝置,這是藉助於open系統呼叫來完成的,並且一般情況下使用的是/dev/dsp檔案。採用何種模式對音效卡進行操作也必須在開啟裝置時指定,對於不支援全雙工的音效卡來說,應該使用只讀或者只寫的方式開啟,只有那些支援全雙工的音效卡,才能以讀寫的方式開啟,並且還要依賴於驅動程式的具體實現。Linux允許應用程式多次開啟或者關閉與音效卡對應的裝置檔案,從而能夠很方便地在放音狀態和錄音狀態之間進行切換,建議在進行音訊程式設計時只要有可能就儘量使用只讀或者只寫的方式開啟裝置檔案,因為這樣不僅能夠充分利用音效卡的硬體資源,而且還有利於驅動程式的最佳化。下面的程式碼示範瞭如何以只寫方式開啟音效卡進行放音(playback)操作:

    int handle = open("/dev/dsp", O_WRONLY);
    if (handle == -1) {
    	perror("open /dev/dsp");
    	return -1;
    }
    

    執行在Linux核心中的音效卡驅動程式專門維護了一個緩衝區,其大小會影響到放音和錄音時的效果,使用ioctl系統呼叫可以對它的尺寸進行恰當的設定。調節驅動程式中緩衝區大小的操作不是必須的,如果沒有特殊的要求,一般採用預設的緩衝區大小也就可以了。但需要注意的是,緩衝區大小的設定通常應緊跟在裝置檔案開啟之後,這是因為對音效卡的其它操作有可能會導致驅動程式無法再修改其緩衝區的大小。下面的程式碼示範了怎樣設定音效卡驅動程式中的核心緩衝區的大小:

    int setting = 0xnnnnssss;
    int result = ioctl(handle, SNDCTL_DSP_SETFRAGMENT, &setting);
    if (result == -1) {
    	perror("ioctl buffer size");
    	return -1;
    }
    // 檢查設定值的正確性
    

    在設定緩衝區大小時,引數setting實際上由兩部分組成,其低16位標明緩衝區的尺寸,相應的計算公式為buffer_size = 2^ssss,即若引數setting低16位的值為16,那麼相應的緩衝區的大小會被設定為65536位元組。引數setting的高16位則用來標明分片(fragment)的最大序號,它的取值範圍從2一直到0x7FFF,其中0x7FFF表示沒有任何限制。

    接下來要做的是設定音效卡工作時的聲道(channel)數目,根據硬體裝置和驅動程式的具體情況,可以將其設定為0(單聲道,mono)或者1(立體聲,stereo)。下面的程式碼示範了應該怎樣設定聲道數目:

    int channels = 0; // 0=mono 1=stereo
    int result = ioctl(handle, SNDCTL_DSP_STEREO, &channels);
    if ( result == -1 ) {
    	perror("ioctl channel number");
    	return -1;
    }
    if (channels != 0) {
    	// 只支援立體聲
    }
    

    取樣格式和取樣頻率是在進行音訊程式設計時需要考慮的另一個問題,音效卡支援的所有采樣格式可以在標頭檔案soundcard.h中找到,而透過ioctl系統呼叫則可以很方便地更改當前所使用的取樣格式。下面的程式碼示範瞭如何設定音效卡的取樣格式:

    int format = AFMT_U8;
    int result = ioctl(handle, SNDCTL_DSP_SETFMT, &format);
    if ( result == -1 ) {
    	perror("ioctl sample format");
    	return -1;
    }
    // 檢查設定值的正確性
    

    音效卡取樣頻率的設定也非常容易,只需在呼叫ioctl時將第二個引數的值設定為SNDCTL_DSP_SPEED,同時在第三個引數中指定取樣頻率的數值就行了。對於大多數音效卡來說,其支援的取樣頻率範圍一般為5kHz到44.1kHz或者48kHz,但並不意味著該範圍內的所有頻率都會被硬體支援,在Linux下進行音訊程式設計時最常用到的幾種取樣頻率是11025Hz、16000Hz、22050Hz、32000Hz和44100Hz。下面的程式碼示範瞭如何設定音效卡的取樣頻率:

    int rate = 22050;
    int result = ioctl(handle, SNDCTL_DSP_SPEED, &rate);
    if ( result == -1 ) {
    	perror("ioctl sample format");
    	return -1;
    }
    // 檢查設定值的正確性
    

    音效卡上的混音器由多個混音通道組成,它們可以透過驅動程式提供的裝置檔案/dev/mixer進行程式設計。對混音器的操作是透過ioctl系統呼叫來完成的,並且所有控制命令都由SOUND_MIXER或者MIXER開頭,表1列出了常用的幾個混音器控制命令:

    名 稱作 用
    SOUND_MIXER_VOLUME主音量調節
    SOUND_MIXER_BASS低音控制
    SOUND_MIXER_TREBLE高音控制
    SOUND_MIXER_SYNTHFM合成器
    SOUND_MIXER_PCM主D/A轉換器
    SOUND_MIXER_SPEAKERPC喇叭
    SOUND_MIXER_LINE音訊線輸入
    SOUND_MIXER_MIC麥克風輸入
    SOUND_MIXER_CDCD輸入
    SOUND_MIXER_IMIX回放音量
    SOUND_MIXER_ALTPCM從D/A 轉換器
    SOUND_MIXER_RECLEV錄音音量
    SOUND_MIXER_IGAIN輸入增益
    SOUND_MIXER_OGAIN輸出增益
    SOUND_MIXER_LINE1音效卡的第1輸入
    SOUND_MIXER_LINE2音效卡的第2輸入
    SOUND_MIXER_LINE3音效卡的第3輸入

    表1 混音器命令

    對音效卡的輸入增益和輸出增益進行調節是混音器的一個主要作用,目前大部分音效卡採用的是8位或者16位的增益控制器,但作為程式設計師來講並不需要關心這些,因為音效卡驅動程式會負責將它們變換成百分比的形式,也就是說無論是輸入增益還是輸出增益,其取值範圍都是從0到100。在進行混音器程式設計時,可以使用SOUND_MIXER_READ宏來讀取混音通道的增益大小,例如在獲取麥克風的輸入增益時,可以使用如下的程式碼:

    int vol;
    ioctl(fd, SOUND_MIXER_READ(SOUND_MIXER_MIC), &vol);
    printf("Mic gain is at %d %%n", vol);
    

    對於只有一個混音通道的單聲道裝置來說,返回的增益大小儲存在低位位元組中。而對於支援多個混音通道的雙聲道裝置來說,返回的增益大小實際上包括兩個部分,分別代表左、右兩個聲道的值,其中低位位元組儲存左聲道的音量,而高位位元組則儲存右聲道的音量。下面的程式碼可以從返回值中依次提取左右聲道的增益大小:

    int left, right;
    left = vol & 0xff;
    right = (vol & 0xff00) >> 8;
    printf("Left gain is %d %%, Right gain is %d %%n", left, right);
    

    類似地,如果想設定混音通道的增益大小,則可以透過SOUND_MIXER_WRITE宏來實現,此時遵循的原則與獲取增益值時的原則基本相同,例如下面的語句可以用來設定麥克風的輸入增益:

    vol = (right << 8) + left;
    ioctl(fd, SOUND_MIXER_WRITE(SOUND_MIXER_MIC), &vol);
    

    在編寫實用的音訊程式時,混音器是在涉及到相容性時需要重點考慮的一個物件,這是因為不同的音效卡所提供的混音器資源是有所區別的。音效卡驅動程式提供了多個ioctl系統呼叫來獲得混音器的資訊,它們通常返回一個整型的位掩碼(bitmask),其中每一位分別代表一個特定的混音通道,如果相應的位為1,則說明與之對應的混音通道是可用的。例如透過SOUND_MIXER_READ_DEVMASK返回的位掩碼,可以查詢出能夠被音效卡支援的每一個混音通道,而透過SOUND_MIXER_READ_RECMAS返回的位掩碼,則可以查詢出能夠被當作錄音源的每一個通道。下面的程式碼可以用來檢查CD輸入是否是一個有效的混音通道:

      ioctl(fd, SOUND_MIXER_READ_DEVMASK, &devmask);
    if (devmask & SOUND_MIXER_CD)
      printf("The CD input is supported");
      

    如果進一步還想知道其是否是一個有效的錄音源,則可以使用如下語句:

    ioctl(fd, SOUND_MIXER_READ_RECMASK, &recmask);
    if (recmask & SOUND_MIXER_CD)
      printf("The CD input can be a recording source");
    

    目前大多數音效卡提供多個錄音源,透過SOUND_MIXER_READ_RECSRC可以查詢出當前正在使用的錄音源,同一時刻能夠使用幾個錄音源是由音效卡硬體決定的。類似地,使用SOUND_MIXER_WRITE_RECSRC可以設定音效卡當前使用的錄音源,例如下面的程式碼可以將CD輸入作為音效卡的錄音源使用:

    devmask = SOUND_MIXER_CD;
    ioctl(fd, SOUND_MIXER_WRITE_DEVMASK, &devmask);
    

    此外,所有的混音通道都有單聲道和雙聲道的區別,如果需要知道哪些混音通道提供了對立體聲的支援,可以透過SOUND_MIXER_READ_STEREODEVS來獲得。

    下面給出一個利用音效卡上的DSP裝置進行聲音錄製和回放的基本框架,它的功能是先錄製幾秒種音訊資料,將其存放在記憶體緩衝區中,然後再進行回放,其所有的功能都是透過讀寫/dev/dsp裝置檔案來完成的:

    /*
     * sound.c
     */
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define LENGTH 3    /* 儲存秒數 */
    #define RATE 8000   /* 取樣頻率 */
    #define SIZE 8      /* 量化位數 */
    #define CHANNELS 1  /* 聲道數目 */
    /* 用於儲存數字音訊資料的記憶體緩衝區 */
    unsigned char buf[LENGTH*RATE*SIZE*CHANNELS/8];
    int main()
    {
      int fd;	/* 聲音裝置的檔案描述符 */
      int arg;	/* 用於ioctl呼叫的引數 */
      int status;   /* 系統呼叫的返回值 */
      /* 開啟聲音裝置 */
      fd = open("/dev/dsp", O_RDWR);
      if (fd < 0) {
        perror("open of /dev/dsp failed");
        exit(1);
      }
      /* 設定取樣時的量化位數 */
      arg = SIZE;
      status = ioctl(fd, SOUND_PCM_WRITE_BITS, &arg);
      if (status == -1)
        perror("SOUND_PCM_WRITE_BITS ioctl failed");
      if (arg != SIZE)
        perror("unable to set sample size");
      /* 設定取樣時的聲道數目 */
      arg = CHANNELS; 
      status = ioctl(fd, SOUND_PCM_WRITE_CHANNELS, &arg);
      if (status == -1)
        perror("SOUND_PCM_WRITE_CHANNELS ioctl failed");
      if (arg != CHANNELS)
        perror("unable to set number of channels");
      /* 設定取樣時的取樣頻率 */
      arg = RATE;
      status = ioctl(fd, SOUND_PCM_WRITE_RATE, &arg);
      if (status == -1)
        perror("SOUND_PCM_WRITE_WRITE ioctl failed");
      /* 迴圈,直到按下Control-C */
      while (1) {
        printf("Say something:n");
        status = read(fd, buf, sizeof(buf)); /* 錄音 */
        if (status != sizeof(buf))
          perror("read wrong number of bytes");
        printf("You said:n");
        status = write(fd, buf, sizeof(buf)); /* 回放 */
        if (status != sizeof(buf))
          perror("wrote wrong number of bytes");
        /* 在繼續錄音前等待回放結束 */
        status = ioctl(fd, SOUND_PCM_SYNC, 0); 
        if (status == -1)
          perror("SOUND_PCM_SYNC ioctl failed");
      }
    }
    

    下面再給出一個對混音器進行程式設計的基本框架,利用它可以對各種混音通道的增益進行調節,其所有的功能都是透過讀寫/dev/mixer裝置檔案來完成的:

    /*
     * mixer.c
     */
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    /* 用來儲存所有可用混音裝置的名稱 */
    const char *sound_device_names[] = SOUND_DEVICE_NAMES;
    int fd;                  /* 混音裝置所對應的檔案描述符 */
    int devmask, stereodevs; /* 混音器資訊對應的點陣圖掩碼 */
    char *name;
    /* 顯示命令的使用方法及所有可用的混音裝置 */
    void usage()
    {
      int i;
      fprintf(stderr, "usage: %s   n"
    	  "       %s  nn"
    	  "Where  is one of:n", name, name);
      for (i = 0 ; i < SOUND_MIXER_NRDEVICES ; i++)
        if ((1 << i) & devmask) /* 只顯示有效的混音裝置 */
          fprintf(stderr, "%s ", sound_device_names[i]);
      fprintf(stderr, "n");
      exit(1);
    }
    int main(int argc, char *argv[])
    {
      int left, right, level;  /* 增益設定 */
      int status;              /* 系統呼叫的返回值 */
      int device;              /* 選用的混音裝置 */
      char *dev;               /* 混音裝置的名稱 */
      int i;
      name = argv[0];
      /* 以只讀方式開啟混音裝置 */
      fd = open("/dev/mixer", O_RDONLY);
      if (fd == -1) {
        perror("unable to open /dev/mixer");
        exit(1);
      }
      
      /* 獲得所需要的資訊 */
      status = ioctl(fd, SOUND_MIXER_READ_DEVMASK, &devmask);
      if (status == -1)
        perror("SOUND_MIXER_READ_DEVMASK ioctl failed");
      status = ioctl(fd, SOUND_MIXER_READ_STEREODEVS, &stereodevs);
      if (status == -1)
        perror("SOUND_MIXER_READ_STEREODEVS ioctl failed");
      /* 檢查使用者輸入 */
      if (argc != 3 && argc != 4)
        usage();
      /* 儲存使用者輸入的混音器名稱 */
      dev = argv[1];
      /* 確定即將用到的混音裝置 */
      for (i = 0 ; i < SOUND_MIXER_NRDEVICES ; i++)
        if (((1 << i) & devmask) && !strcmp(dev, sound_device_names[i]))
          break;
      if (i == SOUND_MIXER_NRDEVICES) { /* 沒有找到匹配項 */
        fprintf(stderr, "%s is not a valid mixer devicen", dev);
        usage();
      }
      /* 查詢到有效的混音裝置 */
      device = i;
      /* 獲取增益值 */
      if (argc == 4) {
        /* 左、右聲道均給定 */
        left  = atoi(argv[2]);
        right = atoi(argv[3]);
      } else {
        /* 左、右聲道設為相等 */
        left  = atoi(argv[2]);
        right = atoi(argv[2]);
      }
      
      /* 對非立體聲裝置給出警告資訊 */
      if ((left != right) && !((1 << i) & stereodevs)) {
        fprintf(stderr, "warning: %s is not a stereo devicen", dev);
      }
      
      /* 將兩個聲道的值合到同一變數中 */
      level = (right << 8) + left;
      
      /* 設定增益 */
      status = ioctl(fd, MIXER_WRITE(device), &level);
      if (status == -1) {
        perror("MIXER_WRITE ioctl failed");
        exit(1);
      }
      /* 獲得從驅動返回的左右聲道的增益 */
      left  = level & 0xff;
      right = (level & 0xff00) >> 8;
      /* 顯示實際設定的增益 */
      fprintf(stderr, "%s gain set to %d%% / %d%%n", dev, left, right);
      /* 關閉混音裝置 */
      close(fd);
      return 0;
    }
    

    編譯好上面的程式之後,先不帶任何引數執行一遍,此時會列出音效卡上所有可用的混音通道:

    [xiaowp@linuxgam sound]$ ./mixer
    usage: ./mixer   
           ./mixer  
     
    Where  is one of:
    vol pcm speaker line mic cd igain line1 phin video
    

    之後就可以很方便地設定各個混音通道的增益大小了,例如下面的命令就能夠將CD輸入的左、右聲道的增益分別設定為80%和90%:

    [xiaowp@linuxgam sound]$ ./mixer cd 80 90
    cd gain set to 80% / 90%
    
  • 隨著Linux平臺下多媒體應用的逐漸深入,需要用到數字音訊的場合必將越來越廣泛。雖然數字音訊牽涉到的概念非常多,但在Linux下進行最基本的音訊程式設計卻並不十分複雜,關鍵是掌握如何與OSS或者ALSA這類音效卡驅動程式進行互動,以及如何充分利用它們提供的各種功能,熟悉一些最基本的音訊程式設計框架和模式對初學者來講大有裨益。


    • 1. OSS是Linux上最早出現的音效卡驅動程式,是它的核心網站,從中可以瞭解到許多與OSS相關的資訊。
    • 2. ALSA是目前廣泛使用的Linux音效卡驅動程式,並且提供了一些庫函式來簡化音訊程式的編寫,在其官方網站上可以瞭解到ALSA的許多資訊,並能夠下載到最新的驅動程式和工具軟體。
    • 3. Ken C. Pohlmann著,蘇菲譯,數字音訊原理與應用(第四合版),北京:電子工業出版社,2002
    • 4. 鍾玉琢等編著,多媒體技術及其應用,北京:機械工業出版社,2003

    本文作者肖文鵬是一名自由軟體愛好者,主要從事作業系統和分散式計算環境的研究,喜愛Linux和Python。你可以透過 與他取得聯絡。

[@more@]

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/24790158/viewspace-1040154/,如需轉載,請註明出處,否則將追究法律責任。

相關文章