DSP

alsa-lib应用层接口分析

2019-07-13 19:51发布

ALSA lib接口调用简介 ALSA逻辑,在我当前看来,总共有两条线:1、录放音流控,2、amixer cset控件。   录放音流控(自定义名称),相当于操作OSS的/dev/dsp设备,可以设置三大参数等,并且启动录放音。这里,aplay,mplayer等播放器,调用ALSA lib中的snd_pcm_***等,标准接口函数,再通过该函数,操作/dev/snd/pcmC0D0c pcmC0D0p pcmC0D1c pcmC0D1p pcmC0D2c等流控设备节点,完成设置音频三大参数,DMA buffer使用大小,预缓冲buffer阀值等,并且启动录放音。因为ALSA功能的丰富,这里录放音,并不是使用write,read这些接口函数,而应该是通过ioctl接口,来完成的。   amixer cset控件,相当于OSS驱动中的/dev/mixer设备节点的操作,可以调节录放音的音量,静音,录放音通道切换等。 amixer cset通过调用ALSA lib中的标准函数,操作/dev/snd/controlC0设备节点,来操作底层codec_***.c驱动中,定义的kcontrol控件。并通过kcontrol控件,直接读写codec中的相关寄存器。这是ALSA与OSS较大不同的一点,将codec相关功能寄存器,零件化,提供给上层,由上层自由设置。这里,也是通过 controlC0设备节点的ioctl接口,来读写codec的kcontrol。 一、ALSA流控接口操作方法,可参考alsa utils源码中的aplay.c文件:   1、snd_pcm_open(&handle, pcm_name, stream, open_mode); &handle,是static snd_pcm_t *handle;该结构体指针,在后面的所有操作中,都要用到。 pcm_name,是打开的pcm设备节点,可以是“default” ,也可以是“hw:0,0”。具体有何区别,可网上查询。 stream,对应放音与录音操作,SND_PCM_STREAM_PLAYBACK,SND_PCM_STREAM_CAPTURE。 open_mode,是一些相关功能设置,比如:是否阻塞,是否重采样,是否软件调音量等。   2、snd_pcm_info(handle, info); 一般用来获取录放音 声卡与设备等相关信息。不是必须的接口。   3、snd_pcm_nonblock(handle, 1); 如果需要非阻塞方式,则将第二个参数,设置为1。snd_pcm_open后,默认为阻塞方式,因此,该接口可以不用调用。   4、snd_pcm_hw_params_alloca(¶ms); 申请一段snd_pcm_hw_params_t *params;结构体空间。使用该结构体,来配置底层ALSA的相关硬件参数,比如:声道,采样率,位宽,buffer大小等。 stream == SND_PCM_STREAM_PLAYBACK playback(argv[optind++])--->playback_go(fd, 0, pbrec_count, FORMAT_AU, name)--->set_params()-->snd_pcm_hw_params_alloca   5、snd_pcm_hw_params_any(handle, params);   6 snd_pcm_hw_params_set_access(handle, params,SND_PCM_ACCESS_RW_INTERLEAVED);设置为交错方式。 snd_pcm_hw_params_set_rate_resample(handle, params, soft_resample);   7、snd_pcm_hw_params_set_format(handle, params, hwparams.format);设置采样位宽格式。   8、snd_pcm_hw_params_set_channels(handle, params, hwparams.channels);设置声道数。   9、snd_pcm_hw_params_set_rate_near(handle, params, &hwparams.rate, 0);设置采样率,之所以是rate_near,是因为上层设置的采样率&hwparams.rate,底层很可能不支持,因此,会返回上层一个底层支持的相近的采样率。   10、snd_pcm_hw_params_get_buffer_time_max(params, &buffer_time, 0);获取底层支持的最大buffer大小,换算为时间。 snd_pcm_hw_params_set_period_time_near(handle, params, &period_time, 0); 设置底层period区间大小,以时间为单位,之所以为time_near,是因为上层设置的大小很可能底层不支持,底层会返回相近的&period_time。 snd_pcm_hw_params_set_period_size_near(handle, params, &period_frames, 0);设置底层period区间大小,以字节为单位,size_near,原因类似。该接口与set_period_time_near接口,都是设置period大小,只是单位不同,二选一即可。 snd_pcm_hw_params_set_buffer_time_near(handle, params, &buffer_time, 0);设置底层buffer区间大小,以时间为单位。与上面接口类似。在底层,buffer是一个大DMA空间,而period是将buffer平均分成的几个小区间。 snd_pcm_hw_params_set_buffer_size_near(handle, params, &buffer_frames);与上类似。 上面设置完成后,都需要调用下面两个函数,获取最终设置的period与buffer大小。 snd_pcm_hw_params_get_period_size(params, &chunk_size, 0); snd_pcm_hw_params_get_buffer_size(params, &buffer_size);   11、snd_pcm_hw_params_is_monotonic(params);??? snd_pcm_hw_params_can_pause(params);查询底层是否支持暂停。   12、snd_pcm_hw_params(handle, params);将上面设置的硬件配置参数,加载,并且会自动调用snd_pcm_prepare( )将stream状态置为SND_PCM_STATE_PREPARED。   13、snd_pcm_sw_params_alloca(&swparams);申请一段snd_pcm_sw_params_t *swparams;结构体空间,存放软件配置参数。   14、snd_pcm_sw_params_current(handle, swparams);获取当前的软件参数配置。   15、snd_pcm_sw_params_set_avail_min(handle, swparams, n);???   16、snd_pcm_sw_params_set_start_threshold(handle, swparams, start_threshold);设置开始的阀值,可以起到预缓冲的效果。对于放音,当上层调用snd_pcm_writei超过阀值时,才会启动播放。   17、snd_pcm_sw_params_set_stop_threshold(handle, swparams, stop_threshold);设置停止的阀值,避免硬件上的underrun或overrun出现。   18、snd_pcm_sw_params(handle, swparams);将上面的软件参数,加载到底层。   19、上面设置完成后,就可以开始read与write了。   20、snd_pcm_writei(snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size);播放交错格式的音频数据。snd_pcm_t *pcm为snd_pcm_open时得到的。size,这里不是字节数,不是采样点数。而是采样帧数。双声道,一帧采样,有两个采样点。ALSA上层接口,一般都是以采样帧数为单位的。 stream == SND_PCM_STREAM_PLAYBACK 播放 playback(argv[optind++])--->playback_go(fd, 0, pbrec_count, FORMAT_AU, name)--->set_params()设置完参数播放歌曲-->pcm_write()--->writei_func(handle, data, count)--->snd_pcm_writei--> _snd_pcm_writei()-->snd_pcm_hw_writei(alsa-lib-1.1.3/src/pcm/pcm_hw.c)-->ioctl(fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &xferi)调进内核   内核中,根据/soc/pcm_native.c文件中.unlocked_ioctl = snd_pcm_capture_ioctl,可知snd_pcm_capture_ioctl1被调用,根据SNDRV_PCM_IOCTL_READI_FRAMES参数可知snd_pcm_lib_write(substream, xferi.buf, xferi.frames);被调用,最终snd_pcm_lib_write1(,,,,snd_pcm_lib_write_transfer)被调用。根据transfer被调用可知snd_pcm_lib_write_transfer被调用,然后调用copy_from_user(hwbuf, buf, frames_to_bytes(runtime, frames),可知,将alsa_lib提供的一个指针所指的内存拷贝到dma端内存的数据,alsa库函数snd_pcm_readi、snd_pcm_writei实现了内存到内存的交互,或者近似地认为是内存到音频文件的交互。   snd_pcm_readi(snd_pcm_t *pcm, void *buffer, snd_pcm_uframes_t size);录制交错格式的音频数据。参数与上相同。 snd_pcm_writen与snd_pcm_readn,与上面两个函数,基本相同,只是录放音数据的格式是非交错的。既一帧数据,只包含一个声道数据,交错格式指,一帧数据中,包含了多个声道的数据。比如:双声道一帧数据中,包含了左声道与右声道,各一个采样点。   21、snd_pcm_format_set_silence(snd_pcm_format_t format, void *data, unsigned int samples); 用于将某个buffer中的数据,转换为静音,也就是0。   22、snd_pcm_nonblock(handle, 0);打开或关闭阻塞模式。0,为阻塞模式,1,为非阻塞模式。   22、snd_pcm_drain(handle);等待buffer中的数据全部播放完成。 snd_pcm_drop(handle);不等待buffer数据播放完成,马上停止。   23、snd_pcm_close(handle);关闭pcm handle。   24、ALSA lib中的dmix插件,只能用于放音通道数据的混音,不能用于录音通道数据的混音。   25、.asoundrc其实是指向asound.conf文件的软链接文件。 asound.conf,是针对pcm与ctl设备的功能插件调用流程表,需要根据具体的功能需求,来配置该文件。否则,会没有相关设备,或无法完成相关功能。   26、alsa lib中,plug插件,可完成重采样,声道转换等操作。asym插件,完成分别声明:播放与录音 下一级slave插件的功能。dsnoop,完成多来源录音 混音的功能,dmix,完成多来源放音,混音的功能。mutil,完成多声道数据,虚拟对应到多codec声道的功能。     二、ALSA amixer cset控件接口操作,可参考alsa utils源码中的amixer.c: 上层amixer程序,调用ALSA lib中的标准接口函数,操作/dev/controlC0设备(主要使用了ioctl接口),来完成对底层codec_***.c中kcontrol的一些操作。在内核kernel/sound/core/control.c中,定义了struct file_operations snd_ctl_f_ops,该operation会注册/dev/controlC*设备的file operations。 比如:amixer cset设置某个element值时,最终会调到kernel/sound/core/control.c中,snd_ctl_ioctl()函数。SNDRV_CTL_IOCTL_ELEM_READ中,来完成读取该element control的值。SNDRV_CTL_IOCTL_ELEM_WRITE中,则完成将上层设置的element control的值,写入kcontrol中。 这两个case中,分别调用snd_ctl_elem_write()和snd_ctl_elem_read()函数,最后调用到codec_***.c驱动中,snd_kcontrol_new结构体中,SOC_SINGLE与SOC_ENUM等宏中,.info,.get,.put,对应的读写codec寄存器的函数。 alsa_amixer-用户态 |->snd_ctl_ioctl-系统调用 |->snd_ctl_elem_write_user-内核钩子函数 |->snd_ctl_elem_wirte- |->snd_ctl_find_id-遍历 kcontrol 链表找到 name 字段匹配的 kctl |->kctl->put()-调用 kctl 的成员函数 put() |->snd_soc_put_volsw   最近发现,ALSA codec_***.c驱动中,调用SOC_SINGLE,SOC_DOUBLE_R_TLV,SOC_ENUM等宏,注册snd_kcontrol_new控件后,alsa上层调用alsa lib中的alsa-lib-v1.0.28/src/mixer/simple.c中的selems()-->show_selem()-->snd_mixer_find_selem()函数,查找匹配的amixer cset控件时,名称与底层的名称不一致。 比如: 底层codec***.c定义了SOC_SINGLE("Aux In Switch", DIGITAL_FILTER_MODE, 1, 1, 0), 上层snd_mixer_find_selem()查找到的s->id->name = Aux In,将Switch省略了。 还有SOC_DOUBLE_R_TLV("Master Playback Volume", LCH_DIGITAL_VOLUME, RCH_DIGITAL_VOLUME, 0, 0xcc, 1, dac_tlv),上层获取s->id->name = Master。将Playback Volume省略了。 但个别的控件名称,没有省略,比如:SOC_SINGLE("Digital Playback mute", MODE_CONTROL_3, 5, 1, 0),上层获取的s->id->name = Digital Playback mute。 经过追查在alsa-lib-v1.0.28/src/control/hcontrol.c中,有对控件名称的简化操作,将一些Switch,Volume,Playback之类的通用单词,省略掉了,只保留控件开头的单词。具体为何这样做,还没去弄清楚。 所以,上层在调用alsa-lib-v1.0.28/src/mixer/simple.c中的snd_mixer_find_selem()函数,查找匹配的amixer cset控件时,最好将s->id->name 打印出来,再设置,否则会查找不到对应名称的控件。 上层查找控件的示例代码如下: static void alsa_linein_init(char *alsa_mix_ctrl, snd_mixer_t **alsa_mix_handle_p, snd_mixer_elem_t **alsa_mix_elem_p) { /* 查找控件函数 */ int alsa_mix_index = 0; char alsa_mix_dev[32] = {0}; snd_mixer_selem_id_t *alsa_mix_sid = NULL; snd_mixer_selem_id_alloca(&alsa_mix_sid); snd_mixer_selem_id_set_index(alsa_mix_sid, alsa_mix_index); snd_mixer_selem_id_set_name(alsa_mix_sid, alsa_mix_ctrl);   if(mozart_ini_getkey("/usr/data/system.ini", "audio", "playback", alsa_mix_dev)) strcpy(alsa_mix_dev,"default");   if ((snd_mixer_open(alsa_mix_handle_p, 0)) < 0) DEBUG ("Failed to open mixer"); if ((snd_mixer_attach(*alsa_mix_handle_p, alsa_mix_dev)) < 0) DEBUG ("Failed to attach mixer"); if ((snd_mixer_selem_register(*alsa_mix_handle_p, NULL, NULL)) < 0) DEBUG ("Failed to register mixer element"); if (snd_mixer_load(*alsa_mix_handle_p) < 0) DEBUG ("Failed to load mixer element");   *alsa_mix_elem_p = snd_mixer_find_selem(*alsa_mix_handle_p, alsa_mix_sid); if (!*alsa_mix_elem_p) DEBUG ("Failed to find mixer element"); return; }     { snd_mixer_t *alsa_mix_linein_handle = NULL; snd_mixer_elem_t *alsa_mix_linein_elem = NULL; char alsa_linein_ctrl[32]; if(mozart_ini_getkey("/usr/data/system.ini", "audio", "linein", alsa_linein_ctrl)) strcpy(alsa_linein_ctrl,"Aux In Switch"); //查找预设控件名称,该代码为私有代码,非标准。   alsa_linein_init(alsa_linein_ctrl, &alsa_mix_linein_handle, &alsa_mix_linein_elem); //查找控件elem snd_mixer_selem_set_playback_switch(alsa_mix_linein_elem, 0, 1); //设置控件elem if(alsa_mix_linein_handle){ snd_mixer_close(alsa_mix_linein_handle); //关闭 } }     三、相关细节解释: 1、ALSA接口中,有交错,非交错的概念。分别对应上层的snd_pcm_writei与snd_pcm_writen。 交错指的是:同一时间采的一帧数据中,包含左右两个声道或多声道的采样点。非交错指的是:同一时间采的一帧数据中,只包含一个声道。我们目前遇到的,都是交错帧。非交错帧,相当于录音端,同一时间只录一个声道的采样点,这样的情况,应该是在一些特殊场合出现的。 上层除了调用的snd_pcm_write,snd_pcm_read函数有区别外,在snd_pcm_hw_params( )设置硬件参数时,也需要调用snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED),通知底层为交错方式。 INTERLEAVED代表是交错方式,我们目前都用交错方式。   转自:https://blog.csdn.net/sinat_37817094/article/details/80817559           http://www.alsa-project.org/alsa-doc/alsa-lib/index.html