转载:http://ivqinwei.blog.163.com/blog/static/110931542011722111421855/
3 ALSA Example
一个典型的声音程序使用PCM0000000000000的程序通常类似下面的伪代码:
打开回放或录音接口
设置硬件参数(访问模式,数据格式,信道数,采样率,等等)
while 有数据要被处理:
读PCM数据(录音)
或 写PCM数据(回放)
关闭接口
设置参数,参数设置不当将会导致音频设备无法正常工作。在设置参数前,我们需要了解一下各个参数的含义以及一些基本概念。
样本长度(sample):样本是记录音频数据最基本的单位,常见的有8位和16位。
通道数(channel):该参数为1表示单声道,2则是立体声。
桢(frame):桢记录了一个声音单元,其长度为样本长度与通道数的乘积。8*1 8*2 16 *1 16*2
采样率(rate):每秒钟采样次数,该次数是针对桢而言。
周期(period):音频设备一次处理所需要的桢数,对于音频设备的数据访问以及音频数据的存储,都是以此为单位。
交错模式(interleaved):是一种音频数据的记录方式,在交错模式下,数据以连续桢的形式存放,即首先记录完桢1的左声道样本和右声道样本(假设为立体声格式),再开始桢2的记录。而在非交错模式下,首先记录的是一个周期内所有桢的左声道样本,再记录右声道样本,数据是以连续通道的方式存储。不过多数情况下,我们只需要使用交错模式就可以了。
我们将在下文中看到一些可以工作的代码。我建议您在你的Linux系统上测试运行这些代码。查看输出并尝试修改推荐的代码。和本文相关的所有实例清单可以从FTP中获取:ftp.ssc.com/pub/lj/listings/issue126/6735.tgz。
3.1 Example1. Display Some PCM Types and Formats.
[QW---例一、显示一些PCM类型和格式]
#include <alsa/asoundlib.h>
int main() {
int val;
printf("ALSA library version: %s\n", SND_LIB_VERSION_STR); //ALSA库的版本号
printf("\nPCM stream types:\n"); //PCM流的类型
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"); //PCM通道类型
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"); //PCM格式
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"); //PCM子模式
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"); //PCM状态
for (val = 0; val <= SND_PCM_STATE_LAST; val++)
printf(" %s\n", snd_pcm_state_name((snd_pcm_state_t)val));
return 0;
}
Example1显示了一些ALSA使用的PCM数据类型和参数。首先需要做的是包括头文件。这些头文件包含了所有库函数的声明。其中之一就是显示ALSA库的版本。
这个程序剩下的部分的迭代一些PCM数据类型,以流类型开始。ALSA为每次迭代的最后值提供符号常量名,并且提供功能函数以显示某个特定值的描述字符串。你将会看到,ALSA支持许多格式。
这个程序必须链接到alsalib库,通过在编译时需要加上-lasound选项。有些alsa库函数使用dlopen函数以及浮点操作,所以您可能还需要加上-ldl,-lm选项。
下面是该程序的Makefile:
CC=gcc
TARGET=test
SRC=$(wildcard *.c)
OBJECT= ${SRC:.c=.o}
INCLUDES=-I/usr/include/alsa
LDFLAGS=-lasound
all:$(TARGET)
$(OBJECT):$(SRC)
$(CC) -c $(INCLUDES) $<
$(TARGET):$(OBJECT)
$(CC) -o $@ $< $(LDFLAGS)
.PHONY:clean
clean:
@rm -rf $(OBJECT) $(TARGET) *~
[QW改写--乱七八糟:
CC=gcc
TARGET=test
SRC=$(wildcard *.c)
OBJECT= ${SRC:.c=.o}
INCLUDES=-I/usr/include/alsa
LDFLAGS=-lasound
all:$(TARGET)
${SRC:.c=.o} :$(wildcard *.c)
gcc -c -I/usr/include/alsa $<
test:${SRC:.c=.o}
gcc -o $@ $< $(LDFLAGS)
.PHONY:clean
clean:
@rm -rf $(OBJECT) $(TARGET) *~
----QW]
在电脑上运行,结果如下:
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)
S16_LE (Signed 16 bit Little Endian)
S16_BE (Signed 16 bit Big Endian)
U16_LE (Unsigned 16 bit Little Endian)
U16_BE (Unsigned 16 bit Big Endian)
S24_LE (Signed 24 bit Little Endian)
S24_BE (Signed 24 bit Big Endian)
U24_LE (Unsigned 24 bit Little Endian)
U24_BE (Unsigned 24 bit Big Endian)
S32_LE (Signed 32 bit Little Endian)
S32_BE (Signed 32 bit Big Endian)
U32_LE (Unsigned 32 bit Little Endian)
U32_BE (Unsigned 32 bit Big Endian)
FLOAT_LE (Float 32 bit Little Endian)
FLOAT_BE (Float 32 bit Big Endian)
FLOAT64_LE (Float 64 bit Little Endian)
FLOAT64_BE (Float 64 bit Big Endian)
IEC958_SUBFRAME_LE (IEC-958 Little Endian)
IEC958_SUBFRAME_BE (IEC-958 Big Endian)
MU_LAW (Mu-Law)
A_LAW (A-Law)
IMA_ADPCM (Ima-ADPCM)
MPEG (MPEG)
GSM (GSM)
SPECIAL (Special)
S24_3LE (Signed 24 bit Little Endian in 3bytes)
S24_3BE (Signed 24 bit Big Endian in 3bytes)
U24_3LE (Unsigned 24 bit Little Endian in 3bytes)
U24_3BE (Unsigned 24 bit Big Endian in 3bytes)
S20_3LE (Signed 20 bit Little Endian in 3bytes)
S20_3BE (Signed 20 bit Big Endian in 3bytes)
U20_3LE (Unsigned 20 bit Little Endian in 3bytes)
U20_3BE (Unsigned 20 bit Big Endian in 3bytes)
S18_3LE (Signed 18 bit Little Endian in 3bytes)
S18_3BE (Signed 18 bit Big Endian in 3bytes)
U18_3LE (Unsigned 18 bit Little Endian in 3bytes)
U18_3BE (Unsigned 18 bit Big Endian in 3bytes)
PCM subformats:
STD (Standard)
PCM states:
OPEN
SETUP
PREPARED
RUNNING
XRUN
DRAINING
PAUSED
SUSPENDED
DISCONNECTED
3.2 Example2. 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.
QW---- 这个例子打开一个默认的PCM设备,设置一些参数,然后显示大部分硬件参数的值。它没有实现任何播放声音或录制声音。
*/
/* Use the newer ALSA API */
#define ALSA_PCM_NEW_HW_PARAMS_API
/* All of the ALSA library API is defined in this header 所有的API都定义在头文件里 */
#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;
rc = snd_pcm_open(&handle, " hw:0,1", SND_PCM_STREAM_PLAYBACK, 0); /* Open PCM device for playback. QW---打开PCM设备以便播放声音*/
if (rc < 0) {
fprintf(stderr, "unable to open pcm device: %s\n", snd_strerror(rc));
exit(1);
}
snd_pcm_hw_params_alloca(¶ms); /* Allocate a hardware parameters object. QW--配一个硬件参数工程*/
snd_pcm_hw_params_any(handle, params); /* Fill it in with default values.QW--都设定为默认值 */
/* Set the desired hardware parameters. QW--定预期的硬件参数*/
snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED); /* Interleaved modeQW--设置交叉模式*/
snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE); /* Signed 16-bit little-endian format QW--有符号低字节16位模式*/
snd_pcm_hw_params_set_channels(handle, params, 2); /* Two channels (stereo)QW--双声道,立体声*/
val = 44100;
snd_pcm_hw_params_set_rate_near(handle, params, &val, &dir); /* 44100 bits/second sampling rate (CD quality) QW--设置采样率*/
rc = snd_pcm_hw_params(handle, params); /* Write the parameters to the driver QW--写这些参数到驱动里边去*/
if (rc < 0) {
fprintf(stderr, "unable to set hw parameters: %s\n", snd_strerror(rc));
exit(1);
}
/* Display information about the PCM interfaceQW---下边就是显示PCM接口的信息了 */
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;
}
example2打开hw:0,1的PCM设备,设置一些硬件参数并且打印出最常用的硬件参数值。它并不做任何回放或录音的操作。snd_pcm_open打开hw:0,1的PCM设备并设置访问模式为PLAYBACK。这个函数返回一个句柄,这个句柄保存在第一个函数参数中。该句柄会在随后的函数中用到。像其它函数一样,这个函数返回一个整数。如果返回值小于0,则代码函数调用出错。如果出错,我们用snd_errstr打开错误信息并退出。
为了设置音频流的硬件参数,我们需要分配一个类型为snd_pcm_hw_param的变量。分配用到函数宏snd_pcm_hw_params_alloca。下一步,我们使用函数snd_pcm_hw_params_any来初始化这个变量,传递先前打开的PCM流句柄。
接下来,我们调用API来设置我们所需的硬件参数。这些函数需要三个参数:PCM流句柄,参数类型,参数值。我们设置流为交错模式,16位的样本大小,2个信道,44100bps的采样率。对于采样率而言,声音硬件并不一定就精确地支持我们所定的采样率,但是我们可以使用函数snd_pcm_hw_params_set_rate_near来设置最接近我们指定的采样率的采样率。
其实只有当我们调用函数snd_pcm_hw_params后,硬件参数才会起作用。
程序的剩余部分获得并打印一些PCM流参数,包括周期和缓冲区大小。结果可能会因为声音硬件的不同而不同。
运行该程序后,做实验,改动一些代码,设置不同的硬件参数然后观察结果的变化。
在电脑上运行,结果如下:
PCM handle name = 'hw:0,1'
PCM state = PREPARED
access type = RW_INTERLEAVED
format = 'S16_LE' (Signed 16 bit Little Endian)
subformat = 'STD' (Standard)
channels = 2
rate = 44100 bps
period time = 362 us
period size = 16 frames
buffer time = 362 us
buffer size = 16384 frames
periods per buffer = 1024 frames
exact rate = 1445100000/32768 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 = 1
can pause = 1
can resume = 0
can sync start = 1
[2011年8月22日15时37分25秒看到这个地方]
3.3 Example3.Simple Sound Playback.
/*Example 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 <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;
rc = snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0); /* Open PCM device for playback. */
if (rc < 0) {
fprintf(stderr, "unable to open pcm device: %s\n", snd_strerror(rc));
exit(1);
}
snd_pcm_hw_params_alloca(¶ms); /* Allocate a hardware parameters object. */
snd_pcm_hw_params_any(handle, params); /* Fill it in with default values. */
/* Set the desired hardware parameters. */
snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED); /* Interleaved mode */
snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE); /* Signed 16-bit little-endian format */
snd_pcm_hw_params_set_channels(handle, params, 2); /* Two channels (stereo) */
val = 44100;
snd_pcm_hw_params_set_rate_near(handle, params, &val, &dir); /* 44100 bits/second sampling rate (CD quality) */
frames = 32;
snd_pcm_hw_params_set_period_size_near(handle, params, &frames, &dir); /* Set period size to 32 frames. */
rc = snd_pcm_hw_params(handle, params); /* Write the parameters to the driver */
if (rc < 0) {
fprintf(stderr, "unable to set hw parameters: %s\n", snd_strerror(rc));
exit(1);
}
snd_pcm_hw_params_get_period_size(params, &frames, &dir); /* Use a buffer large enough to hold one period */
size = frames * 4; /* 2 bytes/sample, 2 channels */
buffer = (char *) malloc(size);
snd_pcm_hw_params_get_period_time(params, &val, &dir); /* We want to loop for 5 seconds */
loops = 5000000 / val; /* 5 seconds in microseconds divided by period time */
while (loops > 0) {
loops--;
rc = read(0, buffer, size); //fd为0,表示为标准输入
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) {
fprintf(stderr, "underrun occurred\n");/* EPIPE means underrun */
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;
}
example3扩展了之前的示例。向声卡中写入了一些声音样本以实现声音回放。在这个例子中,我们从标准输入中读取数据,每个周期读取足够多的数据,然后将它们写入到声卡中,直到5秒钟的数据全部传输完毕。
这个程序的开始处和之前的版本一样---打开PCM设备、设置硬件参数。我们使用由ALSA自己选择的周期大小,申请该大小的缓冲区来存储样本。然后我们找出周期时间,这样我们就能计算出本程序为了能够播放5秒钟,需要多少个周期。
在处理数据的循环中,我们从标准输入中读入数据,并往缓冲区中填充一个周期的样本。然后检查并处理错误,这些错误可能是由到达文件结尾,或读取的数据长度与我期望的数据长度不一致导致的。
我们调用snd_pcm_writei来发送数据。它操作起来很像内核的写系统调用,只是这里的大小参数是以帧来计算的。我们检查其返回代码值。返回值为EPIPE表明发生了underrun,使得PCM音频流进入到XRUN状态并停止处理数据。从该状态中恢复过来的标准方法是调用snd_pcm_prepare函数,把PCM流置于PREPARED状态,这样下次我们向该PCM流中数据时,它就能重新开始处理数据。如果我们得到的错误码不是EPIPE,我们把错误码打印出来,然后继续。最后,如果写入的帧数不是我们期望的,则打印出错误消息。
这个程序一直循环,直到5秒钟的帧全部传输完,或者输入流读到文件结尾。然后我们调用snd_pcm_drain把所有挂起没有传输完的声音样本传输完全,最后关闭该音频流,释放之前动态分配的缓冲区,退出。
我们可以看到这个程序没有什么用,除非标准输入被重定向到了其它的文件。尝试用设备/dev/urandom来运行这个程序,该设备产生随机数据:
./test </dev/urandom
随机数据会产生5秒钟的白色噪声。然后,尝试把标准输入重定向到设备/dev/null和/dev/zero上,并比较结果。改变一些参数,例如采样率和数据格式,然后查看结果的变化。
3.4 Example4. Simple Sound Recording.
/*
Example 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 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;
rc = snd_pcm_open(&handle, "default", SND_PCM_STREAM_CAPTURE, 0); /* Open PCM device for recording (capture). */
if (rc < 0) {
fprintf(stderr, "unable to open pcm device: %s\n", snd_strerror(rc));
exit(1);
}
snd_pcm_hw_params_alloca(¶ms); /* Allocate a hardware parameters object. */
snd_pcm_hw_params_any(handle, params); /* Fill it in with default values. */
/* Set the desired hardware parameters. */
snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED); /* Interleaved mode */
snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE); /* Signed 16-bit little-endian format */
snd_pcm_hw_params_set_channels(handle, params, 2); /* Two channels (stereo) */
val = 44100;
snd_pcm_hw_params_set_rate_near(handle, params, &val, &dir); /* 44100 bits/second sampling rate (CD quality) */
frames = 32;
snd_pcm_hw_params_set_period_size_near(handle, params, &frames, &dir); /* Set period size to 32 frames. */
rc = snd_pcm_hw_params(handle, params); /* Write the parameters to the driver */
if (rc < 0) {
fprintf(stderr,"unable to set hw parameters: %s\n", snd_strerror(rc));
exit(1);
}
snd_pcm_hw_params_get_period_size(params, &frames, &dir); /* Use a buffer large enough to hold one period */
size = frames * 4; /* 2 bytes/sample, 2 channels */
buffer = (char *) malloc(size);
snd_pcm_hw_params_get_period_time(params, &val, &dir); /* We want to loop for 5 seconds */
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;
}
example4类似于example3中的程序,除了这里的程序是做声音的抓取(录音)。当打开PCM设备时我们指定打开模式为SND_PCM_STREAM_CPATURE。在主循环中,我们调用snd_pcm_readi从声卡中读取数据,并把它们写入到标准输出。同样地,我们检查是否有overrun,如果存在,用与前例中相同的方式处理。
运行example4的程序将录制将近5秒钟的声音数据,并把它们发送到标准输出。你也可以重定向到某个文件。如果你有一个麦克风连接到你的声卡,可以使用某个混音程序(mixer)设置录音源和级别。同样地,你也可以运行一个CD播放器程序并把录音源设成CD。尝试运行程序4并把输出定向到某个文件,然后运行程序3播放该文件里的声音数据:
./listing4>sound.raw
./listing3<sound.raw
如果你的声卡支持全双工,你可以通过管道把两个程序连接起来,这样就可以从声卡中听到录制的声音:
./listing4 | ./listing3
同样地,您可以做实验,看看采样率和样本格式的变化会产生什么影响。
3.5 高级特性
在前面的例子中,PCM流是以阻塞模式操作的,也就是说,直到数据已经传送完,PCM接口调用才会返回。在事件驱动的交互式程序中,这样会长时间阻塞应用程序,通常是不能接受的。ALSA支持以非阻塞模式打开音频流,这样读写函数调用后立即返回。如果数据传输被挂起,调用不能被处理,ALSA就是返回一个EBUSY的错误码。
许多图形应用程序使用回调来处理事件。ALSA支持以异步的方式打开一个PCM音频流。这使得当某个周期的样本数据被传输完后,某个已注册的回调函数将会调用。
这里用到的snd_pcm_readi和snd_pcm_writei调用和Linux下的读写系统调用类似。字母i表示处理的帧是交错式(interleaved)的。ALSA中存在非交互模式的对应的函数。Linux下的许多设备也支持mmap系统调用,这个调用将设备内存映射到主内存,这样数据就可以用指针来维护。ALSA也运行以mmap模式打开一个PCM信道,这允许有效的零拷贝(zero copy)方式访问声音数据。