Android Audio-标准ALSA架构

本文是对ALSA标准架构的总结。

1. ALSA Concepts

  • 声卡

接口卡,可以是集成在主板上的声音芯片,也可以是外部插槽卡。

  • PCM参数和配置空间

ALSA访问硬件设备时,参数不是彼此独立的。

  • Alsa devices and plugins

alsa devices 用 string 表示,一般在配置文件中定义。

  • 插件
    • hw插件:不自己处理,只访问硬件。hw:0,0
    • plug插件:做通道复制、采样值转换和重采样。plughw:0,0
    • file 插件:把采样数据写入到文件
    • tee:把音频数据同时输出到文件和设备。用这种方式我们可以在播放电影的时候提取音轨。
1
2
3
\# 将 xy.wav 文件的音频数据通过 plughw:0,0 设备播放,并将音频数据同时输出到 /tmp/alsatee.out 文件中

aplay -Dtee:\'plughw:0,0\',/tmp/alsatee.out,raw xy.wav
  • ALSA层次结构

tinyalsa, tinycap, tinymix ->

ALSA Library API (user space) ->

ALSA Core / ALSA driver (kernel space)

ASoC (kernel space)

Audio Hardware

  • ALSA 代码结构

ALSA 代码主要在 kernel/sound 下:

(1)sound/core

这个目录是alsa架构的核心,也就是alsa-driver

(2)sound/soc

ASoC核心,主要是针对嵌入式系统设计的alsa-driver的封装。

(3)sound/soc/codecs

包含了该音频系统所使用的codec的驱动,与体系结构无关。

(4)sound/soc/(machine)

音频系统的platform驱动和machine驱动,或者说这里包含了与音频系统所使用的主芯片方案相关的代码。

(5)kernel/include/sound

ALSA所有包含的头文件。

2. ALSA声卡的创建

2.1 数据结构

snd-struct

2.2 声卡的建立流程

(1)实例化声卡

init

在创建声卡的时候,也创建了一个control设备组件,一般嵌入式系统中仅需要一个control设备即可。

(2)创建声卡的设备组件

声卡实例化的时候会创建control设备,其他的还有pcm设备用于实现音频流的playback和capture。

pcm设备组件:snd_pcm_new()

control设备组件:snd_ctl_create()

每个部件的创建都会最终调用到snd_device_new() 来生成一个snd_device实例,并把该实例链接到snd_card的devices链表中。

device

小结:

  • 实例化一个snd_device

  • 给snd_device关联一个声卡设备组件操作函数集

  • 将snd_device挂载到声卡设备组件链表上

(3)注册声卡

card-register

小结:

  • 创建sys属性文件

  • 注册声卡下所有挂载的设备组件

  • 注册proc文件

3. control设备组件

一个系统中一般会实例化一个声卡,一个声卡下挂载多个设备组件。control设备组件主要处理声卡中控制设备和控制音频流相关的动作,比如codec路径的搭建、DSP中音频路径的搭建等等

本节介绍control设备的创建以及应用层如何通过controls控制声卡。

3.1 control设备组件的创建

前面提到,创建声卡的时候就会创建control设备。

  • snd_ctl_create()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int snd_ctl_create(struct snd_card *card)

{

static struct snd_device_ops ops = {
.dev_free = snd_ctl_dev_free,
.dev_register = snd_ctl_dev_register,
.dev_disconnect = snd_ctl_dev_disconnect,

};
...
snd_device_initialize(&card->ctl_dev, card);
dev_set_name(&card->ctl_dev, "controlC%d", card->number);

err = snd_device_new(card, SNDRV_DEV_CONTROL, card, &ops);
...
}

注册声卡的时候会注册所有挂载声卡下的设备组件。对于control设备,调用其注册函数snd_ctl_dev_register()。

1
2
3
4
5
6
7
8
static int snd_ctl_dev_register(struct snd_device *device)
{
struct snd_card *card = device->device_data;


return snd_register_device(SNDRV_DEVICE_TYPE_CONTROL, card, -1,
&snd_ctl_f_ops, card, &card->ctl_dev);
}

其操作函数集为snd_ctl_f_ops

1
2
3
4
5
6
7
8
9
10
11
12
static const struct file_operations snd_ctl_f_ops =
{
.owner = THIS_MODULE,
.read = snd_ctl_read,
.open = snd_ctl_open,
.release = snd_ctl_release,
.llseek = no_llseek,
.poll = snd_ctl_poll,
.unlocked_ioctl = snd_ctl_ioctl,
.compat_ioctl = snd_ctl_ioctl_compat,
.fasync = snd_ctl_fasync,
};

ctl

小结:

  • 声卡设备是通过snd_minor[]的全局数组进行统一管理的,通过minor做为索引可以找到对应的设备。

static struct snd_minor *snd_minors[SNDRV_OS_MINORS];

  • 它的主设备号是 116,次设备号是随机分配的。

#define CONFIG_SND_MAJOR 116

  • 这里创建的设备节点,路径为 /dev/snd/name
1
2
3
 # ls -ll /dev/snd/controlC0

crw-rw---- 1 system audio 116, 16 2025-02-06 05:10:35.717999996 +0800 /dev/snd/controlC0
  • 所有声卡设备组件共用主设备号 116,随后可以根据次设备号minor找到对应的设备组件信息。

这里补充一下设备节点注册和调用的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int __init alsa_sound_init(void)
{
snd_major = major; // 116
snd_ecards_limit = cards_limit;
if (register_chrdev(major, "alsa", &snd_fops)) {
pr_err("ALSA core: unable to register native major device number %d\n", major);
return -EIO;
}
if (snd_info_init() < 0) {
unregister_chrdev(major, "alsa");
return -ENOMEM;
}
return 0;
}

static const struct file_operations snd_fops =

{
.owner = THIS_MODULE,
.open = snd_open,
.llseek = noop_llseek,
};

当应用空间open设备节点 /dev/snd/name 的时候,调用到统一的主设备open函数 snd_open()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int snd_open(struct inode *inode, struct file *file)

{
...
mptr = snd_minors[minor]; // 获取次设备号
...
new_fops = fops_get(mptr->f_ops); // 获取设备组对应的操作函数集

...
if (file->f_op->open)
err = file->f_op->open(inode, file); // 执行其open函数。
return err;

}

因此应用层open /dev/snd/controlC0设备时,最终会调用其snd_ctl_f_ops的open函数。

继续看control文件的open流程:

ctl-open

可以看到:当打开/dev/snd/controC0时,control设备组件的open函数主要是实例化一个snd_ctl_file,然后找到声卡 snd_card,将文件关联到声卡中。最后将snd_ctl_file保存到 file->frivate_data,方便其他函数操作使用。

实际上,control设备节点最重要的操作函数是 snd_ctl_ioctl,后面再详细分析。

3.2 controls的创建

前面说了control组件的创建,它用于对声卡进行控制,比如控制声卡的声音和音频流等。

所有的这些控制的实现,是通过一个个独立的controls来进行的。

要对声卡某一个功能进行控制,我们首先需要创建一个controls,然后注册进系统。现在来看下具体的controls是如何创建并注册的。

先定义一个snd_kcontrol_new,然后通过函数 snd_ctl_new1() 获取一个snd_kcontrol,然后通过接口 snd_ctl_add() 就可以创建一个controls。

  • snd_ctl_new1()

动态申请一个snd_kcontrol,然后对成员赋值(把snd_kcontrol_new的数据拷贝到 snd_kcontrol中)。

  • snd_ctl_add()

最核心就是把controls加到card->controls链表中。

每个controls都通过 snd_ctl_elem_id进行唯一指定。

1
2
3
4
5
6
7
8
9
10
static int __snd_ctl_add_replace(struct snd_card *card,
struct snd_kcontrol *kcontrol,
enum snd_ctl_add_mode mode)
...
list_add_tail(&kcontrol->list, &card->controls);
card->controls_count += kcontrol->count;
kcontrol->id.numid = card->last_numid + 1;
card->last_numid += kcontrol->count;
...
}

3.3 应用程序操作controls

ALSA将底层提供给应用空间的统一接口保存在一个头文件 kernel/include/sound/asound.h中,应用要操作controls,需要先包含头文件:#include <sound/asound.h>

前面提到,controls在内核空间是通过数据结构 snd_kcontrol 来表示的,每一个control由 struct snd_ctl_elem_id 数据结构唯一区分和指定:

ctl-struct

因此,用户空间操作某一个controls,必须指定上面的数据结构,以唯一确定某一个controls。

实际上,用户空间是通过如下结构来操作controls的:

ctl-elem

前面说过,应用层操作controls是通过control设备组件进行的,即操作 /dev/snd/controlC0。

上层命令是通过ioctl进行,也就是 snd_ctl_ioctl。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static long snd_ctl_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
...
ctl = file->private_data;
card = ctl->card;

switch (cmd) {
...
case SNDRV_CTL_IOCTL_ELEM_INFO:
return snd_ctl_elem_info_user(ctl, argp);
case SNDRV_CTL_IOCTL_ELEM_READ:
return snd_ctl_elem_read_user(card, argp);
case SNDRV_CTL_IOCTL_ELEM_WRITE:
return snd_ctl_elem_write_user(ctl, argp);
...
}
}

分析最常用的write命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static int snd_ctl_elem_write_user(struct snd_ctl_file *file,
struct snd_ctl_elem_value __user *_control)
{
struct snd_ctl_elem_value *control;
struct snd_card *card;
int result;
// _control是分配在用户空间的内存,必须将其拷贝到内核空间
// 类型就是 struct snd_ctl_elem_value
control = memdup_user(_control, sizeof(*control));
if (IS_ERR(control))
return PTR_ERR(control);

card = file->card;
...

down_write(&card->controls_rwsem);
result = snd_ctl_elem_write(card, file, control);
up_write(&card->controls_rwsem);

...

if (copy_to_user(_control, control, sizeof(*control)))
result = -EFAULT;

error:
kfree(control);
return result;
}

再来到snd_ctl_elem_write:

1
2
3
4
5
6
7
8
9
10
11
static int snd_ctl_elem_write(struct snd_card *card, struct snd_ctl_file *file,
struct snd_ctl_elem_value *control)
{
...
// 由id找到内核空间的 snd_control,遍历 card->controls
kctl = snd_ctl_find_id(card, &control->id);
...
result = kctl->put(kctl, control);
...
return 0;
}

最终调用到kctl->put(),也就是控件的put函数。

总结:

在内核中,control设备组件是用于声卡控制的,其接口一般为:/dev/snd/controlC0,用户控件通过这个文件接口与内核ALSA交互,用于控制声卡;

controls在内核中用 snd_kcontrol 抽象,每一个controls哟用于控制声卡的某一个特定的功能,每一个controls在建立时都会据由一个唯一的id用于标识;

用户空间的程序携带有controls的信息通过control设备文件接口陷入内核中执行该controls拥有的能力。

4. PCM设备组件

PCM设备组件主要用于实现声音的playback以及capture功能。

4.1 数据结构

class-pcm

  • struct snd_pcm
    • card 表示所属声卡
    • device PCM设备组件号
    • streams[2] 表示两个流:一个playback流和一个capture流

声卡中pcm设备组件在ALSA中用此数据结构进行抽象。

  • struct snd_pcm_str

    • stream 表示流的方向
    • pcm 表示流属于哪一个PCM组件
    • subtsream 表示某一个流下面的子流
  • struct snd_pcm_substream

    • pcm 所属PCM设备组件

    • snd_pcm_str 子流所属的流

    • private_data

    • dma_buffer 表示该子流与DMA操作相关的信息

    • ops,表示该子流拥有的PCM操作函数集。对于PCM设备组件而言,PCM操作函数集所操作的载体是子流!

    • snd_pcm_runtume 记录了这个substream的一些重要的软件和硬件运行环境和参数

这个是非常重要的数据结构,表示的是一个PCM设备组件下面的某一个流的子流,它是ALSA执行PCM功能要操作的对象。

  • struct snd_pcm_ops

这个数据结构非常重要,由自己的声卡驱动程序实现。在ALSA中,当应用空间操作PCM设备节点的时候,最终都会转换到底层这里的函数中进行。

  • struct snd_pcm_runtime

表示的是PCM相关的环境信息,如DMA buffer等等。它拥有 hw_params和sw_params配置拷贝,缓冲区指针,mmap记录,自旋锁等。对驱动程序操作集函数是只读的,进pcm中间层可以改变这些信息。

  • const struct file_operations snd_pcm_f_ops[2]

后面会知道,每一个PCM设备组件在注册的时候会创建两个设备节点,一个是playback设备节点,一个是capture设备节点。

小结:

一个声卡下面可以有多个PCM设备组件,每一个PCM设备组件都据由唯一的设备号,每个PCM设备组件下面有两个流:playback和capture。每个流可以由多个子流组成,它是ALSA进行PCM操作最后要作用的对象。每个子流拥有一个PCM操作函数集 snd_pcm_ops,它是自己的声卡驱动要实现的函数。

4.2 创建PCM

pcm-new

  • snd_pcm_new()

通过snd_pcm_new()创建PCM设备组件。

与control设备组件类似的,会创建一个 struct snd_device,并添加到ard->devices链表中。

此外,PCM设备组件会创建两个stream。

  • snd_pcm_new_stream()

    • 根据stream方向取出是playback还是capture;
    • 确定流的方向
    • 确定该流所属的PCM设备组件
    • 根据子流的数量,实例化子流,给子流相关的成员赋值
  • snd_pcm_dev_register()

与control设备组件类似,在new的时候传入了dev->ops,在register设备的时候会调到其注册函数 snd_pcm_dev_register()。

区别在于:

  1. 会根据是playback还是capture修改名字(p/c)

  2. snd_register_device_for_dev() 时传入 snd_pcm_f_ops[idx],playback和capture是两组函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const struct file_operations snd_pcm_f_ops[2] = {
{
{
.owner = THIS_MODULE,
.write = snd_pcm_write,
.write_iter = snd_pcm_writev,
.open = snd_pcm_playback_open,
.release = snd_pcm_release,
.llseek = no_llseek,
.poll = snd_pcm_poll,
.unlocked_ioctl = snd_pcm_ioctl,
.compat_ioctl = snd_pcm_ioctl_compat,
.mmap = snd_pcm_mmap,
.fasync = snd_pcm_fasync,
.get_unmapped_area = snd_pcm_get_unmapped_area,
},
{
.owner = THIS_MODULE,
.read = snd_pcm_read,
.read_iter = snd_pcm_readv,
.open = snd_pcm_capture_open,
.release = snd_pcm_release,
.llseek = no_llseek,
.poll = snd_pcm_poll,
.unlocked_ioctl = snd_pcm_ioctl,
.compat_ioctl = snd_pcm_ioctl_compat,
.mmap = snd_pcm_mmap,
.fasync = snd_pcm_fasync,
.get_unmapped_area = snd_pcm_get_unmapped_area,
}
};

最后,应用层操作这两个设备节点的时候,实际通过系统调用陷入内核中执行这两个设备文件操作函数集中的函数。

4.3 PCM的实现

前面说到PCM的实现主要是实现数据结构 snd_pcm_ops,里面的函数指针都是作为接口供自己的声卡驱动程序实现的。ALSA给我们提供了一个重要的接口 snd_pcm_set_ops(),用于将我们实现好的驱动体现到ALSA架构中。

1
2
3
4
5
6
7
8
9
10
void snd_pcm_set_ops(struct snd_pcm *pcm, int direction,
const struct snd_pcm_ops *ops)
{
struct snd_pcm_str *stream = &pcm->streams[direction];
struct snd_pcm_substream *substream;
for (substream = stream->substream; substream != NULL; substream = substream->next)
substream->ops = ops; // 当操作子流的时候就执行外部的ops。
}

EXPORT_SYMBOL(snd_pcm_set_ops);

高通平台对该结构的实现:

1
2
3
4
5
6
7
8
9
10
static struct snd_pcm_ops q6asm_dai_ops = {
.open = q6asm_dai_open,
.hw_params = q6asm_dai_hw_params,
.close = q6asm_dai_close,
.ioctl = snd_pcm_lib_ioctl,
.prepare = q6asm_dai_prepare,
.trigger = q6asm_dai_trigger,
.pointer = q6asm_dai_pointer,
.mmap = q6asm_dai_mmap,
};

open函数是在上层执行open打开PCM设备节点时调用的。

copy和trigger是在上层调用write向PCM设备节点写数据时调用的。

hw_param是上层调用iotcl,参数为SNDRV_PCM_IOCTL_HW_PARAMS时调用的。

prepare是上层通过ioctl,参数为SNDRV_PCM_IOCTL_PREPARE时调用的。

  • open()

q6asm-dai

高通sw5100平台有一个专门的ADSP,音频经过用户空间解码成PCM格式的音频流以后,通过PCM接口将其copy到内核空间的DMA区域中,接着通过DMA控制器将其传送到ADSP中。

open只是做一些初始准备工作。

  • hw_params()

hw_params函数为substream设定q6asm_dai_rtd中的pcm数据格式。

  • prepare()

dai-prepare

可见,真正打开ADSP的session是在prepare的时候完成的。

内核驱动dsp之间是通过Audio-Pkt命令来完成的。apr驱动的代码路径:

android/kernel/msm-5.4/drivers/soc/qcom/apr.c

  • trigger()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int q6asm_dai_trigger(struct snd_pcm_substream *substream, int cmd)
{
int ret = 0;
struct snd_pcm_runtime *runtime = substream->runtime;
struct q6asm_dai_rtd *prtd = runtime->private_data;

switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
case SNDRV_PCM_TRIGGER_RESUME:
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
ret = q6asm_run_nowait(prtd->audio_client, 0, 0, 0);
break;
case SNDRV_PCM_TRIGGER_STOP:
prtd->state = Q6ASM_STREAM_STOPPED;
ret = q6asm_cmd_nowait(prtd->audio_client, CMD_EOS);
break;
case SNDRV_PCM_TRIGGER_SUSPEND:
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
ret = q6asm_cmd_nowait(prtd->audio_client, CMD_PAUSE);
break;
default:
ret = -EINVAL;
break;
}
return ret;

}

q6asm_run_nowait() 是通过apr_send_pkt()发送命令来控制adsp的启动停止。

小结:

上面分析的是音频从用户空间传递到ADSP中的情况,这一部分是ALSA的核心。不过对于音频的流向而言。实际一条完整的路线是:

用户空间 – 系统调用 –> 内核空间DMA区 – gpr –> ADSP – DAI –> CODEC –> SPK

这条路从软件结构上可以分为前端FE和后端BE,前端负责将音频流高效地送到ADSP,这里还没有音频设备的概念;后端负责将音频流通过平台的DAI送到CODEC,通过搭建不同的路径,最终送到不同的音频设备中。

5. alsa-lib和alsa-utils

alsa-lib:运行在应用层,APP通过调用alsa-lib来实现alsa driver的能力。

alsa-utils:一些alsa调试开发工具。常用的有 amixer,aplay,arecord。