深入了解ALSA



原文 http://www.volkerschatz.com/noise/alsa.html


Intro

任何人如果经常的使用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。

  1. aplay -Dtee:\'plughw:0,0\', /tmp/alsatee.out, rawxy.wav  
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。

  1. pcm.plug0 {  
  2.     type plug  
  3.     slave {  
  4.         pcm "hw:0,0"  
  5.     }  
  6. }  
pcm.plug0 {
    type plug
    slave {
        pcm "hw:0,0"
    }
}

解释下这几行的意思:一个新的device plug0,可以通过pcm接口访问。这个device的数据输出将会被plug plugin处理。这个plugin的slave是pcm device hw:0,0。这个device定义是最常用的定义方式。当然,ALSA在语法上允许一定自由度。比如,可以在参数名和值之间放一个等号,或者在赋值语句间增加分号和逗号。所以我们可以改写上面的定义如下

  1. pcm.plug0 = {  
  2.     type = plug;  
  3.     slave = {  
  4.         pcm = "hw:0,0" ;  
  5.     },  
  6. };  
pcm.plug0 = {
    type = plug;
    slave = {
        pcm = "hw:0,0" ;
    },
};


如你所见, 花括号前是参数名,花括号内则是它的参数值。设置在参数体内的最后一个参数赋值可以跟着逗号或者分号。现在我们知道问什么slave PCM要使用分号了,否则slave PCM中的逗号会导致一个语法错误。

事实上语法还有更大的自由度。子参数名可以通过‘.’定义也可以用括号定义。第一个符号plug0是新设备的名字,也是pcm的一个参数。第二个则定义了所得的子参数。此外配置文件并使面向行的,可以使用空格作为一行的分割。所以上面的定义可以写做如下两种形式

  1. pcm {  
  2.     plug0 {  
  3.         type plug slave { pcm "hw:0,0" }  
  4.     }  
  5. }  
pcm {
    plug0 {
        type plug slave { pcm "hw:0,0" }
    }
}
或者

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

现在我们总结一下ALSA配置文件的机构。参数名是由字母,数字和下划线组成的(这个结论是调试的结果,实际上并没有文档)。参数值包含字母,数字和下划线并使用引号括起来。参数名和参数值都是大小写敏感的。配置文件的注释以 #开始直到本行的结束。

为了是slave的定义更清晰,我们应该单独定义slave。这样上面的plug0的定义应该这样。

  1. pcm_slave.slave0 {  
  2.     pcm "hw:0,0"  
  3. }  
  4. pcm.plug0 {  
  5.     type plug  
  6.     slave slave0  
  7. }  
pcm_slave.slave0 {
    pcm "hw:0,0"
}
pcm.plug0 {
    type plug
    slave slave0
}


配置文件还支持别名。我们可以把参数赋值为一个已存在的设备名,这个参数就是别名。

  1. pcm.alias_plug0 = plug0  
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如下:

  1. pcm.!default { type hw card 0 }  
pcm.!default { type hw card 0 }
叹号使得原来的pcm.default定义被覆盖。这个符号可以在任意配置文件赋值中使用。现在让我们来看看它是如何工作的,正常的赋值是增加一个叶子节点到树结构中。如果叶子节点已经存在,那么他的值就会被覆盖。所以如果你在asoundrc中放入下面两行,那么缺省的声卡就是第二个,而不是第一个

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

如果你把关于default的第一行定义移除,那么你将收到一个error信息default不是一个compund。很明显,参数有几种数据类型,类型不能仅仅通过赋值语句就被修改。通过叹号覆盖一个参数,就可以改变他的类型。叹号使得参数和它的子参数全部被移除。所以不要如下方式使用叹号 !pcm...,这将删除pcm interface的所有定义。

稍微有点使用经验,就会发现别名实际上是一个字符串,包含他们所指向的PCM device的名字。pcm.default就是alsa.conf中定义的一个别名。

和叹号类似,其他的前缀字符可以用来修饰参数赋值。问号?会忽略掉参数值已经存在的参数。

  1. pcm.?default {  
  2.     type hw card 1  
  3. }  
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做了缩略

  1. plughw {  
  2.   @args [ CARD DEV SUBDEV ]  
  3.   @args.CARD {  
  4.     type string  
  5.   }  
  6.   @args.DEV {  
  7.     type integer  
  8.   }  
  9.   @args.SUBDEV {  
  10.     type integer  
  11.   }  
  12.   type plug  
  13.   slave.pcm {  
  14.     type hw  
  15.     card $CARD  
  16.     device $DEV  
  17.     subdevice $SUBDEV  
  18.   }  
  19. }  
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结构的第一行定义了参数列表。这对于我们是一个新的数据类型:数组。他的元素可以一起赋值。

  1. plughw {  
  2.     @args.0 CARD  
  3.     @args.1 DEV  
  4.     @args.2 SUBDEV  
  5. }  
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驱动的能力。

猜你喜欢

转载自blog.csdn.net/zz460833359/article/details/79822214