ALSA框架介绍:
ALSA概述:
ALSA是Advanced Linux Sound Architecture 的缩写,目前已经成为了linux的主流音频体系结构,想了解更多的关于ALSA的这一开源项目的信息和知识,请查看以下网址:http://www.alsa-project.org,也可以从该网站下载相关源码。在内核设备驱动层,ALSA提供了alsa-driver,同时在应用层,ALSA为我们提供了alsa-lib,应用程序只要调用alsa-lib提供的API,即可以完成对底层音频硬件的控制。其总体架构图示如下:
在用户空间中,由alsa-lib对应用程序提供统一的调用接口,这样可以隐藏驱动层的实现细节,简化应用程序的实现难度。在内核空间中,由alsa-soc对alsa-driver核心驱动提供进一步封装,通过采用这种分层模块,可以在驱动层实现强大的嵌入式设备增强支持。
alsa-lib相关概念:
样本(sample):sample即一次采样的样本,是记录音频数据最基本的单位。通常的sample bit指的是一个channnel上,一次采样的bit数(常见的sample bit 8/16/24/32bits)。
通道(channel):即我们熟知的声道数。左/右声道,5.1channel等等。
帧(frame):帧记录了一个声音单元,一个frame是一次采样时所有channel上的sample bit,即frame = channels * (sample bit)。
采样率(rate):每秒钟采样次数,该次数是针对帧而言。
除开帧不说,通常样本、通道、采样率组成了音频采样的三要素。
周期(period):音频设备一次处理所需要的帧数,对于音频设备的数据访问以及音频数据的存储,都是以此为单位。每当hardware buffer 中有peroid size个frame的空间时,硬件就产生中断,来通知alsa driver来往硬件写数据。
buffer size:hardware buffer size 是由多个peroid组成。buffer size = peroid size * peroids。buffer的内存格式如下:
上图演示的是一块有着16个周期数据的缓存,每个周期设定为8个帧,每帧包含2个通道,每通道包含2个字节(即16位采样),帧使用的是交错排布访问方式。
数据访问布局(Data access and layout):在一个周期内,对于多通道数据,通道数据排布有两种方式:
交错模式(interleaved):数据以连续帧的形式存放,即首先记录完帧1的左声道样本和右声道样本,再开始帧2的记录。
非交错模式:首先记录的是一个周期内所有帧的左声道样本,再记录右声道样本,数据是以连续通道的方式存储。
硬件参数(Hardware parameter):作用于声卡硬件的相关参数,包括sample format, sample rate, interupt intervals, data access and layout, buffer size。
软件参数(Software parameter):作用于alsa core的相关参数,控制一些软件策略,比如控制驱动内存缓存到多少帧后再发数据到声卡,以及xrun状态触发的相关控制等等。
关于缓冲区:
每个声卡都有一个硬件缓存区来保存记录下来的样本。当缓存区足够满时,声卡将产生一个中断。内核声卡驱动然后使用直接内存(DMA)访问通道将样本传送到内存中的应用程序缓存区。类似地,对于回放,任何应用程序使用DMA将自己的缓存区数据传送到声卡的硬件缓存区中。硬件缓存区是环缓存。也就是说当数据到达缓存区末尾时将重新回到缓存区的起始位置。应用程序缓存区的大小可以通过ALSA库函数调用来控制。缓存区可以很大,一次传输操作可能会导致不可接受的延迟,我们把它称为延时(latency)。为了解决这个问题,ALSA将缓存区拆分成一系列周期(period)(OSS/Free中叫片断fragments)。ALSA以period为单元来传送数据。
alsa设备文件:
查看系统设备文件:
$ cd /dev/snd
$ ls -lrt
crw-rw----+ 1 root audio 116, 33 5月 25 14:00 timer
crw-rw----+ 1 root audio 116, 1 5月 25 14:00 seq
crw-rw----+ 1 root audio 116, 5 5月 25 14:00 pcmC0D1c
crw-rw----+ 1 root audio 116, 2 5月 25 14:00 controlC0
drwxr-xr-x. 2 root root 60 5月 25 14:00 by-path
crw-rw----+ 1 root audio 116, 4 5月 25 14:01 pcmC0D0c
crw-rw----+ 1 root audio 116, 3 5月 27 14:21 pcmC0D0p
说明:
timer ---- 定时器设备。
seq ---- 音序器设备。
controlC0 ---- 第1块声卡的控制设备。
pcmC0D0c ---- 第1块声卡的第1个录音设备。
pcmC0D0p ---- 第1块声卡的第1个播放设备。
pcmC0D1c ---- 第1块声卡的第2个录音设备。
alsa驱动通过向应用层抽象出设备,应用层通过读写这些设备,和驱动层进行交互。但是alsa-lib提供了对这些设备的封装,通常我们不需要直接访问他们。
PCM格式介绍:
PCM全称Pulse-Code Modulation,就是脉冲调制编码,简单来说就是一种用数字表示采样模拟信号的方法。
从声卡设备生成PCM数据需要三个阶段:采样、量化、编码,关于这三者的细节不多说,我们直接看下PCM的格式:
例如一段有符号的 8-bit 的 pcm 数据:
+---------+-----------+-----------+----
binary | 0010 0000 | 1010 0000 | ...
decimal | 32 | -96 | ...
+---------+-----------+-----------+----
其表示的采样范围是 -128 ~ 127. 当含有多通道时候 PCM 数据就会交叉排列(通常)以双声道为例:
+---------+-----------+-----------+-----------+-----------+----
FL | FR | FL | FR | FL |
+---------+-----------+-----------+-----------+-----------+----
对于 8-bit 有符号的 PCM 数据而言,上图表示第一个字节存放第一个左声道数据(FL),第二个字节放第一个右声道数据(FR),第三个字节放第二个左声道数据(FL)…
不同的驱动程序对于多声道数据的排列方式可能稍有区别,下面是常用的声道排列地图:
2: FL FR (stereo)
3: FL FR LFE (2.1 surround)
4: FL FR BL BR (quad)
5: FL FR FC BL BR (quad + center)
6: FL FR FC LFE SL SR (5.1 surround - last two can also be BL BR)
7: FL FR FC LFE BC SL SR (6.1 surround)
8: FL FR FC LFE BL BR SL SR (7.1 surround)
上面讲的是PCM交错排列,还有一种非交错排列,两者对比如下:
相关API介绍:
本文主要讲解PCM接口的相关操作,使用这块分接口可以实现PCM音频播放。
首先需要安装alsa-lib软件包:
sudo yum install alsa-lib alsa-lib-devel -y
安装完成后,可以/usr/include/alsa/pcm.h找到PCM操作的相关定义,下面介绍基本的数据结构和函数。
结构定义:
PCM操作句柄,这是一个成员不公开的结构:
/** PCM handle */
typedef struct _snd_pcm snd_pcm_t;
PCM硬件参数,这是一个成员不公开的结构:
typedef struct _snd_pcm_hw_params snd_pcm_hw_params_t;
PCM的流类型,主要指播放和录音两种:
/** PCM stream (direction) */
typedef enum _snd_pcm_stream {
/** Playback stream */
SND_PCM_STREAM_PLAYBACK = 0,
/** Capture stream */
SND_PCM_STREAM_CAPTURE,
SND_PCM_STREAM_LAST = SND_PCM_STREAM_CAPTURE
} snd_pcm_stream_t;
PCM的访问类型,暂时主要关注SND_PCM_ACCESS_RW_INTERLEAVED交错排列和SND_PCM_ACCESS_RW_NONINTERLEAVED非交错排列:
/** PCM access type */
typedef enum _snd_pcm_access {
/** mmap access with simple interleaved channels */
SND_PCM_ACCESS_MMAP_INTERLEAVED = 0,
/** mmap access with simple non interleaved channels */
SND_PCM_ACCESS_MMAP_NONINTERLEAVED,
/** mmap access with complex placement */
SND_PCM_ACCESS_MMAP_COMPLEX,
/** snd_pcm_readi/snd_pcm_writei access */
SND_PCM_ACCESS_RW_INTERLEAVED,
/** snd_pcm_readn/snd_pcm_writen access */
SND_PCM_ACCESS_RW_NONINTERLEAVED,
SND_PCM_ACCESS_LAST = SND_PCM_ACCESS_RW_NONINTERLEAVED
} snd_pcm_access_t;
PCM的样本格式:
/** PCM sample format */
typedef enum _snd_pcm_format {
/** Unknown */
SND_PCM_FORMAT_UNKNOWN = -1,
/** Signed 8 bit */
SND_PCM_FORMAT_S8 = 0,
/** Unsigned 8 bit */
SND_PCM_FORMAT_U8,
/** Signed 16 bit Little Endian */
SND_PCM_FORMAT_S16_LE,
/** Signed 16 bit Big Endian */
SND_PCM_FORMAT_S16_BE,
/** Unsigned 16 bit Little Endian */
SND_PCM_FORMAT_U16_LE,
/** Unsigned 16 bit Big Endian */
SND_PCM_FORMAT_U16_BE,
/** Signed 24 bit Little Endian using low three bytes in 32-bit word */
SND_PCM_FORMAT_S24_LE,
/** Signed 24 bit Big Endian using low three bytes in 32-bit word */
SND_PCM_FORMAT_S24_BE,
/** Unsigned 24 bit Little Endian using low three bytes in 32-bit word */
SND_PCM_FORMAT_U24_LE,
/** Unsigned 24 bit Big Endian using low three bytes in 32-bit word */
SND_PCM_FORMAT_U24_BE,
/** Signed 32 bit Little Endian */
SND_PCM_FORMAT_S32_LE,
/** Signed 32 bit Big Endian */
SND_PCM_FORMAT_S32_BE,
/** Unsigned 32 bit Little Endian */
SND_PCM_FORMAT_U32_LE,
/** Unsigned 32 bit Big Endian */
SND_PCM_FORMAT_U32_BE,
/** Float 32 bit Little Endian, Range -1.0 to 1.0 */
SND_PCM_FORMAT_FLOAT_LE,
/** Float 32 bit Big Endian, Range -1.0 to 1.0 */
SND_PCM_FORMAT_FLOAT_BE,
/** Float 64 bit Little Endian, Range -1.0 to 1.0 */
SND_PCM_FORMAT_FLOAT64_LE,
/** Float 64 bit Big Endian, Range -1.0 to 1.0 */
SND_PCM_FORMAT_FLOAT64_BE,
} snd_pcm_format_t;
PCM无符号和有符号帧数量定义:
/** Unsigned frames quantity */
typedef unsigned long snd_pcm_uframes_t;
/** Signed frames quantity */
typedef long snd_pcm_sframes_t;
函数定义:
alsa-lib的函数的返回值,如果类型为int,通常情况下0表示成功,负数表示失败,可以使用snd_strerror(n)获取错误描述。
获取错误描述:
const char *snd_strerror(int errnum);
打开指定的设备,返回PCM句柄,参数name指定设备名称,通常为"default",stream指定流类型,mode通常传0:
int snd_pcm_open(snd_pcm_t **pcm, const char *name, snd_pcm_stream_t stream, int mode);
关闭PCM句柄指向的设备:
int snd_pcm_close(snd_pcm_t *pcm);
释放PCM句柄占用的内存:
int snd_pcm_hw_free(snd_pcm_t *pcm);
获取指定PCM句柄的设备名称:
const char *snd_pcm_name(snd_pcm_t *pcm);
分配PCM硬件参数内存:
int snd_pcm_hw_params_malloc(snd_pcm_hw_params_t **ptr);
释放PCM硬件参数内存:
void snd_pcm_hw_params_free(snd_pcm_hw_params_t *obj);
为PCM硬件参数初使化默认值:
int snd_pcm_hw_params_any(snd_pcm_t *pcm, snd_pcm_hw_params_t *params);
从PCM句柄指向的设备获取当前的硬件参数:
int snd_pcm_hw_params_current(snd_pcm_t *pcm, snd_pcm_hw_params_t *params);
向PCM句柄指向的设备设置新的硬件参数:
int snd_pcm_hw_params(snd_pcm_t *pcm, snd_pcm_hw_params_t *params);
读取指定PCM硬件参数的访问方式:
int snd_pcm_hw_params_get_access(const snd_pcm_hw_params_t *params, snd_pcm_access_t *_access);
为PCM硬件参数设置访问方式:
int snd_pcm_hw_params_set_access(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, snd_pcm_access_t _access);
读取指定PCM硬件参数的样本格式:
int snd_pcm_hw_params_get_format(const snd_pcm_hw_params_t *params, snd_pcm_format_t *val);
为PCM硬件参数设置样本格式:
int snd_pcm_hw_params_set_format(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, snd_pcm_format_t val);
读取指定PCM硬件参数的通道个数:
int snd_pcm_hw_params_get_channels(const snd_pcm_hw_params_t *params, unsigned int *val);
为PCM硬件参数设置通道个数:
int snd_pcm_hw_params_set_channels(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, unsigned int val);
读取指定PCM硬件参数的采样率:
int snd_pcm_hw_params_get_rate(const snd_pcm_hw_params_t *params, unsigned int *val, int *dir);
为PCM硬件参数设置合适的采样率,val给出建议值,同时输出alsa选择的实际值:
int snd_pcm_hw_params_set_rate_near(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, unsigned int *val, int *dir);
读取指定PCM硬件参数的周期大小(单位为帧数):
int snd_pcm_hw_params_get_period_size(const snd_pcm_hw_params_t *params, snd_pcm_uframes_t *frames, int *dir);
为PCM硬件参数设置合适的周期大小(单位为帧数),val给出建议值,同时输出alsa选择的实际值:
int snd_pcm_hw_params_set_period_size_near(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, snd_pcm_uframes_t *val, int *dir);
向PCM句柄指向的设备写入一段样本缓冲,size为帧数量:
snd_pcm_sframes_t snd_pcm_writei(snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size);
从PCM句柄指向的设备读取一段样本缓冲,size为缓冲大小:
snd_pcm_sframes_t snd_pcm_readi(snd_pcm_t *pcm, void *buffer, snd_pcm_uframes_t size);
通知PCM句柄指向的设备准备接收数据:
int snd_pcm_prepare(snd_pcm_t *pcm);
启动PCM句柄指向的设备:
int snd_pcm_start(snd_pcm_t *pcm);
暂停PCM句柄指向的设备,enable取值0表示恢复,1表示暂停:
int snd_pcm_pause(snd_pcm_t *pcm, int enable);
恢复PCM句柄指向的设备:
int snd_pcm_resume(snd_pcm_t *pcm);
排空PCM句柄指向的设备的样本缓冲:
int snd_pcm_drain(snd_pcm_t *pcm);
设置PCM句柄指向的设备的延迟帧数:
int snd_pcm_delay(snd_pcm_t *pcm, snd_pcm_sframes_t *delayp);
重置PCM句柄指向的设备的延迟帧数:
int snd_pcm_reset(snd_pcm_t *pcm);
设置PCM句柄指向的设备执行延迟硬同步:
int snd_pcm_hwsync(snd_pcm_t *pcm);
代码举例:
下面这个例子读取1个PCM白噪声文件,并且按FLOAT_LE格式、2通道交错排列、44.1K采样率来解析,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <alsa/asoundlib.h>
int main(int argc, char* argv[])
{
snd_pcm_t *hPCM = NULL;
int rc = snd_pcm_open(&hPCM, "default", SND_PCM_STREAM_PLAYBACK, 0);
if (rc < 0)
{
printf("snd_pcm_open() ret:[%d:%s] \n", rc, snd_strerror(rc));
return -1;
}
printf("snd_pcm_open() %s ok. \n", snd_pcm_name(hPCM));
snd_pcm_hw_params_t *pHWParams = NULL;
snd_pcm_hw_params_malloc(&pHWParams);
snd_pcm_hw_params_any(hPCM, pHWParams);
rc = snd_pcm_hw_params_set_access(hPCM, pHWParams, SND_PCM_ACCESS_RW_INTERLEAVED);
if (rc < 0)
{
printf("snd_pcm_hw_params_set_access() ret:[%d:%s] \n", rc, snd_strerror(rc));
return -1;
}
printf("snd_pcm_hw_params_set_access() SND_PCM_ACCESS_RW_INTERLEAVED ok. \n");
rc = snd_pcm_hw_params_set_format(hPCM, pHWParams, SND_PCM_FORMAT_FLOAT_LE);
if (rc < 0)
{
printf("snd_pcm_hw_params_set_format() ret:[%d:%s] \n", rc, snd_strerror(rc));
return -1;
}
printf("snd_pcm_hw_params_set_format() SND_PCM_FORMAT_FLOAT_LE ok. \n");
rc = snd_pcm_hw_params_set_channels(hPCM, pHWParams, 2);
if (rc < 0)
{
printf("snd_pcm_hw_params_set_channels() ret:[%d:%s] \n", rc, snd_strerror(rc));
return -1;
}
printf("snd_pcm_hw_params_set_channels() 2 ok. \n");
unsigned int nSampleRate = 44100;
rc = snd_pcm_hw_params_set_rate_near(hPCM, pHWParams, &nSampleRate, NULL);
if (rc < 0)
{
printf("snd_pcm_hw_params_set_rate_near() ret:[%d:%s] \n", rc, snd_strerror(rc));
return -1;
}
printf("snd_pcm_hw_params_set_rate_near() advise:[44100] real:[%d] ok. \n", nSampleRate);
snd_pcm_uframes_t nFrames = 1024;
rc = snd_pcm_hw_params_set_period_size_near(hPCM, pHWParams, &nFrames, NULL);
if (rc < 0)
{
printf("snd_pcm_hw_params_set_period_size_near() ret:[%d:%s] \n", rc, snd_strerror(rc));
return -1;
}
printf("snd_pcm_hw_params_set_period_size_near() advise:[1024] real:[%d] ok. \n", nFrames);
rc = snd_pcm_hw_params(hPCM, pHWParams);
if (rc < 0)
{
printf("snd_pcm_hw_params() ret:[%d:%s] \n", rc, snd_strerror(rc));
return -1;
}
printf("snd_pcm_hw_params() ok. \n");
snd_pcm_hw_params_free(pHWParams);
rc = snd_pcm_prepare(hPCM);
if (rc < 0)
{
printf("snd_pcm_prepare() ret:[%d:%s] \n", rc, snd_strerror(rc));
return -1;
}
rc = snd_pcm_start(hPCM);
if (rc < 0)
{
printf("snd_pcm_start() ret:[%d:%s] \n", rc, snd_strerror(rc));
return -1;
}
int nFrameBuffSize = 4 * 2 * nFrames;
char* pFrameBuff = (char*)malloc(nFrameBuffSize);
FILE* pFile = fopen("test.pcm", "rb");
while (!feof(pFile))
{
int nBytes = fread(pFrameBuff, 1, nFrameBuffSize, pFile);
if (nBytes == nFrameBuffSize)
{
printf("read 1 period frames(%d) \n", nBytes/8);
}
else
{
if (nBytes % 8)
{
printf("read break! \n");
break;
}
printf("no 1 period frames(%d) \n", nBytes/8);
}
rc = snd_pcm_writei(hPCM, pFrameBuff, nBytes/8);
if (rc == -EPIPE)
{
printf("snd_pcm_writei() underrun occurred! \n");
snd_pcm_prepare(hPCM);
}
else if (rc < 0)
{
printf("snd_pcm_writei() ret:[%d:%s] \n", rc, snd_strerror(rc));
break;
}
else if (rc < nFrames)
{
printf("snd_pcm_writei() short write! \n");
}
}
fclose(pFile);
free(pFrameBuff);
rc = snd_pcm_drain(hPCM);
if (rc < 0)
{
printf("snd_pcm_drain() ret:[%d:%s] \n", rc, snd_strerror(rc));
return -1;
}
printf("snd_pcm_hw_drain() ok \n");
rc = snd_pcm_close(hPCM);
if (rc < 0)
{
printf("snd_pcm_hw_close() ret:[%d:%s] \n", rc, snd_strerror(rc));
return -1;
}
printf("snd_pcm_hw_close() ok \n");
snd_pcm_hw_free(hPCM);
return 0;
}
保存上面的代码为alsa1.c,编译生成可执行:
gcc -o alsa1 alsa1.c -lasound
生成PCM文件,执行:
cat /dev/urandom > test.pcm
生成很快,所以需要尽快CTRL+C。
插上耳机,播放PCM文件,可能需要root权限:
sudo ./alsa1
可以听到沙沙的噪声。