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可以分为以下几个主要部分:
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
我们先看看一些示例代码。我推荐你在linux系统编译运行它们,观察输出然后尝试一些修改。这些测试代码的完整列表可以从以下地址下载:ftp.linuxjournal.com/pub/lj/listings/issue126/6735.tgz
Listing 1. Display Some PCM Types and Formats
#include
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
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
为了设置流的硬件参数,我们需要分配一个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
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;
}
这个程序的开始部分和前面的例子一样:打开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
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);
}
/* 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本文的初稿并提供了有用的反馈。