8.声卡驱动06-自己实现alsa驱动-虚拟声卡-widget

平台:ubuntu 16.04,kernel版本是4.15.0, 理论任何平台都可以,甚至是android,只要能编译通过。

需要完成的功能:一个codec里面可能有多个录音通道,如果要打开某一通道录音,需要怎么做?

目的:就像做数学题一样,看一遍答案,以为自己看懂了,就会了,非也,真到自己去做时,不一定能做出来。那就在自己的驱动里实现一遍。

本文只追求应用,不讲原理。想了解细节可以看官方文档或者看https://blog.csdn.net/droidphone/category_1118446.html。

widget是干嘛用的

widget也可以控制寄存器。那问题来了,有了kcontrol,为什么还要有widget?

widget是kcontrol的plus,可以理解为是kcontrol的进一步升级和封装,它同样是指音频系统中的某个部件,比如mixer,mux,输入输出引脚,电源供应器等等。

kcontrol还有以下几点不足:

  • 只能描述自身,无法描述各个kcontrol之间的连接关系;
  • 没有相应的电源管理机制;
  • 没有相应的时间处理机制来响应播放、停止、上电、下电等音频事件;
  • 为了防止pop-pop声,需要用户程序关注各个kcontrol上电和下电的顺序;
  • 当一个音频路径不再有效时,不能自动关闭该路径上的所有的kcontrol;

所以,widget诞生了。

像地图一样的codec

以下是全志A33 内部codec的模块图
在这里插入图片描述

像不像一幅地图。

当录音从mic1输入,经过各个mixer/mux,就像经过各个关卡一样,最终到达ADCL,模拟信号转换成数字信号。

条条大道通罗马,通往罗马的道路千万条,但是我们只需要走其中一条,没必要全部都走一遍;同样的,能到达ADCL的信号,可以是mic1、mic2或者其他的,但是当我只需要mic1输入时,其他路就不需要使能。需要被使能的这一条路,称之为complete path。

目的就是为了省电。

构造两条complete path

为更好理解dapm,在自己的虚拟声卡驱动里构造两条complete path。

先看看路线图,如下:
在这里插入图片描述

路线一:mic1 --> kcontrol(一个开关) --> mixer --> ADCL, 就是个录音

路线二:DACL --> HPOUTL --> speaker(功放), 就是个播放

下面看看代码实现

步骤一:定义widget

static const struct snd_kcontrol_new mic1_input_mixer[] = {
	SOC_DAPM_SINGLE("MIC1 Boost Switch", VCODEC_ADCL_REG, 0, 1, 0),
};

static const struct snd_soc_dapm_widget vcodec_dapm_widgets[] = {
	SND_SOC_DAPM_INPUT("MIC1"),
	SND_SOC_DAPM_MIXER("ADCL Input", SND_SOC_NOPM, 0, 0,
				mic1_input_mixer,
				ARRAY_SIZE(mic1_input_mixer)),
	SND_SOC_DAPM_AIF_OUT_E("ADCL", "Capture", 0, VCODEC_ADCL_REG,
				8, 0,
				vcodec_capture_event,
				SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_POST_PMD),
	
	
	SND_SOC_DAPM_AIF_IN_E("DACL", "Playback", 0, VCODEC_DAC_REG,
				0, 0,
				vcodec_playback_event,
				SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD),
	SND_SOC_DAPM_OUTPUT("HPOUTL"),
	SND_SOC_DAPM_SPK("HpSpeaker", vcodec_hpspeaker_event),
};

这是个数组,成员是snd_soc_dapm_widget,通过各种宏定义来填充不同类型的widget;

  • SND_SOC_DAPM_INPUT:该widget对应一个输入引脚;一条完整的dapm音频路径,必然有起点和终点,我们把位于这些起点和终点的widget称之为端点widget。明显mic1是起点,ADCL是终点;

  • SND_SOC_DAPM_MIXER:该widget对应一个mixer控件,看一下宏定义:

    #define SND_SOC_DAPM_MIXER(wname, wreg, wshift, winvert, \
    	 wcontrols, wncontrols)\
    {	.id = snd_soc_dapm_mixer, .name = wname, \
    	SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \
    	.kcontrol_news = wcontrols, .num_kcontrols = wncontrols}
    

    参数说明:

    • id:表示该widget的类型,snd_soc_dapm_mixer就是一个mixer控件;
    • wname:该widget名称;
    • wreg:该widget对应的寄存器控制位,用于控制widget的电源状态的,没有就是设置成SND_SOC_NOPM;
    • winvert:设定值是否逻辑取反;
    • wcontrols:kcontrol,表示开关,一个mixer可能有多路输入,一个kcontrol表示一个开关,就是上图的m;
    • wncontrols:kcontrol个数;
  • SND_SOC_DAPM_AIF_OUT_E:对应一个数字音频输出接口,看一下宏定义:

    #define SND_SOC_DAPM_AIF_OUT_E(wname, stname, wslot, wreg, wshift, winvert, \
    			     wevent, wflags)				\
    {	.id = snd_soc_dapm_aif_out, .name = wname, .sname = stname, \
    	SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \
    	.event = wevent, .event_flags = wflags }
    

    stname必须要和codec dai_driver 的stream_name一样;

    心细如发的你肯定发现了这个宏后面带了“_E”后缀,相对没带“_E”后缀的多了wevent和wflags参数,wevent用于保存该widget的事件回调函数,同时,wflags用于保存该widget需要关心的dapm事件种类,只有wflags中相应的事件位被设置了的事件才会发到event回调函数中进行处理。dapm有哪些事件种类呢?

    事件类型说明
    SND_SOC_DAPM_PRE_PMUwidget要上电前发出的事件
    SND_SOC_DAPM_POST_PMUwidget要上电后发出的事件
    SND_SOC_DAPM_PRE_PMDwidget要下电前发出的事件
    SND_SOC_DAPM_POST_PMDwidget要下电后发出的事件
    SND_SOC_DAPM_PRE_REG音频路径设置之前发出的事件
    SND_SOC_DAPM_POST_REG音频路径设置之后发出的事件
    SND_SOC_DAPM_WILL_PMU在处理up_list链表之前发出的事件
    SND_SOC_DAPM_WILL_PMD在处理down_list链表之前发出的事件
    SND_SOC_DAPM_PRE_POST_PMDSND_SOC_DAPM_PRE_PMD和
    SND_SOC_DAPM_POST_PMD的合并
  • SND_SOC_DAPM_OUTPUT:该widget对应一个输出引脚;

  • SND_SOC_DAPM_SPK:该widget对应一个扬声器;

步骤二:注册widgets

snd_soc_dapm_new_controls(dapm, vcodec_dapm_widgets,
				ARRAY_SIZE(vcodec_dapm_widgets));

widgets最终会加入到card的widgets链表

步骤三:设置route

static const struct snd_soc_dapm_route vcodec_dapm_routes[] = {
	/* Mic input route */
	{"ADCL Input", "MIC1 Boost Switch", "MIC1"},
	{"ADCL", NULL, "ADCL Input"},
	
	/* Headphone output route */
	{"HPOUTL", NULL, "DACL"},
	{"HpSpeaker", NULL, "HPOUTL"},
	
};

一个widget是有输入和输出的,widget之间是可以动态地进行连接的,连接两个widget的“线”就是path,它把一个widget的输出端和另一个widget的输入端连接在一起;

那route是干嘛用的?可以理解为,route就是用来生成path的,route指定了输出端的widget和输入端的widget,然后生成连接两widget的path;

比如上面的数组,“ADCL Input”是目的widget,"MIC1 Boost Switch"是两个widget之间的开关(kcontrol),没有就是NULL,"MIC1"就是起始widget;

步骤四:注册routes

snd_soc_dapm_add_routes(dapm, vcodec_dapm_routes,
				ARRAY_SIZE(vcodec_dapm_routes));

snd_soc_dapm_add_routes()函数所做的事情,简单点来说就是,先找到route指定的目的widget、起始widget和开关(kcontrol),生成path,然后将path添加到card的paths链表。

怎么用

dapm要给一个widget上电的其中一个前提条件是:这个widget位于一条完整的音频路径上,而一条完整的音频路径的两头,必须是输入/输出引脚,或者是一个外部音频设备,又或者是一个处于激活状态的音频流widget;

以刚定义的widget和route为例,"MIC1"是输入端点widget, "ADCL"是输出端点widget,那么

"MIC1" --> "ADCL Input" --> "ADCL"就是一条complete path(完整的音频路径),如果这条complete path的开关(“MIC1 Boost Switch”)被应用使能了,在打开录音时,这条complete path上的所有widget都会被上电;

同理"DACL" --> "HPOUTL" --> "HpSpeaker"是一条complete path,因为这条complete path没有开关,所以在打开播放的时候,这条complete path上的所有widget都会被上电,当然了,widget设置的event函数也会在此时调用。反之,结束播放的时候,这条complete path上的所有widget都会被下电,widget设置的event函数也会被调用;

可以看看这篇文章:ALSA声卡驱动中的DAPM详解之六:精髓所在,牵一发而动全身

怎么知道什么时候给一条complete path上电/下电,当然是去扫描一下了,

以下几种情况可以触发dapm发起一次扫描操作:

  1. 声卡初始化阶段;
  2. 用户空间修改了kcontrol的配置值;
  3. pcm的打开或关闭;
  4. 驱动改变widget并把它加入到dapm_dirty链表。

测试

在ubuntu下测试需要另外安装驱动,如下:

sudo insmod /lib/modules/4.15.0-112-generic/kernel/sound/core/snd-compress.ko
sudo insmod /lib/modules/4.15.0-112-generic/kernel/sound/core/snd-pcm-dmaengine.ko
sudo insmod /lib/modules/4.15.0-112-generic/kernel/sound/soc/snd-soc-core.ko

为什么是/lib/modules/4.15.0-112-generic/kernel/sound/core/这个目录,请看前几篇文章。

然后安装我们的驱动

sudo insmod vplatform.ko
sudo insmod vcodec.ko
sudo insmod vmachine.ko

如果你是在开发板上测试,就不需要这么麻烦,直接安装驱动

安装完驱动之后,就可以看到自己的声卡,如下:

vbox@vbox-pc:~$ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: I82801AAICH [Intel 82801AA-ICH], device 0: Intel ICH [Intel 82801AA-ICH]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 1: mycodec [my-codec], device 0: MY-CODEC vcodec_dai-0 []
  Subdevices: 1/1
  Subdevice #0: subdevice #0

可见我们注册的是card 1,ubuntu本身有自己的声卡,再看看自己注册的controls

vbox@vbox-pc:~$ amixer -D hw:1 contents
numid=2,iface=MIXER,name='ADCL Input MIC1 Boost Switch'
  ; type=BOOLEAN,access=rw------,values=1
  : values=on
numid=1,iface=MIXER,name='DAC volume'
  ; type=INTEGER,access=rw---R--,values=2,min=0,max=255,step=0
  : values=50,60
  | dBscale-min=-119.25dB,step=0.75dB,mute=0

诶,没看到注册widget,是注册失败了吗?并没有

可以看一下/sys/kernel/debug/asoc/my-codec/codec:vcodec.0/dapm目录

vbox@vbox-pc:~$ sudo ls -l /sys/kernel/debug/asoc/my-codec/codec\:vcodec\.0/dapm
total 0
-r--r--r-- 1 root root 0 1115 18:58 ADCL
-r--r--r-- 1 root root 0 1115 18:58 ADCL Input
-r--r--r-- 1 root root 0 1115 18:58 bias_level
-r--r--r-- 1 root root 0 1115 18:58 Capture
-r--r--r-- 1 root root 0 1115 18:58 DACL
-r--r--r-- 1 root root 0 1115 18:58 HPOUTL
-r--r--r-- 1 root root 0 1115 18:58 HpSpeaker
-r--r--r-- 1 root root 0 1115 18:58 MIC1
-r--r--r-- 1 root root 0 1115 18:58 Playback

ps:ubuntu下要root权限才能看到

可以去打开一下’ADCL Input MIC1 Boost Switch’

vbox@vbox-pc:~$ amixer -D hw:1 cset numid=2 on
numid=2,iface=MIXER,name='ADCL Input MIC1 Boost Switch'
  ; type=BOOLEAN,access=rw------,values=1
  : values=on

你会发现,这个kcontrol的名字变了,定义时是"MIC1 Boost Switch",现在变成了"ADCL Input MIC1 Boost Switch";

如果此时正在录音,那么对应的complete path上的所有widget都会被上电,widget的event函数也会被调用;

看看以下log,会不会清楚一些

[ 2294.464437] vplat_pcm_open,line:235
[ 2294.464441] -vcodec_startup,line:193
[ 2294.464630] my_card_hw_params,rate:48000
[ 2294.464632] -vcodec_hw_params,line:205
[ 2294.464633] playback period size : 32768
[ 2294.464667] -vcodec_prepare,line:247
[ 2294.464697] -vcodec_playback_event,SND_SOC_DAPM_PRE_PMU
[ 2294.464700] -vcodec_reg_read,line:163,reg_data[2] = 0x0
[ 2294.464701] -vcodec_reg_write,line:171,reg=0x2,val=0x1
[ 2294.464703] -vcodec_hpspeaker_event,SND_SOC_DAPM_POST_PMU
[ 2294.466124] vplat_pcm_open,line:235
[ 2294.466127] -vcodec_startup,line:193
[ 2294.466251] my_card_hw_params,rate:48000
[ 2294.466253] -vcodec_hw_params,line:205
[ 2294.466254] capture period size : 32768
[ 2294.466289] -vcodec_prepare,line:247
[ 2294.466314] -vcodec_reg_read,line:163,reg_data[1] = 0x1
[ 2294.466315] -vcodec_reg_write,line:171,reg=0x1,val=0x101
[ 2294.466317] -vcodec_capture_event,SND_SOC_DAPM_POST_PMU
[ 2294.466336] -vcodec_prepare,line:247
[ 2294.466355] -vcodec_prepare,line:247
[ 2294.473240] -vcodec_trigger: catpure start
[ 2294.473243] capture running...
[ 2294.492381] -vcodec_trigger: playback start
[ 2294.492383] playback running...
[ 2304.140557] -vcodec_trigger:playback stop
[ 2304.140559] playback stop...
[ 2304.358123] -vcodec_shutdown,line:212
[ 2304.358126] vplat_pcm_close,line:248
[ 2304.358150] -vcodec_hpspeaker_event,SND_SOC_DAPM_PRE_PMD
[ 2304.358153] -vcodec_reg_read,line:163,reg_data[2] = 0x1
[ 2304.358155] -vcodec_reg_write,line:171,reg=0x2,val=0x0
[ 2304.358156] -vcodec_playback_event,SND_SOC_DAPM_POST_PMD
[ 2304.358178] -vcodec_trigger:catpure stop
[ 2304.358179] capture stop...
[ 2304.358235] -vcodec_shutdown,line:212
[ 2304.358236] vplat_pcm_close,line:248
[ 2304.358250] -vcodec_reg_read,line:163,reg_data[1] = 0x101
[ 2304.358252] -vcodec_reg_write,line:171,reg=0x1,val=0x1
[ 2304.358253] -vcodec_capture_event,SND_SOC_DAPM_POST_PMD

代码

代码位置:https://codechina.csdn.net/u014056414/myalsa

后续会增加其他的功能,完成本文的提交是: 9.加widget, 模拟codec的两条complete path

初学者可以按照此提交学习,以免新提交干扰。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值