ALSA聲音編程介紹(譯文)
聲卡接口中有一個(gè)指針來指示聲卡硬件緩存區(qū)中當(dāng)前的讀寫位置。只要接口在運(yùn)行,這個(gè)指針將循環(huán)地指向緩存區(qū)中的某個(gè)位置。
frame size = sizeof(one sample) * nChannels
alsa中配置的緩存(buffer)和周期(size)大小在runtime中是以幀(frames)形式存儲(chǔ)的。
period_bytes = frames_to_bytes(runtime, runtime->period_size);
bytes_to_frames()
?
The period and buffer sizes are not dependent on the sample format because they are measured in frames; you do not need to change them.
?
ALSA聲音編程介紹
ALSA表示高級(jí)Linux聲音體系結(jié)構(gòu)(Advanced Linux Sound Architecture)。它由一系列內(nèi)核驅(qū)動(dòng),應(yīng)用程序編譯接口(API)以及支持Linux下聲音的實(shí)用程序組成。這篇文章里,我將簡單介紹ALSA項(xiàng)目的基本框架以及它的軟件組成。主要集中介紹PCM接口編程,包括您可以自動(dòng)實(shí)踐的程序示例。
?
您使用ALSA的原因可能就是因?yàn)樗苄?,但它并不是唯一可用的聲音API。如果您想完成低級(jí)的聲音操作,以便能夠最大化地控制聲音并最大化地提高性能,或者如果您使用其它聲音API沒有的特性,那么ALSA是很好的選擇。如果您已經(jīng)寫了一個(gè)音頻程序,你可能想要為ALSA聲卡驅(qū)動(dòng)添加本地支持。如果您對(duì)音頻不感興趣,只是想播放音頻文件,那么高級(jí)的API將是更好的選擇,比如SDL,OpenAL以及那些桌面環(huán)境提供的工具集。另外,您只能在有ALSA支持的Linux環(huán)境中使用ALSA。
?
ALSA歷史
ALSA項(xiàng)目發(fā)起的起因是Linux下的聲卡驅(qū)動(dòng)(OSS/Free drivers)沒有得到積極的維護(hù)。并且落后于新的聲卡技術(shù)。Jaroslav Kysela早先寫了一個(gè)聲卡驅(qū)動(dòng),并由此開始了ALSA項(xiàng)目,隨便,更多的開發(fā)者加入到開發(fā)隊(duì)伍中,更多的聲卡得到支持,API的結(jié)構(gòu)也得到了重組。
?
Linux內(nèi)核2.5在開發(fā)過程中,ALSA被合并到了官方的源碼樹中。在發(fā)布內(nèi)核2.6后,ALSA已經(jīng)內(nèi)建在穩(wěn)定的內(nèi)核版本中并將廣泛地使用。
?
數(shù)字音頻基礎(chǔ)
聲音由變化的氣壓組成。它被麥克風(fēng)這樣的轉(zhuǎn)換器轉(zhuǎn)換成電子形式。模/數(shù)(ADC)轉(zhuǎn)換器將模擬電壓轉(zhuǎn)換成離散的樣本值。聲音以固定的時(shí)間間隔被采樣,采樣的速率稱為采樣率。把樣本輸出到數(shù)/模(DAC)轉(zhuǎn)換器,比如擴(kuò)音器,最后轉(zhuǎn)換成原來的模擬信號(hào)。
樣本大小以位來表示。樣本大小是影響聲音被轉(zhuǎn)換成數(shù)字信號(hào)的精確程度的因素之一。另一個(gè)主要的因素是采樣率。奈奎斯特(Nyquist)理論中,只要離散系統(tǒng)的奈奎斯特頻率高于采樣信號(hào)的最高頻率或帶寬,就可以避免混疊現(xiàn)象。
?
ALSA基礎(chǔ)
ALSA由許多聲卡的聲卡驅(qū)動(dòng)程序組成,同時(shí)它也提供一個(gè)稱為libasound的API庫。應(yīng)用程序開發(fā)者應(yīng)該使用libasound而不是內(nèi)核中的ALSA接口。因?yàn)閘ibasound提供最高級(jí)并且編程方便的編程接口。并且提供一個(gè)設(shè)備邏輯命名功能,這樣開發(fā)者甚至不需要知道類似設(shè)備文件這樣的低層接口。相反,OSS/Free驅(qū)動(dòng)是在內(nèi)核系統(tǒng)調(diào)用級(jí)上編程,它要求開發(fā)者提供設(shè)備文件名并且利用ioctrl來實(shí)現(xiàn)相應(yīng)的功能。為了向后兼容,ALSA提供內(nèi)核模塊來模擬OSS,這樣之前的許多在OSS基礎(chǔ)上開發(fā)的應(yīng)用程序不需要任何改動(dòng)就可以在ALSA上運(yùn)行。另外,libaoss庫也可以模擬OSS,而它不需要內(nèi)核模塊。
ALSA包含插件功能,使用插件可以擴(kuò)展新的聲卡驅(qū)動(dòng),包括完全用軟件實(shí)現(xiàn)的虛擬聲卡。ALSA提供一系列基于命令行的工具集,比如混音器(mixer),音頻文件播放器(aplay),以及控制特定聲卡特定屬性的工具。
?
ALSA體系結(jié)構(gòu)
ALSA API可以分解成以下幾個(gè)主要的接口:
1 控制接口:提供管理聲卡注冊(cè)和請(qǐng)求可用設(shè)備的通用功能
2 PCM接口:管理數(shù)字音頻回放(playback)和錄音(capture)的接口。本文后續(xù)總結(jié)重點(diǎn)放在這個(gè)接口上,因?yàn)樗情_發(fā)數(shù)字音頻程序最常用到的接口。
3 Raw MIDI接口:支持MIDI(Musical Instrument Digital Interface),標(biāo)準(zhǔn)的電子樂器。這些API提供對(duì)聲卡上MIDI總線的訪問。這個(gè)原始接口基于MIDI事件工作,由程序員負(fù)責(zé)管理協(xié)議以及時(shí)間處理。
4 定時(shí)器(Timer)接口:為同步音頻事件提供對(duì)聲卡上時(shí)間處理硬件的訪問。
5 時(shí)序器(Sequencer)接口
6 混音器(Mixer)接口
?
設(shè)備命名
API庫使用邏輯設(shè)備名而不是設(shè)備文件。設(shè)備名字可以是真實(shí)的硬件名字也可以是插件名字。硬件名字使用hw:i,j這樣的格式。其中i是卡號(hào),j是這塊聲卡上的設(shè)備號(hào)。第一個(gè)聲音設(shè)備是hw:0,0.這個(gè)別名默認(rèn)引用第一塊聲音設(shè)備并且在本文示例中一直會(huì)被用到。插件使用另外的唯一名字。比如plughw:,表示一個(gè)插件,這個(gè)插件不提供對(duì)硬件設(shè)備的訪問,而是提供像采樣率轉(zhuǎn)換這樣的軟件特性,硬件本身并不支持這樣的特性。
?
聲音緩存和數(shù)據(jù)傳輸
每個(gè)聲卡都有一個(gè)硬件緩存區(qū)來保存記錄下來的樣本。當(dāng)緩存區(qū)足夠滿時(shí),聲卡將產(chǎn)生一個(gè)中斷。內(nèi)核聲卡驅(qū)動(dòng)然后使用直接內(nèi)存(DMA)訪問通
道將樣本傳送到內(nèi)存中的應(yīng)用程序緩存區(qū)。類似地,對(duì)于回放,任何應(yīng)用程序使用DMA將自己的緩存區(qū)數(shù)據(jù)傳送到聲卡的硬件緩存區(qū)中。
這樣硬件緩存區(qū)是環(huán)緩存。也就是說當(dāng)數(shù)據(jù)到達(dá)緩存區(qū)末尾時(shí)將重新回到緩存區(qū)的起始位置。ALSA維護(hù)一個(gè)指針來指向硬件緩存以及應(yīng)用程序緩存區(qū)中數(shù)據(jù)操作的當(dāng)前位置。從內(nèi)核外部看,我們只對(duì)應(yīng)用程序的緩存區(qū)感興趣,所以本文只討論應(yīng)用程序緩存區(qū)。
?
應(yīng)用程序緩存區(qū)的大小可以通過ALSA庫函數(shù)調(diào)用來控制。緩存區(qū)可以很大,一次傳輸操作可能會(huì)導(dǎo)致不可接受的延遲,我們把它稱為延時(shí)(latency)。為了解決這個(gè)問題,ALSA將緩存區(qū)拆分成一系列周期(period)(OSS/Free中叫片斷fragments).ALSA以period為單元來傳送數(shù)據(jù)。
????????? 圖1 ALSA數(shù)據(jù)緩沖組織示例
一個(gè)周期(period)存儲(chǔ)一些幀(frames)。每一幀包含時(shí)間上一個(gè)點(diǎn)所抓取的樣本。對(duì)于立體聲設(shè)備,一個(gè)幀會(huì)包含兩個(gè)信道上的樣本。圖1展示了分解過程:一個(gè)緩存區(qū)分解成周期,然后是幀,然后是樣本。圖中包含一些假定的數(shù)值。圖中左右信道信息被交替地存儲(chǔ)在一個(gè)幀內(nèi)。這稱為交錯(cuò)(interleaved)模式。在非交錯(cuò)模式中,一個(gè)信道的所有樣本數(shù)據(jù)存儲(chǔ)在另外一個(gè)信道的數(shù)據(jù)之后。
?
Over and Under Run
當(dāng)一個(gè)聲卡活動(dòng)時(shí),數(shù)據(jù)總是連續(xù)地在硬件緩存區(qū)和應(yīng)用程序緩存區(qū)間傳輸。但是也有例外。在錄音例子中,如果應(yīng)用程序讀取數(shù)據(jù)不夠快,循環(huán)緩存區(qū)將會(huì)被新的數(shù)據(jù)覆蓋。這種數(shù)據(jù)的丟失被稱為overrun.在回放例子中,如果應(yīng)用程序?qū)懭霐?shù)據(jù)到緩存區(qū)中的速度不夠快,緩存區(qū)將會(huì)"餓死"。這樣的錯(cuò)誤被稱為"underrun"。在ALSA文檔中,有時(shí)將這兩種情形統(tǒng)稱為"XRUN"。適當(dāng)?shù)卦O(shè)計(jì)應(yīng)用程序可以最小化XRUN并且可以從中恢復(fù)過來。
?
一個(gè)典型的聲音程序
使用PCM的程序通常類似下面的偽代碼:
?
??? 打開回放或錄音接口
??? 設(shè)置硬件參數(shù)(訪問模式,數(shù)據(jù)格式,信道數(shù),采樣率,等等)
??? while 有數(shù)據(jù)要被處理:
??????? 讀PCM數(shù)據(jù)(錄音)
??????? 或 寫PCM數(shù)據(jù)(回放)
??? 關(guān)閉接口
?
我們將在下文中看到一些可以工作的代碼。我建議您在你的Linux系統(tǒng)上測(cè)試運(yùn)行這些代碼。查看輸出并嘗試修改推薦的代碼。和本文相關(guān)的所有實(shí)例清單可以從FTP中獲?。篺tp.ssc.com/pub/lj/listings/issue126/6735.tgz。
?
Listing 1. Display Some PCM Types and Formats
?
??? #include
??? ?
??? int main() {
??? int val;
??? ?
??? printf("ALSA library version: %sn",
????????????? SND_LIB_VERSION_STR);
??? ?
??? printf("nPCM stream types:n");
??? for (val = 0; val <= SND_PCM_STREAM_LAST; val++)
??????? printf(" %sn",
????????? snd_pcm_stream_name((snd_pcm_stream_t)val));
??? ?
??? printf("nPCM access types:n");
??? for (val = 0; val <= SND_PCM_ACCESS_LAST; val++)
??????? printf(" %sn",
????????? snd_pcm_access_name((snd_pcm_access_t)val));
??? ?
??? printf("nPCM formats:n");
??? for (val = 0; val <= SND_PCM_FORMAT_LAST; val++)
??????? if (snd_pcm_format_name((snd_pcm_format_t)val)
????????? != NULL)
????????? printf(" %s (%s)n",
??????????? snd_pcm_format_name((snd_pcm_format_t)val),
??????????? snd_pcm_format_description(
?????????????????????????????? (snd_pcm_format_t)val));
??? ?
??? printf("nPCM subformats:n");
??? for (val = 0; val <= SND_PCM_SUBFORMAT_LAST;
?????????? val++)
??????? printf(" %s (%s)n",
????????? snd_pcm_subformat_name((
??????????? snd_pcm_subformat_t)val),
????????? snd_pcm_subformat_description((
??????????? snd_pcm_subformat_t)val));
??? ?
??? printf("nPCM states:n");
??? for (val = 0; val <= SND_PCM_STATE_LAST; val++)
??????? printf(" %sn",
?????????????? snd_pcm_state_name((snd_pcm_state_t)val));
??? ?
??? return 0;
??? }
清單一顯示了一些ALSA使用的PCM數(shù)據(jù)類型和參數(shù)。首先需要做的是包括頭文件。這些頭文件包含了所有庫函數(shù)的聲明。其中之一就是顯示ALSA庫的版本。
這個(gè)程序剩下的部分的迭代一些PCM數(shù)據(jù)類型,以流類型開始。ALSA為每次迭代的最后值提供符號(hào)常量名,并且提供功能函數(shù)以顯示某個(gè)特定值的描述字符串。你將會(huì)看到,ALSA支持許多格式,在我的1.0.15版本里,支持多達(dá)36種格式。
這個(gè)程序必須鏈接到alsalib庫,通過在編譯時(shí)需要加上-lasound選項(xiàng)。有些alsa庫函數(shù)使用dlopen函數(shù)以及浮點(diǎn)操作,所以您可能還需要加上-ldl,-lm選項(xiàng)。
下面是該程序的Makefile:
??? CC=gcc
??? TARGET=test
??? SRC=$(wildcard *.c)
??? ?
??? OBJECT= ${SRC:.c=.o}
??? INCLUDES=-I/usr/include/alsa
??? LDFLAGS=-lasound
??? ?
??? all:$(TARGET)
??? ?
??? $(OBJECT):$(SRC)
??????? $(CC) -c $(INCLUDES) $<
??? ?
??? $(TARGET):$(OBJECT)
??????? $(CC) -o $@ $< $(LDFLAGS)
??? ?
??? .PHONY:clean
??? ?
??? clean:
??????? @rm -rf $(OBJECT) $(TARGET) *~
????? ?
??? ?
??? Listing 2. Opening PCM Device and Setting Parameters
??? ?
??? /*
??? ?
??? This example opens the default PCM device, sets
??? some parameters, and then displays the value
??? of most of the hardware parameters. It does not
??? perform any sound playback or recording.
??? ?
??? */
??? ?
??? /* Use the newer ALSA API */
??? #define ALSA_PCM_NEW_HW_PARAMS_API
??? ?
??? /* All of the ALSA library API is defined
??? * in this header */
??? #include
??? ?
??? int main() {
??? int rc;
??? snd_pcm_t *handle;
??? snd_pcm_hw_params_t *params;
??? unsigned int val, val2;
??? int dir;
??? snd_pcm_uframes_t frames;
??? ?
??? /* Open PCM device for playback. */
??? rc = snd_pcm_open(&handle, "default",
??????????????????????? SND_PCM_STREAM_PLAYBACK, 0);
??? if (rc < 0) {
??????? fprintf(stderr,
??????????????? "unable to open pcm device: %sn",
??????????????? snd_strerror(rc));
??????? exit(1);
??? }
??? ?
??? /* Allocate a hardware parameters object. */
??? snd_pcm_hw_params_alloca(¶ms);
??? ?
??? /* Fill it in with default values. */
??? snd_pcm_hw_params_any(handle, params);
??? ?
??? /* Set the desired hardware parameters. */
??? ?
??? /* Interleaved mode */
??? snd_pcm_hw_params_set_access(handle, params,
????????????????????????? SND_PCM_ACCESS_RW_INTERLEAVED);
??? ?
??? /* Signed 16-bit little-endian format */
??? snd_pcm_hw_params_set_format(handle, params,
????????????????????????????????? SND_PCM_FORMAT_S16_LE);
??? ?
??? /* Two channels (stereo) */
??? snd_pcm_hw_params_set_channels(handle, params, 2);
??? ?
??? /* 44100 bits/second sampling rate (CD quality) */
??? val = 44100;
??? snd_pcm_hw_params_set_rate_near(handle,
???????????????????????????????????? params, &val, &dir);
??? ?
??? /* Write the parameters to the driver */
??? rc = snd_pcm_hw_params(handle, params);
??? if (rc < 0) {
??????? fprintf(stderr,
??????????????? "unable to set hw parameters: %sn",
??????????????? snd_strerror(rc));
??????? exit(1);
??? }
??? ?
??? /* Display information about the PCM interface */
??? ?
??? printf("PCM handle name = '%s'n",
???????????? snd_pcm_name(handle));
??? ?
??? printf("PCM state = %sn",
???????????? snd_pcm_state_name(snd_pcm_state(handle)));
??? ?
??? snd_pcm_hw_params_get_access(params,
????????????????????????????? (snd_pcm_access_t *) &val);
??? printf("access type = %sn",
???????????? snd_pcm_access_name((snd_pcm_access_t)val));
??? ?
??? snd_pcm_hw_params_get_format(params, &val);
??? printf("format = '%s' (%s)n",
??????? snd_pcm_format_name((snd_pcm_format_t)val),
??????? snd_pcm_format_description(
???????????????????????????????? (snd_pcm_format_t)val));
??? ?
??? snd_pcm_hw_params_get_subformat(params,
??????????????????????????? (snd_pcm_subformat_t *)&val);
??? printf("subformat = '%s' (%s)n",
??????? snd_pcm_subformat_name((snd_pcm_subformat_t)val),
??????? snd_pcm_subformat_description(
????????????????????????????? (snd_pcm_subformat_t)val));
??? ?
??? snd_pcm_hw_params_get_channels(params, &val);
??? printf("channels = %dn", val);
??? ?
??? snd_pcm_hw_params_get_rate(params, &val, &dir);
??? printf("rate = %d bpsn", val);
??? ?
??? snd_pcm_hw_params_get_period_time(params,
??????????????????????????????????????? &val, &dir);
??? printf("period time = %d usn", val);
??? ?
??? snd_pcm_hw_params_get_period_size(params,
??????????????????????????????????????? &frames, &dir);
??? printf("period size = %d framesn", (int)frames);
??? ?
??? snd_pcm_hw_params_get_buffer_time(params,
??????????????????????????????????????? &val, &dir);
??? printf("buffer time = %d usn", val);
??? ?
??? snd_pcm_hw_params_get_buffer_size(params,
???????????????????????????? (snd_pcm_uframes_t *) &val);
??? printf("buffer size = %d framesn", val);
??? ?
??? snd_pcm_hw_params_get_periods(params, &val, &dir);
??? printf("periods per buffer = %d framesn", val);
??? ?
??? snd_pcm_hw_params_get_rate_numden(params,
??????????????????????????????????????? &val, &val2);
??? printf("exact rate = %d/%d bpsn", val, val2);
??? ?
??? val = snd_pcm_hw_params_get_sbits(params);
??? printf("significant bits = %dn", val);
??? ?
??? snd_pcm_hw_params_get_tick_time(params,
????????????????????????????????????? &val, &dir);
??? printf("tick time = %d usn", val);
??? ?
??? val = snd_pcm_hw_params_is_batch(params);
??? printf("is batch = %dn", val);
??? ?
??? val = snd_pcm_hw_params_is_block_transfer(params);
??? printf("is block transfer = %dn", val);
??? ?
??? val = snd_pcm_hw_params_is_double(params);
??? printf("is double = %dn", val);
??? ?
??? val = snd_pcm_hw_params_is_half_duplex(params);
??? printf("is half duplex = %dn", val);
??? ?
??? val = snd_pcm_hw_params_is_joint_duplex(params);
??? printf("is joint duplex = %dn", val);
??? ?
??? val = snd_pcm_hw_params_can_overrange(params);
??? printf("can overrange = %dn", val);
??? ?
??? val = snd_pcm_hw_params_can_mmap_sample_resolution(params);
??? printf("can mmap = %dn", val);
??? ?
??? val = snd_pcm_hw_params_can_pause(params);
??? printf("can pause = %dn", val);
??? ?
??? val = snd_pcm_hw_params_can_resume(params);
??? printf("can resume = %dn", val);
??? ?
??? val = snd_pcm_hw_params_can_sync_start(params);
??? printf("can sync start = %dn", val);
??? ?
??? snd_pcm_close(handle);
??? ?
??? return 0;
??? }
?
清單2打開默認(rèn)的PCM設(shè)備,設(shè)置一些硬件參數(shù)并且打印出最常用的硬件參數(shù)值。它并不做任何回放或錄音的操作。snd_pcm_open打開默認(rèn)的PCM設(shè)備并設(shè)置訪問模式為PLAYBACK。這個(gè)函數(shù)返回一個(gè)句柄,這個(gè)句柄保存在第一個(gè)函數(shù)參數(shù)中。該句柄會(huì)在隨后的函數(shù)中用到。像其它函數(shù)一樣,這個(gè)函數(shù)返回一個(gè)整數(shù)。如果返回值小于0,則代碼函數(shù)調(diào)用出錯(cuò)。如果出錯(cuò),我們用snd_errstr打開錯(cuò)誤信息并退出。
為了設(shè)置音頻流的硬件參數(shù),我們需要分配一個(gè)類型為snd_pcm_hw_param的變量。分配用到函數(shù)宏snd_pcm_hw_params_alloca。下一步,我們使用函數(shù)snd_pcm_hw_params_any來初始化這個(gè)變量,傳遞先前打開的PCM流句柄。
接下來,我們調(diào)用API來設(shè)置我們所需的硬件參數(shù)。這些函數(shù)需要三個(gè)參數(shù):PCM流句柄,參數(shù)類型,參數(shù)值。我們?cè)O(shè)置流為交錯(cuò)模式,16位的樣本大小,2個(gè)信道,44100bps的采樣率。對(duì)于采樣率而言,聲音硬件并不一定就精確地支持我們所定的采樣率,但是我們可以使用函數(shù)snd_pcm_hw_params_set_rate_near來設(shè)置最接近我們指定的采樣率的采樣率。其實(shí)只有當(dāng)我們調(diào)用函數(shù)snd_pcm_hw_params后,硬件參數(shù)才會(huì)起作用。
程序的剩余部分獲得并打印一些PCM流參數(shù),包括周期和緩沖區(qū)大小。結(jié)果可能會(huì)因?yàn)槁曇粲布牟煌煌?br />
運(yùn)行該程序后,做實(shí)驗(yàn),改動(dòng)一些代碼。把設(shè)備名字改成hw:0,0,然后看結(jié)果是否會(huì)有變化。設(shè)置不同的硬件參數(shù)然后觀察結(jié)果的變化。
?
Listing 3. Simple Sound Playback
?
?
??? /*
??? ?
??? This example reads standard from input and writes
??? to the default PCM device for 5 seconds of data.
??? ?
??? */
??? ?
??? /* Use the newer ALSA API */
??? #define ALSA_PCM_NEW_HW_PARAMS_API
??? ?
??? #include
??? ?
??? int main() {
????? long loops;
????? int rc;
????? int size;
????? snd_pcm_t *handle;
????? snd_pcm_hw_params_t *params;
????? unsigned int val;
????? int dir;
????? snd_pcm_uframes_t frames;
????? char *buffer;
??? ?
????? /* Open PCM device for playback. */
????? rc = snd_pcm_open(&handle, "default",
??????????????????????? SND_PCM_STREAM_PLAYBACK, 0);
????? if (rc < 0) {
??????? fprintf(stderr,
??????????????? "unable to open pcm device: %sn",
??????????????? snd_strerror(rc));
??????? exit(1);
????? }
??? ?
????? /* Allocate a hardware parameters object. */
????? snd_pcm_hw_params_alloca(¶ms);
??? ?
????? /* Fill it in with default values. */
????? snd_pcm_hw_params_any(handle, params);
??? ?
????? /* Set the desired hardware parameters. */
??? ?
????? /* Interleaved mode */
????? snd_pcm_hw_params_set_access(handle, params,
????????????????????????? SND_PCM_ACCESS_RW_INTERLEAVED);
??? ?
????? /* Signed 16-bit little-endian format */
????? snd_pcm_hw_params_set_format(handle, params,
????????????????????????????????? SND_PCM_FORMAT_S16_LE);
??? ?
????? /* Two channels (stereo) */
????? snd_pcm_hw_params_set_channels(handle, params, 2);
??? ?
????? /* 44100 bits/second sampling rate (CD quality) */
????? val = 44100;
????? snd_pcm_hw_params_set_rate_near(handle, params,
????????????????????????????????????? &val, &dir);
??? ?
????? /* Set period size to 32 frames. */
????? frames = 32;
????? snd_pcm_hw_params_set_period_size_near(handle,
????????????????????????????????? params, &frames, &dir);
??? ?
????? /* Write the parameters to the driver */
????? rc = snd_pcm_hw_params(handle, params);
????? if (rc < 0) {
??????? fprintf(stderr,
??????????????? "unable to set hw parameters: %sn",
??????????????? snd_strerror(rc));
??????? exit(1);
????? }
??? ?
????? /* Use a buffer large enough to hold one period */
????? snd_pcm_hw_params_get_period_size(params, &frames,
??????????????????????????????????????? &dir);
????? size = frames * 4; /* 2 bytes/sample, 2 channels */
????? buffer = (char *) malloc(size);
??? ?
????? /* We want to loop for 5 seconds */
????? snd_pcm_hw_params_get_period_time(params,
??????????????????????????????????????? &val, &dir);
????? /* 5 seconds in microseconds divided by
?????? * period time */
????? loops = 5000000 / val;
??? ?
????? while (loops > 0) {
??????? loops--;
??????? rc = read(0, buffer, size);
??????? if (rc == 0) {
????????? fprintf(stderr, "end of file on inputn");
????????? break;
??????? } else if (rc != size) {
????????? fprintf(stderr,
????????????????? "short read: read %d bytesn", rc);
??????? }
??????? rc = snd_pcm_writei(handle, buffer, frames);
??????? if (rc == -EPIPE) {
????????? /* EPIPE means underrun */
????????? fprintf(stderr, "underrun occurredn");
????????? snd_pcm_prepare(handle);
??????? } else if (rc < 0) {
????????? fprintf(stderr,
????????????????? "error from writei: %sn",
????????????????? snd_strerror(rc));
??????? } else if (rc != (int)frames) {
????????? fprintf(stderr,
????????????????? "short write, write %d framesn", rc);
??????? }
????? }
??? ?
????? snd_pcm_drain(handle);
????? snd_pcm_close(handle);
????? free(buffer);
??? ?
????? return 0;
??? }
清單3擴(kuò)展了之前的示例。向聲卡中寫入了一些聲音樣本以實(shí)現(xiàn)聲音回放。在這個(gè)例子中,我們從標(biāo)準(zhǔn)輸入中讀取數(shù)據(jù),每個(gè)周期讀取足夠多的數(shù)據(jù),然后將它們寫入到聲卡中,直到5秒鐘的數(shù)據(jù)全部傳輸完畢。
這個(gè)程序的開始處和之前的版本一樣---打開PCM設(shè)備、設(shè)置硬件參數(shù)。我們使用由ALSA自己選擇的周期大小,申請(qǐng)?jiān)摯笮〉木彌_區(qū)來存儲(chǔ)樣本。然后我們找出周期時(shí)間,這樣我們就能計(jì)算出本程序?yàn)榱四軌虿シ?秒鐘,需要多少個(gè)周期。
在處理數(shù)據(jù)的循環(huán)中,我們從標(biāo)準(zhǔn)輸入中讀入數(shù)據(jù),并往緩沖區(qū)中填充一個(gè)周期的樣本。然后檢查并處理錯(cuò)誤,這些錯(cuò)誤可能是由到達(dá)文件結(jié)尾,或讀取的數(shù)據(jù)長度與我期望的數(shù)據(jù)長度不一致導(dǎo)致的。
我們調(diào)用snd_pcm_writei來發(fā)送數(shù)據(jù)。它操作起來很像內(nèi)核的寫系統(tǒng)調(diào)用,只是這里的大小參數(shù)是以幀來計(jì)算的。我們檢查其返回代碼值。返回值為EPIPE表明發(fā)生了underrun,使得PCM音頻流進(jìn)入到XRUN狀態(tài)并停止處理數(shù)據(jù)。從該狀態(tài)中恢復(fù)過來的標(biāo)準(zhǔn)方法是調(diào)用snd_pcm_prepare函數(shù),把PCM流置于PREPARED狀態(tài),這樣下次我們向該P(yáng)CM流中數(shù)據(jù)時(shí),它就能重新開始處理數(shù)據(jù)。如果我們得到的錯(cuò)誤碼不是EPIPE,我們把錯(cuò)誤碼打印出來,然后繼續(xù)。最后,如果寫入的幀數(shù)不是我們期望的,則打印出錯(cuò)誤消息。
這個(gè)程序一直循環(huán),直到5秒鐘的幀全部傳輸完,或者輸入流讀到文件結(jié)尾。然后我們調(diào)用snd_pcm_drain把所有掛起沒有傳輸完的聲音樣本傳輸完全,最后關(guān)閉該音頻流,釋放之前動(dòng)態(tài)分配的緩沖區(qū),退出。
我們可以看到這個(gè)程序沒有什么用,除非標(biāo)準(zhǔn)輸入被重定向到了其它其它的文件。嘗試用設(shè)備/dev/urandom來運(yùn)行這個(gè)程序,該設(shè)備產(chǎn)生隨機(jī)數(shù)據(jù):
?./example3
隨機(jī)數(shù)據(jù)會(huì)產(chǎn)生5秒鐘的白色噪聲。
然后,嘗試把標(biāo)準(zhǔn)輸入重定向到設(shè)備/dev/null和/dev/zero上,并比較結(jié)果。改變一些參數(shù),例如采樣率和數(shù)據(jù)格式,然后查看結(jié)果的變化。
?
Listing 4. Simple Sound Recording
?
?
??? /*
??? ?
??? This example reads from the default PCM device
??? and writes to standard output for 5 seconds of data.
??? ?
??? */
??? ?
??? /* Use the newer ALSA API */
??? #define ALSA_PCM_NEW_HW_PARAMS_API
??? ?
??? #include
??? ?
??? int main() {
??? long loops;
??? int rc;
??? int size;
??? snd_pcm_t *handle;
??? snd_pcm_hw_params_t *params;
??? unsigned int val;
??? int dir;
??? snd_pcm_uframes_t frames;
??? char *buffer;
??? ?
??? /* Open PCM device for recording (capture). */
??? rc = snd_pcm_open(&handle, "default",
??????????????????????? SND_PCM_STREAM_CAPTURE, 0);
??? if (rc < 0) {
??????? fprintf(stderr,
??????????????? "unable to open pcm device: %sn",
??????????????? snd_strerror(rc));
??????? exit(1);
??? }
??? ?
??? /* Allocate a hardware parameters object. */
??? snd_pcm_hw_params_alloca(¶ms);
??? ?
??? /* Fill it in with default values. */
??? snd_pcm_hw_params_any(handle, params);
??? ?
??? /* Set the desired hardware parameters. */
??? ?
??? /* Interleaved mode */
??? snd_pcm_hw_params_set_access(handle, params,
????????????????????????? SND_PCM_ACCESS_RW_INTERLEAVED);
??? ?
??? /* Signed 16-bit little-endian format */
??? snd_pcm_hw_params_set_format(handle, params,
????????????????????????????????? SND_PCM_FORMAT_S16_LE);
??? ?
??? /* Two channels (stereo) */
??? snd_pcm_hw_params_set_channels(handle, params, 2);
??? ?
??? /* 44100 bits/second sampling rate (CD quality) */
??? val = 44100;
??? snd_pcm_hw_params_set_rate_near(handle, params,
????????????????????????????????????? &val, &dir);
??? ?
??? /* Set period size to 32 frames. */
??? frames = 32;
??? snd_pcm_hw_params_set_period_size_near(handle,
????????????????????????????????? params, &frames, &dir);
??? ?
??? /* Write the parameters to the driver */
??? rc = snd_pcm_hw_params(handle, params);
??? if (rc < 0) {
??????? fprintf(stderr,
??????????????? "unable to set hw parameters: %sn",
??????????????? snd_strerror(rc));
??????? exit(1);
??? }
??? ?
??? /* Use a buffer large enough to hold one period */
??? snd_pcm_hw_params_get_period_size(params,
????????????????????????????????????????? &frames, &dir);
??? size = frames * 4; /* 2 bytes/sample, 2 channels */
??? buffer = (char *) malloc(size);
??? ?
??? /* We want to loop for 5 seconds */
??? snd_pcm_hw_params_get_period_time(params,
???????????????????????????????????????????? &val, &dir);
??? loops = 5000000 / val;
??? ?
??? while (loops > 0) {
??????? loops--;
??????? rc = snd_pcm_readi(handle, buffer, frames);
??????? if (rc == -EPIPE) {
????????? /* EPIPE means overrun */
????????? fprintf(stderr, "overrun occurredn");
????????? snd_pcm_prepare(handle);
??????? } else if (rc < 0) {
????????? fprintf(stderr,
????????????????? "error from read: %sn",
????????????????? snd_strerror(rc));
??????? } else if (rc != (int)frames) {
????????? fprintf(stderr, "short read, read %d framesn", rc);
??????? }
??????? rc = write(1, buffer, size);
??????? if (rc != size)
????????? fprintf(stderr,
????????????????? "short write: wrote %d bytesn", rc);
??? }
??? ?
??? snd_pcm_drain(handle);
??? snd_pcm_close(handle);
??? free(buffer);
??? ?
??? return 0;
??? }
清單4類似于清單3中的程序,除了這里的程序時(shí)做聲音的抓取(錄音)。當(dāng)打開PCM設(shè)備時(shí)我們指定打開模式為SND_PCM_STREAM_CPATURE。在主循環(huán)中,我們調(diào)用snd_pcm_readi從聲卡中讀取數(shù)據(jù),并把它們寫入到標(biāo)準(zhǔn)輸出。同樣地,我們檢查是否有overrun,如果存在,用與前例中相同的方式處理。
運(yùn)行清單4的程序?qū)浿茖⒔?秒鐘的聲音數(shù)據(jù),并把它們發(fā)送到標(biāo)準(zhǔn)輸出。你也可以重定向到某個(gè)文件。如果你有一個(gè)麥克風(fēng)連接到你的聲卡,可以使用某個(gè)混音程序(mixer)設(shè)置錄音源和級(jí)別。同樣地,你也可以運(yùn)行一個(gè)CD播放器程序并把錄音源設(shè)成CD。嘗試運(yùn)行程序4并把輸出定向到某個(gè)文件,然后運(yùn)行程序3播放該文件里的聲音數(shù)據(jù):
./listing4 > sound.raw
./listing3 < sound.raw
如果你的聲卡支持全雙工,你可以通過管道把兩個(gè)程序連接起來,這樣就可以從聲卡中聽到錄制的聲音:
./listing4 | ./listing3
同樣地,您可以做實(shí)驗(yàn),看看采樣率和樣本格式的變化會(huì)產(chǎn)生什么影響。
?
高級(jí)特性
在前面的例子中,PCM流是以阻塞模式操作的,也就是說,直到數(shù)據(jù)已經(jīng)傳送完,PCM接口調(diào)用才會(huì)返回。在事件驅(qū)動(dòng)的交互式程序中,這樣會(huì)長時(shí)間阻塞應(yīng)用程序,通常是不能接受的。ALSA支持以非阻塞模式打開音頻流,這樣讀寫函數(shù)調(diào)用后立即返回。如果數(shù)據(jù)傳輸被掛起,調(diào)用不能被處理,ALSA就是返回一個(gè)EBUSY的錯(cuò)誤碼。
許多圖形應(yīng)用程序使用回調(diào)來處理事件。ALSA支持以異步的方式打開一個(gè)PCM音頻流。這使得當(dāng)某個(gè)周期的樣本數(shù)據(jù)被傳輸完后,某個(gè)已注冊(cè)的回調(diào)函數(shù)將會(huì)調(diào)用。
?
這里用到的snd_pcm_readi和snd_pcm_writei調(diào)用和Linux下的讀寫系統(tǒng)調(diào)用類似。字母i表示處理的幀是交錯(cuò)式(interleaved)的。ALSA中存在非交互模式的對(duì)應(yīng)的函數(shù)。Linux下的許多設(shè)備也支持mmap系統(tǒng)調(diào)用,這個(gè)調(diào)用將設(shè)備內(nèi)存映射到主內(nèi)存,這樣數(shù)據(jù)就可以用指針來維護(hù)。ALSA也運(yùn)行以mmap模式打開一個(gè)PCM信道,這允許有效的零拷貝(zero copy)方式訪問聲音數(shù)據(jù)。
?
總結(jié)
我希望這篇文章能夠激勵(lì)你嘗試編寫某些ALSA程序。伴隨著2.6內(nèi)核在Linux發(fā)布版本(distributions)中被廣泛地使用,ALSA也將被廣泛地采用。它的高級(jí)特征將幫助Linux音頻程序更好地向前發(fā)展。
Jaroslav Kysela和Takashi lwai幫助查閱了本文的草稿并提出了寶貴的意見,在此表示感謝。
?