ALSA 代表高级 Linux 声音架构。它由一组内核驱动程序、一个应用程序编程接口 (API) 库和用于在 Linux 下支持声音的实用程序组成。在本文中,我简要概述了 ALSA 项目及其软件组件。重点是对 ALSA 的 PCM 接口进行编程,包括您可以进行试验的编程示例。
您可能只想探索 ALSA,因为它是新的,但它并不是唯一可用的声音 API。如果您正在执行低级音频功能以获得最大控制和性能,或者想要利用其他声音 API 不支持的特殊功能,则 ALSA 是一个不错的选择。如果您已经编写了一个音频应用程序,您可能需要添加对 ALSA 声音驱动程序的本机支持。如果您的主要兴趣不是音频,而您只是想播放声音文件,那么使用更高级别的声音工具包之一,例如 SDL、OpenAL 或桌面环境中提供的工具包,可能是更好的选择。通过使用 ALSA,您只能使用运行支持 ALSA 的 Linux 内核的系统。
ALSA 项目的启动是因为 Linux 内核中的声音驱动程序(OSS/Free 驱动程序)没有得到积极维护,并且落后于新声音技术的能力。之前编写过声卡驱动程序的 Jaroslav Kysela 开始了这个项目。随着时间的推移,越来越多的开发人员加入,增加了对许多声卡的支持,并完善了 API 的结构。
在 2.5 系列的 Linux 内核开发过程中,ALSA 被合并到官方内核源中。随着 2.6 内核的发布,ALSA 将成为稳定的 Linux 内核的一部分,应该被广泛使用。
声音由不同气压的波组成,通过换能器(例如麦克风)转换为其电形式。模数转换器 (ADC) 以规则的时间间隔(称为采样率)将模拟电压转换为离散值(称为样本)。通过将样本发送到数模转换器和输出换能器(例如扬声器),可以再现原始声音。
以位表示的样本大小是决定声音以数字形式表示的准确程度的一个因素。影响音质的另一个主要因素是采样率。奈奎斯特定理指出,可以准确表示的最高频率最多是采样率的二分之一。
ALSA由许多声卡的声卡驱动程序组成,同时它也提供一个称为libasound的API库。应用程序开发者应该使用libasound而不是内核中的 ALSA接口。因为libasound提供最高级并且编程方便的编程接口。并且提供一个设备逻辑命名功能,这样开发者甚至不需要知道类似设备文件这样的低层接口。相反,OSS/Free驱动是在内核系统调用级上编程,它要求开发者提供设备文件名并且利用ioctrl来实现相应的功能。
为了向后兼容,ALSA提供内核模块来模拟OSS,这样之前的许多在OSS基础上开发的应用程序不需要任何改动就可以在ALSA上运行。另外,libaoss库也可以模拟OSS,而它不需要内核模块。
ALSA包含插件功能,使用插件可以扩展新的声卡驱动,包括完全用软件实现的虚拟声卡。ALSA提供一系列基于命令行的工具集,比如混音器(mixer),音频文件播放器(aplay),以及控制特定声卡特定属性的工具。
该ALSA API可以被分解成它支持主流的接口:
API库使用逻辑设备名而不是设备文件。设备名字可以是真实的硬件名字也可以是插件名字。硬件名字使用hw:i,j这样的格式。其中i是卡号,j是这块声卡上的设备号。
第一个声音设备是hw:0,0.这个别名默认引用第一块声音设备并且在本文示例中一真会被用到。
插件使用另外的唯一名字,比如 plughw:,表示一个插件,这个插件不提供对硬件设备的访问,而是提供像采样率转换这样的软件特性,硬件本身并不支持这样的特性。
声卡有一个硬件缓冲区,用于存储记录的样本。当缓冲区足够满时,它会产生一个中断。然后内核声音驱动程序使用直接内存访问 (DMA) 将样本传输到内存中的应用程序缓冲区。类似地,对于播放,另一个应用程序缓冲区使用 DMA 从内存传输到声卡的硬件缓冲区。
这些硬件缓冲区是环形缓冲区,这意味着当到达缓冲区末尾时数据会回绕到开头。维护一个指针以跟踪硬件缓冲区和应用程序缓冲区中的当前位置。在内核之外,只有应用程序缓冲区是有意义的,所以从这里我们只讨论应用程序缓冲区。
缓冲区的大小可以通过 ALSA 库调用进行编程。缓冲区可能非常大,在一次操作中传输它可能会导致无法接受的延迟,称为延迟。为了解决这个问题,ALSA 将缓冲区拆分为一系列周期(在 OSS/Free 中称为片段)并以周期为单位传输数据。
一个周期存储帧,每个帧都包含在一个时间点捕获的样本。对于立体声设备,帧将包含两个通道的样本。图 1 说明了将缓冲区分解为具有一些假设值的周期、帧和样本。这里,左右声道信息在一个帧内交替存储;这称为交错模式。还支持非交错模式,其中存储一个通道的所有样本数据,然后是下一通道的数据。
当一个声卡活动时,数据总是连续地在硬件缓存区和应用程序缓存区间传输。但是也有例外。在录音例子中,如果应用程序读取数据不够快,循环缓存区将会被新的数据覆盖。这种数据的丢失被称为over run.在回放例子中,如果应用程序写入数据到缓存区中的速度不够快,缓存区将会"饿死"。这样的错误被称为"under run"。在ALSA文档中,有时将这两种情形统称为"XRUN"。适当地设计应用程序可以最小化XRUN并且可以从中恢复过来。
使用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下载。
#include
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;
}
Listing 1显示了 ALSA 使用的一些 PCM 数据类型和参数。第一个要求是包含引入所有 ALSA 库函数定义的头文件。定义之一是显示的 ALSA 版本。
程序的其余部分从流类型开始遍历许多 PCM 数据类型。ALSA 为最后一个枚举值提供符号名称和一个返回值的描述性字符串的实用函数。正如您在输出中看到的那样,ALSA 支持许多不同的数据格式,我系统上的 ALSA 版本支持 38 种数据格式。
该程序必须与 ALSA 库 libasound 链接才能运行。通常,您会在链接器命令行上添加选项 -lasound。一些 ALSA 库函数使用 dlopen 函数和浮点运算,因此您可能还需要添加 -ldl 和 -lm。
/*
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) {
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);
/* 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。此函数返回第一个函数参数中的句柄,该句柄在后续调用中用于操作 PCM 流。与大多数 ALSA 库调用一样,该函数返回一个整数返回状态,一个表示错误条件的负值。在这种情况下,我们检查返回码;如果它指示失败,我们使用 snd_strerror 函数显示错误消息并退出。为了清楚起见,我省略了示例程序中的大部分错误检查。在生产应用程序中,
为了设置流的硬件参数,我们需要分配一个类型为 snd_pcm_hw_params_t 的变量。我们使用宏 snd_pcm_hw_params_alloca 来做到这一点。接下来,我们使用函数 snd_pcm_hw_params_any 初始化变量,传递之前打开的 PCM 流。
我们现在使用采用 PCM 流句柄、硬件参数结构和参数值的 API 调用来设置所需的硬件参数。我们将流设置为交错模式、16 位样本大小、2 个通道和 44,100 bps 采样率。在采样率的情况下,声音硬件并不总是能够准确地支持每个采样率。我们使用函数 snd_pcm_hw_params_set_rate_near 来请求与请求值最接近的支持采样率。在我们调用函数 snd_pcm_hw_params 之前,硬件参数实际上并没有被激活。
程序的其余部分获取并显示许多 PCM 流参数,包括周期和缓冲区大小。显示的结果因声音硬件而有所不同。
在您的系统上运行该程序后,进行试验并进行一些更改。将设备名称从默认更改为 hw:0,0 或 plughw: 并查看结果是否发生变化。更改硬件参数值并观察显示结果如何变化。
/*
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) {
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);
/* 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 选择的周期大小,并将其设为我们用于存储样本的缓冲区的大小。然后我们找出那个周期时间,这样我们就可以计算程序应该处理多少个周期才能运行五秒钟。
在管理数据的循环中,我们从标准输入中读取数据并用一段样本填充我们的缓冲区。我们检查并处理由于到达文件末尾或读取与预期不同的字节数而导致的错误。
要将数据发送到 PCM 设备,我们使用 snd_pcm_writei
调用。它的操作与内核写入系统调用非常相似,只是大小以帧为单位指定。我们检查返回码是否存在一些错误情况。EPIPE 的返回码表示发生欠载,这会导致 PCM 流进入 XRUN 状态并停止处理数据。从此状态恢复的标准方法是使用 snd_pcm_prepare
函数调用将流置于 PREPARED 状态,以便下次我们将数据写入流时它可以再次启动。如果我们收到不同的错误结果,我们会显示错误代码并继续。最后,如果写入的帧数不是预期的,我们会显示一条错误消息。
程序循环直到传输了 5 秒的帧或在输入上发生文件读取结束。然后我们调用 snd_pcm_drain
以允许传输任何挂起的声音样本,然后关闭流。我们释放动态分配的缓冲区并退出。
我们应该看到程序没有用,除非输入被重定向到控制台以外的地方。尝试使用设备 /dev/urandom 运行它,它会产生随机数据,如下所示:
./example3 < /dev/urandom
随机数据应该会产生五秒钟的白噪声。
接下来,尝试将输入重定向到 /dev/null 或 /dev/zero 并比较结果。更改一些参数,例如采样率和数据格式,看看它如何影响结果。
/*
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
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(¶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;
}
清单 4 与清单 3 非常相似,只是我们执行 PCM 捕获(记录)。当我们打开 PCM 流时,我们将模式指定为 SND_PCM_STREAM_CAPTURE。在主处理循环中,我们使用 snd_pcm_readi 从声音硬件读取样本,并使用 write 将其写入标准输出。我们检查超限并按照清单 3 中的超限处理方式进行处理。
运行清单 4 记录大约 5 秒的数据并将其发送到标准输出;你应该将它重定向到一个文件。如果您的声卡上连接了麦克风,请使用混音器程序来设置录音源和电平。或者,您可以运行 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 表示帧是交错的;非交错模式存在相应的功能。Linux 下的许多设备也支持 mmap 系统调用,它将它们映射到可以用指针操作的内存中。最后,ALSA 支持在 mmap 模式下打开 PCM 通道,这允许对声音数据进行高效的零拷贝访问。
我希望这篇文章能激励你尝试一些 ALSA 编程。随着 2.6 内核成为 Linux 发行版的普遍使用,ALSA 应该得到更广泛的使用,其高级功能应该有助于 Linux 音频应用程序向前发展。
摘自https://www.linuxjournal.com/article/6735
原文为英语