ALSA 声音编程简介
ALSA 代表高级 Linux 声音架构。它由一组内核驱动程序、一个应用程序编程接口 (API) 库和用于支持 Linux 下声音的实用程序组成。在本文中,我将简要介绍 ALSA 项目及其软件组件。重点介绍 ALSA 的 PCM 接口编程,包括可供您试验的编程示例。
您可能想探索 ALSA,因为它是新的,但它并不是唯一可用的声音 API。如果您要执行低级音频功能以实现最大控制和性能,或者想要使用其他声音 API 不支持的特殊功能,那么 ALSA 是一个不错的选择。如果您已经编写了音频应用程序,则可能希望添加对 ALSA 声音驱动程序的本机支持。如果您的主要兴趣不是音频,而只是想播放声音文件,那么使用更高级的声音工具包之一(例如 SDL、OpenAL 或桌面环境中提供的工具包)可能是更好的选择。使用 ALSA 时,您只能使用运行支持 ALSA 的 Linux 内核的系统。
ALSA 的历史
ALSA 项目的启动是因为 Linux 内核中的音频驱动程序(OSS/Free 驱动程序)没有得到积极的维护,并且落后于新音频技术的功能。之前曾编写过声卡驱动程序的 Jaroslav Kysela 启动了该项目。随着时间的推移,越来越多的开发人员加入进来,增加了对许多声卡的支持,并且 API 的结构得到了改进。
在 2.5 系列 Linux 内核开发过程中,ALSA 被合并到官方内核源代码中。随着 2.6 内核的发布,ALSA 将成为稳定 Linux 内核的一部分,并将得到广泛使用。
数字音频基础知识
声音由不同气压的波组成,通过传感器(例如麦克风)转换为电形式。模拟数字转换器 (ADC) 以固定的时间间隔(称为采样率)将模拟电压转换为离散值(称为样本)。通过将样本发送到数字模拟转换器和输出传感器(例如扬声器),可以重现原始声音。
样本的大小(以位表示)是决定声音以数字形式表示的准确性的一个因素。影响音质的另一个主要因素是采样率。奈奎斯特定理指出,可以准确表示的最高频率最多是采样率的一半。
ALSA 基础知识
ALSA 包含一系列适用于多种不同声卡的内核设备驱动程序,它还提供了一个 API 库 libasound。我们鼓励应用程序开发人员使用库 API 而不是内核接口进行编程。该库提供了一个更高级、更方便开发人员使用的编程接口以及设备的逻辑命名,这样开发人员就无需了解设备文件等底层细节。
相比之下,OSS/Free 驱动程序是在内核系统调用级别进行编程的,需要开发人员指定设备文件名并使用 ioctl 调用执行许多功能。为了向后兼容,ALSA 提供了模拟 OSS/Free 声音驱动程序的内核模块,因此大多数现有声音应用程序无需更改即可继续运行。可以使用模拟包装器库 libaoss 来模拟 OSS/Free API,而无需内核模块。
ALSA 具有插件功能,允许扩展新设备,包括完全用软件实现的虚拟设备。ALSA 提供了许多命令行实用程序,包括混音器、声音文件播放器和用于控制特定声卡特殊功能的工具。
ALSA 架构
ALSA API 可以分解为其支持的主要接口:
- 控制接口:用于管理声卡寄存器和查询可用设备的通用设施。
- PCM 接口:用于管理数字音频捕获和播放的接口。本文的其余部分将重点介绍此接口,因为它是数字音频应用程序最常用的接口。
- 原始 MIDI 接口:支持 MIDI(乐器数字接口),这是电子乐器的标准。此 API 提供对声卡上 MIDI 总线的访问。原始接口直接与 MIDI 事件配合使用,程序员负责管理协议和时间。
- 计时器接口:提供对用于同步声音事件的声卡上的计时硬件的访问。
- 音序器接口:比原始 MIDI 接口更高级的 MIDI 编程和声音合成接口。它处理大部分 MIDI 协议和时间。
- 混音器接口:控制声卡上路由信号和控制音量的设备。它建立在控制接口之上。
设备命名
库 API 使用逻辑设备名称而不是设备文件。设备名称可以是实际的硬件设备或插件。硬件设备使用格式 hw: i , j,其中 i是卡号,j是该卡上的设备。第一个声音设备是 hw:0,0。别名 default 指的是第一个声音设备,并在本文的所有示例中使用。插件使用其他唯一名称;例如,plughw: 是一个提供对硬件设备的访问但在软件中为不直接支持的硬件提供诸如采样率转换等功能的插件。dmix 和 dshare 插件允许您对多个流进行下混并在不同的应用程序之间动态分割单个流。
声音缓冲区和数据传输
声卡有一个硬件缓冲区,用于存储录制的样本。当缓冲区足够满时,它会生成中断。然后,内核声音驱动程序使用直接内存访问 (DMA) 将样本传输到内存中的应用程序缓冲区。同样,对于播放,另一个应用程序缓冲区使用 DMA 从内存传输到声卡的硬件缓冲区。
这些硬件缓冲区是环形缓冲区,这意味着当到达缓冲区末尾时,数据会绕回到起始位置。会维护一个指针来跟踪硬件缓冲区和应用程序缓冲区中的当前位置。在内核之外,我们只关注应用程序缓冲区,因此从这里开始我们只讨论应用程序缓冲区。
缓冲区的大小可以通过 ALSA 库调用进行编程。缓冲区可能非常大,一次传输可能会导致不可接受的延迟,称为延迟。为了解决这个问题,ALSA 将缓冲区拆分为一系列周期(在 OSS/Free 中称为片段),并以周期为单位传输数据。
周期存储帧,每个帧都包含在某一时间点捕获的样本。对于立体声设备,帧将包含两个通道的样本。图 1 说明了缓冲区分解为周期、帧和样本以及一些假设值。在这里,左声道和右声道信息交替存储在一个帧内;这称为交错模式。还支持非交错模式,其中存储一个通道的所有样本数据,然后存储下一个通道的数据。
当声音设备处于活动状态时,数据会在硬件和应用程序缓冲区之间连续传输。在数据捕获(录制)的情况下,如果应用程序没有足够快地读取缓冲区中的数据,循环缓冲区将被新数据覆盖。由此产生的数据丢失称为溢出。在播放过程中,如果应用程序没有足够快地将数据传递到缓冲区,它将无法获得数据,从而导致称为欠载的错误。ALSA 文档有时使用术语 XRUN 来指代这两种情况。设计正确的应用程序可以最大限度地减少 XRUN 并在发生时恢复。
典型声音应用
使用PCM接口的程序通常遵循以下伪代码:
开放接口用于捕获或回放
设置硬件参数
(访问模式、数据格式、通道、速率等)
当有数据需要处理时:
读取 PCM 数据(捕获)
或写入 PCM 数据(播放)
关闭接口
我们将在以下部分中查看一些工作代码。我建议您在 Linux 系统上编译并运行这些代码,查看输出并尝试一些建议的修改。
清单 1. 显示一些 PCM 类型和格式
#包括 <alsa/asoundlib.h>
int 主要() {
int 值;
printf("ALSA 库版本:%s\n",
SND_LIB_VERSION_STR);
printf("\nPCM流类型:\n");
对于(val = 0;val <= SND_PCM_STREAM_LAST;val++)
printf(" %s\n",
snd_pcm_stream_name((snd_pcm_stream_t)val));
printf("\nPCM 访问类型:\n");
对于(val = 0;val <= SND_PCM_ACCESS_LAST;val++)
printf(" %s\n",
snd_pcm_access_name((snd_pcm_access_t)val));
printf("\nPCM 格式:\n");
对于(val = 0;val <= SND_PCM_FORMAT_LAST;val++)
如果(snd_pcm_format_name((snd_pcm_format_t)val)
!= 空)
printf(" %s(%s)\n",
snd_pcm_format_name((snd_pcm_format_t)val),
snd_pcm_format_description(
(snd_pcm_format_t)值);
printf("\nPCM子格式:\n");
对于(val = 0; val <= SND_PCM_SUBFORMAT_LAST;
值++)
printf(" %s(%s)\n",
snd_pcm_subformat_name((
snd_pcm_subformat_t) 值),
snd_pcm_subformat_description((
snd_pcm_subformat_t)val));
printf("\nPCM 状态:\n");
对于(val = 0;val <= SND_PCM_STATE_LAST;val++)
printf(" %s\n",
snd_pcm_state_name((snd_pcm_state_t)val));
返回0;
}
清单 1 显示了 ALSA 使用的一些 PCM 数据类型和参数。第一个要求是包含引入所有 ALSA 库函数定义的头文件。其中一个定义是显示的 ALSA 版本。
程序的其余部分遍历了多种 PCM 数据类型,从流类型开始。ALSA 为最后一个枚举值提供了符号名称,并提供了一个实用函数,该函数返回值的描述性字符串。正如您在输出中看到的那样,ALSA 支持许多不同的数据格式,对于我的系统上的 ALSA 版本,有 38 种。
该程序必须与 ALSA 库 libasound 链接才能运行。通常,您会在链接器命令行上添加选项 -lasound。一些 ALSA 库函数使用 dlopen 函数和浮点运算,因此您可能还需要添加 -ldl 和 -lm。
清单 2. 打开 PCM 设备并设置参数
/*
本示例打开默认 PCM 设备,设置
一些参数,然后显示值
大多数硬件参数。它不
执行任何声音播放或录音。
*/
/* 使用较新的 ALSA API */
#定义 ALSA_PCM_NEW_HW_PARAMS_API
/* 所有 ALSA 库 API 均已定义
* 在此标题中 */
#包括 <alsa/asoundlib.h>
int 主要() {
int rc;
snd_pcm_t *句柄;
snd_pcm_hw_params_t *参数;
无符号整数val,val2;
int 目录;
snd_pcm_uframes_t 帧;
/* 打开 PCM 设备进行播放。 */
rc = snd_pcm_open(&handle, “默认”,
SND_PCM_STREAM_PLAYBACK, 0);
如果 (rc < 0) {
fprintf(标准错误,
“无法打开 pcm 设备:%s\n”,
snd_strerror(rc));
退出(1);
}
/* 分配硬件参数对象。 */
snd_pcm_hw_params_alloca(&params);
/* 用默认值填充。 */
snd_pcm_hw_params_any(句柄,参数);
/* 设置所需的硬件参数。 */
/* 交错模式 */
snd_pcm_hw_params_set_access(句柄,参数,
SND_PCM_ACCESS_RW_INTERLEAVED);
/* 有符号 16 位小端格式 */
snd_pcm_hw_params_set_format(句柄,参数,
SND_PCM_格式_S16_LE);
/* 双通道(立体声) */
snd_pcm_hw_params_set_channels(句柄,参数,2);
/* 44100 比特/秒采样率(CD 质量) */
瓦尔=44100;
snd_pcm_hw_params_set_rate_near(句柄,
参数,&val,&dir);
/* 将参数写入驱动程序 */
rc = snd_pcm_hw_params(句柄,参数);
如果 (rc < 0) {
fprintf(标准错误,
“无法设置硬件参数:%s \n”,
snd_strerror(rc));
退出(1);
}
/*显示有关PCM接口的信息*/
printf("PCM 句柄名称 = '%s'\n",
snd_pcm_name(句柄));
printf("PCM 状态 = %s\n",
snd_pcm_state_name(snd_pcm_state(句柄)));
snd_pcm_hw_params_get_access(参数,
(snd_pcm_access_t *)&val);
printf("访问类型 = %s\n",
snd_pcm_access_name((snd_pcm_access_t)val));
snd_pcm_hw_params_get_format(params,&val);
printf("格式 = ‘%s’ (%s)\n",
snd_pcm_format_name((snd_pcm_format_t)val),
snd_pcm_format_description(
(snd_pcm_format_t)值);
snd_pcm_hw_params_get_subformat(参数,
(snd_pcm_subformat_t *)&val);
printf("子格式 = ‘%s’ (%s)\n",
snd_pcm_subformat_name((snd_pcm_subformat_t)val),
snd_pcm_subformat_description(
(snd_pcm_subformat_t)值);
snd_pcm_hw_params_get_channels(参数,&val);
printf("通道= %d\n", val);
snd_pcm_hw_params_get_rate(参数,&val,&dir);
printf("速率=%d bps\n",val);
snd_pcm_hw_params_get_period_time(参数,
&val,&dir);
printf("周期时间 = %d us\n", val);
snd_pcm_hw_params_get_period_size(参数,
&框架,&目录);
printf("周期大小 = %d 帧\n", (int)frames);
snd_pcm_hw_params_get_buffer_time(参数,
&val,&dir);
printf("缓冲时间 = %d us\n", val);
snd_pcm_hw_params_get_buffer_size(参数,
(snd_pcm_uframes_t *)&val);
printf("缓冲区大小 = %d 帧\n", val);
snd_pcm_hw_params_get_periods(参数,&val,&dir);
printf("每个缓冲区的周期数 = %d 帧\n", val);
snd_pcm_hw_params_get_rate_numden(参数,
&val,&val2);
printf("精确速率 = %d/%d bps\n", val, val2);
val = snd_pcm_hw_params_get_sbits(参数);
printf("有效位 = %d\n", val);
snd_pcm_hw_params_get_tick_time(参数,
&val,&dir);
printf("滴答时间 = %d us\n", val);
val = snd_pcm_hw_params_is_batch(参数);
printf("批次=%d\n", val);
val = snd_pcm_hw_params_is_block_transfer(参数);
printf("是块传输= %d\n", val);
val = snd_pcm_hw_params_is_double(参数);
printf("是双精度数 = %d\n", val);
val = snd_pcm_hw_params_is_half_duplex(参数);
printf("是半双工 = %d\n", val);
val = snd_pcm_hw_params_is_joint_duplex(参数);
printf("是联合双工 = %d\n", val);
val = snd_pcm_hw_params_can_overrange(参数);
printf("可能超出范围 = %d\n", val);
val = snd_pcm_hw_params_can_mmap_sample_resolution(参数);
printf("可以 mmap = %d\n", val);
val = snd_pcm_hw_params_can_pause(参数);
printf("可以暂停= %d\n", val);
val = snd_pcm_hw_params_can_resume(参数);
printf("可以恢复=%d\n",val);
val = snd_pcm_hw_params_can_sync_start(参数);
printf("可以同步开始=%d\n", val);
snd_pcm_close(句柄);
返回0;
}
清单 2 打开默认 PCM 设备,设置一些参数,然后显示大多数硬件参数的值。它不执行任何声音播放或录制。对 snd_pcm_open 的调用打开默认 PCM 设备并将访问模式设置为 PLAYBACK。此函数在第一个函数参数中返回一个句柄,该句柄在后续调用中用于操作 PCM 流。与大多数 ALSA 库调用一样,该函数返回一个整数返回状态,负值表示错误情况。在本例中,我们检查返回代码;如果它表示失败,我们将使用 snd_strerror 函数显示错误消息并退出。为了清楚起见,我从示例程序中省略了大部分错误检查。在生产应用程序中,应该检查每个 API 调用的返回代码并提供适当的错误处理。
为了设置流的硬件参数,我们需要分配一个 snd_pcm_hw_params_t 类型的变量。我们使用宏 snd_pcm_hw_params_alloca 执行此操作。接下来,我们使用函数 snd_pcm_hw_params_any 初始化变量,并传递先前打开的 PCM 流。
现在,我们使用 API 调用来设置所需的硬件参数,这些调用采用 PCM 流句柄、硬件参数结构和参数值。我们将流设置为交错模式、16 位样本大小、2 个通道和 44,100 bps 采样率。就采样率而言,声音硬件并不总是能够准确支持每个采样率。我们使用函数 snd_pcm_hw_params_set_rate_near 来请求最接近请求值的受支持采样率。直到我们调用函数 snd_pcm_hw_params 时,硬件参数才真正生效。
程序的其余部分获取并显示许多 PCM 流参数,包括周期和缓冲区大小。显示的结果会根据声音硬件的不同而略有不同。
在系统上运行该程序后,进行实验并进行一些更改。将设备名称从默认更改为 hw:0,0 或 plughw:,看看结果是否发生变化。更改硬件参数值并观察显示的结果如何变化。
清单 3. 简单的声音播放
/*
此示例从输入中读取标准并写入
向默认 PCM 设备发送 5 秒的数据。
*/
/* 使用较新的 ALSA API */
#定义 ALSA_PCM_NEW_HW_PARAMS_API
#包括 <alsa/asoundlib.h>
int 主要() {
长循环;
int rc;
int 大小;
snd_pcm_t *句柄;
snd_pcm_hw_params_t *参数;
无符号整数值;
int 目录;
snd_pcm_uframes_t 帧;
字符*缓冲区;
/* 打开 PCM 设备进行播放。 */
rc = snd_pcm_open(&handle, “默认”,
SND_PCM_STREAM_PLAYBACK, 0);
如果 (rc < 0) {
fprintf(标准错误,
“无法打开 pcm 设备:%s\n”,
snd_strerror(rc));
退出(1);
}
/* 分配硬件参数对象。 */
snd_pcm_hw_params_alloca(&params);
/* 用默认值填充。 */
snd_pcm_hw_params_any(句柄,参数);
/* 设置所需的硬件参数。 */
/* 交错模式 */
snd_pcm_hw_params_set_access(句柄,参数,
SND_PCM_ACCESS_RW_INTERLEAVED);
/* 有符号 16 位小端格式 */
snd_pcm_hw_params_set_format(句柄,参数,
SND_PCM_格式_S16_LE);
/* 双通道(立体声) */
snd_pcm_hw_params_set_channels(句柄,参数,2);
/* 44100 比特/秒采样率(CD 质量) */
瓦尔=44100;
snd_pcm_hw_params_set_rate_near(句柄,参数,
&val,&dir);
/* 将周期大小设置为 32 帧。 */
帧=32;
snd_pcm_hw_params_set_period_size_near(句柄,
参数,&框架,&目录);
/* 将参数写入驱动程序 */
rc = snd_pcm_hw_params(句柄,参数);
如果 (rc < 0) {
fprintf(标准错误,
“无法设置硬件参数:%s \n”,
snd_strerror(rc));
退出(1);
}
/* 使用足够大的缓冲区来容纳一个句点 */
snd_pcm_hw_params_get_period_size(参数,&帧,
&目录);
大小 = 帧 * 4; /* 2 字节/样本,2 个通道 */
缓冲区 = (char *)malloc(大小);
/* 我们希望循环 5 秒 */
snd_pcm_hw_params_get_period_time(参数,
&val,&dir);
/* 5 秒(以微秒为单位)除以
* 时间段 */
循环 = 5000000 / 值;
while (循环 > 0) {
循环--;
rc = 读取(0,缓冲区,大小);
如果 (rc == 0) {
fprintf(stderr, "输入文件结束\n");
休息;
} 否则,如果 (rc != 大小) {
fprintf(标准错误,
“短读:读取%d字节\n”,rc);
}
rc = snd_pcm_writei(句柄,缓冲区,帧);
如果 (rc == -EPIPE) {
/* EPIPE 表示欠载 */
fprintf(stderr, “发生欠载\n”);
snd_pcm_prepare(句柄);
} 否则,如果 (rc < 0) {
fprintf(标准错误,
“写入错误:%s\n”,
snd_strerror(rc));
} 否则,如果 (rc != (int)frames) {
fprintf(标准错误,
“短写入,写入 %d 帧\n”,rc);
}
}
snd_pcm_drain(句柄);
snd_pcm_close(句柄);
释放(缓冲区);
返回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,并比较结果。更改一些参数,例如采样率和数据格式,看看它如何影响结果。
清单 4. 简单录音
/*
此示例从默认 PCM 设备读取
并将5秒的数据写入标准输出。
*/
/* 使用较新的 ALSA API */
#定义 ALSA_PCM_NEW_HW_PARAMS_API
#包括 <alsa/asoundlib.h>
int 主要() {
长循环;
int rc;
int 大小;
snd_pcm_t *句柄;
snd_pcm_hw_params_t *参数;
无符号整数值;
int 目录;
snd_pcm_uframes_t 帧;
字符*缓冲区;
/* 打开 PCM 设备进行录音(捕获)。 */
rc = snd_pcm_open(&handle, “默认”,
SND_PCM_STREAM_CAPTURE, 0);
如果 (rc < 0) {
fprintf(标准错误,
“无法打开 pcm 设备:%s\n”,
snd_strerror(rc));
退出(1);
}
/* 分配硬件参数对象。 */
snd_pcm_hw_params_alloca(&params);
/* 用默认值填充。 */
snd_pcm_hw_params_any(句柄,参数);
/* 设置所需的硬件参数。 */
/* 交错模式 */
snd_pcm_hw_params_set_access(句柄,参数,
SND_PCM_ACCESS_RW_INTERLEAVED);
/* 有符号 16 位小端格式 */
snd_pcm_hw_params_set_format(句柄,参数,
SND_PCM_格式_S16_LE);
/* 双通道(立体声) */
snd_pcm_hw_params_set_channels(句柄,参数,2);
/* 44100 比特/秒采样率(CD 质量) */
瓦尔=44100;
snd_pcm_hw_params_set_rate_near(句柄,参数,
&val,&dir);
/* 将周期大小设置为 32 帧。 */
帧=32;
snd_pcm_hw_params_set_period_size_near(句柄,
参数,&框架,&目录);
/* 将参数写入驱动程序 */
rc = snd_pcm_hw_params(句柄,参数);
如果 (rc < 0) {
fprintf(标准错误,
“无法设置硬件参数:%s \n”,
snd_strerror(rc));
退出(1);
}
/* 使用足够大的缓冲区来容纳一个句点 */
snd_pcm_hw_params_get_period_size(参数,
&框架,&目录);
大小 = 帧 * 4; /* 2 字节/样本,2 个通道 */
缓冲区 = (char *)malloc(大小);
/* 我们希望循环 5 秒 */
snd_pcm_hw_params_get_period_time(参数,
&val,&dir);
循环 = 5000000 / 值;
while (循环 > 0) {
循环--;
rc = snd_pcm_readi(句柄,缓冲区,帧);
如果 (rc == -EPIPE) {
/* EPIPE 表示溢出 */
fprintf(stderr, “发生溢出\n”);
snd_pcm_prepare(句柄);
} 否则,如果 (rc < 0) {
fprintf(标准错误,
“读取错误:%s\n”,
snd_strerror(rc));
} 否则,如果 (rc != (int)frames) {
fprintf(stderr, "短读,读取 %d 帧\n", rc);
}
rc = 写入(1,缓冲区,大小);
如果 (rc != 大小)
fprintf(标准错误,
“短写入:写入 %d 字节\n”,rc);
}
snd_pcm_drain(句柄);
snd_pcm_close(句柄);
释放(缓冲区);
返回0;
}
清单 4 与清单 3 非常相似,只是我们执行 PCM 捕获(录制)。打开 PCM 流时,我们将模式指定为 SND_PCM_STREAM_CAPTURE。在主处理循环中,我们使用 snd_pcm_readi 从声音硬件读取样本,并使用 write 将其写入标准输出。我们检查是否溢出,并以与清单 3 中处理欠载相同的方式处理它。
运行清单 4 会记录大约五秒钟的数据并将其发送到标准输出;您应该将其重定向到文件。如果您的声卡上连接了麦克风,请使用混音器程序来设置录音源和音量。或者,您可以运行 CD 播放器程序并将录音源设置为 CD。尝试运行清单 4 并将输出重定向到文件。然后您可以运行清单 3 来播放数据:
./listing4 > sound.raw
./listing3 < 声音.raw
如果您的声卡支持全双工声音,您应该能够将程序连接在一起,并通过键入以下内容收听从声卡发出的录制声音:./listing4 | ./listing3
。通过更改 PCM 参数,您可以试验采样率和格式的效果。
高级功能
在前面的示例中,PCM 流以阻塞模式运行,也就是说,在数据传输完成之前,调用不会返回。在交互式事件驱动应用程序中,这种情况可能会使应用程序锁定很长时间。ALSA 允许以非阻塞模式打开流,其中读取和写入函数会立即返回。如果数据传输处于待处理状态并且无法处理调用,则 ALSA 将返回错误代码 EBUSY。
许多图形应用程序使用回调来处理事件。ALSA 支持以异步模式打开 PCM 流。这允许注册一个回调函数,以便在传输一段样本数据时调用该函数。
这里使用的 snd_pcm_readi 和 snd_pcm_writei 调用类似于 Linux 的 read 和 write 系统调用。字母 i 表示帧是交错的;非交错模式有相应的函数。Linux 下的许多设备还支持 mmap 系统调用,该系统调用将它们映射到内存中,然后可以使用指针对其进行操作。最后,ALSA 支持在 mmap 模式下打开 PCM 通道,从而实现对声音数据的高效零拷贝访问。