Linux ALSA驱动框架分析之(一):架构介绍
Linux ALSA驱动框架分析之(二):pcm逻辑设备的创建
Linux ALSA驱动框架分析之(三):Control逻辑设备的创建
音频相关概念
声音的采样
我们知道在现实生活中,人耳听到的声音是模拟信号,PCM就是要把声音从模拟转换成数字信号的一种技术,他的原理简单地说就是利用一个固定的频率对模拟信号进行采样,采样后的信号在波形上看就像一串连续的幅值不一的脉冲,把这些脉冲的幅值按一定的精度进行量化,量化之后就行进行编码。
PCM(Pulse Code Modulation)即脉冲编码调制。在PCM过程中,将输入的模拟信号进行采样、量化和编码,PCM信号的两个重要指标是采样频率和量化精度。
衡量量化的指标就是采样位数(深度):即指描述数字信号所使用的的位数,位数越多,分级就越细,量化误差就越小。
采样频率
指每秒钟取得声音样本的次数,为了复原波形,采样频率越高,声音的质量也就越好,声音的还原也就越真实,但同时它占的资源比较多。由于人耳的分辨率很有限,太高的频率并不能分辨出来。22050的采样频率是常用的,44100已是CD音质,超过48000或96000的采样对人耳已经没有意义。
通道数
由于音频的采集和播放是可以叠加的,因此,可以同时从多个音频源采集声音,并分别输出到不同的扬声器,故声道数一般表示声音录制时的音源数量或回放时相应的扬声器数量。
常见的单声道和立体声(双声道),现在发展到了四声环绕(四声道)和5.1声道等更多声道。
交错模式
在交错模式下,数据以连续帧的形式存放,即首先记录完帧1的左声道样本和右声道样本(假设为立体声格式),再开始帧2的记录。
帧
帧记录了一个声音单元,其长度为样本长度与通道数的乘积,一段音频数据就是由苦干帧组成的。
帧大小
一帧占用多少字节。
周期(period)
音频设备一次处理所需要的桢数,取决于硬件缓冲区的大小,对于音频设备的数据访问以及音频数据的存储,都是以此为单位。
码率
即每秒的数据量,码率 = 采样率 * 帧大小。
ALSA驱动框架
ALSA是Advanced Linux Sound Architecture的缩写,即高级Linux声音架构。在Linux 2.6的内核版本后,ALSA目前已经成为了Linux的主流音频体系结构。
ALSA系统框架如下图:
ALSA Lib:ALSA还包含在用户空间的alsa-lib函数库,具有更加友好的编程接口,开发者可以通过这些高级API编写应用程序,不必直接操作设备文件。
ALSA Core:ALSA核心层,抽象了一套数据结构与接口,向应用层提供了统一的编程接口(字符设备)。
ASoc Core:ASoc是建立在标准ALSA Core基础上,为了更好支持嵌入式系统和应用于移动设备的音频Codec的一套软件体系。
Hardware Driver:硬件设备驱动。
来看看具体的硬件框架:
Codec
音频编解码器主要的功能有:
- 对PCM音频信号进行D/A转换,把数字的音频信号转换为模拟信号,输送给耳机或音响,即播放声音;
- 对麦克风或者其他输入源的模拟信号进行A/D转换,把模拟的声音信号转变CPU能够处理的数字信号,即录音;
- 对音频信号做出相应的处理,例如音量控制,功率放大,EQ控制等等;
Audio Interface
用于传输数字音频信号,主流使用I2S接口。
Control Interface
用于配置Codec,主流使用I2C接口。
ALSA Core抽象了struct snd_card结构体,描述一个声卡,在大多数嵌入式系统中,声卡就是Codec,结构体定义如下:
struct snd_card {
//该声卡的编号,系统中可能有多张声卡,需要编号
int number;
//声卡的id标识符
char id[16];
//声卡驱动的name
char driver[16]; /* driver name */
......
struct module *module; /* top-level module */
//链表,声卡的所有逻辑设备都会挂入该链表
struct list_head devices;
//声卡的控制逻辑设备对应的device
struct device ctl_dev; /* control device */
unsigned int last_numid; /* last used numeric ID */
int controls_count; /* count of all controls */
int user_ctl_count; /* count of all user controls */
struct list_head controls; /* all controls for this card */
struct list_head ctl_files; /* active control files */
......
//声卡对应的device
struct device card_dev;
bool registered; /* snd_card已注册的话,该域为True */
......
};
某款Codec内部结构图如下:
内部主要功能部件有:
- Mixer,混音器,多路模拟声音混合输出;
- ADC,模拟信号转数字信号;
- DAC,数字信号转模拟信号;
- Filter,滤波器,把数字或模拟信号转换到不同的频率,控制播放快慢。
内核把这些功能部件看作是一个个的逻辑设备,ALSA Core抽象了struct snd_device结构体,描述一个声卡的逻辑设备,结构体定义如下:
struct snd_device {
struct list_head list; /* 用于挂入snd_card的devices链表 */
struct snd_card *card; /* 指向所属的snd_card */
enum snd_device_state state; /* state of the device */
enum snd_device_type type; /* 逻辑设备的类型 */
void *device_data; /* device structure */
struct snd_device_ops *ops; /* 操作集 */
};
struct snd_device_ops {
//释放逻辑设备时,会调用dev_free回调
int (*dev_free)(struct snd_device *dev);
//注册逻辑设备时,会调用dev_register回调
int (*dev_register)(struct snd_device *dev);
//注销逻辑设备时,会调用dev_disconnect回调
int (*dev_disconnect)(struct snd_device *dev);
};
逻辑设备的类型有很多:
enum snd_device_type {
SNDRV_DEV_LOWLEVEL,
SNDRV_DEV_CONTROL, //控制逻辑设备,对应与Codec里面的控制部件,控制音量大小、快慢等
SNDRV_DEV_INFO,
SNDRV_DEV_BUS,
SNDRV_DEV_CODEC,
SNDRV_DEV_PCM, //pcm逻辑设备,对应与Codec里面的ADC或DAC,实现录音或播放声音的功能
SNDRV_DEV_COMPRESS,
SNDRV_DEV_RAWMIDI,
SNDRV_DEV_TIMER,
SNDRV_DEV_SEQUENCER,
SNDRV_DEV_HWDEP,
SNDRV_DEV_JACK,
};
snd_card有一devices链表,声卡所有的逻辑设备会挂入该链表。
内核提供了一些常用部件的创建函数,如下:
//创建pcm逻辑设备
int snd_pcm_new(struct snd_card *card, const char *id, int device,
int playback_count, int capture_count, struct snd_pcm **rpcm);
//创建控制逻辑设备
int snd_ctl_create(struct snd_card *card);
int snd_timer_new(struct snd_card *card);
int snd_rawmidi_new(struct snd_card *card);
......
来看看ALSA的设备文件结构:
- controlC0,用于声卡的控制,例如音量,混音,麦克风的控制等等;
- pcmC0D0c,用于录音的pcm设备0;
- pcmC0D0p,用于播放的pcm设备0;
- pcmC0D1c,用于录音的pcm设备1;
- pcmC0D1p,用于播放的pcm设备1;
ALSA驱动
ALSA驱动主要工作就是:
- 创建snd_card的一个实例;
- 设置snd_card;
- 创建声卡的功能部件(逻辑设备),例如PCM、Control等;
- 注册snd_card;
调用snd_card_new函数,创建与初始化一个snd_card实例:
/* 参数说明:
* parent,声卡的父设备
* idx,声卡的编号,如何传入-1,则让系统分配一个没占用的编号
* xid,字符串,声卡标识符,alsa-lib通过标识符找到对应的snd_card
* extra_size,该参数决定在创建snd_card实例时,需要同时额外分配的私有数据的大小,额外分配的内存
由snd_card->private_data指向
* card_ret,保存创建的snd_card实例的指针
*/
int snd_card_new(struct device *parent, int idx, const char *xid,
struct module *module, int extra_size,
struct snd_card **card_ret)
{
struct snd_card *card;
int err;
......
if (extra_size < 0)
extra_size = 0;
//创建一个snd_card
card = kzalloc(sizeof(*card) + extra_size, GFP_KERNEL);
......
if (extra_size > 0)
card->private_data = (char *)card + sizeof(struct snd_card);
if (xid)
strlcpy(card->id, xid, sizeof(card->id));
......
//初始化snd_card的锁、链表等
card->dev = parent;
card->number = idx;
card->module = module;
INIT_LIST_HEAD(&card->devices);
init_rwsem(&card->controls_rwsem);
rwlock_init(&card->ctl_files_rwlock);
mutex_init(&card->user_ctl_lock);
INIT_LIST_HEAD(&card->controls);
INIT_LIST_HEAD(&card->ctl_files);
spin_lock_init(&card->files_lock);
INIT_LIST_HEAD(&card->files_list);
......
//设置snd_card的card_dev
device_initialize(&card->card_dev);
card->card_dev.parent = parent;
card->card_dev.class = sound_class;
card->card_dev.release = release_card_device;
card->card_dev.groups = card->dev_groups;
card->dev_groups[0] = &card_dev_attr_group;
err = kobject_set_name(&card->card_dev.kobj, "card%d", idx);
......
//创建声卡的控制逻辑设备
err = snd_ctl_create(card);
......
*card_ret = card;
return 0;
......
}
在初始化并创建好声卡的逻辑设备后,就可以调用snd_card_register函数进行注册声卡了:
int snd_card_register(struct snd_card *card)
{
int err;
......
if (!card->registered) {
err = device_add(&card->card_dev);
if (err < 0)
return err;
card->registered = true;
}
//注册声卡的逻辑设备
if ((err = snd_device_register_all(card)) < 0)
return err;
mutex_lock(&snd_card_mutex);
......
/* cards为struct snd_card *型的全局数组,用于保存注册的snd_card,
以后以声卡编号为下标可以在该数组找到对应的snd_card
*/
cards[card->number] = card;
mutex_unlock(&snd_card_mutex);
......
return 0;
}
在sound/core/init.c中定义了一个snd_card指针的全局数组:
/* number of supported soundcards */
#ifdef CONFIG_SND_DYNAMIC_MINORS
#define SNDRV_CARDS CONFIG_SND_MAX_CARDS
#else
#define SNDRV_CARDS 8 /* don't change - minor numbers */
#endif
struct snd_card *snd_cards[SNDRV_CARDS]; /* sound/core/init.c */