ALSA编程学习(翻译自ALSA Programming HOWTO)
1.ALSA音频知识点
转载自:https://blog.csdn.net/jkan2001/article/details/52985329?utm_source=blogkpcl8
声音是连续模拟量,计算机将它离散化之后用数字表示,主要有以下的一些术语。
1 样本长度(sample):样本是记录音频数据最基本的单位,计算机对每个通道采样量化时数字比特位数,常见的有8位和16位。样本长度是影响声音被转换成数字信号的精确程度的因素之一。
2 通道数(channel):该参数为1表示单声道,2则是立体声。
3 帧(frame):帧记录了一个声音单元,其长度为样本长度与通道数的乘积(如立体声16位的pcm数据长度为4byte),一段音频数据就是由若干帧组成的。为啥搞个frame出来?因为对于多声道的话,用1个采样点的字节数表示不全,因为播放的时候肯定是多个声道的数据都要播出来才行。所以为了方便,就说1秒钟有多少个frame,这样就能抛开声道数,把意思表示全了。
4 交错模式(interleaved):是一种音频数据的记录方式。在交错模式下,数据以连续桢的形式存放,即首先记录完第一个帧的左声道样本和右声道样本(假设为立体声格式),再开始第二个帧的记录。而在非交错模式下,首先记录的是一个周期内所有帧的左声道样本,再记录右声道样本,数据是以连续通道的方式存储。不过多数情况下,只需要使用交错模式就可以了。
5 采样率(rate):每秒钟采样次数,该次数是针对帧而言,常用的采样率如8KHz的人声, 44.1KHz的mp3音乐, 96Khz的蓝光音频。
6 比特率(Bits Per Second):比特率表示每秒的比特数,比特率=采样率×通道数×样本长度。
7 ALSA开发接口:由许多声卡的声卡驱动程序组成,同时它也提供一个称为libasound的API库。应用程序开发者应该使用libasound而不是内核中的 ALSA接口。因为libasound提供最高级并且编程方便的编程接口。并且提供一个设备逻辑命名功能,这样开发者甚至不需要知道类似设备文件这样的低层接口。
8 缓存区:每个声卡都有一个硬件缓存区来保存记录下来的样本。当缓存区足够满时,声卡将产生一个中断。内核声卡驱动然后使用直接内存(DMA)访问通道将样本传送到内存中的应用程序缓存区。类似地,对于回放,任何应用程序使用DMA将自己的缓存区数据传送到声卡的硬件缓存区中。这样硬件缓存区是通常为环缓存。也就是说当数据到达缓存区末尾时将重新回到缓存区的起始位置。ALSA维护一个指针来指向硬件缓存以及应用程序缓存区中数据操作的当前位置。
9 XRUN:当一个声卡工作时,数据总是连续地在硬件缓存区和应用程序缓存区间传输。但是也有例外。1)在录音过程中,如果应用程序读取数据不够快,循环缓存区将会被新的数据覆盖。这种数据的丢失被称为"over run".2)在回放过程中,如果应用程序写入数据到缓存区中的速度不够快,缓存区将会"饿死"。这样的错误被称为"under run"。在ALSA中将这两种情形统称为"XRUN"。适当地设计应用程序可以最小化XRUN并且可以从中恢复过来。因此,简单的说,XRUN状态分有两种,在播放时,用户空间没及时写数据导致缓冲区空了,硬件没有可用数据播放导致"under run"; 录制时,用户空间没有及时读取数据导致缓冲区满后溢出, 硬件录制的数据没有空闲缓冲可写导致"over run".
补充:一个缓存区分解成周期,然后是帧,然后是样本。图中包含一些假定的数值。图中左右信道信息被交替地存储在一个帧内(立体设备,一个period就是每两次硬件中断之间的帧数。poll()会每个周期return一次。buffer是一个环形buffer,大小一般来说比一个period size大,一般设做 2 * period size)。这称为交错 (interleaved)模式。在非交错模式中,一个信道的所有样本数据存储在另外一个信道的数据之后。
2. Basic PCM audio
一个简单的PCM例程在操作设备进行数据传输前,开始需要指定pcm设备处理的句柄,然后指定pcm数据流的流向(playbck or capture),最后设定一些参数,比如采样率,通道数,buffer大小,pcm数据格式等。
/* Handle for the PCM device */
snd_pcm_t *pcm_handle;
/* Playback stream */
snd_pcm_stream_t stream = SND_PCM_STREAM_PLAYBACK;
/* This structure contains information about */
/* the hardware and can be used to specify the */
/* configuration to be used for the PCM stream. */
snd_pcm_hw_params_t *hwparams;
有两个借口可获取到pcm设备,“plughw”和“hw”,“plughw”当声卡不支持你的设置时会自动转换,而“hw”在这种情况下会报错。
/* Name of the PCM device, like plughw:0,0 */
/* The first number is the number of the soundcard, */
/* the second number is the number of the device. */
char *pcm_name;
/* Init pcm_name. Of course, later you */
/* will make this configurable ;-) */
pcm_name = strdup("plughw:0,0");
/* Allocate the snd_pcm_hw_params_t structure on the stack. */
snd_pcm_hw_params_alloca(&hwparams);
现在可以open pcm设备了。
/* Open PCM. The last parameter of this function is the mode. */
/* If this is set to 0, the standard mode is used. Possible */
/* other values are SND_PCM_NONBLOCK and SND_PCM_ASYNC. */
/* If SND_PCM_NONBLOCK is used, read / write access to the */
/* PCM device will return immediately. If SND_PCM_ASYNC is */
/* specified, SIGIO will be emitted whenever a period has */
/* been completely processed by the soundcard. */
if (snd_pcm_open(&pcm_handle, pcm_name, stream, 0) < 0) {
fprintf(stderr, "Error opening PCM device %s\n", pcm_name);
return(-1);
}
在向pcm设备写入数据之前,需要制定访问类型,采样格式,采样率,通道数,周期数,周期尺寸。访问类型指定多通道数据在buffer中的存储方式。下边是一些关于参数的函数命名方式及例子。
snd_pcm_hw_params_can_<capability>
snd_pcm_hw_params_is_<property>
snd_pcm_hw_params_get_<parameter>
snd_pcm_hw_params_test_<parameter>
snd_pcm_hw_params_set_<parameter>
/* Set access type. This can be either */
/* SND_PCM_ACCESS_RW_INTERLEAVED or */
/* SND_PCM_ACCESS_RW_NONINTERLEAVED. */
/* There are also access types for MMAPed */
/* access, but this is beyond the scope */
/* of this introduction. */
if (snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) {
fprintf(stderr, "Error setting access.\n");
return(-1);
}
/* /* Signed 16-bit little-endian format */ */
if (snd_pcm_hw_params_set_format(pcm_handle, hwparams, SND_PCM_FORMAT_S16_LE) < 0) {
fprintf(stderr, "Error setting format.\n");
return(-1);
}
/* Set sample rate. If the exact rate is not supported */
/* by the hardware, use nearest possible rate. */
exact_rate = rate;
if (snd_pcm_hw_params_set_rate_near(pcm_handle, hwparams, &exact_rate, 0) < 0) {
fprintf(stderr, "Error setting rate.\n");
return(-1);
}
if (rate != exact_rate) {
fprintf(stderr, "The rate %d Hz is not supported by your hardware.\n
==> Using %d Hz instead.\n", rate, exact_rate);
}
/* Set number of channels */
if (snd_pcm_hw_params_set_channels(pcm_handle, hwparams, 2) < 0) {
fprintf(stderr, "Error setting channels.\n");
return(-1);
}
/* Set number of periods. Periods used to be called fragments. */
if (snd_pcm_hw_params_set_periods(pcm_handle, hwparams, periods, 0) < 0) {
fprintf(stderr, "Error setting periods.\n");
return(-1);
}
/* Set buffer size (in frames). The resulting latency is given by */
/*If your hardware does not support a buffersize of 2^n, you can use the function snd_pcm_hw_params_set_buffer_size_near. This works similar to snd_pcm_hw_params_set_rate_near. */
/* latency = periodsize * periods / (rate * bytes_per_frame) */
if (snd_pcm_hw_params_set_buffer_size(pcm_handle, hwparams, (periodsize * periods)>>2) < 0) {
fprintf(stderr, "Error setting buffersize.\n");
return(-1);
}
将配置写入驱动。
/* Apply HW parameter settings to */
/* PCM device and prepare device */
if (snd_pcm_hw_params(pcm_handle, hwparams) < 0) {
fprintf(stderr, "Error setting HW params.\n");
return(-1);
}
根据access type调用对应的函数向设备写入数据。
/* Write num_frames frames from buffer data to */
/* the PCM device pointed to by pcm_handle. */
/* Returns the number of frames actually written. */
//interleaved mode
snd_pcm_sframes_t snd_pcm_writei(pcm_handle, data, num_frames);
//non-interleaved mode
snd_pcm_sframes_t snd_pcm_writen(pcm_handle, data, num_frames);
当设备开始运行时,需要保证应用发送足够的数据到声卡buffer(避免出现underrun)。
/*A simple stereo saw wave could be generated this way*/
unsigned char *data;
int pcmreturn, l1, l2;
short s1, s2;
int frames;
data = (unsigned char *)malloc(periodsize);
frames = periodsize >> 2;
for(l1 = 0; l1 < 100; l1++) {
for(l2 = 0; l2 < num_frames; l2++) {
s1 = (l2 % 128) * 100 - 5000;
s2 = (l2 % 256) * 100 - 5000;
data[4*l2] = (unsigned char)s1;
data[4*l2+1] = s1 >> 8;
data[4*l2+2] = (unsigned char)s2;
data[4*l2+3] = s2 >> 8;
}
while ((pcmreturn = snd_pcm_writei(pcm_handle, data, frames)) < 0) {
//重新准备设备
snd_pcm_prepare(pcm_handle);
fprintf(stderr, "<<<<<<<<<<<<<<< Buffer Underrun >>>>>>>>>>>>>>>\n");
}
}
停止playback有两个函数。
/* Stop PCM device and drop pending frames */
snd_pcm_drop(pcm_handle);
/* Stop PCM device after pending frames have been played */
snd_pcm_drain(pcm_handle);
/*close PCM device*/
snd_pcm_close(pcm_handle);
3.PCM capture
一个句柄不能即用作playback又用作capture,所以需配置新的句柄,方法和配置playback相似,open时pcm流的方向配置为SND_PCM_STREAM_CAPTURE。
同样读取数据有两个可用函数。
/* Read num_frames frames from the PCM device */
/* pointed to by pcm_handle to buffer capdata. */
/* Returns the number of frames actually read. */
snd_pcm_readi(pcm_capture_handle, capdata, num_frames);
snd_pcm_readn(pcm_capture_handle, capdata, num_frames);
和playback时类似,为避免应用读取buffer时发生overrun(buffer满leer未读取)
int pcmreturn;
while ((pcmreturn = snd_pcm_readi(pcm_capture_handle, capdata, periodsize>>2)) < 0) {
snd_pcm_prepare(pcm_capture_handle);
fprintf(stderr, "<<<<<<<<<<<<<<< Buffer Overrun >>>>>>>>>>>>>>>\n");
}
4.More
api说明见:https://www.alsa-project.org/alsa-doc/alsa-lib/modules.html
alsa说明及例子:https://www.linuxjournal.com/article/6735