ALSA聲音程式設計介紹
ALSA是Advanced Linux Sound Architecture簡稱。它包含一組kernel 驅動,一個應用程式設計介面(API)庫以及一組工具函式。本文中,我們會向讀者展示ALSA專案和組成部件的概況。後面會重點介紹ALSA PCM介面的程式設計。
ALSA不僅僅是sound API。選擇ALSA可以讓你最大程度的控制和執行執行低階的audio函式,或者使用其它sound API不支援的特定功能。如果你已經寫了一個audio應用程式,那麼你可能希望為它增加ALSA sound驅動支援。如果你的主要興趣不是audio,僅僅是想播放聲音,那麼你最好使用高階的sound toolkits,比如SDL,OpenAL或者桌面環境提供的其他開發包。如果你想使用ALSA,那麼要確保你的Linux系統支援ALSA。
History of ALSA
ALSA專案的出現是因為Linux kernel sound driver OSS/Free drivers不能得到很好的維護,而導致驅動無法支援新的sound技術。Jaroslav Kysela最初為一個sound card寫了一個驅動,啟動了這個專案,隨著時間的推移越來越多的開發者加入進來,重新定義了API以支援更多的音效卡。
在Linux kernel 2.5的開發過程中,ALSA被merged到官方核心中。隨著kernel2.6的釋出,ALSA成為穩定版核心的一部分,並且得到了廣泛使用。
Digital Audio Basics
聲音由空氣壓力變化的波形組成,被轉換器(如麥克風)轉換為電訊號。一個模數轉換器(ADC)把模擬電壓訊號轉換為離散值,稱為取樣,取樣是按照固定時間間隔進行的,稱為取樣率。傳送這些取樣到數模轉換器(DAC),再輸出到loudspeaker,原始的聲音就被重現了。
取樣的位數大小,是決定聲音數字精度的一個因素,另外一個主要因素是取樣率。奈奎斯特理論指出當訊號頻寬小於1/2取樣率時,可通過取樣訊號還原出原始訊號。
ALSA Basics
ALSA包括支援各種音效卡的Kernel裝置驅動,API庫libasound。應用開發者應該使用API而不是kernel系統呼叫介面。API庫函式提供了高層次,程式設計友好的介面,開發者使用邏輯裝置名而不需要考慮低階細節(如裝置檔案)。
與ALSA相反,OSS/Free要求應用在kernel系統呼叫級進行程式設計,這就需要開發者指定裝置檔名並且使用ioctl來完成功能。為了相容OSS,ALSA提供了核心模組來模擬OSS/Free聲音驅動,所以大部分現存的audio應用都無須修。libaoss是模擬包裝庫,可以用來模擬OSS/Free API而不需要核心模組的模擬。
ALSA還提供了plugins能力,允許擴充套件一個新裝置,甚至包括完全用軟體實現的虛擬裝置。ALSA提供了一組命令列工具,包括mixer,聲音檔案播放,以及對特定音效卡特定功能的控制。
ALSA Architecture
ALSA API可以分為以下幾個主要部分:
- Control介面:一個通用的功能,用來管理音效卡的暫存器以及查詢可用裝置。
- PCM 介面:管理數字audio capture和playback的介面,本文的其餘部分將主要介紹著個介面,因為這是audio應用最常用的介面
- Raw MIDI介面:支援MIDI(Musical Instrument DIgital Interface,電子音樂裝置的標準)。這個API提供了對音效卡MIDI bus的訪問。Raw介面由MIDI events驅動,程式負責管理協議和計時
- Timer 介面:提供對音效卡上計時硬體的訪問,用於同步聲音事件。
- Sequencer interface:一個MIPI程式設計和聲音同步介面,比raw MIDI介面級別更高,它管理大部分MIDI協議和計時。
- Mixer介面:控制音效卡上的訊號路由和音量調節的裝置。它是建立在control介面之上的。
Device Naming
API操作的是邏輯裝置名而不是裝置檔案,裝置名可以是真正的硬體裝置或者外掛。硬體裝置使用hw:i,j這種格式,i是卡號而j是在這個卡上的裝置。第一個聲音裝置是hw:0,0。第一個sound裝置的別名為defalut,本文後面的例子都使用default。Plugins使用另外一種命名模式:例如plughw:是一個外掛除了提供對硬體裝置的訪問還提供某種功能,比如軟體實現取樣率轉換,因為硬體不支援這種操作。dmix外掛允許合成幾路資料 dshare外掛允許把單路資料動態分配到不同的應用中。
Sound Buffers and Data Transfer
音效卡有一個硬體buffer。在錄音時(capture)儲存錄音的取樣值,當buffer填滿後音效卡生成一箇中斷,kernel sound驅動使用DMA傳輸取樣資料到記憶體buffer。類似的在playback時,應用buffer資料通過DMA傳輸給sound card的硬體buffer
這個硬體buffer是一個ring buffers,意味著當到達buffer末端後,就會重新回到起始端。一個指標用來維護硬體buffer和應用buffer的當前位置。在核心外部,僅能訪問application buffer,所以在這裡我們主要討論application buffer。
buffer的尺寸可以通過ALSA庫函式呼叫指定。buffer可以非常的大,一次傳輸完整個buffer的資料可能導致無法接收的延遲,所以,ALSA把這個buffer分割為一系列periods,以period做為傳輸資料的單位。
Period由多個frames組成,每一個frames包含在一個時間點的取樣值,對於立體聲裝置來說,一個frames包含兩個channels的採用,圖1演示了一個buffer, period,sample之間的關係,在這裡,左右聲道的資料儲存在一幀中,這種模式稱為interleaved。對於non-interleaved 模式,所有的左聲道資料存放在一起,然後所有的右聲道儲存在一起。
Over and Under Run
當一個聲音裝置被啟用,資料持續的在硬體和應用buffer間傳送。在錄音情況下(capture),如果應用沒有快速的從buffer中讀取資料,環狀buffer將被新到的資料覆蓋,導致資料丟失,我們稱之為overrun。在playback情況下,如果應用無法快速傳送資料到buffer中,那麼導致hardware無資料可播放,這種情況我們稱之為underrun。ALSA文件有時把這兩種情況統稱為XRUN。正確設計的應用最小化XRUN的發生並且在XRUN發生後能夠恢復操作。
A typical Sound Application
通常可以用下面偽程式碼表示PCM介面程式設計模式:
open interface for capture or playback
set hardware parameters(access mode, data format, channels, rate, etc.)
while there is data to be processed:
read PCM data(capture) or write PCM data(playback)
close interface
Listing 1. Display Some PCM Types and Formats
#include <alsa/asoundlib.h>
int main() {
int val;
printf("ALSA library version: %s\n", SND_LIB_VERSION_STR);
printf("PCM stream types:\n");
for (val = 0; val <= SND_PCM_STREAM_LAST; val++) {
printf(" %s\n", snd_pcm_stream_name((snd_pcm_stream_t)val));
}
printf("PCM access types:\n");
for (val = 0; val <= SND_PCM_ACCESS_LAST; val++) {
printf(" %s\n", snd_pcm_access_name((snd_pcm_access_t)val));
}
printf("PCM formats:\n");
for (val = 0; val <= SND_PCM_STREAM_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));
}
return 0;
}
gcc -o test test.c -lasound
編譯為可執行檔案,程式必須連結ALSA庫,libasound。有些ALSA庫函式需要使用dlopen功能和浮點運算,所以有時需要增加-ldl和-lmgcc -o listing1 listing1.c -lasound在我的機器上,執行結果如下
ALSA library version: 1.0.22
PCM stream types:
PLAYBACK
CAPTURE
PCM access types:
MMAP_INTERLEAVED
MMAP_NONINTERLEAVED
MMAP_COMPLEX
RW_INTERLEAVED
RW_NONINTERLEAVED
PCM formats:
S8 (Signed 8 bit)
U8 (Unsigned 8 bit)
PCM subformats:
STD (Standard)
listing1 展示了一些ALSA使用的PCM資料型別和引數。需要包含標頭檔案alsa/asoundlib.h,ALSA庫函式以及常用巨集都在這個檔案中定義。這個程式的其餘部分重複的列印了PCM資料型別。
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 <alsa/asoundlib.h>
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) {
printf("unable to open pcm device\n");
}
/* 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 (stero) */
snd_pcm_hw_params_set_channels(handle, params, 2);
/* 44100 bits/second sampling rate */
val = 44100;
snd_pcm_hw_params_set_rate_near(handle, params, &val, &dir);
/* Write the parameters to the dirver */
rc = snd_pcm_hw_params(handle, params);
if (rc < 0) {
printf("unable to set hw parameters: %s\n", snd_strerror(rc));
exit(1);
}
/* Display information about the PCM interface */
printf("PCM handle name = '%s'\n", snd_pcm_name(handle));
printf("PCM state = %s\n", snd_pcm_state_name(snd_pcm_state(handle)));
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=%d\n", val);
snd_pcm_hw_params_get_rate(params, &val, &dir);
printf("rate = %d bps\n", val);
snd_pcm_hw_params_get_period_time(params, &val, &dir);
printf("period time = %d us\n", val);
snd_pcm_hw_params_get_period_size(params, &frames, &dir);
printf("period size = %d frames\n", (int)frames);
snd_pcm_hw_params_get_buffer_time(params, &val, &dir);
printf("buffer time = %d us\n", val);
snd_pcm_hw_params_get_buffer_size(params, (snd_pcm_uframes_t *) &val);
printf("buffer size = %d frames\n", val);
snd_pcm_hw_params_get_periods(params, &val, &dir);
printf("periods per buffer = %d frames\n", val);
snd_pcm_hw_params_get_rate_numden(params, &val, &val2);
printf("exact rate=%d/%d bps\n", val, val2);
val = snd_pcm_hw_params_get_sbits(params);
printf("significant bits = %d\n", val);
snd_pcm_hw_params_get_tick_time(params, &val, &dir);
printf("tick time = %d us\n", val);
val = snd_pcm_hw_params_is_batch(params);
printf("is batch = %d\n", val);
val = snd_pcm_hw_params_is_block_transfer(params);
printf("is block transfer = %d\n", val);
val = snd_pcm_hw_params_is_double(params);
printf("is double = %d\n", val);
val = snd_pcm_hw_params_is_half_duplex(params);
printf("is half duplex = %d\n", val);
val = snd_pcm_hw_params_is_joint_duplex(params);
printf("is joint duplex = %d\n", val);
val = snd_pcm_hw_params_can_overrange(params);
printf("can overrange = %d\n", val);
val = snd_pcm_hw_params_can_mmap_sample_resolution(params);
printf("can mmap = %d\n", val);
val = snd_pcm_hw_params_can_pause(params);
printf("can overrange = %d\n", val);
val = snd_pcm_hw_params_can_sync_start(params);
printf("can sync start = %d\n", val);
snd_pcm_close(handle);
return 0;
}
執行結果為
PCM handle name = 'default'
PCM state = PREPARED
format = 'S16_LE'(Signed 16 bit Little Endian)
subformat = 'STD' (Standard)
channels=2
rate = 44100 bps
period time = 23219 us
period size = 1024 frames
buffer time = 23219 us
buffer size = 1048576 frames
periods per buffer = 1024 frames
exact rate=44100/1 bps
significant bits = 16
tick time = 0 us
is batch = 0
is block transfer = 1
is double = 0
is half duplex = 0
is joint duplex = 0
can overrange = 0
can mmap = 0
can overrange = 1
can sync start = 0
List2開啟預設的PCM裝置,設定一些引數,然後顯示大部分的硬體引數。這個測試程式不會執行playback和recording。呼叫snd_pcm_open開啟預設的PCM裝置,開啟模式為PLAYBACK。這個函式通過第一個引數返回一個控制代碼,後續的操作使用這個控制代碼操作這個PCM裝置。像大部分ALSA庫函式呼叫,snd_pcm_open返回一個整數表示呼叫狀態,負數指明出錯原因。為了使測試程式碼更簡潔,我忽略了大部分的函式返回值。在產品級應用中,開發者應該檢查每一個API呼叫,以便提供相應的出錯處理。
為了設定流的硬體引數,我們需要分配一個snd_pcm_hw_param_t變數,首先呼叫macro snd_pcm_hw_params_alloca,接下來使用snd_pcm_hw_params_any來初始化這個變數。接下來使用ALSA提供的API設定PCM 流的硬體引數,這些API的引數形式為(PCM handle, snd_pcm_hw_param_t *, val)。我們設定流為interleaved mode,16 bit sample size,2 channels和44100Hz取樣率。對於取樣率,音效卡硬體不一定支援指定的取樣率。我們使用函式snd_pcm_hw_params_set_rate_near請求設定指定值附近的取樣率。在呼叫snd_pcm_hw_params函式之前,所有設定的硬體引數並不會被啟用。
這個程式的其他部分獲得並顯示PCM 流的引數,包括週期和buffer sizes。顯示結果依賴於測試機器的硬體。
在你的機器上執行這個程式,並嘗試做一些修改。比如把device名從defalut改為hw:0,0或者plughw:檢視結果是否改變。修改硬體引數的值,檢視顯示部分的變化。
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 <alsa/asoundlib.h>
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) {
printf("unable to open pcm device: %s\n", snd_strerror(rc));
exit(1);
}
snd_pcm_hw_params_alloca(¶ms);
snd_pcm_hw_params_any(handle, params);
snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED);
snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE);
snd_pcm_hw_params_set_channels(handle, params, 2);
val = 44100;
snd_pcm_hw_params_set_rate_near(handle, params, &val, &dir);
frames = 32;
snd_pcm_hw_params_set_period_size_near(handle, params, &frames, &dir);
rc = snd_pcm_hw_params(handle, params);
if (rc < 0) {
printf("unable to set hw parameters: %s\n", snd_strerror(rc));
exit(1);
}
snd_pcm_hw_params_get_period_size(params, &frames, &dir);
size = frames * 4;
buffer = (char *)malloc(size);
snd_pcm_hw_params_get_period_time(params, &val, &dir);
loops = 5000000 / val;
while (loops > 0) {
loops--;
rc = read(0, buffer, size);
rc = snd_pcm_writei(handle, buffer, frames);
if (rc == -EPIPE) {
printf("underrun occured\n");
}
else if (rc < 0) {
printf("error from writei: %s\n", snd_strerror(rc));
}
}
snd_pcm_drain(handle);
snd_pcm_close(handle);
free(buffer);
return 0;
}
Listing 3擴充套件了前面的例子,寫了一些隨機的取樣資料到音效卡的playback。在這個例子中,我們從標準輸入獲取資料,當獲取的資料達到一個period時,就把取樣資料寫入音效卡。
這個程式的開始部分和前面的例子一樣:開啟PCM裝置並設定硬體引數。我們使用ALSA選擇的period size作為儲存取樣資料buffer的大小。通過這個period time我們就可以計算出5秒鐘大概需要多少個periods。
在迴圈中我們從標準輸入讀取資料填充一個period的取樣資料到buffer。我們檢查並處理檔案結束以及讀取位元組數和期望數不一致的情況。
使用snd_pcm_write函式傳送資料到PCM裝置。這個操作很像核心的write系統呼叫,除了size的單位是frames。檢查返回的錯誤碼,錯誤碼EPIPE表示underrun錯誤發生,這會導致PCM 流進入了XRUN狀態,停止處理資料。從這種狀態恢復的標準方法是呼叫snd_pcm_prepare,使得流進入PREPARED狀態,這樣我們就可以再次向流中寫入資料了,如果接收的是其他的錯誤碼,那麼我們顯示錯誤碼,並且繼續執行。
這個程式迴圈執行5秒或者到達輸入檔案的結束符。我們呼叫snd_pcm_drain使得所有pending的取樣資料被傳輸,然後關閉這個流。釋放分配的buffer並退出。
執行這個程式,並使用/dev/urandom作為輸入
./example < /dev/urandom
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 ALI */
#define ALSA_PCM_NEW_HW_PARAMS_API
#include <alsa/asoundlib.h>
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;
rc = snd_pcm_open(&handle, "default", SND_PCM_STREAM_CAPTURE, 0);
<pre name="code" class="programlisting"> if (rc < 0) {
fprintf(stderr,
"unable to open pcm device: %s\n",
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: %s\n",
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 occurred\n");
snd_pcm_prepare(handle);
} else if (rc < 0) {
fprintf(stderr,
"error from read: %s\n",
snd_strerror(rc));
} else if (rc != (int)frames) {
fprintf(stderr, "short read, read %d frames\n", rc);
}
rc = write(1, buffer, size);
if (rc != size)
fprintf(stderr,
"short write: wrote %d bytes\n", rc);
}
snd_pcm_drain(handle);
snd_pcm_close(handle);
free(buffer);
return 0;
}
Listing 4和Listing3很相像,除了執行了PCM capture。在我們開啟PCM流時,我們指定了操作模式為SND_PCM_STREAM_CAPTURE。主迴圈中,使用snd_pcm_readi從本地聲音硬體讀取samples,然後把取樣輸出到標準輸出。如果你有一個microphone連線到音效卡,使用mixer程式設定錄音源和級別。相應的,你可以執行一個CD player程式然後設定錄音源為CD。如果你把listing4的輸出重定向到一個檔案,那麼你可以用listing3來播放listing4的錄音資料:
./listing4 > sound.raw
./listing3 < sound.raw
如果你的音效卡支援全雙工,那麼你可以通過管道把這個執行命令連線起來,可以播放錄音資料
./listing4 | ./listing3
通過修改PCM引數,你可以體驗取樣率和格式變化的效果。
Advanced Features
在前面的例子中,PCM流工作在阻塞模式,也就是說,在資料傳輸結束完之前不會函式不會返回。在一個互動式為主導的應用中,這種情況將使得應用長時間無響應。ALSA允許非阻塞模式操縱流,這種方式下read/write操作立刻返回,如果資料傳輸無法立刻進行,那麼read/write立刻返回一個EBUSY錯誤碼。
有些圖形介面應用使用事件callback機制。ALSA支援非同步PCM stream,當一個period的取樣資料完成以後,註冊的callback被呼叫。
snd_pcm_readi和snd_pcm_writei呼叫類似於linux的read/write系統呼叫。字母i表示幀是interleaved;相對的是non-interleaved模式。Linux下的一些裝置也支援mmap系統呼叫,ALSA支援mmap模式開啟PCM channel,應用層不需要資料copy就可以有效訪問聲音資料。
Conclusion
我希望這篇文章能夠成為你使用ALSA的動力。當2.6 kernel被大部分發行版使用後,ALSA的使用更加廣泛,它的高階features應該能幫助linux audio應用開發者。
感謝Jaroslav Kysela和Takashi Iwai reviewing本文的初稿並提供了有用的反饋。