天天看點

ALSA聲音程式設計介紹+underrunALSA聲音程式設計介紹+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

#define ALSA_PCM_NEW_HW_PARAMS_API

#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;

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);

}

snd_pcm_hw_params_alloca(&params);

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);

rc = snd_pcm_hw_params(handle, params);

if (rc < 0) {

    fprintf(stderr,

            "unable to set hw parameters: %s/n",

            snd_strerror(rc));

    exit(1);

}

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

#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);

if (rc < 0) {

    fprintf(stderr,

            "unable to open pcm device: %s/n",

            snd_strerror(rc));

    exit(1);

}

snd_pcm_hw_params_alloca(&params);

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) {

    fprintf(stderr,

            "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 = snd_pcm_readi(handle, buffer, frames);

    if (rc == -EPIPE) {

      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幫助查閱了本文的草稿并提出了寶貴的意見,在此表示感謝。