ALSA 音频编程入门

作者:Jeff Tranter

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 的结构得到了改进。

在 Linux 内核 2.5 系列的开发过程中,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 说明了缓冲区分解为周期、帧和样本以及一些假设值。在这里,左右声道信息在帧内交替存储;这称为交错模式。还支持非交错模式,其中存储一个通道的所有样本数据,然后存储下一个通道的数据。

Introduction to Sound Programming with ALSA

图 1. 应用程序缓冲区

上溢和下溢

当声音设备处于活动状态时,数据在硬件缓冲区和应用程序缓冲区之间连续传输。在数据捕获(录制)的情况下,如果应用程序没有足够快地读取缓冲区中的数据,则循环缓冲区将被新数据覆盖。由此产生的数据丢失称为上溢。在播放期间,如果应用程序没有足够快地将数据传递到缓冲区中,则缓冲区会因数据不足而变得饥饿,从而导致称为下溢的错误。ALSA 文档有时使用术语 XRUN 来指代这两种情况。设计合理的应用程序可以最大限度地减少 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://ftp.linuxjournal.com/pub/lj/listings/issue126/6735.tgz 下载。

列表 1. 显示一些 PCM 类型和格式

#include <alsa/asoundlib.h>

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;
}


列表 1 显示了 ALSA 使用的一些 PCM 数据类型和参数。第一个要求是包含头文件,该文件引入了所有 ALSA 库函数的定义。其中一个定义是显示的 ALSA 版本。

程序的其余部分迭代遍历许多 PCM 数据类型,从流类型开始。ALSA 为最后一个枚举值提供符号名称,并提供一个实用程序函数,该函数返回值的描述性字符串。正如您在输出中看到的那样,ALSA 支持许多不同的数据格式,在我的系统上的 ALSA 版本中为 38 种。

程序必须与 ALSA 库 libasound 链接才能运行。通常,您会在链接器命令行中添加选项 -lasound。某些 ALSA 库函数使用 dlopen 函数和浮点运算,因此您可能还需要添加 -ldl 和 -lm。

列表 2. 打开 PCM 设备并设置参数

/*

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 <alsa/asoundlib.h>

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(&params);

  /* 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 函数显示错误消息并退出。为了清晰起见,我从示例程序中省略了大部分错误检查。在生产应用程序中,应检查每个 API 调用的返回代码并提供适当的错误处理。

为了设置流的硬件参数,我们需要分配一个 snd_pcm_hw_params_t 类型的变量。我们使用宏 snd_pcm_hw_params_alloca 来执行此操作。接下来,我们使用函数 snd_pcm_hw_params_any 初始化变量,传递先前打开的 PCM 流。

我们现在使用 API 调用设置所需的硬件参数,这些 API 调用采用 PCM 流句柄、硬件参数结构和参数值。我们将流设置为交错模式、16 位样本大小、2 个通道和 44,100 bps 采样率。在采样率的情况下,声音硬件并不总是能够完全支持每个采样率。我们使用函数 snd_pcm_hw_params_set_rate_near 来请求最接近请求值的支持采样率。硬件参数直到我们调用函数 snd_pcm_hw_params 后才真正生效。

程序的其余部分获取并显示许多 PCM 流参数,包括周期和缓冲区大小。显示的結果在某种程度上取决于声音硬件。

在您的系统上运行程序后,进行实验并进行一些更改。将设备名称从 default 更改为 hw:0,0 或 plughw:,看看结果是否发生变化。更改硬件参数值并观察显示的結果如何变化。

列表 3. 简单声音播放


/*

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 <alsa/asoundlib.h>

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(&params);

  /* 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 通过将声音样本写入声卡以产生播放来扩展前面的示例。在本例中,我们从标准输入读取字节,足够一个周期,并将它们写入声卡,直到传输了五秒钟的数据。

程序的开头与前面的示例相同——打开 PCM 设备并设置硬件参数。我们使用 ALSA 选择的周期大小,并将此大小作为我们用于存储样本的缓冲区的大小。然后我们找出周期时间,以便我们可以计算程序应处理多少个周期才能运行五秒钟。

在管理数据的循环中,我们从标准输入读取并将我们的缓冲区填充一个周期的样本。我们检查并处理因到达文件末尾或读取与预期字节数不同的字节数而产生的错误。

为了将数据发送到 PCM 设备,我们使用 snd_pcm_writei 调用。它的操作方式很像内核 write 系统调用,只是大小以帧为单位指定。我们检查返回代码以查找多种错误情况。返回代码 EPIPE 表示发生了下溢,这会导致 PCM 流进入 XRUN 状态并停止处理数据。从这种状态恢复的标准方法是使用 snd_pcm_prepare 函数调用将流置于 PREPARED 状态,以便下次我们向流写入数据时它可以再次启动。如果我们收到不同的错误结果,我们将显示错误代码并继续。最后,如果写入的帧数不是预期的帧数,我们将显示错误消息。

程序循环直到传输了五秒钟的帧或在输入上读取到文件末尾为止。然后我们调用 snd_pcm_drain 以允许传输任何挂起的声音样本,然后关闭流。我们释放动态分配的缓冲区并退出。

我们应该看到,除非输入重定向到控制台以外的其他内容,否则该程序没有用处。尝试使用设备 /dev/urandom 运行它,该设备生成随机数据,如下所示

./example3 < /dev/urandom

随机数据应产生五秒钟的白噪声。

接下来,尝试将输入重定向到 /dev/null 或 /dev/zero 并比较结果。更改一些参数,例如采样率和数据格式,看看它如何影响结果。

列表 4. 简单声音录制


/*

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 <alsa/asoundlib.h>

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(&params);

  /* 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 记录大约五秒钟的数据并将其发送到标准输出;您应该将其重定向到文件。如果您的声卡连接了麦克风,请使用混音器程序设置录音源和电平。或者,您可以运行 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 read 和 write 系统调用。字母 i 表示帧是交错的;存在用于非交错模式的相应函数。Linux 下的许多设备也支持 mmap 系统调用,该调用将它们映射到内存中,可以在其中使用指针进行操作。最后,ALSA 支持在 mmap 模式下打开 PCM 通道,这允许对声音数据进行高效的零复制访问。

结论

我希望本文能激发您尝试一些 ALSA 编程。随着 2.6 内核被 Linux 发行版普遍使用,ALSA 应该会得到更广泛的应用,其高级功能应该有助于 Linux 音频应用程序向前发展。

感谢 Jaroslav Kysela 和 Takashi Iwai 审阅了本文草稿并为我提供了有用的意见。

本文资源: /article/7705

Jeff Tranter 自 1992 年以来一直在使用、撰写和贡献 Linux。他在加拿大渥太华的 Xandros 公司工作。

加载 Disqus 评论