1. 程式人生 > >ALSA聲音程式設計介紹+underrun

ALSA聲音程式設計介紹+underrun

ALSA聲音程式設計介紹+underrun

2014年09月02日 14:18:28 武溪嵌人 閱讀數:2810

這裡瞭解一下各個引數的含義以及一些基本概念。

樣本長度(sample):樣本是記錄音訊資料最基本的單位,常見的有8位和16位。

通道數(channel):該引數為1表示單聲道,2則是立體聲。

楨(frame):楨記錄了一個聲音單元,其長度為樣本長度與通道數的乘積。

取樣率(rate):每秒鐘取樣次數,該次數是針對楨而言。

週期(period):音訊裝置一次處理所需要的楨數,對於音訊裝置的資料訪問以及音訊資料的儲存,都是以此為單位。

交錯模式(interleaved):是一種音訊資料的記錄方式,在交錯模式下,資料以連續楨的形式存放,即首先記錄完楨1的左聲道樣本和右聲道樣本(假設為立體聲格式),再開始楨2的記錄。而在非交錯模式下,首先記錄的是一個週期內所有楨的左聲道樣本,再記錄右聲道樣本,資料是以連續通道的方式儲存。不過多數情況下,我們只需要使用交錯模式就可以了。

 

 

英文原文:http://www.linuxjournal.com/article/6735
period(週期):硬體中中斷間的間隔時間。它表示輸入延時。
音效卡介面中有一個指標來指示音效卡硬體快取區中當前的讀寫位置。只要介面在執行,這個指標將迴圈地指向快取區中的某個位置。
frame size = sizeof(one sample) * nChannels
alsa中配置的快取(buffer)和週期(size)大小在runtime中是以幀(frames)形式儲存的。
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表示高階Linux聲音體系結構(Advanced Linux Sound Architecture)。它由一系列核心驅動,應用程式編譯介面(API)以及支援Linux下聲音的實用程式組成。這篇文章裡,我將簡單介紹ALSA專案的基本框架以及它的軟體組成。主要集中介紹PCM介面程式設計,包括您可以自動實踐的程式示例。

您使用ALSA的原因可能就是因為它很新,但它並不是唯一可用的聲音API。如果您想完成低階的聲音操作,以便能夠最大化地控制聲音並最大化地提高效能,或者如果您使用其它聲音API沒有的特性,那麼ALSA是很好的選擇。如果您已經寫了一個音訊程式,你可能想要為ALSA音效卡驅動新增本地支援。如果您對音訊不感興趣,只是想播放音訊檔案,那麼高階的API將是更好的選擇,比如SDL,OpenAL以及那些桌面環境提供的工具集。另外,您只能在有ALSA支援的Linux環境中使用ALSA。

ALSA歷史
ALSA專案發起的起因是Linux下的音效卡驅動(OSS/Free drivers)沒有得到積極的維護。並且落後於新的音效卡技術。Jaroslav Kysela早先寫了一個音效卡驅動,並由此開始了ALSA專案,隨便,更多的開發者加入到開發隊伍中,更多的音效卡得到支援,API的結構也得到了重組。

Linux核心2.5在開發過程中,ALSA被合併到了官方的原始碼樹中。在釋出核心2.6後,ALSA已經內建在穩定的核心版本中並將廣泛地使用。

數字音訊基礎
聲音由變化的氣壓組成。它被麥克風這樣的轉換器轉換成電子形式。模/數(ADC)轉換器將模擬電壓轉換成離散的樣本值。聲音以固定的時間間隔被取樣,取樣的速率稱為取樣率。把樣本輸出到數/模(DAC)轉換器,比如擴音器,最後轉換成原來的模擬訊號。
樣本大小以位來表示。樣本大小是影響聲音被轉換成數字訊號的精確程度的因素之一。另一個主要的因素是取樣率。奈奎斯特(Nyquist)理論中,只要離散系統的奈奎斯特頻率高於取樣訊號的最高頻率或頻寬,就可以避免混疊現象。

ALSA基礎
ALSA由許多音效卡的音效卡驅動程式組成,同時它也提供一個稱為libasound的API庫。應用程式開發者應該使用libasound而不是核心中的ALSA介面。因為libasound提供最高階並且程式設計方便的程式設計介面。並且提供一個裝置邏輯命名功能,這樣開發者甚至不需要知道類似裝置檔案這樣的低層介面。相反,OSS/Free驅動是在核心系統呼叫級上程式設計,它要求開發者提供裝置檔名並且利用ioctrl來實現相應的功能。為了向後相容,ALSA提供核心模組來模擬OSS,這樣之前的許多在OSS基礎上開發的應用程式不需要任何改動就可以在ALSA上執行。另外,libaoss庫也可以模擬OSS,而它不需要核心模組。
ALSA包含外掛功能,使用外掛可以擴充套件新的音效卡驅動,包括完全用軟體實現的虛擬音效卡。ALSA提供一系列基於命令列的工具集,比如混音器(mixer),音訊檔案播放器(aplay),以及控制特定音效卡特定屬性的工具。

ALSA體系結構
ALSA API可以分解成以下幾個主要的介面:
1 控制介面:提供管理音效卡註冊和請求可用裝置的通用功能
2 PCM介面:管理數字音訊回放(playback)和錄音(capture)的介面。本文後續總結重點放在這個介面上,因為它是開發數字音訊程式最常用到的介面。
3 Raw MIDI介面:支援MIDI(Musical Instrument Digital Interface),標準的電子樂器。這些API提供對音效卡上MIDI匯流排的訪問。這個原始介面基於MIDI事件工作,由程式設計師負責管理協議以及時間處理。
4 定時器(Timer)介面:為同步音訊事件提供對音效卡上時間處理硬體的訪問。
5 時序器(Sequencer)介面
6 混音器(Mixer)介面

裝置命名
API庫使用邏輯裝置名而不是裝置檔案。裝置名字可以是真實的硬體名字也可以是外掛名字。硬體名字使用hw:i,j這樣的格式。其中i是卡號,j是這塊音效卡上的裝置號。第一個聲音裝置是hw:0,0.這個別名預設引用第一塊聲音裝置並且在本文示例中一真會被用到。外掛使用另外的唯一名字。比如plughw:,表示一個外掛,這個外掛不提供對硬體裝置的訪問,而是提供像取樣率轉換這樣的軟體特性,硬體本身並不支援這樣的特性。

聲音快取和資料傳輸
每個音效卡都有一個硬體快取區來儲存記錄下來的樣本。當快取區足夠滿時,音效卡將產生一箇中斷。核心音效卡驅動然後使用直接記憶體(DMA)訪問通道將樣本傳送到記憶體中的應用程式快取區。類似地,對於回放,任何應用程式使用DMA將自己的快取區資料傳送到音效卡的硬體快取區中。
這樣硬體快取區是環快取。也就是說當資料到達快取區末尾時將重新回到快取區的起始位置。ALSA維護一個指標來指向硬體快取以及應用程式快取區中資料操作的當前位置。從核心外部看,我們只對應用程式的快取區感興趣,所以本文只討論應用程式快取區。

應用程式快取區的大小可以通過ALSA庫函式呼叫來控制。快取區可以很大,一次傳輸操作可能會導致不可接受的延遲,我們把它稱為延時(latency)。為了解決這個問題,ALSA將快取區拆分成一系列週期(period)(OSS/Free中叫片斷fragments).ALSA以period為單元來傳送資料。
一個週期(period)儲存一些幀(frames)。每一幀包含時間上一個點所抓取的樣本。對於立體聲裝置,一個幀會包含兩個通道上的樣本。圖1展示了分解過程:一個快取區分解成周期,然後是幀,然後是樣本。圖中包含一些假定的數值。圖中左右通道資訊被交替地儲存在一個幀內。這稱為交錯(interleaved)模式。在非交錯模式中,一個通道的所有樣本資料儲存在另外一個通道的資料之後。

Over and Under Run
當一個音效卡活動時,資料總是連續地在硬體快取區和應用程式快取區間傳輸。但是也有例外。在錄音例子中,如果應用程式讀取資料不夠快,迴圈快取區將會被新的資料覆蓋。這種資料的丟失被稱為overrun.在回放例子中,如果應用程式寫入資料到快取區中的速度不夠快,快取區將會"餓死"。這樣的錯誤被稱為"underrun"。在ALSA文件中,有時將這兩種情形統稱為"XRUN"。適當地設計應用程式可以最小化XRUN並且可以從中恢復過來。

一個典型的聲音程式
使用PCM的程式通常類似下面的虛擬碼:
打開回放或錄音介面
設定硬體引數(訪問模式,資料格式,通道數,取樣率,等等)
while 有資料要被處理:
    讀PCM資料(錄音)
    或 寫PCM資料(回放)
關閉介面

我們將在下文中看到一些可以工作的程式碼。我建議您在你的Linux系統上測試執行這些程式碼。檢視輸出並嘗試修改推薦的程式碼。和本文相關的所有例項清單可以從FTP中獲取:

ftp.ssc.com/pub/lj/listings/issue126/6735.tgz

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("/nPCM 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("/nPCM 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("/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(" %s/n",
           snd_pcm_state_name((snd_pcm_state_t)val));

return 0;
}
清單一顯示了一些ALSA使用的PCM資料型別和引數。首先需要做的是包括標頭檔案。這些標頭檔案包含了所有庫函式的宣告。其中之一就是顯示ALSA庫的版本。
這個程式剩下的部分的迭代一些PCM資料型別,以流型別開始。ALSA為每次迭代的最後值提供符號常量名,並且提供功能函式以顯示某個特定值的描述字串。你將會看到,ALSA支援許多格式,在我的1.0.15版本里,支援多達36種格式。
這個程式必須連結到alsalib庫,通過在編譯時需要加上-lasound選項。有些alsa庫函式使用dlopen函式以及浮點操作,所以您可能還需要加上-ldl,-lm選項。
下面是該程式的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
[email protected]
$< $(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 <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) {
    fprintf(stderr,
            "unable to open pcm device: %s/n",
            snd_strerror(rc));
    exit(1);
}

/* Allocate a hardware parameters object. */
snd_pcm_hw_params_alloca(&params);

/* 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: %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_access(params,
                          (snd_pcm_access_t *) &val);
printf("access type = %s/n",
         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 = %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 pause = %d/n", val);

val = snd_pcm_hw_params_can_resume(params);
printf("can resume = %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;
}

清單2開啟預設的PCM裝置,設定一些硬體引數並且打印出最常用的硬體引數值。它並不做任何回放或錄音的操作。snd_pcm_open開啟預設的PCM裝置並設定訪問模式為PLAYBACK。這個函式返回一個控制代碼,這個控制代碼儲存在第一個函式引數中。該控制代碼會在隨後的函式中用到。像其它函式一樣,這個函式返回一個整數。如果返回值小於0,則程式碼函式調用出錯。如果出錯,我們用snd_errstr開啟錯誤資訊並退出。
為了設定音訊流的硬體引數,我們需要分配一個型別為snd_pcm_hw_param的變數。分配用到函式巨集snd_pcm_hw_params_alloca。下一步,我們使用函式snd_pcm_hw_params_any來初始化這個變數,傳遞先前開啟的PCM流控制代碼。
接下來,我們呼叫API來設定我們所需的硬體引數。這些函式需要三個引數:PCM流控制代碼,引數型別,引數值。我們設定流為交錯模式,16位的樣本大小,2個通道,44100bps的取樣率。對於取樣率而言,聲音硬體並不一定就精確地支援我們所定的取樣率,但是我們可以使用函式snd_pcm_hw_params_set_rate_near來設定最接近我們指定的取樣率的取樣率。其實只有當我們呼叫函式snd_pcm_hw_params後,硬體引數才會起作用。
程式的剩餘部分獲得並列印一些PCM流引數,包括週期和緩衝區大小。結果可能會因為聲音硬體的不同而不同。
執行該程式後,做實驗,改動一些程式碼。把裝置名字改成hw:0,0,然後看結果是否會有變化。設定不同的硬體引數然後觀察結果的變化。

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) {
    fprintf(stderr,
            "unable to open pcm device: %s/n",
            snd_strerror(rc));
    exit(1);
  }

  /* Allocate a hardware parameters object. */
  snd_pcm_hw_params_alloca(&params);

  /* 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);
  /* 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 input/n");
      break;
    } else if (rc != size) {
      fprintf(stderr,
              "short read: read %d bytes/n", rc);
    }
    rc = snd_pcm_writei(handle, buffer, frames);
    if (rc == -EPIPE) {
      /* EPIPE means underrun */
      fprintf(stderr, "underrun occurred/n");
      snd_pcm_prepare(handle);
    } else if (rc < 0) {
      fprintf(stderr,
              "error from writei: %s/n",
              snd_strerror(rc));
    }  else if (rc != (int)frames) {
      fprintf(stderr,
              "short write, write %d frames/n", rc);
    }
  }

  snd_pcm_drain(handle);
  snd_pcm_close(handle);
  free(buffer);

  return 0;
}

清單3擴充套件了之前的示例。向音效卡中寫入了一些聲音樣本以實現聲音回放。在這個例子中,我們從標準輸入中讀取資料,每個週期讀取足夠多的資料,然後將它們寫入到音效卡中,直到5秒鐘的資料全部傳輸完畢。
這個程式的開始處和之前的版本一樣---開啟PCM裝置、設定硬體引數。我們使用由ALSA自己選擇的週期大小,申請該大小的緩衝區來儲存樣本。然後我們找出週期時間,這樣我們就能計算出本程式為了能夠播放5秒鐘,需要多少個週期。
在處理資料的迴圈中,我們從標準輸入中讀入資料,並往緩衝區中填充一個週期的樣本。然後檢查並處理錯誤,這些錯誤可能是由到達檔案結尾,或讀取的資料長度與我期望的資料長度不一致導致的。
我們呼叫snd_pcm_writei來發送資料。它操作起來很像核心的寫系統呼叫,只是這裡的大小引數是以幀來計算的。我們檢查其返回程式碼值。返回值為EPIPE表明發生了underrun,使得PCM音訊流進入到XRUN狀態並停止處理資料。從該狀態中恢復過來的標準方法是呼叫snd_pcm_prepare函式,把PCM流置於PREPARED狀態,這樣下次我們向該PCM流中資料時,它就能重新開始處理資料。如果我們得到的錯誤碼不是EPIPE,我們把錯誤碼打印出來,然後繼續。最後,如果寫入的幀數不是我們期望的,則打印出錯誤訊息。
這個程式一直迴圈,直到5秒鐘的幀全部傳輸完,或者輸入流讀到檔案結尾。然後我們呼叫snd_pcm_drain把所有掛起沒有傳輸完的聲音樣本傳輸完全,最後關閉該音訊流,釋放之前動態分配的緩衝區,退出。
我們可以看到這個程式沒有什麼用,除非標準輸入被重定向到了其它其它的檔案。嘗試用裝置/dev/urandom來執行這個程式,該裝置產生隨機資料:
 ./example3 </dev/urandom

隨機資料會產生5秒鐘的白色噪聲。
然後,嘗試把標準輸入重定向到裝置/dev/null和/dev/zero上,並比較結果。改變一些引數,例如取樣率和資料格式,然後檢視結果的變化。

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 <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 recording (capture). */
rc = snd_pcm_open(&handle, "default",
                    SND_PCM_STREAM_CAPTURE, 0);
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(&params);

/* 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;
}

清單4類似於清單3中的程式,除了這裡的程式時做聲音的抓取(錄音)。當開啟PCM裝置時我們指定開啟模式為SND_PCM_STREAM_CPATURE。在主迴圈中,我們呼叫snd_pcm_readi從音效卡中讀取資料,並把它們寫入到標準輸出。同樣地,我們檢查是否有overrun,如果存在,用與前例中相同的方式處理。
執行清單4的程式將錄製將近5秒鐘的聲音資料,並把它們傳送到標準輸出。你也可以重定向到某個檔案。如果你有一個麥克風連線到你的音效卡,可以使用某個混音程式(mixer)設定錄音源和級別。同樣地,你也可以執行一個CD播放器程式並把錄音源設成CD。嘗試執行程式4並把輸出定向到某個檔案,然後執行程式3播放該檔案裡的聲音資料:
./listing4 > sound.raw
./listing3 < sound.raw
如果你的音效卡支援全雙工,你可以通過管道把兩個程式連線起來,這樣就可以從音效卡中聽到錄製的聲音:
./listing4 | ./listing3
同樣地,您可以做實驗,看看取樣率和樣本格式的變化會產生什麼影響。

高階特性
在前面的例子中,PCM流是以阻塞模式操作的,也就是說,直到資料已經傳送完,PCM介面呼叫才會返回。在事件驅動的互動式程式中,這樣會長時間阻塞應用程式,通常是不能接受的。ALSA支援以非阻塞模式開啟音訊流,這樣讀寫函式呼叫後立即返回。如果資料傳輸被掛起,呼叫不能被處理,ALSA就是返回一個EBUSY的錯誤碼。
許多圖形應用程式使用回撥來處理事件。ALSA支援以非同步的方式開啟一個PCM音訊流。這使得當某個週期的樣本資料被傳輸完後,某個已註冊的回撥函式將會呼叫。

這裡用到的snd_pcm_readi和snd_pcm_writei呼叫和Linux下的讀寫系統呼叫類似。字母i表示處理的幀是交錯式(interleaved)的。ALSA中存在非互動模式的對應的函式。Linux下的許多裝置也支援mmap系統呼叫,這個呼叫將裝置記憶體對映到主記憶體,這樣資料就可以用指標來維護。ALSA也執行以mmap模式開啟一個PCM通道,這允許有效的零拷貝(zero copy)方式訪問聲音資料。

總結
我希望這篇文章能夠激勵你嘗試編寫某些ALSA程式。伴隨著2.6核心在Linux釋出版本(distributions)中被廣泛地使用,ALSA也將被廣泛地採用。它的高階特徵將幫助Linux音訊程式更好地向前發展。
Jaroslav Kysela和Takashi lwai幫助查閱了本文的草稿並提出了寶貴的意見,在此表示感謝。