Linux音頻編程指南
http://www.ibm.com/developerworks/cn/linux/l-audio/
一、數(shù)字音頻
音頻信號(hào)是一種連續(xù)變化的模擬信號(hào),但計(jì)算機(jī)只能處理和記錄二進(jìn)制的數(shù)字信號(hào),由自然音源得到的音頻信號(hào)必須經(jīng)過(guò)一定的變換,成為數(shù)字音頻信號(hào)之后,才能送到計(jì)算機(jī)中作進(jìn)一步的處理。
數(shù)字音頻系統(tǒng)通過(guò)將聲波的波型轉(zhuǎn)換成一系列二進(jìn)制數(shù)據(jù),來(lái)實(shí)現(xiàn)對(duì)原始聲音的重現(xiàn),實(shí)現(xiàn)這一步驟的設(shè)備常被稱為模/數(shù)轉(zhuǎn)換器(A/D)。A/D轉(zhuǎn)換器以每秒鐘上萬(wàn)次的速率對(duì)聲波進(jìn)行采樣,每個(gè)采樣點(diǎn)都記錄下了原始模擬聲波在某一時(shí)刻的狀態(tài),通常稱之為樣本(sample),而每一秒鐘所采樣的數(shù)目則稱為采樣頻率,通過(guò)將一串連續(xù)的樣本連接起來(lái),就可以在計(jì)算機(jī)中描述一段聲音了。對(duì)于采樣過(guò)程中的每一個(gè)樣本來(lái)說(shuō),數(shù)字音頻系統(tǒng)會(huì)分配一定存儲(chǔ)位來(lái)記錄聲波的振幅,一般稱之為采樣分辯率或者采樣精度,采樣精度越高,聲音還原時(shí)就會(huì)越細(xì)膩。
數(shù)字音頻涉及到的概念非常多,對(duì)于在Linux下進(jìn)行音頻編程的程序員來(lái)說(shuō),最重要的是理解聲音數(shù)字化的兩個(gè)關(guān)鍵步驟:采樣和量化。采樣就是每隔一定時(shí)間就讀一次聲音信號(hào)的幅度,而量化則是將采樣得到的聲音信號(hào)幅度轉(zhuǎn)換為數(shù)字值,從本質(zhì)上講,采樣是時(shí)間上的數(shù)字化,而量化則是幅度上的數(shù)字化。下面介紹幾個(gè)在進(jìn)行音頻編程時(shí)經(jīng)常需要用到的技術(shù)指標(biāo):
采樣頻率采樣頻率是指將模擬聲音波形進(jìn)行數(shù)字化時(shí),每秒鐘抽取聲波幅度樣本的次數(shù)。采樣頻率的選擇應(yīng)該遵循奈奎斯特(Harry Nyquist)采樣理論:如果對(duì)某一模擬信號(hào)進(jìn)行采樣,則采樣后可還原的最高信號(hào)頻率只有采樣頻率的一半,或者說(shuō)只要采樣頻率高于輸入信號(hào)最高頻率的兩 倍,就能從采樣信號(hào)系列重構(gòu)原始信號(hào)。正常人聽(tīng)覺(jué)的頻率范圍大約在20Hz~20kHz之間,根據(jù)奈奎斯特采樣理論,為了保證聲音不失真,采樣頻率應(yīng)該在 40kHz左右。常用的音頻采樣頻率有8kHz、11.025kHz、22.05kHz、16kHz、37.8kHz、44.1kHz、48kHz等,如 果采用更高的采樣頻率,還可以達(dá)到DVD的音質(zhì)。 量化位數(shù)
量化位數(shù)是對(duì)模擬音頻信號(hào)的幅度進(jìn)行數(shù)字化,它決定了模擬信號(hào)數(shù)字化以后的動(dòng)態(tài)范圍,常用的有8位、12位和16位。量化位越高,信號(hào)的動(dòng)態(tài)范圍越大,數(shù)字化后的音頻信號(hào)就越可能接近原始信號(hào),但所需要的存貯空間也越大。 聲道數(shù)
聲道數(shù)是反映音頻數(shù)字化質(zhì)量的另一個(gè)重要因素,它有單聲道和雙聲道之分。雙聲道又稱為立體聲,在硬件中有兩條線路,音質(zhì)和音色都要優(yōu)于單聲道,但數(shù)字化后占據(jù)的存儲(chǔ)空間的大小要比單聲道多一倍。
回頁(yè)首
二、聲卡驅(qū)動(dòng)
出于對(duì)安全性方面的考慮,Linux下的應(yīng)用程序無(wú)法直接對(duì)聲卡這類硬件設(shè)備進(jìn)行操作,而是必須通過(guò)內(nèi)核提供的驅(qū)動(dòng)程序才能完成。在Linux上進(jìn)行音頻編程的本質(zhì)就是要借助于驅(qū)動(dòng)程序,來(lái)完成對(duì)聲卡的各種操作。
對(duì)硬件的控制涉及到寄存器中各個(gè)比特位的操作,通常這是與設(shè)備直接相關(guān)并且對(duì)時(shí)序的要求非常嚴(yán)格,如果這些工作都交由應(yīng)用程序員來(lái)負(fù)責(zé),那么對(duì)聲卡的編程將變得異常復(fù)雜而困難起來(lái),驅(qū)動(dòng)程序的作用正是要屏蔽硬件的這些底層細(xì)節(jié),從而簡(jiǎn)化應(yīng)用程序的編寫。目前Linux下常用的聲卡驅(qū)動(dòng)程序主要有兩種:OSS和ALSA。
最早出現(xiàn)在Linux上的音頻編程接口是OSS(Open SoundSystem),它由一套完整的內(nèi)核驅(qū)動(dòng)程序模塊組成,可以為絕大多數(shù)聲卡提供統(tǒng)一的編程接口。OSS出現(xiàn)的歷史相對(duì)較長(zhǎng),這些內(nèi)核模塊中的一部分(OSS/Free)是與Linux內(nèi)核源碼共同免費(fèi)發(fā)布的,另外一些則以二進(jìn)制的形式由4FrontTechnologies公司提供。由于得到了商業(yè)公司的鼎力支持,OSS已經(jīng)成為在Linux下進(jìn)行音頻編程的事實(shí)標(biāo)準(zhǔn),支持OSS的應(yīng)用程序能夠在絕大多數(shù)聲卡上工作良好。
雖然OSS已經(jīng)非常成熟,但它畢竟是一個(gè)沒(méi)有完全開(kāi)放源代碼的商業(yè)產(chǎn)品,ALSA(Advanced Linux SoundArchitecture)恰好彌補(bǔ)了這一空白,它是在Linux下進(jìn)行音頻編程時(shí)另一個(gè)可供選擇的聲卡驅(qū)動(dòng)程序。ALSA除了像OSS那樣提供了一組內(nèi)核驅(qū)動(dòng)程序模塊之外,還專門為簡(jiǎn)化應(yīng)用程序的編寫提供了相應(yīng)的函數(shù)庫(kù),與OSS提供的基于ioctl的原始編程接口相比,ALSA函數(shù)庫(kù)使用起來(lái)要更加方便一些。ALSA的主要特點(diǎn)有:
支持多種聲卡設(shè)備模塊化的內(nèi)核驅(qū)動(dòng)程序支持SMP和多線程提供應(yīng)用開(kāi)發(fā)函數(shù)庫(kù)兼容OSS應(yīng)用程序ALSA和OSS最大的不同之處在于ALSA是由志愿者維護(hù)的自由項(xiàng)目,而OSS則是由公司提供的商業(yè)產(chǎn)品,因此在對(duì)硬件的適應(yīng)程度上OSS要優(yōu)于ALSA,它能夠支持的聲卡種類更多。ALSA雖然不及OSS運(yùn)用得廣泛,但卻具有更加友好的編程接口,并且完全兼容于OSS,對(duì)應(yīng)用程序員來(lái)講無(wú)疑是一個(gè)更佳的選擇。
回頁(yè)首
三、編程接口
如何對(duì)各種音頻設(shè)備進(jìn)行操作是在Linux上進(jìn)行音頻編程的關(guān)鍵,通過(guò)內(nèi)核提供的一組系統(tǒng)調(diào)用,應(yīng)用程序能夠訪問(wèn)聲卡驅(qū)動(dòng)程序提供的各種音頻設(shè)備接口,這是在Linux下進(jìn)行音頻編程最簡(jiǎn)單也是最直接的方法。
3.1 訪問(wèn)音頻設(shè)備
無(wú)論是OSS還是ALSA,都是以內(nèi)核驅(qū)動(dòng)程序的形式運(yùn)行在Linux內(nèi)核空間中的,應(yīng)用程序要想訪問(wèn)聲卡這一硬件設(shè)備,必須借助于Linux內(nèi)核所提供的系統(tǒng)調(diào)用(systemcall)。從程序員的角度來(lái)說(shuō),對(duì)聲卡的操作在很大程度上等同于對(duì)磁盤文件的操作:首先使用open系統(tǒng)調(diào)用建立起與硬件間的聯(lián)系,此時(shí)返回的文件描述符將作為隨后操作的標(biāo)識(shí);接著使用read系統(tǒng)調(diào)用從設(shè)備接收數(shù)據(jù),或者使用write系統(tǒng)調(diào)用向設(shè)備寫入數(shù)據(jù),而其它所有不符合讀/寫這一基本模式的操作都可以由ioctl系統(tǒng)調(diào)用來(lái)完成;最后,使用close系統(tǒng)調(diào)用告訴Linux內(nèi)核不會(huì)再對(duì)該設(shè)備做進(jìn)一步的處理。
open系統(tǒng)調(diào)用系統(tǒng)調(diào)用open可以獲得對(duì)聲卡的訪問(wèn)權(quán),同時(shí)還能為隨后的系統(tǒng)調(diào)用做好準(zhǔn)備,其函數(shù)原型如下所示:
int open(const char *pathname, int flags, int mode);
參數(shù)pathname是將要被打開(kāi)的設(shè)備文件的名稱,對(duì)于聲卡來(lái)講一般是/dev/dsp。參數(shù)flags用來(lái)指明應(yīng)該以什么方式打開(kāi)設(shè)備文件,它可以是 O_RDONLY、O_WRONLY或者O_RDWR,分別表示以只讀、只寫或者讀寫的方式打開(kāi)設(shè)備文件;參數(shù)mode通常是可選的,它只有在指定的設(shè)備 文件不存在時(shí)才會(huì)用到,指明新創(chuàng)建的文件應(yīng)該具有怎樣的權(quán)限。
如果open系統(tǒng)調(diào)用能夠成功完成,它將返回一個(gè)正整數(shù)作為文件標(biāo)識(shí)符,在隨后的系統(tǒng)調(diào)用中需要用到該標(biāo)識(shí)符。如果open系統(tǒng)調(diào)用失敗,它將返回-1,同時(shí)還會(huì)設(shè)置全局變量errno,指明是什么原因?qū)е铝隋e(cuò)誤的發(fā)生。 read系統(tǒng)調(diào)用
系統(tǒng)調(diào)用read用來(lái)從聲卡讀取數(shù)據(jù),其函數(shù)原型如下所示:
int read(int fd, char *buf, size_t count);
參數(shù)fd是設(shè)備文件的標(biāo)識(shí)符,它是通過(guò)之前的open系統(tǒng)調(diào)用獲得的;參數(shù)buf是指向緩沖區(qū)的字符指針,它用來(lái)保存從聲卡獲得的數(shù)據(jù);參數(shù)count則 用來(lái)限定從聲卡獲得的最大字節(jié)數(shù)。如果read系統(tǒng)調(diào)用成功完成,它將返回從聲卡實(shí)際讀取的字節(jié)數(shù),通常情況會(huì)比count的值要小一些;如果read系 統(tǒng)調(diào)用失敗,它將返回-1,同時(shí)還會(huì)設(shè)置全局變量errno,來(lái)指明是什么原因?qū)е铝隋e(cuò)誤的發(fā)生。 write系統(tǒng)調(diào)用
系統(tǒng)調(diào)用write用來(lái)向聲卡寫入數(shù)據(jù),其函數(shù)原型如下所示:
size_t write(int fd, const char *buf, size_t count);
系統(tǒng)調(diào)用write和系統(tǒng)調(diào)用read在很大程度是類似的,差別只在于write是向聲卡寫入數(shù)據(jù),而read則是從聲卡讀入數(shù)據(jù)。參數(shù)fd同樣是設(shè)備文 件的標(biāo)識(shí)符,它也是通過(guò)之前的open系統(tǒng)調(diào)用獲得的;參數(shù)buf是指向緩沖區(qū)的字符指針,它保存著即將向聲卡寫入的數(shù)據(jù);參數(shù)count則用來(lái)限定向聲 卡寫入的最大字節(jié)數(shù)。
如果write系統(tǒng)調(diào)用成功完成,它將返回向聲卡實(shí)際寫入的字節(jié)數(shù);如果read系統(tǒng)調(diào)用失敗,它將返回-1,同時(shí)還會(huì)設(shè)置全局變量errno,來(lái)指明是 什么原因?qū)е铝隋e(cuò)誤的發(fā)生。無(wú)論是read還是write,一旦調(diào)用之后Linux內(nèi)核就會(huì)阻塞當(dāng)前應(yīng)用程序,直到數(shù)據(jù)成功地從聲卡讀出或者寫入為止。 ioctl系統(tǒng)調(diào)用
系統(tǒng)調(diào)用ioctl可以對(duì)聲卡進(jìn)行控制,凡是對(duì)設(shè)備文件的操作不符合讀/寫基本模式的,都是通過(guò)ioctl來(lái)完成的,它可以影響設(shè)備的行為,或者返回設(shè)備的狀態(tài),其函數(shù)原型如下所示:
int ioctl(int fd, int request, ...);
參數(shù)fd是設(shè)備文件的標(biāo)識(shí)符,它是在設(shè)備打開(kāi)時(shí)獲得的;如果設(shè)備比較復(fù)雜,那么對(duì)它的控制請(qǐng)求相應(yīng)地也會(huì)有很多種,參數(shù)request的目的就是用來(lái)區(qū)分 不同的控制請(qǐng)求;通常說(shuō)來(lái),在對(duì)設(shè)備進(jìn)行控制時(shí)還需要有其它參數(shù),這要根據(jù)不同的控制請(qǐng)求才能確定,并且可能是與硬件設(shè)備直接相關(guān)的。 close系統(tǒng)調(diào)用
當(dāng)應(yīng)用程序使用完聲卡之后,需要用close系統(tǒng)調(diào)用將其關(guān)閉,以便及時(shí)釋放占用的硬件資源,其函數(shù)原型如下所示:
int close(int fd);
參數(shù)fd是設(shè)備文件的標(biāo)識(shí)符,它是在設(shè)備打開(kāi)時(shí)獲得的。一旦應(yīng)用程序調(diào)用了close系統(tǒng)調(diào)用,Linux內(nèi)核就會(huì)釋放與之相關(guān)的各種資源,因此建議在不需要的時(shí)候盡量及時(shí)關(guān)閉已經(jīng)打開(kāi)的設(shè)備。
3.2 音頻設(shè)備文件
對(duì)于Linux應(yīng)用程序員來(lái)講,音頻編程接口實(shí)際上就是一組音頻設(shè)備文件,通過(guò)它們可以從聲卡讀取數(shù)據(jù),或者向聲卡寫入數(shù)據(jù),并且能夠?qū)β暱ㄟM(jìn)行控制,設(shè)置采樣頻率和聲道數(shù)目等等。
/dev/sndstat設(shè)備文件/dev/sndstat是聲卡驅(qū)動(dòng)程序提供的最簡(jiǎn)單的接口,通常它是一個(gè)只讀文件,作用也僅僅只限于匯報(bào)聲卡的當(dāng)前狀態(tài)。一般說(shuō)來(lái),/dev/sndstat是提供給最終用戶來(lái)檢測(cè)聲卡的,不宜用于程序當(dāng)中,因?yàn)樗械男畔⒍伎梢酝ㄟ^(guò)ioctl系統(tǒng)調(diào)用來(lái)獲得。 Linux提供的cat命令可以很方便地從/dev/sndstat獲得聲卡的當(dāng)前狀態(tài): [xiaowp@linuxgam sound]$ cat /dev/sndstat /dev/dsp
聲卡驅(qū)動(dòng)程序提供的/dev/dsp是用于數(shù)字采樣(sampling)和數(shù)字錄音(recording)的設(shè)備文件,它對(duì)于 Linux下的音頻編程來(lái)講非常重要:向該設(shè)備寫數(shù)據(jù)即意味著激活聲卡上的D/A轉(zhuǎn)換器進(jìn)行放音,而向該設(shè)備讀數(shù)據(jù)則意味著激活聲卡上的A/D轉(zhuǎn)換器進(jìn)行 錄音。目前許多聲卡都提供有多個(gè)數(shù)字采樣設(shè)備,它們?cè)贚inux下可以通過(guò)/dev/dsp1等設(shè)備文件進(jìn)行訪問(wèn)。
DSP是數(shù)字信號(hào)處理器(Digital Signal Processor)的簡(jiǎn)稱,它是用來(lái)進(jìn)行數(shù)字信號(hào)處理的特殊芯片,聲卡使用它來(lái)實(shí)現(xiàn)模擬信號(hào)和數(shù)字信號(hào)的轉(zhuǎn)換。聲卡中的DSP設(shè)備實(shí)際上包含兩個(gè)組成部 分:在以只讀方式打開(kāi)時(shí),能夠使用A/D轉(zhuǎn)換器進(jìn)行聲音的輸入;而在以只寫方式打開(kāi)時(shí),則能夠使用D/A轉(zhuǎn)換器進(jìn)行聲音的輸出。嚴(yán)格說(shuō)來(lái),Linux下的 應(yīng)用程序要么以只讀方式打開(kāi)/dev/dsp輸入聲音,要么以只寫方式打開(kāi)/dev/dsp輸出聲音,但事實(shí)上某些聲卡驅(qū)動(dòng)程序仍允許以讀寫的方式打開(kāi) /dev/dsp,以便同時(shí)進(jìn)行聲音的輸入和輸出,這對(duì)于某些應(yīng)用場(chǎng)合(如IP電話)來(lái)講是非常關(guān)鍵的。
在從DSP設(shè)備讀取數(shù)據(jù)時(shí),從聲卡輸入的模擬信號(hào)經(jīng)過(guò)A/D轉(zhuǎn)換器變成數(shù)字采樣后的樣本(sample),保存在聲卡驅(qū)動(dòng)程序 的內(nèi)核緩沖區(qū)中,當(dāng)應(yīng)用程序通過(guò)read系統(tǒng)調(diào)用從聲卡讀取數(shù)據(jù)時(shí),保存在內(nèi)核緩沖區(qū)中的數(shù)字采樣結(jié)果將被復(fù)制到應(yīng)用程序所指定的用戶緩沖區(qū)中。需要指出 的是,聲卡采樣頻率是由內(nèi)核中的驅(qū)動(dòng)程序所決定的,而不取決于應(yīng)用程序從聲卡讀取數(shù)據(jù)的速度。如果應(yīng)用程序讀取數(shù)據(jù)的速度過(guò)慢,以致低于聲卡的采樣頻率, 那么多余的數(shù)據(jù)將會(huì)被丟棄;如果讀取數(shù)據(jù)的速度過(guò)快,以致高于聲卡的采樣頻率,那么聲卡驅(qū)動(dòng)程序?qū)?huì)阻塞那些請(qǐng)求數(shù)據(jù)的應(yīng)用程序,直到新的數(shù)據(jù)到來(lái)為止。
在向DSP設(shè)備寫入數(shù)據(jù)時(shí),數(shù)字信號(hào)會(huì)經(jīng)過(guò)D/A轉(zhuǎn)換器變成模擬信號(hào),然后產(chǎn)生出聲音。應(yīng)用程序?qū)懭霐?shù)據(jù)的速度同樣應(yīng)該與聲卡 的采樣頻率相匹配,否則過(guò)慢的話會(huì)產(chǎn)生聲音暫?;蛘咄nD的現(xiàn)象,過(guò)快的話又會(huì)被內(nèi)核中的聲卡驅(qū)動(dòng)程序阻塞,直到硬件有能力處理新的數(shù)據(jù)為止。與其它設(shè)備有 所不同,聲卡通常不會(huì)支持非阻塞(non-blocking)的I/O操作。
無(wú)論是從聲卡讀取數(shù)據(jù),或是向聲卡寫入數(shù)據(jù),事實(shí)上都具有特定的格式(format),默認(rèn)為8位無(wú)符號(hào)數(shù)據(jù)、單聲道、 8KHz采樣率,如果默認(rèn)值無(wú)法達(dá)到要求,可以通過(guò)ioctl系統(tǒng)調(diào)用來(lái)改變它們。通常說(shuō)來(lái),在應(yīng)用程序中打開(kāi)設(shè)備文件/dev/dsp之后,接下去就應(yīng) 該為其設(shè)置恰當(dāng)?shù)母袷?,然后才能從聲卡讀取或者寫入數(shù)據(jù)。
/dev/audio/dev/audio類似于/dev/dsp,它兼容于Sun工作站上的音頻設(shè)備,使用的是mu-law編碼方式。如果聲卡驅(qū)動(dòng)程序提供了對(duì)/dev /audio的支持,那么在Linux上就可以通過(guò)cat命令,來(lái)播放在Sun工作站上用mu-law進(jìn)行編碼的音頻文件:
[xiaowp@linuxgam sound]$ cat audio.au > /dev/audio
由于設(shè)備文件/dev/audio主要出于對(duì)兼容性的考慮,所以在新開(kāi)發(fā)的應(yīng)用程序中最好不要嘗試用它,而應(yīng)該以/dev/dsp進(jìn)行替代。對(duì)于應(yīng)用程序來(lái)說(shuō),同一時(shí)刻只能使用/dev/audio或者/dev/dsp其中之一,因?yàn)樗鼈兪窍嗤布牟煌浖涌凇?/dev/mixer
在聲卡的硬件電路中,混音器(mixer)是一個(gè)很重要的組成部分,它的作用是將多個(gè)信號(hào)組合或者疊加在一起,對(duì)于不同的聲卡來(lái)說(shuō),其混音器的作用可能各 不相同。運(yùn)行在Linux內(nèi)核中的聲卡驅(qū)動(dòng)程序一般都會(huì)提供/dev/mixer這一設(shè)備文件,它是應(yīng)用程序?qū)煲羝鬟M(jìn)行操作的軟件接口。混音器電路通常 由兩個(gè)部分組成:輸入混音器(input mixer)和輸出混音器(output mixer)。
輸入混音器負(fù)責(zé)從多個(gè)不同的信號(hào)源接收模擬信號(hào),這些信號(hào)源有時(shí)也被稱為混音通道或者混音設(shè)備。模擬信號(hào)通過(guò)增益控制器和由軟件控制的音量調(diào)節(jié)器后,在不 同的混音通道中進(jìn)行級(jí)別(level)調(diào)制,然后被送到輸入混音器中進(jìn)行聲音的合成?;煲羝魃系碾娮娱_(kāi)關(guān)可以控制哪些通道中有信號(hào)與混音器相連,有些聲卡 只允許連接一個(gè)混音通道作為錄音的音源,而有些聲卡則允許對(duì)混音通道做任意的連接。經(jīng)過(guò)輸入混音器處理后的信號(hào)仍然為模擬信號(hào),它們將被送到A/D轉(zhuǎn)換器 進(jìn)行數(shù)字化處理。
輸出混音器的工作原理與輸入混音器類似,同樣也有多個(gè)信號(hào)源與混音器相連,并且事先都經(jīng)過(guò)了增益調(diào)節(jié)。當(dāng)輸出混音器對(duì)所有的模擬信號(hào)進(jìn)行了混合之后,通常 還會(huì)有一個(gè)總控增益調(diào)節(jié)器來(lái)控制輸出聲音的大小,此外還有一些音調(diào)控制器來(lái)調(diào)節(jié)輸出聲音的音調(diào)。經(jīng)過(guò)輸出混音器處理后的信號(hào)也是模擬信號(hào),它們最終會(huì)被送 給喇叭或者其它的模擬輸出設(shè)備。 對(duì)混音器的編程包括如何設(shè)置增益控制器的級(jí)別,以及怎樣在不同的音源間進(jìn)行切換,這些操作通常來(lái)講是不連續(xù)的,而且不會(huì)像錄音或者放音那樣需要占用大量的 計(jì)算機(jī)資源。由于混音器的操作不符合典型的讀/寫操作模式,因此除了open和close兩個(gè)系統(tǒng)調(diào)用之外,大部分的操作都是通過(guò)ioctl系統(tǒng)調(diào)用來(lái)完 成的。與/dev/dsp不同,/dev/mixer允許多個(gè)應(yīng)用程序同時(shí)訪問(wèn),并且混音器的設(shè)置值會(huì)一直保持到對(duì)應(yīng)的設(shè)備文件被關(guān)閉為止。
為了簡(jiǎn)化應(yīng)用程序的設(shè)計(jì),Linux上的聲卡驅(qū)動(dòng)程序大多都支持將混音器的ioctl操作直接應(yīng)用到聲音設(shè)備上,也就是說(shuō)如果已經(jīng)打開(kāi)了/dev /dsp,那么就不用再打開(kāi)/dev/mixer來(lái)對(duì)混音器進(jìn)行操作,而是可以直接用打開(kāi)/dev/dsp時(shí)得到的文件標(biāo)識(shí)符來(lái)設(shè)置混音器。 /dev/sequencer
目前大多數(shù)聲卡驅(qū)動(dòng)程序還會(huì)提供/dev/sequencer這一設(shè)備文件,用來(lái)對(duì)聲卡內(nèi)建的波表合成器進(jìn)行操作,或者對(duì)MIDI總線上的樂(lè)器進(jìn)行控制,一般只用于計(jì)算機(jī)音樂(lè)軟件中。
回頁(yè)首
四、應(yīng)用框架
在Linux下進(jìn)行音頻編程時(shí),重點(diǎn)在于如何正確地操作聲卡驅(qū)動(dòng)程序所提供的各種設(shè)備文件,由于涉及到的概念和因素比較多,所以遵循一個(gè)通用的框架無(wú)疑將有助于簡(jiǎn)化應(yīng)用程序的設(shè)計(jì)。
4.1 DSP編程
對(duì)聲卡進(jìn)行編程時(shí)首先要做的是打開(kāi)與之對(duì)應(yīng)的硬件設(shè)備,這是借助于open系統(tǒng)調(diào)用來(lái)完成的,并且一般情況下使用的是/dev/dsp文件。采用何種模式對(duì)聲卡進(jìn)行操作也必須在打開(kāi)設(shè)備時(shí)指定,對(duì)于不支持全雙工的聲卡來(lái)說(shuō),應(yīng)該使用只讀或者只寫的方式打開(kāi),只有那些支持全雙工的聲卡,才能以讀寫的方式打開(kāi),并且還要依賴于驅(qū)動(dòng)程序的具體實(shí)現(xiàn)。Linux允許應(yīng)用程序多次打開(kāi)或者關(guān)閉與聲卡對(duì)應(yīng)的設(shè)備文件,從而能夠很方便地在放音狀態(tài)和錄音狀態(tài)之間進(jìn)行切換,建議在進(jìn)行音頻編程時(shí)只要有可能就盡量使用只讀或者只寫的方式打開(kāi)設(shè)備文件,因?yàn)檫@樣不僅能夠充分利用聲卡的硬件資源,而且還有利于驅(qū)動(dòng)程序的優(yōu)化。下面的代碼示范了如何以只寫方式打開(kāi)聲卡進(jìn)行放音(playback)操作:
int handle = open("/dev/dsp", O_WRONLY); if (handle == -1) { perror("open /dev/dsp"); return -1; }
運(yùn)行在Linux內(nèi)核中的聲卡驅(qū)動(dòng)程序?qū)iT維護(hù)了一個(gè)緩沖區(qū),其大小會(huì)影響到放音和錄音時(shí)的效果,使用ioctl系統(tǒng)調(diào)用可以對(duì)它的尺寸進(jìn)行恰當(dāng)?shù)脑O(shè)置。調(diào)節(jié)驅(qū)動(dòng)程序中緩沖區(qū)大小的操作不是必須的,如果沒(méi)有特殊的要求,一般采用默認(rèn)的緩沖區(qū)大小也就可以了。但需要注意的是,緩沖區(qū)大小的設(shè)置通常應(yīng)緊跟在設(shè)備文件打開(kāi)之后,這是因?yàn)閷?duì)聲卡的其它操作有可能會(huì)導(dǎo)致驅(qū)動(dòng)程序無(wú)法再修改其緩沖區(qū)的大小。下面的代碼示范了怎樣設(shè)置聲卡驅(qū)動(dòng)程序中的內(nèi)核緩沖區(qū)的大?。?/p>
int setting = 0xnnnnssss; int result = ioctl(handle, SNDCTL_DSP_SETFRAGMENT, &setting); if (result == -1) { perror("ioctl buffer size"); return -1; } // 檢查設(shè)置值的正確性
在設(shè)置緩沖區(qū)大小時(shí),參數(shù)setting實(shí)際上由兩部分組成,其低16位標(biāo)明緩沖區(qū)的尺寸,相應(yīng)的計(jì)算公式為buffer_size =2^ssss,即若參數(shù)setting低16位的值為16,那么相應(yīng)的緩沖區(qū)的大小會(huì)被設(shè)置為65536字節(jié)。參數(shù)setting的高16位則用來(lái)標(biāo)明分片(fragment)的最大序號(hào),它的取值范圍從2一直到0x7FFF,其中0x7FFF表示沒(méi)有任何限制。
接下來(lái)要做的是設(shè)置聲卡工作時(shí)的聲道(channel)數(shù)目,根據(jù)硬件設(shè)備和驅(qū)動(dòng)程序的具體情況,可以將其設(shè)置為0(單聲道,mono)或者1(立體聲,stereo)。下面的代碼示范了應(yīng)該怎樣設(shè)置聲道數(shù)目:
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) { // 只支持立體聲 }
采樣格式和采樣頻率是在進(jìn)行音頻編程時(shí)需要考慮的另一個(gè)問(wèn)題,聲卡支持的所有采樣格式可以在頭文件soundcard.h中找到,而通過(guò)ioctl系統(tǒng)調(diào)用則可以很方便地更改當(dāng)前所使用的采樣格式。下面的代碼示范了如何設(shè)置聲卡的采樣格式:
int format = AFMT_U8; int result = ioctl(handle, SNDCTL_DSP_SETFMT, &format); if ( result == -1 ) { perror("ioctl sample format"); return -1; } // 檢查設(shè)置值的正確性
聲卡采樣頻率的設(shè)置也非常容易,只需在調(diào)用ioctl時(shí)將第二個(gè)參數(shù)的值設(shè)置為SNDCTL_DSP_SPEED,同時(shí)在第三個(gè)參數(shù)中指定采樣頻率的數(shù)值就行了。對(duì)于大多數(shù)聲卡來(lái)說(shuō),其支持的采樣頻率范圍一般為5kHz到44.1kHz或者48kHz,但并不意味著該范圍內(nèi)的所有頻率都會(huì)被硬件支持,在Linux下進(jìn)行音頻編程時(shí)最常用到的幾種采樣頻率是11025Hz、16000Hz、22050Hz、32000Hz和44100Hz。下面的代碼示范了如何設(shè)置聲卡的采樣頻率:
int rate = 22050; int result = ioctl(handle, SNDCTL_DSP_SPEED, &rate); if ( result == -1 ) { perror("ioctl sample format"); return -1; } // 檢查設(shè)置值的正確性
4.2 Mixer編程
聲卡上的混音器由多個(gè)混音通道組成,它們可以通過(guò)驅(qū)動(dòng)程序提供的設(shè)備文件/dev/mixer進(jìn)行編程。對(duì)混音器的操作是通過(guò)ioctl系統(tǒng)調(diào)用來(lái)完成的,并且所有控制命令都由SOUND_MIXER或者M(jìn)IXER開(kāi)頭,表1列出了常用的幾個(gè)混音器控制命令:
名 稱 作 用 SOUND_MIXER_VOLUME 主音量調(diào)節(jié) SOUND_MIXER_BASS 低音控制 SOUND_MIXER_TREBLE 高音控制 SOUND_MIXER_SYNTH FM合成器 SOUND_MIXER_PCM 主D/A轉(zhuǎn)換器 SOUND_MIXER_SPEAKER PC喇叭 SOUND_MIXER_LINE 音頻線輸入 SOUND_MIXER_MIC 麥克風(fēng)輸入 SOUND_MIXER_CD CD輸入 SOUND_MIXER_IMIX 回放音量 SOUND_MIXER_ALTPCM 從D/A 轉(zhuǎn)換器 SOUND_MIXER_RECLEV 錄音音量 SOUND_MIXER_IGAIN 輸入增益 SOUND_MIXER_OGAIN 輸出增益 SOUND_MIXER_LINE1 聲卡的第1輸入 SOUND_MIXER_LINE2 聲卡的第2輸入 SOUND_MIXER_LINE3 聲卡的第3輸入表1 混音器命令
對(duì)聲卡的輸入增益和輸出增益進(jìn)行調(diào)節(jié)是混音器的一個(gè)主要作用,目前大部分聲卡采用的是8位或者16位的增益控制器,但作為程序員來(lái)講并不需要關(guān)心這些,因?yàn)槁暱?qū)動(dòng)程序會(huì)負(fù)責(zé)將它們變換成百分比的形式,也就是說(shuō)無(wú)論是輸入增益還是輸出增益,其取值范圍都是從0到100。在進(jìn)行混音器編程時(shí),可以使用SOUND_MIXER_READ宏來(lái)讀取混音通道的增益大小,例如在獲取麥克風(fēng)的輸入增益時(shí),可以使用如下的代碼:
int vol; ioctl(fd, SOUND_MIXER_READ(SOUND_MIXER_MIC), &vol); printf("Mic gain is at %d %%n", vol);
對(duì)于只有一個(gè)混音通道的單聲道設(shè)備來(lái)說(shuō),返回的增益大小保存在低位字節(jié)中。而對(duì)于支持多個(gè)混音通道的雙聲道設(shè)備來(lái)說(shuō),返回的增益大小實(shí)際上包括兩個(gè)部分,分別代表左、右兩個(gè)聲道的值,其中低位字節(jié)保存左聲道的音量,而高位字節(jié)則保存右聲道的音量。下面的代碼可以從返回值中依次提取左右聲道的增益大?。?/p>
int left, right; left = vol & 0xff; right = (vol & 0xff00) >> 8; printf("Left gain is %d %%, Right gain is %d %%n", left, right);
類似地,如果想設(shè)置混音通道的增益大小,則可以通過(guò)SOUND_MIXER_WRITE宏來(lái)實(shí)現(xiàn),此時(shí)遵循的原則與獲取增益值時(shí)的原則基本相同,例如下面的語(yǔ)句可以用來(lái)設(shè)置麥克風(fēng)的輸入增益:
vol = (right << 8) + left; ioctl(fd, SOUND_MIXER_WRITE(SOUND_MIXER_MIC), &vol);
在編寫實(shí)用的音頻程序時(shí),混音器是在涉及到兼容性時(shí)需要重點(diǎn)考慮的一個(gè)對(duì)象,這是因?yàn)椴煌穆暱ㄋ峁┑幕煲羝髻Y源是有所區(qū)別的。聲卡驅(qū)動(dòng)程序提供了多個(gè)ioctl系統(tǒng)調(diào)用來(lái)獲得混音器的信息,它們通常返回一個(gè)整型的位掩碼(bitmask),其中每一位分別代表一個(gè)特定的混音通道,如果相應(yīng)的位為1,則說(shuō)明與之對(duì)應(yīng)的混音通道是可用的。例如通過(guò)SOUND_MIXER_READ_DEVMASK返回的位掩碼,可以查詢出能夠被聲卡支持的每一個(gè)混音通道,而通過(guò)SOUND_MIXER_READ_RECMAS返回的位掩碼,則可以查詢出能夠被當(dāng)作錄音源的每一個(gè)通道。下面的代碼可以用來(lái)檢查CD輸入是否是一個(gè)有效的混音通道:
ioctl(fd, SOUND_MIXER_READ_DEVMASK, &devmask); if (devmask & SOUND_MIXER_CD) printf("The CD input is supported");
如果進(jìn)一步還想知道其是否是一個(gè)有效的錄音源,則可以使用如下語(yǔ)句:
ioctl(fd, SOUND_MIXER_READ_RECMASK, &recmask); if (recmask & SOUND_MIXER_CD) printf("The CD input can be a recording source");
目前大多數(shù)聲卡提供多個(gè)錄音源,通過(guò)SOUND_MIXER_READ_RECSRC可以查詢出當(dāng)前正在使用的錄音源,同一時(shí)刻能夠使用幾個(gè)錄音源是由聲卡硬件決定的。類似地,使用SOUND_MIXER_WRITE_RECSRC可以設(shè)置聲卡當(dāng)前使用的錄音源,例如下面的代碼可以將CD輸入作為聲卡的錄音源使用:
devmask = SOUND_MIXER_CD; ioctl(fd, SOUND_MIXER_WRITE_DEVMASK, &devmask);
此外,所有的混音通道都有單聲道和雙聲道的區(qū)別,如果需要知道哪些混音通道提供了對(duì)立體聲的支持,可以通過(guò)SOUND_MIXER_READ_STEREODEVS來(lái)獲得。
4.3 音頻錄放框架
下面給出一個(gè)利用聲卡上的DSP設(shè)備進(jìn)行聲音錄制和回放的基本框架,它的功能是先錄制幾秒種音頻數(shù)據(jù),將其存放在內(nèi)存緩沖區(qū)中,然后再進(jìn)行回放,其所有的功能都是通過(guò)讀寫/dev/dsp設(shè)備文件來(lái)完成的:
/* * sound.c */ #include#include #include #include #include #include #include #define LENGTH 3 /* 存儲(chǔ)秒數(shù) */ #define RATE 8000 /* 采樣頻率 */ #define SIZE 8 /* 量化位數(shù) */ #define CHANNELS 1 /* 聲道數(shù)目 */ /* 用于保存數(shù)字音頻數(shù)據(jù)的內(nèi)存緩沖區(qū) */ unsigned char buf[LENGTH*RATE*SIZE*CHANNELS/8]; int main() { int fd; /* 聲音設(shè)備的文件描述符 */ int arg; /* 用于ioctl調(diào)用的參數(shù) */ int status; /* 系統(tǒng)調(diào)用的返回值 */ /* 打開(kāi)聲音設(shè)備 */ fd = open("/dev/dsp", O_RDWR); if (fd < 0) { perror("open of /dev/dsp failed"); exit(1); } /* 設(shè)置采樣時(shí)的量化位數(shù) */ 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"); /* 設(shè)置采樣時(shí)的聲道數(shù)目 */ 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"); /* 設(shè)置采樣時(shí)的采樣頻率 */ arg = RATE; status = ioctl(fd, SOUND_PCM_WRITE_RATE, &arg); if (status == -1) perror("SOUND_PCM_WRITE_WRITE ioctl failed"); /* 循環(huán),直到按下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"); /* 在繼續(xù)錄音前等待回放結(jié)束 */ status = ioctl(fd, SOUND_PCM_SYNC, 0); if (status == -1) perror("SOUND_PCM_SYNC ioctl failed"); } }
4.4 混音器框架
下面再給出一個(gè)對(duì)混音器進(jìn)行編程的基本框架,利用它可以對(duì)各種混音通道的增益進(jìn)行調(diào)節(jié),其所有的功能都是通過(guò)讀寫/dev/mixer設(shè)備文件來(lái)完成的:
/* * mixer.c */ #include#include #include #include #include #include /* 用來(lái)存儲(chǔ)所有可用混音設(shè)備的名稱 */ const char *sound_device_names[] = SOUND_DEVICE_NAMES; int fd; /* 混音設(shè)備所對(duì)應(yīng)的文件描述符 */ int devmask, stereodevs; /* 混音器信息對(duì)應(yīng)的位圖掩碼 */ char *name; /* 顯示命令的使用方法及所有可用的混音設(shè)備 */ 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) /* 只顯示有效的混音設(shè)備 */ fprintf(stderr, "%s ", sound_device_names[i]); fprintf(stderr, "n"); exit(1); } int main(int argc, char *argv[]) { int left, right, level; /* 增益設(shè)置 */ int status; /* 系統(tǒng)調(diào)用的返回值 */ int device; /* 選用的混音設(shè)備 */ char *dev; /* 混音設(shè)備的名稱 */ int i; name = argv[0]; /* 以只讀方式打開(kāi)混音設(shè)備 */ 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]; /* 確定即將用到的混音設(shè)備 */ for (i = 0 ; i < SOUND_MIXER_NRDEVICES ; i++) if (((1 << i) & devmask) && !strcmp(dev, sound_device_names[i])) break; if (i == SOUND_MIXER_NRDEVICES) { /* 沒(méi)有找到匹配項(xiàng) */ fprintf(stderr, "%s is not a valid mixer devicen", dev); usage(); } /* 查找到有效的混音設(shè)備 */ device = i; /* 獲取增益值 */ if (argc == 4) { /* 左、右聲道均給定 */ left = atoi(argv[2]); right = atoi(argv[3]); } else { /* 左、右聲道設(shè)為相等 */ left = atoi(argv[2]); right = atoi(argv[2]); } /* 對(duì)非立體聲設(shè)備給出警告信息 */ if ((left != right) && !((1 << i) & stereodevs)) { fprintf(stderr, "warning: %s is not a stereo devicen", dev); } /* 將兩個(gè)聲道的值合到同一變量中 */ level = (right << 8) + left; /* 設(shè)置增益 */ status = ioctl(fd, MIXER_WRITE(device), &level); if (status == -1) { perror("MIXER_WRITE ioctl failed"); exit(1); } /* 獲得從驅(qū)動(dòng)返回的左右聲道的增益 */ left = level & 0xff; right = (level & 0xff00) >> 8; /* 顯示實(shí)際設(shè)置的增益 */ fprintf(stderr, "%s gain set to %d%% / %d%%n", dev, left, right); /* 關(guān)閉混音設(shè)備 */ close(fd); return 0; }
編譯好上面的程序之后,先不帶任何參數(shù)執(zhí)行一遍,此時(shí)會(huì)列出聲卡上所有可用的混音通道:
[xiaowp@linuxgam sound]$ ./mixer usage: ./mixer./mixer Where is one of: vol pcm speaker line mic cd igain line1 phin video
之后就可以很方便地設(shè)置各個(gè)混音通道的增益大小了,例如下面的命令就能夠?qū)D輸入的左、右聲道的增益分別設(shè)置為80%和90%:
[xiaowp@linuxgam sound]$ ./mixer cd 80 90 cd gain set to 80% / 90%
回頁(yè)首
五、小結(jié)
隨著Linux平臺(tái)下多媒體應(yīng)用的逐漸深入,需要用到數(shù)字音頻的場(chǎng)合必將越來(lái)越廣泛。雖然數(shù)字音頻牽涉到的概念非常多,但在Linux下進(jìn)行最基本的音頻編程卻并不十分復(fù)雜,關(guān)鍵是掌握如何與OSS或者ALSA這類聲卡驅(qū)動(dòng)程序進(jìn)行交互,以及如何充分利用它們提供的各種功能,熟悉一些最基本的音頻編程框架和模式對(duì)初學(xué)者來(lái)講大有裨益。
參考資料
1. OSS是Linux上最早出現(xiàn)的聲卡驅(qū)動(dòng)程序,http://www.opensound.com是它的核心網(wǎng)站,從中可以了解到許多與OSS相關(guān)的信息。2. ALSA是目前廣泛使用的Linux聲卡驅(qū)動(dòng)程序,并且提供了一些庫(kù)函數(shù)來(lái)簡(jiǎn)化音頻程序的編寫,在其官方網(wǎng)站http://www.alsa-project.org/上可以了解到ALSA的許多信息,并能夠下載到最新的驅(qū)動(dòng)程序和工具軟件。
3. Ken C. Pohlmann著,蘇菲譯,數(shù)字音頻原理與應(yīng)用(第四合版),北京:電子工業(yè)出版社,2002
4. 鐘玉琢等編著,多媒體技術(shù)及其應(yīng)用,北京:機(jī)械工業(yè)出版社,2003