1.OSS簡介html
OSS的層次結構很是簡單,應用程序經過API(定義於 <soundcard.h>)訪問OSS driver,OSS driver控制聲卡。以下圖所示:git
oss結構網絡
聲卡中主要有兩個基本裝置:Mixer和CODEC(ADC/DAC)。Mixer用來控制輸入音量的大小,對應的設備文件爲/dev /mixer;CODEC用來實現錄音(模擬信號轉變爲數字信號)和播放聲音(數字信號轉變爲模擬信號)的功能,對應的設備文件爲/dev/dsp。app
開發OSS應用程序的通常流程是:模塊化
1)包含OSS頭文件:#include <soundcard.h> 2)打開設備文件,返回文件描述符 3)使用ioctl設置設備的參數,控制設備的特性 4)對於錄音,從設備讀(read) 5)對於播放,向設備寫(write) 6)關閉打開的設備函數
2.緩衝區設置的性能分析工具
在設置驅動內部的緩衝區時,存在一個矛盾:在聲卡驅動程序中,爲了防止抖動的出現,保證播放的性能,設置了內部緩衝區-DMA buffer。在播放時,應用程序經過驅動程序首先將音頻數據從應用程序緩衝區-APP buffer,寫入到DMA buffer。接着,由DMA控制器把DMA buffer中的音頻數據發送到DAC(Digital-Analog Converter)。某些時刻CPU很是的繁忙,好比正在從磁盤讀入數據,或者正在重畫屏幕,沒有時間向DMA buffer放入新的音頻數據。DAC因爲沒有輸入新的音頻數據,致使聲音播放的間斷,這就出現了聲音的抖動現象。此時,須要將DMA buffer設置的足夠大,使得DAC始終有數據播放。可是,DMA buffer的增大使得每次從APP buffer拷貝的時間也變長,致使了更大的播放延遲。這對於那些延遲敏感的應用場合,如與用戶有交互的音頻應用程序,就會出現問題。性能
對於這個矛盾,能夠從兩個不一樣的方面分別着手解決。驅動程序採用多緩衝(Multi-buffering)的方式,即將大的DMA buffer分割成多個小的緩衝區,稱之爲fragment,它們的大小相同。驅動程序開始時只需等待兩個fragment滿了就開始播放。這樣能夠經過 增長fragment的個數來增長緩衝區的大小,但同時每一個fragment被限制在合適的大小,也不影響時延。音頻驅動程序中的多緩衝機制通常會利用底 層DMA控制器的scatter-gather功能。測試
另外一方面,應用程序也可指導驅動程序選擇合適大小的緩衝區,使得在沒有抖動的狀況下,時延儘量的小。特別的,應用程序將驅動程序中的緩衝經過 mmap映射到本身地址空間後,會以本身的方式來處理這些緩衝區(與驅動程序的不必定一致),這時應用程序每每會先根據本身的須要設置驅動程序中內部緩衝 區的大小。spa
在OSS的ioctl接口中,SNDCTL_DSP_SETFRAGMENT就是用來設置驅動程序內部緩衝區大小。具體的用法以下:
int param; param = ( 0×0004 << 16) + 0x000a; if (ioctl(audio_fd, SNDCTL_DSP_SETFRAGMENT, ¶m) == -1) { …error handling… }
參數param由兩部分組成:低16位爲fragment的大小,此處0x000a表示fragment大小爲2^0xa,即1024字節;高16 位爲fragment的數量,此處爲0×0004,即4個fragement。設置好fragment參數後,經過ioctl的 SNDCTL_DSP_SETFRAGMENT命令調整驅動程序中的緩衝區。
爲了給音頻程序的開發者展現緩衝區配置對播放效果的影響,咱們將對緩衝區配置與播放性能的關係進行測試。下面首先介紹測試的環境,包括測試方法的原理和測試結果的含義;接着針對兩種狀況進行測試,並解釋測試的結果。
測試環境
測試是在PC機上進行的,具體的測試環境參見下表。
項目 參數 CPU PIII 800 內存 256M SDRAM 硬盤 ST 80G UDMA 顯卡 TNT2 m64 16M 聲卡 主板集成(工做在44.1KHz,立體聲,16bit的模式) 內核 Linux kernel 2.4.20(Redhat 9.0)
測試軟件(latencytest)由兩部分組成:音頻播放測試程序、系統運行負載模擬程序。(注:latencytest軟件主要目的是測試內核的時延,但這裏做爲對不一樣緩衝配置進行比較的工具。)
音頻播放測試程序的工做流程見下面的代碼。爲了保證音頻播放在調度上的優先性,音頻播放測試程序使用SCHED_FIFO調度策略(經過sched_setscheduler())。
while(1) { time1=my_gettime(); 經過空循環消耗必定的CPU時間 time2=my_gettime(); write(audio_fd,playbuffer,fragmentsize); time3=my_gettime(); }
my_gettime返回當前的時刻,在每一個操做的開始和結束分別記錄下時間,就能夠獲得操做所花費的時間。audio_fd爲打開音頻設備的文件 描述符,playbuffer是應用程序中存放音頻數據的緩衝區,也就是APP buffer,fragmentsize爲一個fragment的大小,write操做控制向驅動寫入一個fragment。空循環用來模擬在播放音頻時 的CPU運算負載,典型的例子是合成器(synthesizer)實時產生波形後,再進行播放(write)。空循環消耗的時間長度設置爲一個 fragment播放時延的80%。
相關指標的計算方法以下:
1) 一個fragment的播放時延(fragm.latency) = fragment大小/(頻率22)。以fragment大小爲512字節和以上的測試環境爲例,一個fragment時延 = 512/(4410022) = 2.90ms[44100表示44.1KHz的採樣頻率,第一個2表示立體聲的兩個聲道,第二個2表示16bit爲2個字節]。 2) 一個fragment的傳輸時延 = 將一個fragment從APP buffer拷貝到DMA buffer的時延。 3) time3-time1 = 一次循環持續的時間 = 空循環消耗的CPU時間 + 一個fragment的傳輸時延。 4) time2-time1 = 空循環消耗的實際CPU時間(cpu latency)。
爲了模擬真實的系統運行狀況,在測試程序播放音頻數據的同時,還運行了一個系統負載。一共設置5種負載場景,按順序分別是:
1) 高強度的圖形輸出(使用x11perf來模擬大量的BitBlt操做) 2) 高強度對/proc文件系統的訪問(使用top,更新頻率爲0.01秒) 3) 高強度的磁盤寫(向硬盤寫一個大文件) 4) 高強度的磁盤拷貝(將一個文件拷貝到另外一個地方) 5) 高強度的磁盤讀(從硬盤讀一個大文件)
針對不一樣的系統負載場景,測試分別給出了各自的結果。測試結果以圖形的形式表示,測試結果中圖形的含義留待性能分析時再行解釋。
性能分析
下面,咱們分別對兩種緩衝區的配置進行性能比較,
1) 狀況1:fragment大小爲512字節,fragment個數爲2。 測試結果1(2×512.html) 2) 狀況2:fragment大小爲2048字節,fragment個數爲4。 測試結果2(4×2048.html)
爲了看懂測試結果,須要瞭解測試結果圖形中各類標識的含義:
1) 紅線:所有緩衝區的播放時延。所有緩衝區播放時延 = 一個fragment時延 x fragment的個數。對於測試的第一種狀況,所有緩衝區時延 = 2.90ms x 2 = 5.8ms。 2) 白線:實際的調度時延,即一次循環的時間(time3-time1)。若是白線越過了紅線,則說明全部的緩衝區中音頻數據播放結束後,應用程序仍然沒有來得及將新的數據放入到緩衝區中,此時會出現聲音的丟失,同時overruns相應的增長1。 3) 綠線:CPU執行空循環的時間(即前面的time2-time1)。綠線的標稱值爲fragm.latency x 80%。因爲播放進程使用SCHED_FIFO調度策略,因此若是綠線所表明的時間變大,則說明出現了總線競爭,或者是系統長時間的處於內核中。 4) 黃線:一個fragment播放時延。白線應該接近於黃線。 5) 白色的between +/-1ms:實際的調度時延落入到fragm.latency +/-1ms範圍的比例。 6) 白色的between +/-2ms:實際的調度時延落入到fragm.latency +/-2ms範圍的比例。 7) 綠色的between +/-0.2ms:CPU的空循環時延波動+/-0.2ms範圍的比例(即落入到標稱值+/-0.2ms範圍的比例)。 8) 綠色的between +/-0.1ms:CPU的空循環時延波動+/-0.1ms範圍的比例(即落入到標稱值+/-0.1ms範圍的比例)。
第一種狀況的緩衝區很小,每一個fragment只有512字節,總共的緩衝區大小爲2 x 512 = 1024字節。1024字節只能播放5.8ms。根據OSS的說明,因爲Unix是一個多任務的操做系統,有多個進程共享CPU,播放程序必需要保證選擇 的緩衝區配置要提供足夠的大小,使得當CPU被其它進程使用時(此時不能繼續向聲卡傳送新的音頻數據),不至於出現欠載的現象。欠載是指應用程序提供音頻 數據的速度跟不上聲卡播放的速度,這時播放就會出現暫停或滴答聲。所以,不推薦使用fragment大小小於256字節的設置。從測試結果中看到,無論使 用那種系統負載,都會出現欠載的現象,特別是在寫硬盤的狀況下,一共發生了14次欠載(overruns = 14)。
固然,對於那些實時性要求高的音頻播放程序,但願使用較小的緩衝區,由於只有這樣才能保證較小的時延。在上面的測試結果咱們看到了欠載的現象,但 是,這並不徹底是緩衝區太小所致使的。實際上,因爲Linux內核是不可搶佔的,因此沒法確知Linux在內核中停留的時間,所以也就沒法保證以肯定的速 度調度某個進程,即便如今播放程序使用了SCHED_FIFO調度策略。從這個角度來講,多媒體應用(如音頻播放)對操做系統內核提出了更高的要求。在目 前Linux內核的狀況下,較小的調度時延能夠經過一些專門的內核補丁(low-latency patch)達到。不過咱們相信Linux2.6新內核會有更好的表現。
第二種狀況的緩衝區要大得多,總共的緩衝區大小爲4 x 2048 = 8192字節。8192字節能夠播放0.046秒。從測試的圖形來看,結果比較理想,即便在系統負載較重的狀況,仍然可以基本保證播放時延的要求,並且沒有出現一次欠載的現象。
固然,並非說緩衝區越大越好,若是繼續選擇更大的緩衝區,將會產生比較大的時延,對於實時性要求比較高的音頻流來講,是不能接受的。從測試結果中 能夠看到,第二種配置的時延抖動比第一種配置要大得多。不過,在通常狀況下,驅動程序會根據硬件的狀況,選擇一個缺省的緩衝區配置,播放程序一般不須要修 改驅動程序的緩衝區配置,而能夠得到較好的播放效果。
3.非阻塞寫(non-blocking write)
若是播放程序寫入的速度超過了DAC的播放速度,DMA buffer就會充滿了音頻數據。應用程序調用write時就會由於沒有空閒的DMA buffer而被阻塞,直到DMA buffer出現空閒爲止。此時,從某種程度來講,應用程序的推動速度依賴於播放的速度,不一樣的播放速度就會產生不一樣的推動速度。所以,有時咱們不但願 write被阻塞,這就須要咱們可以知道DMA buffer的使用狀況。
for (;;) { audio_buf_info info; /* Ask OSS if there is any free space in the buffer. / if (ioctl(dsp,SNDCTL_DSP_GETOSPACE,&info) != 0) { perror(「Unable to query buffer space」); close(dsp); return 1; }; / Any empty fragments? / if (info.fragments > 0) break; / Not enough free space in the buffer. Waste time. */ usleep(100); };
以上的代碼不停的查詢驅動程序中是否有空的fragment(SNDCTL_DSP_GETOSPACE),若是沒有,則進入睡眠 (usleep(100)),此時應用程序作其它的事情,好比更新畫面,網絡傳輸等。若是有空閒的fragment(info.fragments > 0),則退出循環,接着就能夠進行非阻塞的write了。
4.DMA buffer的直接訪問(mmap)
除了依賴於操做系統內核提供更好的調度性能,音頻播放應用程序也能夠採用一些技術以提升音頻播放的實時性。繞過APP buffer,直接訪問DMA buffer的mmap方法就是其中之一。
咱們知道,將音頻數據輸出到音頻設備一般使用系統調用write,可是這會帶來性能上的損失,由於要進行一次從用戶空間到內核空間的緩衝區拷貝。這 時,能夠考慮利用mmap系統調用,得到直接訪問DMA buffer的能力。DMA控制器不停的掃描DMA buffer,將數據發送到DAC。這有點相似於顯卡對顯存的操做,你們都知道,GUI能夠經過mmap將framebuffer(顯存)映射到本身的地 址空間,而後直接操縱顯存。這裏的DMA buffer就是聲卡的framebuffer。
理解mmap方法的最好方法是經過實際的例子, 代碼1(list1.c)。
代碼中有詳細的註釋,這裏只給出一些說明。
PlayerDMA函數的參數samples指向存放音頻數據的緩衝,rate/bits/channels分別說明音頻數據的採樣速率、每次採樣的位數、聲道數。
在打開/dev/dsp之後,根據/rate/bits/channels參數的要求配置驅動程序。須要注意的是,這些要求並必定能獲得知足,驅動程序要根據本身的狀況選擇,所以在配置後,須要再次查詢,獲取驅動程序真正使用的參數值。
在使用mmap以前,要查看驅動程序是否支持這種模式(SNDCTL_DSP_GETCAPS)。使用SNDCTL_DSP_GETOSPACE得知驅動選擇的framgment大小和個數,就能夠計算出所有DMA buffer的大小dmabuffer_size。
mmap將dmabuffer_size大小的DMA buffer映射到調用進程的地址空間,DMA buffer在應用進程的起始地址爲dmabuffer。之後就能夠直接使用指針dmabuffer訪問DMA buffer了。這裏須要對mmap中的參數作些解釋。
音頻驅動程序針對播放和錄音分別有各自的緩衝區,mmap不能同時映射這兩組緩衝,具體選擇映射哪一個緩衝取決於mmap的prot參數。 PROT_READ選擇輸入(錄音)緩衝,PROT_WRITE選擇輸出(播放)緩衝,代碼中使用了PROT_WRITE|PROT_READ,也是選擇 輸出緩衝。(這是BSD系統的要求,若是隻有PROT_WRITE,那麼每次對緩衝的訪問都會出現segmentation/bus error)。
一旦DMA buffer被mmap後,就不能再經過read/write接口來控制驅動程序了。只能經過SNDCTL_DSP_SETTRIGGER打開DAC的使能位,固然,先要關閉使能位。
DMA一旦啓動後,就會周而復始的掃描DMA buffer。固然咱們老是但願提早爲DMA準備好新的數據,使得DMA的播放始終連續。所以,PlayerDMA函數將mmap後的DMA buffer分割成先後兩塊,中間設置一個界限。當DMA掃描前面一塊時,就填充後面一塊。一旦DMA越過了界限,就去填充前面一塊。
使用mmap的問題是,不是全部的聲卡驅動程序都支持mmap方式。所以,在出現不兼容的狀況下,應用程序要可以轉而去使用傳統的方式。
最後,爲了能深刻的理解mmap的實現原理,咱們以某種聲卡驅動程序爲例,介紹了其內部mmap函數時具體實現。 代碼2(list2.c)
audio_mmap()是實現mmap接口的函數,它首先根據mmap調用的prot參數(vma->vm_flags),選擇合適的緩衝 (輸入仍是輸出);vma->vm_end – vma->vm_start爲須要映射到應用進程地址空間的大小,必須和DMA buffer的大小(s->fragsize * s->nbfrags)一致;若是DMA buffer尚未創建,則調用audio_setup_buf(s)創建;接着對全部的fragment,從映射起始地址開始 (vma->vm_start),創建實際物理地址與映射的虛擬地址之間的對應關係(remap_page_range)。最後設置mmap標誌 (s->mapped = 1)。
5.結束語
固然,除了上面所討論的問題之外,音頻應用的開發還有不少實際的問題須要去面對,好比多路音頻流的合併,各類音頻文件格式的打開等等。
OSS音頻接口存在於Linux內核中許多年了,因爲在體系結構上有許多的侷限性,在Linux 2.6內核中引入了一種全新的音頻體系和接口——ALSA(Advanced Linux Sound Architecture),它提供了不少比OSS更好的特性,包括徹底的thread-safe和SMP-safe,模塊化的設計,支持多個聲卡等等。 爲了保持和OSS接口的兼容性,ALSA還提供了OSS的仿真接口,使得那些爲OSS接口開發的大量應用程序仍然可以在新的ALSA體系下正常的工做。