Linux ALSA 音訊處理深入解析

kickxxx的專欄發表於2014-10-11

簡介

任何人如果經常的使用linux機器處理音樂,那麼他遲早會和ALSA打交道。ALSA是Advanced Linux Sound Architecture的簡稱,和過時的Open Sound System(OSS)比起來更強大功能更多。事實上,你可能已經不知不覺的使用了ALSA,比如ALSA的OSS模擬功能。當在web上搜尋關於ALSA的答案時,我發現都是提問和自相矛盾的宣告,鮮有確切的答案。我想有兩個原因:首先,有些聲音問題不像看起來那麼簡單,此外ALSA文件簡直是一團糟。本文會嘗試解決這些聲音問題,並矯正一團糟的ALSA文件。

在我們正式開始之前,你最好先瀏覽一下其他資源。他們有的包含一些例子程式,可以用這些程式執行呼叫ALSA來播放或者錄音;此外他們可能也包含了你要找的答案。

我首先特別推薦一篇資訊全面的頁面http://www.sabi.co.uk/Notes/linuxSoundALSA.html, 和本文側重於深層次研究相比,它的覆蓋面更廣。

其他的資源包括:

a LINUX Journal article about basic ALSA programming 包含一些示例程式碼

tutorials on the ALSA project web site 包含一些示例程式碼

one of the developers’ home page 比較舊

有些文件內嵌在ALSA library的原始碼中,可以使用原始碼文件生成工具doxygen生成,使用命令make doc。也可以線上閱讀API文件, 不幸的是它並不完整,瀏覽起來很麻煩。以我的觀點,開發者最初設想不止提供API文件,但是結果是一樣都沒達到。如果你真得想理解ALSA如何工作的,那麼你最好結合著原始碼來看這些文件。

ALSA Concepts

音效卡和硬體裝置

ALSA用cards,device和subdevices的分層結構表示audio硬體裝置和他們的元件。這個分層結構是ALSA看待硬體裝置結構和能力的視角。如果音效卡這個分層結構和音效卡的文件有差別,那麼可能是由於驅動沒有支援所有的功能。

ALSA cards和音效卡硬體是一一對應的。ALSA cards的主要儲存每塊卡上的裝置列表。一個card可以通過一個ID(字串)或者從0開始的數字表示。

大部分ALSA硬體訪問發生在device級別。可以從0開始列舉每個卡的devices,不同的devices可以獨立的開啟和使用。典型的,音效卡和裝置這兩個標識足以決定聲音訊號從哪裡讀取,送到哪裡。

Subdevices是ALSA能夠區分的更細粒度的物件。最常見的場景是一個device的每個channel都對應一個subdevice或者總共只有一個subdevice。一個device的subdevice理論上可以單獨使用,但是在一個subdevice上播放multi-channel訊號時,也會使用其餘的subdevices。和device一樣,subdevices索引標識從0開始。

PCM引數和配置空間

數字化聲音有一定數目的引數:取樣率,通道數和取樣值儲存格式。如果你已經使用OSS程式設計,那麼你可能習慣於在播放音樂檔案之前設定這些引數。類似於ALSA文件中所提到的配置空間。

現實的答案沒有那麼簡單:比如一些音效卡無法結合所有支援的取樣格式,取樣率和通道數。所以這些引數不是獨立的。ALSA考慮到了這種情況,使用一個n維空間的引數集,每一維對應著取樣率,取樣格式,通道數等等。如果一個給定音效卡的引數是獨立的,那麼所有合法配置就在一個n維盒子中,在這種情況下,我們需要做的就是描述每一維的取值範圍。如果引數之間不是獨立的,那麼允許的配置集比較複雜了。

當一個硬體裝置通過ALSA訪問,引數並不總是獨立的。進一步說,一個裝置由於某些特定引數的限制,它的合法配置空間被相應的壓縮小了。這就使得我們可以使用一個較小空間而不是確定的數值。此外,還導致依賴於引數設定順序的問題。也就是說ALSA plugin能夠自動選擇最合適的硬體引數並執行格式轉換。我們在接下來的小節中會討論這個plugin和其他的plugin

ALSA devices and plugins

為了避免混淆,我們先簡單介紹一下ALSA device。這裡說的ALSA device和上面提到的hardware devices完全不同。ALSA device用字串表示。他們定義在ALSA的一個配置檔案中。更復雜的是,一些標準的ALSA devices是:屬類:card,devicce,subdevice。但是硬體card和device的規範不能做為ALSA裝置,事實上有些ALSA devices的引數不是硬體相關的。

不誇張的說ALSA幾乎都是由plugins組成的。不論什麼時候一個player或者程式使用ALSA裝置時,plugins做髒活累活。plugins的完整列表在ALSA library doxygen 文件的pcm_plugins.html中。注意plugins列表並不等同於ALSA devices列表。一些標準devices的名字和他們使用的plugins同名,但是有些devices並不是這樣,有時不同的ALSA devices使用相同的plugin。因此本節我們會給出每一個plugin的名字,如果可能,還會給出特定硬體裝置使用這個plugin允許的名字。

最重要的plugin無疑是hw plugin。它本身不做任何處理,僅僅訪問硬體驅動。如果應用選擇硬體不支援的PCM引數(sampling rate, channel count或者sample format),hw plugin返回出錯資訊。因此下一個重要的plugin是’plug’ plugin,plug plugin執行channel複製,取樣率轉換以及必要的重取樣。不像hw plugin被device hw:0,0使用,plug plugin對應的device命名是不同的:plughw:0,0。但二者的裝置名都包含要訪問的硬體cards,device以及subdevice。(事實上,plug device也存在,也使用plug plugin,並且它的引數SLAVE指明資料要傳送到哪裡,因此這個plugin一定和其他的plugins連結到一起)

此外一個很有用的plugin是file plugin,它會把取樣資料寫到一個檔案中。它有兩個ALSA devices:file 和 tee。前者有兩個引數,檔名和格式。後者傳送資料到另外一個device以便寫到一個檔案中,第一個引數就是那個device。如果第二個裝置有任何引數(比如 “plughw:0,0″),那麼你要把他的名字用引號括起來,以防止被命令列解釋。假定你想使用第一個音效卡上的第一個裝置播放聲音,你可以用如下方式獲取輸出聲音的copy。

aplay -Dtee:\'plughw:0,0\', /tmp/alsatee.out, rawxy.wav

得承認這看起來沒什麼意義(因為你可以簡單的copy xy.wav或者轉換為sox),然而使用這個方法,基於ALSA的movie player可以抽取聲音內容。tee’s 輸出是raw 取樣資料沒有任何檔案頭。file plugin也可以用來從檔案中讀取資料,但是沒有預定義的裝置使用這種方式。

現存的許多plugins用來mixer和rerouting channels。由於這些plugins需要大量的引數,沒有預定以的ALSA devices使用他們。route plugin是一個mixing矩陣。channels不僅僅可以被交換或者任意賦值,而且可以被混音。多個plugin僅僅允許reroute channels,但是可以有幾個slave devices,因此可以在不同音效卡的channels上播放。dmix和dshare plugins允許一個device被多個clients(player application)使用。dshare plugin把可用channels分配給需要的clients,而dmix則是把多個clients播放的內容混音到一個channels。

簡短的介紹了最重要的幾個plugins以及預定義的ALSA devices。為了使用更復雜的plugins,需要寫自己的配置檔案,下一節將詳細描述配置檔案。device定的例子可以參照ALSA專案documentation of its configuration file 以及list of plugins

Configuring ALSA

configuration files

配置檔案用來定義ALSA devices。沒有這些配置檔案,你就無法使用ALSA的任何功能。雖然你並不需要寫任何配置檔案就可以進行簡單的播放和錄音,這是因為內建的配置檔案alsa.conf包含一些devices的定義。但是如果你有特殊的需求或者碰到了問題,你可能就需要增加自己的定義了。

ALSA支援三類配置檔案。第一個是alsa.conf,位於ALSA 資料目錄下,通常是/usr/share/als。這個目錄和它的子目錄包含了更多的配置檔案,關於音效卡的或者特定的plugin,他們都被包含進alsa.conf。這個目錄下的配置檔案通常被看作built-in,所以系統管理員和普通使用者都不應該修改它。

其他兩個配置檔案也被alsa.conf包含進來,系統範圍內的配置檔案儲存在/etc/asoundrc。使用者可以儲存他們自己的配置檔案到HOME目錄下的.asoundrc中。每次開啟ALSA devices所有的配置檔案都會被重新分析。所以配置檔案的變化是立即生效的,不需要重啟任何東西。

Basic Configuration file format

ALSA配置檔案儲存著分層結構的引數-值對。分層結構的頂層對應著ALSA提供的介面。一個介面包含著一組ALSA API功能:允許開啟一個device,對介面做操作,然後關閉device。不同的介面有不同的作用,比如aplay和其他的player使用PCM介面,程式alsactl使用ctl介面。amixer使用mixer介面,amidi使用rawmidi介面。(事實上他們大部分最終都會使用ctl介面,至少部分功能)。為了讓這些程式能夠使用device,必須在相應的介面定義他們。

我給的例子大部分都是和pcm介面相關的,主要因為PCM幾乎是最重要的並且我大部分時間也都花在PCM。(PCM代表Pulse code Modulation,是把聲音數字化為取樣值流)。此外命令aplay -L 允許你列出pcm interface上的所有定義。

層次結構的第二級儲存著ALSA devices的名字,可以通過這個device操作device所在的介面

讓我們看一個例子,假定你想建立一個ALSA PCM device訪問你的第一個音效卡並且做必要的格式轉換。plugin是通過plug plugin實現的自動格式轉換。在asoundrc中加入下面幾行,建立這個PCM device。

pcm.plug0 {
    type plug
    slave {
        pcm "hw:0,0"
    }
}

解釋下這幾行的意思:一個新的device plug0,可以通過pcm介面訪問。這個device的資料輸出將會被plug plugin處理。這個plugin的slave是pcm device hw:0,0。這個device定義是最常用的定義方式。當然,ALSA在語法上允許一定自由度。比如,可以在引數名和值之間放一個等號,或者在賦值語句間增加分號和逗號。所以我們可以改寫上面的定義如下

pcm.plug0 = {
    type = plug;
    slave = {
        pcm = "hw:0,0" ;
    },
};

如你所見, 花括號前是引數名,花括號內則是它的引數值。設定在引數體內的最後一個引數賦值可以跟著逗號或者分號。現在我們知道問什麼slave PCM要使用分號了,否則slave PCM中的逗號會導致一個語法錯誤。

事實上語法還有更大的自由度。子引數名可以通過‘.’定義也可以用括號定義。第一個符號plug0是新裝置的名字,也是pcm的一個引數。第二個則定義了所得的子引數。此外配置檔案並使面向行的,可以使用空格作為一行的分割。所以上面的定義可以寫做如下兩種形式

pcm {
    plug0 {
        type plug slave { pcm "hw:0,0" }
    }
}

或者

pcm.plug0.type = plug; pcm.plug0.slave.pcm = "hw:0,0"

現在我們總結一下ALSA配置檔案的機構。引數名是由字母,數字和下劃線組成的(這個結論是除錯的結果,實際上並沒有文件)。引數值包含字母,數字和下劃線並使用引號括起來。引數名和引數值都是大小寫敏感的。配置檔案的註釋以 #開始直到本行的結束。

為了是slave的定義更清晰,我們應該單獨定義slave。這樣上面的plug0的定義應該這樣。

pcm_slave.slave0 {
    pcm "hw:0,0"
}
pcm.plug0 {
    type plug
    slave slave0
}

配置檔案還支援別名。我們可以把引數賦值為一個已存在的裝置名,這個引數就是別名。

pcm.alias_plug0 = plug0

alias_plug0和plug0都是介面pcm的一部分,注意不能定義為(pcm.alias_plug0 = pcm.plug0),因為ALSA devices的別名不能帶引數

現在我們已經有了大概的瞭解,你可以繼續閱讀http://alsa.opensrc.org/.asoundrc和ALSA library的doxygen文件,他們提供了更多的例子。

Advanced configuration file features

Overriding parameters and parameter data types

如果你已經閱讀了其他的asoundrc,那麼你可能已經學會了重新定義ALSA’s default device如下:

pcm.!default { type hw card 0 }

歎號使得原來的pcm.default定義被覆蓋。這個符號可以在任意配置檔案賦值中使用。現在讓我們來看看它是如何工作的,正常的賦值是增加一個葉子節點到樹結構中。如果葉子節點已經存在,那麼他的值就會被覆蓋。所以如果你在asoundrc中放入下面兩行,那麼預設的音效卡就是第二個,而不是第一個

pcm.!default { type hw card 1 }
pcm.default.card 1

如果你把關於default的第一行定義移除,那麼你將收到一個error資訊default不是一個compund。很明顯,引數有幾種資料型別,型別不能僅僅通過賦值語句就被修改。通過歎號覆蓋一個引數,就可以改變他的型別。歎號使得引數和它的子引數全部被移除。所以不要如下方式使用歎號 !pcm…,這將刪除pcm interface的所有定義。

稍微有點使用經驗,就會發現別名實際上是一個字串,包含他們所指向的PCM device的名字。pcm.default就是alsa.conf中定義的一個別名。

和歎號類似,其他的字首字元可以用來修飾引數賦值。問號?會忽略掉引數值已經存在的引數。

pcm.?default {
    type hw card 1
}

定義第一個裝置的第二個音效卡作為default device,但是如果default在之前已經定義過,那麼這個宣告無效。

其他的兩個字首是+和-。

Parameterised device definitions

在plugins一節中,我們碰到了很多ALSA devices需要給定引數,跟隨在device name後使用”,”分割。就像我上面提到的,這些device和他們後面的plugins是完全不同的東西,devices可以看作是plugins的封裝。在basic configuration section一節我們使用plug plugin定義了一個device。我們在plugin一節碰到的預定義裝置和這種簡單的裝置是不同的。plughw裝置可以被任意的音效卡和硬體裝置使用,因為我們可以使用card 和device號作為引數。儘管所有的引數化裝置都是在alsa.conf中定義的,並且沒有ALSA文件說明他們的語法,你仍然可以定義自己的devices

讓我們看一下plughw在alsa.conf中的定義,這裡的plughw做了縮略

plughw {
  @args [ CARD DEV SUBDEV ]
  @args.CARD {
    type string
  }
  @args.DEV {
    type integer
  }
  @args.SUBDEV {
    type integer
  }
  type plug
  slave.pcm {
    type hw
    card $CARD
    device $DEV
    subdevice $SUBDEV
  }
}

plughw結構的第一行定義了引數列表。這對於我們是一個新的資料型別:陣列。他的元素可以一起賦值。

plughw {
    @args.0 CARD
    @args.1 DEV
    @args.2 SUBDEV
}

和結構賦值一樣,元素或組賦值可以插入等號。接下來的幾行定義了每個引數的資料型別,在plughw的原始定義中,結構體定義引數時也包括了預設的定義,如果你對此感興趣,可以看下alsa.conf。

Troubleshooting

Useful tools

檢視音效卡裝置的最簡單方法是呼叫

aplay -l 或者arecord -l

這兩個命令會列出可以playback和recording的音效卡,硬體裝置以及子裝置。

如果你對ALSA解析asoundrc有任何疑問,你可以使用命令aplay -L,列印出pcm介面的所有配置。注意字首”pcm.”沒有出現在引數名中,因為它一直是隱含的。

aplay的另外一個選項是-v,這個選項在playback時會列印出每個subdevice的硬體引數。不像其他兩個提到的選項,這個選項只能在playback時起作用。當你使用plug plugin來播放聲音,可以用這個選項來檢查是否plug做了resampling。

但是如果你使用其他的audio或者movie player時怎麼辦? 可以使用/proc/ file system。當subdevice正在使用時,可以從/proc/asound/card#/pcm#p/sub#/hw_params獲取硬體引數。通過比較aplay test.wav -v和/proc/asound/card#/pcm#p/sub#/hw_params的輸出結果,我們可以知道哪些引數是模擬的。如果subdevice沒有被使用沒,那麼引數檔案hw_params僅僅輸出”closed”。如果在播放音樂時,顯示的是”closed“,那麼說明聲音沒有流到這個subdevice。例外情況:如果一個ALSA device被一個multi-channel playback使用,那麼第一個subdevice記錄總channel數,其他的則彙報為closed。如果使用了ALSA’s OSS模擬,那麼hw_params檔案也包含OSS的引數設定。

amidi程式使用rawmidi介面,選項-l列出所有可用的MIDI devices,-L選項列印出rawmidi介面的配置。

speaker-test,當你聽不到任何聲音,想確定一下是否player有問題,那麼可以使用speaker-test做為基準測試程式。這個測試程式可以順序的給每個speaker傳送鈴音或者噪聲。

alsacap

alsacap是作者寫的一個小程式,用來抓取一些作者特別關注的一些硬體配置。我認為這個工具對於查詢問題很有幫助。原始碼地址http://www.volkerschatz.com/noise/alsacap.c, 原始碼頂端的註釋有編譯說明。這個工具名意思是ALSA capability,用來顯示你的音效卡和ALSA驅動的能力。

相關文章