ALSA分析

http://blog.csdn.net/sepnic/article/category/778492

最近闲了,将去年分析的alsa库分析一下,和大家共享一下 ,请多多指点。 里面有一部分链接,图片等有需要可联系我 哈

  硬 件: Arm 
 软件: Android 2.1
 无论是在录音还是在放音,都要打开一个PCM流,具体对应的函数原型为:
 int snd_pcm_open(snd_pcm_t **pcmp, const char *name,  snd_pcm_stream_t stream, int mode);
 这里就开始分析一下它的流程。
 在开始之前读者最好先看一下alsa-lib是如何对conf文件进行解析的,后面就简单提一下,这只重点在snd_pcm_open的流程。
 本文以录音为例介绍一下它的流程。录音时传入name==AndroidCapture
 先解一下各个参数的函义:pcmp是带回的pcm流,name是要找开的流即放音还是录音。放音对应AndroidPlayback_Speaker_normal,
录音对应AndroidCapture.放音的字符串会因为当前使用设备和设备状况不一样而有所不同,不同之处在下划线后面那部分。录音则因为大多
数板子成型后,都只有一个录音设备或者接口,因为录音串对应的就是AndroidCapture.stream对应流的类型,是放音(0)还是录音(1).mode是
指设备打开模式,可以暂时不用管。
 打开流程首先会根据传入的name,到配置树中查找对应的结点。
 snd_config_search_definition(root, "pcm", name, &pcm_conf); 此时会在配置树中查找对应的结点,然后将查找到的结点复制一份到
pcm_conf中。结合本系统配置文件信息,查找到结点信息如图所示。(建议在阅读本文时看一下图示,可以便于您理解)
 之后,系统会调用snd_pcm_open_conf(pcmp, name, root, pcm_conf, stream, mode);进一步分析,pcm_conf即为刚才根据传入name到
配置树查找到的结点。
 static int snd_pcm_open_conf(snd_pcm_t **pcmp, const char *name,
        snd_config_t *pcm_root, snd_config_t *pcm_conf,
        snd_pcm_stream_t stream, int mode):
 snd_pcm_open_conf会根据配置树的情况,获取子结点中名为type结点(它是一个叶子结点),并获取它的值hooks,用str存储。然后会在root
对应配置树中查找名为pcm_type hooks的结点。查找成功后,会在查找到的结点中搜索所索要的lib,open_name字符串。(后面要打开)。
 在本系统中没有找到pcm_type hooks的结点。那么lib和open_name均为空,下面就要对lib/open_name初始化,(如果找到pcm_type empyt结点
则会用该结点的孩子结点初始化)。首先对lib初始化,首先查找str是否包含在
 static const char *const build_in_pcms[] = {
 "adpcm", "alaw", "copy", "dmix", "file", "hooks", "hw", "ladspa", "lfloat",
 "linear", "meter", "mulaw", "multi", "null", "empty", "plug", "rate", "route", "share",
 "shm", "dsnoop", "dshare", "asym", "iec958", "softvol", "mmap_emul",
 NULL
 };这个数组中,如果在,就将lib设为null.如果不在设置lib为默认库
 sprintf(lib, "%s/libasound_module_pcm_%s.so", ALSA_PLUGIN_DIR, str); /usr/lib/alsa-lib/libasound_module_pcm_hooks.so
本系统查找发现hooks在数组中所以lib为null.下面对open_name初始化sprintf(open_name, "_snd_pcm_%s_open", open_name);即
_snd_pcm_hooks_open函数。
 在这里需要说明的,为什么查找到的串的库名取null,而没有找到的取 /usr/lib/alsa-lib/libasound_module_pcm_hooks.so.个人理解因为我们
alsa-lib源码是开源的,alsa-lib初始化是会加载进来的。没有找到的库名为 /usr/lib/alsa-lib/libasound_module_pcm_hooks.so,这个可能是根据
我们不同的硬件来实现的吧,只要我们实现这个库存的接口(库名)一致alas-lib就可以正确加载 。
 在我们的系统中包含对应的串hooks,所以lib为null.后续要开库时,传入库名为null,系统会返回一默认库名柄。
 下面就是打开库,并在库中查找对应的open_name.因为我们代码是开源的,当然就可以在alsa-lib源码中找到对应的open_name,此处为_snd_pcm_hooks_open.然后就是调用这个函数 open_func(pcmp, name, pcm_root, pcm_conf, stream, mode)。
 到此为至,简单来说snd_pcm_open_conf函数的主要功能,就是分析传递过来的配置树pcm_conf的type子结点对应的字符串:hooks,然后调
用由type值(hooks)构成的字符串函数(_snd_pcm_hooks_open)的过程。在我们系统中后面还要陆续调用此函数,而且在查找lib名时,都能在build_in_pcms中找到,所以对我们来说这个函数的主要功能就是查找pcm_conf子结点type对应的字符串构成的函数。
 
 int _snd_pcm_hooks_open(snd_pcm_t **pcmp, const char *name, snd_config_t *root, snd_config_t *conf, snd_pcm_stream_t stream, int mode)
 这个函数主要完成三件事,在传入的配置树中查找子孩子ID为slave的结点,然后调用snd_pcm_open_slave和snd_pcm_hooks_open,最后再分析配置树
的是否有hooks结点,有的话调用snd_pcm_hook_add_conf。为了方便大家理解,还是先看一下传入_snd_pcm_hooks_open配置树的情况.
 先来看一下snd_pcm_open_slave(&spcm, root, sconf, stream, mode, conf);函数,在这里sconf即为slave结点中ID为pcm对应的配置树。root对应的是总的配置树(系统启动时会根据配置文件asound.conf/alsa-lib.conf加载构建)。
 
 snd_pcm_open_slave-->snd_pcm_open_named_slave-->snd_pcm_open_noupdate,由此可见snd_pcm_open_slave仍然会走到snd_pcm_open_noupdate这个函数处。然后会根据传入的参数name=default,在总的配置树中查找pcm default结点。snd_config_search_definition最终根据pcm default串查找对应的树。之后会调用 snd_pcm_open_conf(pcmp, name, root, pcm_conf, stream, mode)前面已经详细介过本函数了,本次调用只是传递过来的配置树不一样。

  snd_pcm_open_conf(pcmp, name, root, pcm_conf, stream, mode)根据当前传入树pcm_conf的情况会调用如下函数_snd_pcm_empty_open。这个函数会从当前传入树取下slave孩子结点的pcm结点对应的配置树做为根结点,并调用 snd_pcm_open_named_slave.
 snd_pcm_open_named_slave-->snd_pcm_open_conf可以看出又回到了snd_pcm_open_conf函数,只是传入的树不一样。snd_pcm_open_conf会调用_snd_pcm_type_plug_open.
 函数_snd_pcm_type_plug_open会调 用snd_pcm_open_slave及snd_pcm_plug_open。
 在传入树中摘下这样的子树,调用snd_pcm_open_slave-->snd_pcm_open_named_slave-->snd_pcm_open_conf-->_snd_pcm_hw_open.下面简单介绍一下_snd_pcm_hw_open.
 _snd_pcm_hw_open-->snd_pcm_hw_open,此函数中会根据树中的card号码,调用snd_ctl_hw_open打开/dev/snd/ControlC0,然后再打开/dev/snd/pcmC0D0c,完成后调用snd_ctl_close关闭/dev/snd/ControlC0.并调用snd_pcm_hw_open_fd,传入fd为打开的/dev/snd/pcmC0D0c.该函数完成PCM流的创建,首先创建pcm的私有数据hw (snd_pcm_hw_t结构),然后用参数(参数获取是通过ioctl命令发送到fd,然后内核将请求信息带回最后赋值给hw的成员中)初始化它,接着创建pcm(_snd_pcm结构),并将hw赋值给pcm->private_data成员。最后再调用snd_pcm_hw_mmap_status/snd_pcm_hw_mmap_control两个函数完成映射。需要注意在这里pcm有一个类型,本处创建的pcm类型为SND_PCM_TYPE_HW.
 对pcm的初始化代码如下:
 //创建类型为SND_PCM_TYPE_HW的pcm流,并完成一些基本初始化。
 ret = snd_pcm_new(&pcm, SND_PCM_TYPE_HW, name, info.stream, mode);
 if (ret < 0) {
  free(hw);
  close(fd);
  return ret;
 }
 //初始化ops函数集
 pcm->ops = &snd_pcm_hw_ops;
 //初始化fast_ops函数集
 pcm->fast_ops = &snd_pcm_hw_fast_ops;
 pcm->private_data = hw;//如上所述私有数据赋值
 pcm->poll_fd = fd;/dev/snd/pcmC0D0c
 pcm->poll_events = info.stream == SND_PCM_STREAM_PLAYBACK ? POLLOUT : POLLIN;
 pcm->monotonic = monotonic;
 //以下两函数可能是设置映射指针。读、写指针
 ret = snd_pcm_hw_mmap_status(pcm);
 ret = snd_pcm_hw_mmap_control(pcm);
 至此一个完整的类型为SND_PCM_TYPE_HW的pcm流对象已建立成功,但是整个函数流程并没有完成,层层深入的函数调用将要返回,此处完成后将返回snd_pcm_plug_open函数的调用,先看一下这个函数,代码不多
int snd_pcm_plug_open(snd_pcm_t **pcmp,
        const char *name,
        snd_pcm_format_t sformat, int schannels, int srate,
        const snd_config_t *rate_converter,
        enum snd_pcm_plug_route_policy route_policy,
        snd_pcm_route_ttable_entry_t *ttable,
        unsigned int tt_ssize,
        unsigned int tt_cused, unsigned int tt_sused,
        snd_pcm_t *slave, int close_slave)
{
 //这里传入的slave是刚刚我们创建的SND_PCM_TYPE_HW类型的pcm流对象
 snd_pcm_t *pcm;
 snd_pcm_plug_t *plug;
 int err;
 assert(pcmp && slave);
 //创建SND_PCM_TYPE_PLUG类型pcm流的私有数据对象
 plug = calloc(1, sizeof(snd_pcm_plug_t));
 if (!plug)
  return -ENOMEM;
 //根据传入参数和slave的相关值初始化plug成员
 plug->sformat = sformat;
 plug->schannels = schannels;
 plug->srate = srate;
 plug->rate_converter = rate_converter;
 //这里大家需要留意一下,后面还要介绍,知道这里有个赋值
 plug->gen.slave = plug->req_slave = slave;
 plug->gen.close_slave = close_slave;
 plug->route_policy = route_policy;
 plug->ttable = ttable;
 plug->tt_ssize = tt_ssize;
 plug->tt_cused = tt_cused;
 plug->tt_sused = tt_sused;
 //创建SND_PCM_TYPE_PLUG类型的流对象
 err = snd_pcm_new(&pcm, SND_PCM_TYPE_PLUG, name, slave->stream, slave->mode);
 if (err < 0) {
  free(plug);
  return err;
 }
 //用plug对应的一组函数赋值给它的ops/fast_ops成员
 pcm->ops = &snd_pcm_plug_ops;
 pcm->fast_ops = slave->fast_ops;
 pcm->fast_op_arg = slave->fast_op_arg;
 //设置私有成员
 pcm->private_data = plug;
 pcm->poll_fd = slave->poll_fd;
 pcm->poll_events = slave->poll_events;
 pcm->mmap_shadow = 1;
 pcm->monotonic = slave->monotonic;
 snd_pcm_link_hw_ptr(pcm, slave);
 snd_pcm_link_appl_ptr(pcm, slave);
 *pcmp = pcm;
 return 0;
}
 这个函数完成后,我们仍然没有完成整个建立流程,此时会返回到_snd_pcm_hooks_open函数中调用snd_pcm_hooks_open,代码也不是很多
int snd_pcm_hooks_open(snd_pcm_t **pcmp, const char *name, snd_pcm_t *slave, int close_slave)
{
 //这里的slave是类型为plug的pcm流对象
 snd_pcm_t *pcm;
 snd_pcm_hooks_t *h;
 unsigned int k;
 int err;
 assert(pcmp && slave);
 //分配私有数据对象
 h = calloc(1, sizeof(snd_pcm_hooks_t));
 if (!h)
  return -ENOMEM;
 //留意一下
 h->gen.slave = slave;
 h->gen.close_slave = close_slave;
 for (k = 0; k <= SND_PCM_HOOK_TYPE_LAST; ++k) {
  INIT_LIST_HEAD(&h->hooks[k]);
 }
 //创建流类型为SND_PCM_TYPE_HOOKS的pcm流对象。
 err = snd_pcm_new(&pcm, SND_PCM_TYPE_HOOKS, name, slave->stream, slave->mode);
 if (err < 0) {
  free(h);
  return err;
 }
 //用hooks对应的一组函数初始化ops/fast_ops
 pcm->ops = &snd_pcm_hooks_ops;
 pcm->fast_ops = &snd_pcm_hooks_fast_ops;
 //私有成员链接到pcm 对象中
 pcm->private_data = h;
 pcm->poll_fd = slave->poll_fd;
 pcm->poll_events = slave->poll_events;
 pcm->mmap_shadow = 1;
 pcm->monotonic = slave->monotonic;
 snd_pcm_link_hw_ptr(pcm, slave);
 snd_pcm_link_appl_ptr(pcm, slave);
 *pcmp = pcm;
 return 0;
}
 至此pcm流的创建过程,基本完成,可是还有最后一步_snd_pcm_hooks_open中的snd_pcm_hook_add_conf,因为我们的树中有hooks结点,所以必需调用这个函数进行处理。
 snd_pcm_hook_add_conf(rpcm, root, n);其中n对应的配置树是这颗树中的子树。snd_pcm_hook_add_conf函数功能和snd_pcm_open_conf函数功能类似,只要我们知道n这颗树信息,就可以很容易阅 读完它的代码。snd_pcm_hook_add_conf--->_snd_pcm_hook_ctl_elems_install.
 int _snd_pcm_hook_ctl_elems_install(snd_pcm_t *pcm, snd_config_t *conf)根据我们本次传入的子树,然后再来分析代码会变得很容易,slave为我们最后一次创建的HOOKS类型的pcm.
 本函数会调用snd_ctl_open(&ctl, ctl_name, 0);打开对应的ctl,下面看一下
int snd_ctl_open(snd_ctl_t **ctlp, const char *name, int mode)
{
 int err;
 assert(ctlp && name);
 err = snd_config_update();
 if (err < 0)
  return err;
 return snd_ctl_open_noupdate(ctlp, snd_config, name, mode);
}
 是否似曾相似,同样是到配置树中查找对应的结点。通过snd_config_search_definition找到这样配置树,然后调用snd_ctl_open_conf->_snd_ctl_hw_open.函数返回带回一个ctl控件。
 snd_ctl_open完成后调用snd_config_imake_pointer(&pcm_conf, "pcm_handle", pcm);创建一个结点信息,并点pcm赋值给结点的指针域。
 之后,会调用snd_sctl_build(&sctl, ctl, conf, pcm_conf, 0);
 int snd_sctl_build(snd_sctl_t **sctl, snd_ctl_t *handle, snd_config_t *conf, snd_config_t *private_data, int mode)
{
 //handle刚创建的ctl,conf传入_snd_pcm_hook_add_conf中的conf,private_data刚创建的指针结点。
 snd_sctl_t *h;
 snd_config_iterator_t i, next;
 int err;

 assert(sctl);
 assert(handle);
 assert(conf);
 *sctl = NULL;
 if (snd_config_get_type(conf) != SND_CONFIG_TYPE_COMPOUND)
  return -EINVAL;
 h = calloc(1, sizeof(*h));
 if (!h) {
  if (mode & SND_SCTL_NOFREE)
   return -ENOMEM;
  snd_ctl_close(handle);
  return -ENOMEM;
 }
 h->mode = mode;
 h->ctl = handle;
 INIT_LIST_HEAD(&h->elems);
 //在这里依次添加hook_args中的参数添加到h中。
 snd_config_for_each(i, next, conf) {
  snd_config_t *n = snd_config_iterator_entry(i);
  err = add_elem(h, n, private_data);
  if (err < 0) {
   free_elems(h);
   return err;
  }
 }
 *sctl = h;
 return 0;
}
 不幸的是根据我们的配置文件在这里添加的信息add_elem时会将每个参数信息通过调用snd_pcm_expand展开,我们的配置有问题,add_elem时会返回错误信息。
 这样错误信息会一层层返回,并释放掉刚才我们创建的那么多pcm对象和私有数据等。但是不要紧,我们可以从中分析知道snd_pcm_open函数的流程。
 稍后会对snd_pcm_open创建流程做一个小结,然后再说一下我们系统中pcm在后面是如何创建成功的。

配置树获取

allsa-lib将它的配置文件(asound.conf/alsa-lib.conf)构建成树的结构.
在创建PCM或者CTL时会不断地用到这个配置树,所以时刻了解这个配置树的状态,可以使我们方便地理解程序。
树如何获取呢?我们可以打印LOG,用先序或中序或后序显示出树的状态。
的确可以这样, 我也是这样做的。可是由于我们的树太大了,用LOGCAT显示时,只能显示树的一部分结构。所以为了使我们更容易地理解程序流程
需要时刻关注配置树状态。
既然LOGCAT不行。后来编写了C函数,将配置树信息输出到文件系统中,然后就可以打开文件进行分析,使我们时刻了解配置树动态信息。
代码如下:
int  show_hl(snd_config_t *config)
{ assert(config);
 switch (config->type) {
 case SND_CONFIG_TYPE_COMPOUND:
 {
  int err;
  struct list_head *i;
  i = config->u.compound.fields.next;
  loghl("Start Compound---> node id =%s\n",config->id?config->id:"null");
  while (i != &config->u.compound.fields) {
   struct list_head *nexti = i->next;
   snd_config_t *leaf = snd_config_iterator_entry(i);
   err = show_hl(leaf);
   if (err < 0)
    return err;
   i = nexti;
  }
  loghl("End Compound---> ---> node id =%s\n",config->id?config->id:"null");
  break;
 }
 case SND_CONFIG_TYPE_STRING:

  loghl("SND_CONFIG_TYPE_STRING---> leaf id=%s,str=%s\n",config->id?config->id:"null",config->u.string?config->u.string:"null");
  break;
 case SND_CONFIG_TYPE_INTEGER:
  loghl("SND_CONFIG_TYPE_ INTGER --->leaf id=%s,num=%ld\n",config->id?config->id:"null",config->u.integer); 
  break;
  case SND_CONFIG_TYPE_REAL:
  loghl("SND_CONFIG_TYPE_ REAL--->leaf id=%s,num=%f\n",config->id?config->id:"null",config->u.real);  
   break;
  case   SND_CONFIG_TYPE_INTEGER64:
  loghl("SND_CONFIG_TYPE_INTEGER64--->leaf id=%s,num=%ld\n",config->id?config->id:"null",config->u.integer64); 
    break;
  case SND_CONFIG_TYPE_POINTER:
  loghl("SND_CONFIG_TYPE_POINTER--->leaf id=%s,num=%p\n",config->id?config->id:"null",config->u.ptr); 
    break;
 default:
  break;
 }
 return 0;
}
 类先序遍历树。
 其中loghl是我书写的一个变参函数,意思是将后面的数据以指定格式输出到文件中。代码如下:
char pathhelun[]="/data/helun";
FILE * open_start_once();
void loghl(char *fmt,...)
{
 
 FILE *fp=NULL;
   va_list arg;
 fp=open_start_once();
 if(!fp)
  {
  printf("create %s error \n",pathhelun);
   return ;
  }
    
     va_start(arg, fmt); 
 vfprintf(fp, fmt,arg);
     va_end(arg);
 fclose(fp); //每次输入日志后必需关闭,因为无法在终端关掉它。这样会影响速度,但是必需这样做。


}
FILE * open_start_once()
{
 FILE *fp=NULL;
 /*
 if(once==0)
  {
  fp=fopen(pathhelun,"w+");
  once++;
 }
 else 
  {
   fp=fopen(pathhelun,"a+");

 }*/
 fp=fopen(pathhelun,"a+"); 
 return fp;
}

snd_pcm_open 总结

  
  在阅读本总结前,请认真看一下snd_pcm_open。
  本文就以树的结构来分析一下pcm流建立的过程。
        
     

  


 
       default树
 为了简便起见,下面就称第一颗树为capture树,第二颗为default树。
 int  snd_pcm_open(snd_pcm_t **pcmp, const char *name, snd_pcm_stream_t stream, int mode)
 {
  int err;
  assert(pcmp && name);
  //刷新/system/usr/share/alsa/alsa.conf文件内容到snd_config_t结构中,构建配置树
  err = snd_config_update();
  if (err < 0)
   return err;
  //真正执行树创建的例程
  return snd_pcm_open_noupdate(pcmp, snd_config, name, stream, mode, 0);
 }
 该函数主要完成如下两步:
 第一步:构建配置树
 第二步:创建PCM流
 点击录音时传入的name为AndroidCapture(其它模式类似),在第二步中根据传入的name参数在配置树中查找对应的snd_config_t结点。根据我们的配置asla-lib.conf,查找到capture树。
 下面看一下这个函数,snd_pcm_open_noupdate会调用到这里
 static int snd_pcm_open_conf(snd_pcm_t **pcmp, const char *name,
        snd_config_t *pcm_root, snd_config_t *pcm_conf,
        snd_pcm_stream_t stream, int mode);
 这个函数的功能是取下pcm_conf配置树中type孩子结点对应的字符串值,并利用该字符串构造一新的函数并调用。在我们的系统中可以简单地这样理解。下面以capture树为例说明一下。
 snd_pcm_open-->snd_pcm_open_noupdate-->snd_pcm_open_conf此时传入的pcm_conf配置树为capture树,它会取下type孩子结点的字符串str(hooks),构建函数名sprintf(open_name, "_snd_pcm_%s_open", str);之后调用函数_snd_pcm_hooks_open.下snd_pcm_open的主要流程:
 _snd_pcm_hooks_open-->_snd_pcm_empty_open--->_snd_pcm_plug_open--->_snd_pcm_hw_open
 结合capture树和default树,不难看出,后三个函数名也是用类似构造_snd_pcm_hook_open的方法生成的,之后调用。事实确实是这样的,在snd_pcm_open过程中多次调用snd_pcm_open_conf只是每次传入的pcm_conf配置树不一样而已。那么我们的default树是如何获取的呢?
 在_snd_pcm_hook_open中会查找capture树的slave孩子结点,并将slave的pcm子结点的str(default)作为新的name重新调用snd_pcm_open_noupdate.然后根据name参数在配置树中查找到default对应的结点,即default树。
 前面说过snd_pcm_open_noupdate会调用到snd_pcm_open_conf,由此不难想象到它会构造_snd_pcm_empty_open,并调用它。之后_snd_pcm_plug_open-->_snd_pcm_hw_open函数的构造也是类似的这里都不多介绍了。读者可认真看一下_snd_pcm_xxx_open,会发现它都会调用到snd_pcm_open_conf函数来加载下一层的type xxxx.就这样对default树进行逐层分析。
 _snd_pcm_hooks_open-->_snd_pcm_empty_open->_snd_pcm_plug_open->_snd_pcm_hw_open
 上面是我给大家介绍的重点之一。
 此流程执行完,也要逐层返回,在_snd_pcm_hooks_open/_snd_pcm_plug_open/_snd_pcm_hw_open三层分别还会创建三种类型的pcm流对象和它的私有数据成员及和它对应的函数集。
 下面就从里到外顺序介绍一下相关代码,先看一下hw层
 ......
 //pcm_hw.c 创建类型为SND_PCM_TYPE_HW的pcm流 
 ret = snd_pcm_new(&pcm, SND_PCM_TYPE_HW, name, info.stream, mode);
 if (ret < 0) {
  free(hw);
  close(fd);
  return ret;
 }
 //HW层的函数集
 pcm->ops = &snd_pcm_hw_ops;
 pcm->fast_ops = &snd_pcm_hw_fast_ops;
 //snd_pcm_hw_t结构hw作为私有数据成员,存储一些信息。
 pcm->private_data = hw;
 //打开/dev/snd/pcmC0D0c
 pcm->poll_fd = fd;
 ....
 返回plug层
 //pcm_plug.c
 int snd_pcm_plug_open(snd_pcm_t **pcmp,
        const char *name,
        snd_pcm_format_t sformat, int schannels, int srate,
        const snd_config_t *rate_converter,
        enum snd_pcm_plug_route_policy route_policy,
        snd_pcm_route_ttable_entry_t *ttable,
        unsigned int tt_ssize,
        unsigned int tt_cused, unsigned int tt_sused,
        snd_pcm_t *slave, int close_slave)
{
 snd_pcm_t *pcm;
 snd_pcm_plug_t *plug;
 int err;
 assert(pcmp && slave);
 //分配私有数据成员
 plug = calloc(1, sizeof(snd_pcm_plug_t));
 if (!plug)
  return -ENOMEM;
 plug->sformat = sformat;
 plug->schannels = schannels;
 plug->srate = srate;
 plug->rate_converter = rate_converter;
 //将HW层的pcm流对象存放在PLUG层pcm流的私有数据成员中
 plug->gen.slave = plug->req_slave = slave;
 plug->gen.close_slave = close_slave;
 plug->route_policy = route_policy;
 plug->ttable = ttable;
 plug->tt_ssize = tt_ssize;
 plug->tt_cused = tt_cused;
 plug->tt_sused = tt_sused; 
 //创建SND_PCM_TYPE_PLUG类型的pcm流对象
 err = snd_pcm_new(&pcm, SND_PCM_TYPE_PLUG, name, slave->stream, slave->mode);
 if (err < 0) {
  free(plug);
  return err;
 }
 //PLUG层函数集
 pcm->ops = &snd_pcm_plug_ops;
 pcm->fast_ops = slave->fast_ops;
 pcm->fast_op_arg = slave->fast_op_arg;
 //plug关联到私有成员中
 pcm->private_data = plug;
 pcm->poll_fd = slave->poll_fd;
 pcm->poll_events = slave->poll_events;
 pcm->mmap_shadow = 1;
 pcm->monotonic = slave->monotonic;
 snd_pcm_link_hw_ptr(pcm, slave);
 snd_pcm_link_appl_ptr(pcm, slave);
 *pcmp = pcm;
 return 0;

 最后再来看一下HOOKS层
 //pcm_hooks.c
 int snd_pcm_hooks_open(snd_pcm_t **pcmp, const char *name, snd_pcm_t *slave, int close_slave)
{
 snd_pcm_t *pcm;
 snd_pcm_hooks_t *h;
 unsigned int k;
 int err;
 assert(pcmp && slave);
 //分配私有成员空间
 h = calloc(1, sizeof(snd_pcm_hooks_t));
 if (!h)
  return -ENOMEM;
 //寄存PLUG层的pcm流对象
 h->gen.slave = slave;
 h->gen.close_slave = close_slave;
 for (k = 0; k <= SND_PCM_HOOK_TYPE_LAST; ++k) {
  INIT_LIST_HEAD(&h->hooks[k]);
 }
 //SND_PCM_TYPE_HOOKS类型的pcm流对象创建
 err = snd_pcm_new(&pcm, SND_PCM_TYPE_HOOKS, name, slave->stream, slave->mode);
 if (err < 0) {
  free(h);
  return err;
 }
 //HOOKS层函数集
 pcm->ops = &snd_pcm_hooks_ops;
 pcm->fast_ops = &snd_pcm_hooks_fast_ops;
 //关联私有数据
 pcm->private_data = h;
 pcm->poll_fd = slave->poll_fd;
 pcm->poll_events = slave->poll_events;
 pcm->mmap_shadow = 1;
 pcm->monotonic = slave->monotonic;
 snd_pcm_link_hw_ptr(pcm, slave);
 snd_pcm_link_appl_ptr(pcm, slave);
 *pcmp = pcm;
 return 0;
}
 由此可见,在整个过程中创建了三种类型的pcm流对象,HOOKS,PLUG,HW
 _snd_pcm_hooks_open-->_snd_pcm_empty_open--->_snd_pcm_plug_open--->_snd_pcm_hw_open层层递进分析配置树
 _snd_pcm_hooks_open<--....................................<---_snd_pcm_plug_open<--_snd_pcm_hw_open层层回溯。
 回溯过程依次创建了HW pcm-->PLUG pcm-->HOOKS pcm及和它们区配的私有数据及相关操作函数集,而且可以通过HOOKS层的pcm流,查找到plug,hw的pcm.它们都被寄存在上层的private_data成员中。如下图
 
  
 至此返回_snd_pcm_hooks_open,下面这个函数还会对capture树的hooks孩子结点分析。
 static int snd_pcm_hook_add_conf(snd_pcm_t *pcm, snd_config_t *root, snd_config_t *conf)
 它类似snd_pcm_open_conf,它会调用_snd_pcm_hook_ctl_elems_install-->snd_ctl_open打开对应的/dev/snd/ControlC0(snd_ctl_open和snd_pcm_open类似都是通过传入字符串查找配置树,然后分析找开设备之类操作),执行成功能会带回一个snd_sctl_t结构类型对象。
 由此可以PCM流打开需要两步:snd_pcm_open /dev/snd/pcmC0D0c 的打开和snd_ctl_open /dev/snd/ControlC0的打开,两个设备文件。
 之后_snd_pcm_hook_ctl_elems_install->snd_sctl_build,但是根据我们的配置在这里会返回错误信息(snd_sctl_build会添加hook_args下面所有的参数信息并生成一个新的snd_sctl_t对象(add_elem),就是在添加参数信息时我们的配置有问题,所以出错返回)。错误发生,会释放HOOKS层的pcm流对象,级联PLUG和HW层。最终snd_pcm_open会返回错误信息。pcm流创建失败。
 这是我们系统中snd_pcm_open的整个流程,虽然这里创建失败但是可以让我们清晰地认识流创建过程:它是一个分层的流建立过程。hooks->plug->hw(pcm) 及hooks->empty->plug->hw函数层次调用过程。
 这个层次结构就是我给大家介绍的第二个要点,特别是多种pcm流类型,及它们相互引用和各自的私有数据成员及对应的函数操作集。有了这个认识,我们才能在pcm->fast_ops->read(..)时知道调用的是哪一层的函数集。
 虽然这里创建PCM流失败,但是我们的系统会有另一种方案重新创建default pcm流。可以参考我们系统中流的创建过程。





转载自:http://blog.chinaunix.net/uid-25272011-id-3153434.html

 

最近在调试一款原相PAP7501摄像头中的USB的麦克风,USB层走的应该是标准的UAC协议,具体可以见USB的官网:http://www.usb.org/developers/devclass_docs#approved,而音频部分则可以跑目前Linux标准的ALSA的PCM接口,对于硬件CODEC来说,与其是完全兼容的。
     给出一份参考代码:
     这个是仿照arecord写的一个简略的测试代码,保存为wav格式的。
1、recod.c


  1. /*

  2. This example reads from the default PCM device
  3. and writes to standard output for 5 seconds of data.

  4. */

  5. /* Use the newer ALSA API */
  6. #define ALSA_PCM_NEW_HW_PARAMS_API

  7. #include <alsa/asoundlib.h>

  8. /**************************************************************/
  9. #define ID_RIFF 0x46464952
  10. #define ID_WAVE 0x45564157
  11. #define ID_FMT 0x20746d66
  12. #define ID_DATA 0x61746164

  13. typedef unsigned long uint32_t;
  14. typedef unsigned short uint16_t;

  15. #define FORMAT_PCM 1

  16. static uint32_t totle_size = 0;

  17. struct wav_header {
  18.     /* RIFF WAVE Chunk */
  19.     uint32_t riff_id;
  20.     uint32_t riff_sz;
  21.     uint32_t riff_fmt;
  22.     /* Format Chunk */
  23.     uint32_t fmt_id;
  24.     uint32_t fmt_sz;
  25.     uint16_t audio_format;
  26.     uint16_t num_channels;
  27.     uint32_t sample_rate;
  28.     uint32_t byte_rate; /* sample_rate * num_channels * bps / 8 */
  29.     uint16_t block_align; /* num_channels * bps / 8 */
  30.     uint16_t bits_per_sample;
  31.     /* Data Chunk */
  32.     uint32_t data_id;
  33.     uint32_t data_sz;
  34. }__attribute__((packed));

  35. static struct wav_header hdr;

  36. /**************************************************************/
  37. int record_file(unsigned rate, unsigned channels, int fd, unsigned count)
  38. {
  39.     long loops;
  40.     int val;
  41.     int rc;
  42.     int size;
  43.     snd_pcm_t *handle;
  44.     snd_pcm_hw_params_t *params;
  45.     int dir;
  46.     snd_pcm_uframes_t frames;
  47.     char *buffer;                    /* TODO */

  48.     /* Open PCM device for recording (capture). */
  49.     rc = snd_pcm_open(&handle, "plughw:0,0",SND_PCM_STREAM_CAPTURE, 0);
  50.     if (rc < 0) {
  51.         fprintf(stderr, "unable to open pcm device: %s\n",snd_strerror(rc));
  52.         exit(1);
  53.     }

  54.     /* Allocate a hardware parameters object. */
  55.     snd_pcm_hw_params_alloca(&params);

  56.     /* Fill it in with default values. */
  57.     snd_pcm_hw_params_any(handle, params);

  58.     /* Set the desired hardware parameters. */

  59.     /* Interleaved mode */
  60.     snd_pcm_hw_params_set_access(handle, params,SND_PCM_ACCESS_RW_INTERLEAVED);

  61.     /* Signed 16-bit little-endian format */
  62.     snd_pcm_hw_params_set_format(handle, params,SND_PCM_FORMAT_S16_LE);

  63.     /* Two channels (stereo) */
  64.     snd_pcm_hw_params_set_channels(handle, params, channels);

  65.     /* rate bits/second sampling rate (CD quality) */
  66.     snd_pcm_hw_params_set_rate_near(handle, params, &rate, &dir);

  67.     /* Set period size to 32 frames. */
  68.     frames = 320;     /* 这边的大小也不是绝对的,可以调整 */
  69.     snd_pcm_hw_params_set_period_size_near(handle, params,&frames, &dir);

  70.     /* Write the parameters to the driver */
  71.     rc = snd_pcm_hw_params(handle, params);
  72.     if (rc < 0) {
  73.         fprintf(stderr, "unable to set hw parameters: %s\n",snd_strerror(rc));
  74.         exit(1);
  75.     }

  76.     /* Use a buffer large enough to hold one period */
  77.     snd_pcm_hw_params_get_period_size(params, &frames, &dir);/* 获取实际的frames */
  78.     
  79.     size = frames * 2; /* 2 bytes/sample, 1 channels */
  80.     buffer = (char *) malloc(size);

  81.     /* We want to loop for 20 seconds 时间不一定准确 */
  82.     snd_pcm_hw_params_get_period_time(params, &val, &dir);
  83.     loops = 20000000 / val;
  84.     
  85.     while (loops > 0) {
  86.         loops--;
  87.         rc = snd_pcm_readi(handle, buffer, frames);
  88.         if (rc == -EPIPE) {
  89.          /* EPIPE means overrun */
  90.          fprintf(stderr, "overrun occurred\n");
  91.          snd_pcm_prepare(handle);
  92.         } else if (rc < 0) {
  93.          fprintf(stderr, "error from read: %s\n", snd_strerror(rc));
  94.         } else if (rc != (int)frames) {
  95.          fprintf(stderr, "short read, read %d frames\n", rc);
  96.         }
  97.         rc = write(fd, buffer, size);
  98.         totle_size += rc;                        /* totle data size */
  99.         if (rc != size)
  100.          fprintf(stderr, "short write: wrote %d bytes\n", rc);
  101.     }
  102.     
  103.     lseek(fd, 0, SEEK_SET);      /* 回到文件头,重新更新音频文件大小 */
  104.     hdr.riff_sz = totle_size + 36;
  105.     hdr.data_sz = totle_size;
  106.     
  107.     if (write(fd, &hdr, sizeof(hdr)) != sizeof(hdr)) {
  108.         fprintf(stderr, "arec: cannot write header\n");
  109.         return -1;
  110.     }
  111.     
  112.     snd_pcm_drain(handle);
  113.     snd_pcm_close(handle);
  114.     free(buffer);

  115.     return 0;
  116. }

  117. /**************************************************************/
  118. int rec_wav(const char *fn)
  119. {
  120.     unsigned rate, channels;
  121.     int fd;
  122.     fd = open(fn, O_WRONLY | O_CREAT | O_TRUNC, 0664);
  123.     if (fd < 0) {
  124.         fprintf(stderr, "arec: cannot open '%s'\n", fn);
  125.         return -1;
  126.     }

  127.     hdr.riff_id = ID_RIFF;
  128.     hdr.riff_fmt = ID_WAVE;
  129.     hdr.fmt_id = ID_FMT;
  130.     hdr.audio_format = FORMAT_PCM;
  131.     hdr.fmt_sz = 16;
  132.     hdr.bits_per_sample = 16;
  133.     hdr.num_channels = 1;
  134.     hdr.data_sz = 0;                        /* TODO before record over */
  135.     hdr.sample_rate = 16000;
  136.     hdr.data_id = ID_DATA;
  137.     
  138.     hdr.byte_rate = hdr.sample_rate * hdr.num_channels *hdr.bits_per_sample / 8;
  139.     hdr.block_align = hdr.num_channels * hdr.bits_per_sample / 8;
  140.     
  141.     if (write(fd, &hdr, sizeof(hdr)) != sizeof(hdr)) {
  142.         fprintf(stderr, "arec: cannot write header\n");
  143.         return -1;
  144.     }
  145.     fprintf(stderr,"arec: %d ch, %ld hz, %d bit, %s\n",
  146.             hdr.num_channels, hdr.sample_rate, hdr.bits_per_sample,
  147.             hdr.audio_format == FORMAT_PCM ? "PCM" : "unknown");
  148.     
  149.     return record_file(hdr.sample_rate, hdr.num_channels, fd,hdr.data_sz);
  150. }

  151. int main(int argc, char **argv)
  152. {
  153.     if (argc != 2) {
  154.         fprintf(stderr,"usage: arec <file>\n");
  155.         return -1;
  156.     }

  157.     return rec_wav(argv[1]);
  158. }
对于上述代码补充说明一点,这个是设计ALSA的一点概念:

样本长度(sample):样本是记录音频数据最基本的单位,常见的有8位和16位。

通道数(channel):该参数为1表示单声道,2则是立体声。

桢(frame):桢记录了一个声音单元,其长度为样本长度与通道数的乘积。

采样率(rate):每秒钟采样次数,该次数是针对桢而言。

周期(period):音频设备一次处理所需要的桢数,对于音频设备的数据访问以及音频数据的存储,都是以此为单位。

交错模式(interleaved):是一种音频数据的记录方式,在交错模式下,数据以连续桢的形式存放,即首先记录完桢1的左声道样本和右声道样本(假设为立体声格式),再开始桢2的记录。而在非交错模式下,首先记录的是一个周期内所有桢的左声道样本,再记录右声道样本,数据是以连续通道的方式存储。不过多数情况下,我们只需要使用交错模式就可以了。

具体可以参照:http://blog.chinaunix.net/uid-25272011-id-3151136.html

一开始我犯过一个错误就是rc = snd_pcm_readi(handle, buffer, frames),这个函数的参数3的单位应该是帧大小,而一个帧的大小是根据你的量化位数和声道数决定的,对于本代码,是16bit单声道,自然一个帧大小是2字节,起初我将申请的buffer大小传给了这个参数,结果必然导致“卡顿”或者“快进”,“卡顿”是因为我在项目中是实时传输,会导致阻塞,毕竟数据量大了一倍,“快进”则是因为缓冲区的大小是实际读取数据的一半,有一半的数据在buffer中被自己给覆盖掉了,所以要慎重啊。


2、Makefile

  1. exe = record
  2. src = record.c
  3. CC = arm-linux-gcc
  4. INC = -I/nfs/usr/local/arm-alsa/include
  5. LDFLAGS = -lpthread -L/nfs/usr/local/arm-alsa/lib -lasound

  6. $(exe) : $(src) FORCE
  7.     $(CC) -o $@ $(src) $(LDFLAGS) $(INC)


  8. FORCE:

  9. clean:
  10.     rm -./*.o $(exe)

      此处的alsa-lib库就是之前介绍的安装的库的路径,编译可以引用该路径的库,而运行之后库的路径可不受限制,按照你定义的环境变量找到即可。
     对于内核的配置则在
     
  1. Device Drivers --->
  2. <*> Sound card support ---> 
  3. <*> Advanced Linux Sound Architecture --->
  4. [*] USB sound devices ---> 
  5. <*> USB Audio/MIDI driver


     对于这款USB麦克风,我正常的去录音的时候,上层的直观感觉就是卡顿,这个与上面提到的是有区别的,因为同样的代码在arm上是好的,所以就怀疑是底层读慢了,(我们的应用背景是开发板实时录音,通过USB-WIFI发到上位机同步播放)很明显的是读取音频数据慢了。而同样的代码跑硬件的CODEC是很好的,不卡顿,所以很有可能问题出在USB上。我们刚好有USB的协议分析仪,我们USB是跑的全速模式,其描述符为

  1. Interface Descriptor:
  2.       bLength 9
  3.       bDescriptorType 4
  4.       bInterfaceNumber 3
  5.       bAlternateSetting 0
  6.       bNumEndpoints 0
  7.       bInterfaceClass 1 
  8.       bInterfaceSubClass 2 
  9.       bInterfaceProtocol 0 
  10.       iInterface 0 
  11.     Interface Descriptor:
  12.       bLength 9
  13.       bDescriptorType 4
  14.       bInterfaceNumber 3
  15.       bAlternateSetting 1
  16.       bNumEndpoints 1
  17.       bInterfaceClass 1 
  18.       bInterfaceSubClass 2 
  19.       bInterfaceProtocol 0 
  20.       iInterface 0 
  21.       AudioStreaming Interface Descriptor:
  22.         bLength 7
  23.         bDescriptorType 36
  24.         bDescriptorSubtype 1 (AS_GENERAL)
  25.         bTerminalLink 3
  26.         bDelay 1 frames
  27.         wFormatTag 1 PCM
  28.       AudioStreaming Interface Descriptor:
  29.         bLength 11
  30.         bDescriptorType 36
  31.         bDescriptorSubtype 2 (FORMAT_TYPE)
  32.         bFormatType 1 (FORMAT_TYPE_I)
  33.         bNrChannels 1
  34.         bSubframeSize 2
  35.         bBitResolution 16
  36.         bSamFreqType 1 Discrete
  37.         tSamFreq[ 0] 16000
  38.       Endpoint Descriptor:
  39.         bLength 9
  40.         bDescriptorType 5
  41.         bEndpointAddress 0x83 EP 3 IN
  42.         bmAttributes 5
  43.           Transfer Type Isochronous
  44.           Synch Type Asynchronous
  45.           Usage Type Data
  46.         wMaxPacketSize 0x0020 1x 32 bytes
  47.         bInterval 4
  48.         bRefresh 0
  49.         bSynchAddress 0
  50.         AudioControl Endpoint Descriptor:
  51.           bLength 7
  52.           bDescriptorType 37
  53.           bDescriptorSubtype 1 (EP_GENERAL)
  54.           bmAttributes 0x01
  55.             Sampling Frequency
  56.           bLockDelayUnits 0 Undefined
  57.           wLockDelay 0 Undefined
  58. /************************************************************************/
  59.     Interface Descriptor:
  60.       bLength 9
  61.       bDescriptorType 4
  62.       bInterfaceNumber 3
  63.       bAlternateSetting 2
  64.       bNumEndpoints 1
  65.       bInterfaceClass 1 
  66.       bInterfaceSubClass 2 
  67.       bInterfaceProtocol 0 
  68.       iInterface 0 
  69.       AudioStreaming Interface Descriptor:
  70.         bLength 7
  71.         bDescriptorType 36
  72.         bDescriptorSubtype 1 (AS_GENERAL)
  73.         bTerminalLink 3
  74.         bDelay 1 frames
  75.         wFormatTag 1 PCM
  76.       AudioStreaming Interface Descriptor:
  77.         bLength 11
  78.         bDescriptorType 36
  79.         bDescriptorSubtype 2 (FORMAT_TYPE)
  80.         bFormatType 1 (FORMAT_TYPE_I)
  81.         bNrChannels 1
  82.         bSubframeSize 2
  83.         bBitResolution 16
  84.         bSamFreqType 1 Discrete
  85.         tSamFreq[ 0] 48000
  86.       Endpoint Descriptor:
  87.         bLength 9
  88.         bDescriptorType 5
  89.         bEndpointAddress 0x83 EP 3 IN
  90.         bmAttributes 5
  91.           Transfer Type Isochronous
  92.           Synch Type Asynchronous
  93.           Usage Type Data
  94.         wMaxPacketSize 0x0060 1x 96 bytes
  95.         bInterval 4
  96.         bRefresh 0
  97.         bSynchAddress 0
  98.         AudioControl Endpoint Descriptor:
  99.           bLength 7
  100.           bDescriptorType 37
  101.           bDescriptorSubtype 1 (EP_GENERAL)
  102.           bmAttributes 0x01
  103.             Sampling Frequency
  104.           bLockDelayUnits 0 Undefined
  105.           wLockDelay 0 Undefined
    
    看到描述符,顺便插一句,对照端点大小计算一下,蓝色字体,这款USB-AUDIO只支持16K和48K的16bit单声道录音,拿16K为例,1s数据量应该是16K*16/8=32KB,对应于端点的大小32B*1000=32KB,也就是说全速模式下应该是每帧(1ms)请求一次才对,而对于图中的红色字体,说明的意思是全速模式下的ISO传输请求间隔参数是4,对应我们的USB的控制器,意思即为每8帧才发起一次ISO请求,抓包验证确实如此,这一点确实比较诡异,问题可能就出在这里:



      但是对于几乎同样的驱动版本,我们611的是2.6.32.9而arm的是2.6.32.2,其sound目录下的usbaudio.c基本是相同的,描述符又是相同的,所以上层获取描述符进行的配置应该也是相同的,唯一的区别就是USB的控制器,我们611的是musb,而arm的是OHCI,我们musb对这个bInterval的配置是


因此驱动固然是8帧请求一次。我在OHCI的控制器中未找到类似的寄存器,猜想可能是OHCI默认的就是ISO传输每一帧会保证一次,其抓包图如下:



所以只好强制去修改musb的驱动配置,/drivers/usb/musb/musb_host.c

  1. 2013 /* precompute rxtype/txtype/type0 register */
  2. 2014 type_reg = (qh->type << 4) | qh->epnum;
  3. 2015 switch (urb->dev->speed) {
  4. 2016 case USB_SPEED_LOW:
  5. 2017 type_reg |= 0xc0;
  6. 2018 break;
  7. 2019 case USB_SPEED_FULL:
  8. 2020 type_reg |= 0x80;
  9. 2021 break;
  10. 2022 default:
  11. 2023 type_reg |= 0x40;
  12. 2024 }
  13. 2025 qh->type_reg = type_reg;
  14. 2026 
  15. 2027 /* Precompute RXINTERVAL/TXINTERVAL register */
  16. 2028 switch (qh->type) {
  17. 2029 case USB_ENDPOINT_XFER_INT:
  18. 2030 /*
  19. 2031 * Full/low speeds use the linear encoding,
  20. 2032 * high speed uses the logarithmic encoding.
  21. 2033 */
  22. 2034 if (urb->dev->speed <= USB_SPEED_FULL) {
  23. 2035 interval = max_t(u8, epd->bInterval, 1);
  24. 2036 break;
  25. 2037 }
  26. 2038 /* FALLTHROUGH */
  27. 2039 case USB_ENDPOINT_XFER_ISOC:
  28. 2040 /* ISO always uses logarithmic encoding */
  29. 2041 //interval = min_t(u8, epd->bInterval, 16);
  30. 2042 interval = min_t(u8, epd->bInterval, 1); //JGF
  31. 2043 break; 
  32. 2044 default:

这样USB就是每帧请求一次,同样的代码,效果和2440的也一样了。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值