alsa驱动心得

主要分三部分驱动(ASoc架构):

cpu_dai + pcm(dma)  <===> machine(platform)  <===> codec

 cpu侧                             <===> 平台                            <===> 编解码芯片侧

sound_card对象管理多个snd_device,比如control,pcm,timer都是snd_device。

 基于两个平台分析:

1)第1个平台atmel平台

硬件:

atmel的at91sam9x35板子为例,使用的codec是wm8731。

软件:

linux-2.6.39

 2)第2个平台altera cyclone平台

硬件:

基于altera的cyclone V处理器的公司自主开发的开发板。

软件:

linux-3.10

 

系统启动时需要执行以下脚本设置音频:

    amixer set 'Master Playback ZC' unmute
    amixer set 'Playback Deemphasis' unmute
    amixer set 'Output Mixer HiFi' unmute
    amixer set 'Output Mixer Line Bypass' unmute
    amixer set 'Store DC Offset' unmute

使用amixer命令查看:

[root@Mini69X5:/]# amixer
Simple mixer control 'Master',0
  Capabilities: pvolume penum
  Playback channels: Front Left - Front Right
  Limits: Playback 0 - 127
  Mono:
  Front Left: Playback 121 [95%] [0.00dB]
  Front Right: Playback 121 [95%] [0.00dB]
Simple mixer control 'Master Playback ZC',0
  Capabilities: pswitch penum
  Playback channels: Front Left - Front Right
  Mono:
  Front Left: Playback [on]
  Front Right: Playback [on]
Simple mixer control 'Sidetone',0
  Capabilities: pvolume pvolume-joined penum
  Playback channels: Mono
  Limits: Playback 0 - 3
  Mono: Playback 3 [100%] [-6.00dB]
Simple mixer control 'Line',0
  Capabilities: cswitch penum
  Capture channels: Front Left - Front Right
  Front Left: Capture [off]
  Front Right: Capture [off]
Simple mixer control 'Mic',0
  Capabilities: cswitch cswitch-joined penum
  Capture channels: Mono
  Mono: Capture [off]
Simple mixer control 'Mic Boost',0
  Capabilities: volume volume-joined penum
  Playback channels: Mono
  Capture channels: Mono
  Limits: 0 - 1
  Mono: 0 [0%] [0.00dB]
Simple mixer control 'Playback Deemphasis',0
  Capabilities: pswitch pswitch-joined penum
  Playback channels: Mono
  Mono: Playback [on]
Simple mixer control 'Capture',0
  Capabilities: cvolume penum
  Capture channels: Front Left - Front Right
  Limits: Capture 0 - 31
  Front Left: Capture 23 [74%] [0.00dB]
  Front Right: Capture 23 [74%] [0.00dB]
Simple mixer control 'ADC High Pass Filter',0
  Capabilities: pswitch pswitch-joined penum
  Playback channels: Mono
  Mono: Playback [on]
Simple mixer control 'Input Mux',0
  Capabilities: enum
  Items: 'Line In' 'Mic'
  Item0: 'Line In'
Simple mixer control 'Output Mixer HiFi',0
  Capabilities: pswitch pswitch-joined penum
  Playback channels: Mono
  Mono: Playback [on]
Simple mixer control 'Output Mixer Line Bypass',0
  Capabilities: pswitch pswitch-joined penum
  Playback channels: Mono
  Mono: Playback [on]
Simple mixer control 'Output Mixer Mic Sidetone',0
  Capabilities: pswitch pswitch-joined penum
  Playback channels: Mono
  Mono: Playback [off]
Simple mixer control 'Store DC Offset',0
  Capabilities: pswitch pswitch-joined penum
  Playback channels: Mono
  Mono: Playback [on]
[root@Mini69X5:/]#

[root@Mini69X5:/]# amixer controls
numid=2,iface=MIXER,name='Master Playback ZC Switch'
numid=1,iface=MIXER,name='Master Playback Volume'
numid=4,iface=MIXER,name='Line Capture Switch'
numid=5,iface=MIXER,name='Mic Boost Volume'
numid=6,iface=MIXER,name='Mic Capture Switch'
numid=8,iface=MIXER,name='ADC High Pass Filter Switch'
numid=3,iface=MIXER,name='Capture Volume'
numid=10,iface=MIXER,name='Playback Deemphasis Switch'
numid=11,iface=MIXER,name='Input Mux'                                     /* 这里control显示的不是Input Select,而是Input Mux  */
numid=14,iface=MIXER,name='Output Mixer HiFi Playback Switch'  /* 这里的control显示的是SND_SOC_DAPM_MIXER("Output Mixer",...)的控件的名字加上wm8731_output_mixer_controls的kcontrol名字的组合 */
numid=12,iface=MIXER,name='Output Mixer Line Bypass Switch'
numid=13,iface=MIXER,name='Output Mixer Mic Sidetone Switch'
numid=7,iface=MIXER,name='Sidetone Playback Volume'
numid=9,iface=MIXER,name='Store DC Offset Switch'

/* 查看某个control的具体内容 */

[root@Mini69X5:/]# amixer cget numid=2,iface=MIXER,name='Master Playback ZC Swit
ch'
numid=2,iface=MIXER,name='Master Playback ZC Switch'
  ; type=BOOLEAN,access=rw------,values=2
  : values=on,on

/* 设置特定的control的具体内容 */
[root@Mini69X5:/]# amixer cset numid=2,iface=MIXER,name='Master Playback ZC Swit
ch' 0,0
numid=2,iface=MIXER,name='Master Playback ZC Switch'
  ; type=BOOLEAN,access=rw------,values=2
  : values=off,off

[root@Mini69X5:/]# amixer contents
numid=2,iface=MIXER,name='Master Playback ZC Switch'
  ; type=BOOLEAN,access=rw------,values=2
  : values=on,on
numid=1,iface=MIXER,name='Master Playback Volume'
  ; type=INTEGER,access=rw---R--,values=2,min=0,max=127,step=0
  : values=121,121
  | dBscale-min=-121.00dB,step=1.00dB,mute=1
numid=4,iface=MIXER,name='Line Capture Switch'
  ; type=BOOLEAN,access=rw------,values=2
  : values=off,off
numid=5,iface=MIXER,name='Mic Boost Volume'
  ; type=INTEGER,access=rw---R--,values=1,min=0,max=1,step=0
  : values=0
  | dBscale-min=0.00dB,step=20.00dB,mute=0
numid=6,iface=MIXER,name='Mic Capture Switch'
  ; type=BOOLEAN,access=rw------,values=1
  : values=off
numid=8,iface=MIXER,name='ADC High Pass Filter Switch'
  ; type=BOOLEAN,access=rw------,values=1
  : values=on
numid=3,iface=MIXER,name='Capture Volume'
  ; type=INTEGER,access=rw---R--,values=2,min=0,max=31,step=0
  : values=23,23
  | dBscale-min=-34.50dB,step=1.50dB,mute=0
numid=10,iface=MIXER,name='Playback Deemphasis Switch'
  ; type=BOOLEAN,access=rw------,values=1
  : values=on
numid=11,iface=MIXER,name='Input Mux'
  ; type=ENUMERATED,access=rw------,values=1,items=2
  ; Item #0 'Line In'
  ; Item #1 'Mic'
  : values=0
numid=14,iface=MIXER,name='Output Mixer HiFi Playback Switch'
  ; type=BOOLEAN,access=rw------,values=1
  : values=on
numid=12,iface=MIXER,name='Output Mixer Line Bypass Switch'
  ; type=BOOLEAN,access=rw------,values=1
  : values=on
numid=13,iface=MIXER,name='Output Mixer Mic Sidetone Switch'
  ; type=BOOLEAN,access=rw------,values=1
  : values=off
numid=7,iface=MIXER,name='Sidetone Playback Volume'
  ; type=INTEGER,access=rw---R--,values=1,min=0,max=3,step=0
  : values=3
  | dBscale-min=-15.00dB,step=3.00dB,mute=0
numid=9,iface=MIXER,name='Store DC Offset Switch'
  ; type=BOOLEAN,access=rw------,values=1
  : values=on
[root@Mini69X5:/]#

 

 

关于音频路由:

sound/soc/atmel/sam9x5_wm8731.c是machine驱动,其中的音频路由表是和板级相关的。

/*
 * Audio paths on at91sam9x5ek board:
 *
 *  |A| ------------>        |           | ---R----> Headphone Jack
 *  |T| <----\                 |  WM   | ---L--/
 *  |9| ---> CLK <-->  | 8751 | <--R----- Line In Jack
 *  |1| <------------       |            | <--L--/
 */

static const struct snd_soc_dapm_route intercon[] = {
 /* headphone jack connected to HPOUT */
 {"Headphone Jack", NULL, "RHPOUT"},
 {"Headphone Jack", NULL, "LHPOUT"},

 /* line in jack connected LINEIN */
 {"LLINEIN", NULL, "Line In Jack"},
 {"RLINEIN", NULL, "Line In Jack"},
};

这里的路由格式:“目的”- “控制”- “源”

含义:“RHPOUT(开发板的右耳机输出孔)”通过“控制(无)”连接到“耳机插头Headphone Jack

              同理左耳机输出孔。

             “line in jack(线性耳机输入插头,比如mic的插头)”通过“控制(无)”连接到“LLINEIN(开发板的左LINEIN插孔)”

 

而sound/soc/codecs/wm8731.c中:

这是codec部分:

static const char *wm8731_input_select[] = {"Line In", "Mic"};

static const struct soc_enum wm8731_insel_enum =
 SOC_ENUM_SINGLE(WM8731_APANA, 2, 2, wm8731_input_select); /*  输入选择控制寄存器是寄存器4的bit2,0是Line In,1是Mic,最大值为2*/

 

static int wm8731_check_osc(struct snd_soc_dapm_widget *source,
       struct snd_soc_dapm_widget *sink)
{
 struct wm8731_priv *wm8731 = snd_soc_codec_get_drvdata(source->codec);

 return wm8731->sysclk_type == WM8731_SYSCLK_XTAL;
}

 

/* Output Mixer */
static const struct snd_kcontrol_new wm8731_output_mixer_controls[] = {
SOC_DAPM_SINGLE("Line Bypass Switch", WM8731_APANA, 3, 1, 0),
SOC_DAPM_SINGLE("Mic Sidetone Switch", WM8731_APANA, 5, 1, 0),
SOC_DAPM_SINGLE("HiFi Playback Switch", WM8731_APANA, 4, 1, 0),
};

 

/* Input mux */
static const struct snd_kcontrol_new wm8731_input_mux_controls=
SOC_DAPM_ENUM("Input Select", wm8731_insel_enum);

static const struct snd_soc_dapm_widget wm8731_dapm_widgets[] = {
SND_SOC_DAPM_SUPPLY("ACTIVE",WM8731_ACTIVE, 0, 0, NULL, 0),
SND_SOC_DAPM_SUPPLY("OSC", WM8731_PWR, 5, 1, NULL, 0),
SND_SOC_DAPM_MIXER("Output Mixer", WM8731_PWR, 4, 1,
 &wm8731_output_mixer_controls[0],
 ARRAY_SIZE(wm8731_output_mixer_controls)),
SND_SOC_DAPM_DAC("DAC", "HiFi Playback", WM8731_PWR, 3, 1),
SND_SOC_DAPM_OUTPUT("LOUT"),
SND_SOC_DAPM_OUTPUT("LHPOUT"),
SND_SOC_DAPM_OUTPUT("ROUT"),
SND_SOC_DAPM_OUTPUT("RHPOUT"),
SND_SOC_DAPM_ADC("ADC", "HiFi Capture", WM8731_PWR, 2, 1),
SND_SOC_DAPM_MUX("Input Mux", SND_SOC_NOPM, 0, 0, &wm8731_input_mux_controls),/* 这里的控制放到wm8731_input_mux_controls中去了 */
SND_SOC_DAPM_PGA("Line Input", WM8731_PWR, 0, 1, NULL, 0),/* LLINE控件操作WM8731的电源寄存器的bit0,写0则关闭 Line Input Power Down,所以需要翻转 */
SND_SOC_DAPM_MICBIAS("Mic Bias", WM8731_PWR, 1, 1),
SND_SOC_DAPM_INPUT("MICIN"),
SND_SOC_DAPM_INPUT("RLINEIN"),
SND_SOC_DAPM_INPUT("LLINEIN"), /* LLINE控件 */
};

 

/*这是wm8731的音频路由表*/

static const struct snd_soc_dapm_route intercon[ ] = {

/* 这个路由是检查codec的DA和AD所需要的时钟是否存在 */
 {"DAC", NULL, "OSC", wm8731_check_osc},
 {"ADC", NULL, "OSC", wm8731_check_osc},
 {"DAC", NULL, "ACTIVE"},
 {"ADC", NULL, "ACTIVE"},

 /* output mixer */
 {"Output Mixer", "Line Bypass Switch", "Line Input"},
 {"Output Mixer", "HiFi Playback Switch", "DAC"},
 {"Output Mixer", "Mic Sidetone Switch", "Mic Bias"},

 /* outputs */
 {"RHPOUT", NULL, "Output Mixer"},
 {"ROUT", NULL, "Output Mixer"},
 {"LHPOUT", NULL, "Output Mixer"},
 {"LOUT", NULL, "Output Mixer"},

 /* input mux */
 {"Input Mux", "Line In", "Line Input"},
 {"Input Mux", "Mic", "Mic Bias"},
 {"ADC", NULL, "Input Mux"},

 /* inputs */
 {"Line Input", NULL, "LLINEIN"},
 {"Line Input", NULL, "RLINEIN"},
 {"Mic Bias", NULL, "MICIN"},
};

分析一条录音通路:

Line In Jack(输入)  --> LLINEIN(开发板上的LLINEIN插孔) -->Line Input(codec内部)-->

从输入设备Line In Jack(machine driver)的插入到LLINEIN插孔上,LLINEIN在WM8731中定义为一个控件SND_SOC_DAPM_INPUT("LLINEIN"),它又和控件SND_SOC_DAPM_PGA("Line Input", WM8731_PWR, 0, 1, NULL, 0)连接到一起。而"Line Input"控件又同过名为"Line In"的control连接到"Input Mux"控件上,控件"Input Mux"则和"wm8731_input_mux_controls"控制连接,这个kcontrol是个枚举类型的。"Line In"是指定的control的枚举值(字符串)。

我们查看一下这个Input Mux,它有两个枚举值:Line In, Mic

[root@Mini69X5:/]# amixer cget numid=11,iface=MIXER,name='Input Mux'
numid=11,iface=MIXER,name='Input Mux'
  ; type=ENUMERATED,access=rw------,values=1,items=2
  ; Item #0 'Line In'
  ; Item #1 'Mic'
  : values=0

同理观察播放路径Headphone Jack(蓝色字体部分),可以看出音频路由作用是把各个相关控件连接起来的。

不过要注意录音和放音在amixer controls中的conrols名字,和数组定义的不完全相同,为什么呢?

soc_probe_dai_link-->soc_post_component_init-->snd_soc_dapm_new_widgets-->dapm_new_mux-->snd_soc_cnew-->snd_ctl_new1中的设置snd_kcontrol成员struct snd_ctl_elem_id id的name,这个name就是上层amixer controls中显示的名称。

static int dapm_new_mux(struct snd_soc_dapm_context *dapm,
 struct snd_soc_dapm_widget *w)
{

......

kcontrol = snd_soc_cnew(&w->kcontrols[0], w, w->name+ prefix_len, prefix);  /* w->name就控件的名称,w->kcontrols[0]的name没有被使用,见下面分析 */

......

}

 

struct snd_kcontrol *snd_soc_cnew(const struct snd_kcontrol_new *_template,
      void *data, char *long_name,
      const char *prefix)
{
 struct snd_kcontrol_new template;
 struct snd_kcontrol *kcontrol;
 char *name = NULL;
 int name_len;

 memcpy(&template, _template, sizeof(template));
 template.index = 0;

 if (!long_name)
  long_name = template.name;

 if (prefix) {
  name_len = strlen(long_name) + strlen(prefix) + 2;
  name = kmalloc(name_len, GFP_ATOMIC);
  if (!name)
   return NULL;

  snprintf(name, name_len, "%s %s", prefix, long_name);

  template.name = name;
 } else {
  template.name = long_name;    /* 这个long_name就是widget的名称,就控件的名称,这里就是"Input Mux",而w->kcontrols[0]的name被覆盖 */
 }

 kcontrol = snd_ctl_new1(&template, data);

 kfree(name);

 return kcontrol;
}

struct snd_kcontrol *snd_ctl_new1(const struct snd_kcontrol_new *ncontrol,
      void *private_data)
{

......

if (ncontrol->name) {
  strlcpy(kctl.id.name, ncontrol->name, sizeof(kctl.id.name));  /* 就是long_name */
  if (strcmp(ncontrol->name, kctl.id.name) != 0)
   snd_printk(KERN_WARNING
       "Control name '%s' truncated to '%s'\n",
       ncontrol->name, kctl.id.name);
 }

......

}

 所以最后amxier controls显示的是"Input Mux"而不是"Input Select"。amixer分析参考alsa-utils和alsa-lib中的amxier.c相关源码。

同理分析一下:

soc_probe_dai_link-->soc_post_component_init-->snd_soc_dapm_new_widgets-->dapm_new_mixer-->snd_soc_cnew-->snd_ctl_new1

其中dapm_new_mixer会把snd_soc_dapm_widget->name和snd_kcontrol_new[i]->name串联起来作为control名字。

所以我们看到的amixer controls中的3个Output Mixer的wm8731_output_mixer_controls中显示的名称为:

numid=14,iface=MIXER,name='Output Mixer HiFi Playback Switch'
numid=12,iface=MIXER,name='Output Mixer Line Bypass Switch'
numid=13,iface=MIXER,name='Output Mixer Mic Sidetone Switch'

 

-------------------------------------------------------------------

再分析一下如何设置单或双声道:

分析了一下了aplay代码:

下面的分析基于altera cylone V处理器平台:

 

aplay--> playback_go-->set_params-->snd_pcm_hw_params_set_channels(pcm.c)-->snd_pcm_hw_param_set(pcm, params, SND_TRY, SND_PCM_HW_PARAM_CHANNELS, val, 0) (pcm_params.c)-->snd_pcm_hw_refine(pcm_params.c)-->snd_pcm_hw_hw_refine(pcm_hw.c中的snd_pcm_hw_ops成员函数)-->hw_refine_call会调用驱动的ioctl命令码为SNDRV_PCM_IOCTL_HW_REFINE的命令。

而alsa驱动的snd_pcm_playback_ioctl-->snd_pcm_playback_ioctl1-->snd_pcm_common_ioctl1-->snd_pcm_hw_refine-->snd_interval_refine保存到struct snd_pcm_hw_constraints类型intervals数组中。其中snd_pcm_hw_refine函数会遍历所有的参数规则设置函数来检查和设置对应的参数,比如声道,速率,格式等。

所以我们可以在驱动的hw_params成员函数用如下代码设置硬件声道:

static int xxxx_pcm_hw_params(struct snd_pcm_substream *substream,
         struct snd_pcm_hw_params *params)
{
       unsigned int channels = params_channels(params);

      ......

}

而在i2s,codec中,struct snd_soc_dai_ops中hw_params函数可以设置各硬件参数,I2S的单,双声道则在I2S的struct snd_pcm_ops中设置。

而在machine部分,则是struct  snd_pcm_ops中hw_param函数,不过这个应该一步不用,一般只需要设置i2s和codec的声道即可。

aplay-->playback_go-->set_params-->snd_pcm_hw_params-->最后调用pcm_hw.c中的snd_pcm_hw_hw_params-->然后会调用驱动的命令码为SNDRV_PCM_IOCTL_HW_PARAMS的ioctl命令完成实际硬件的hw parameters设置,这个ioctl函数会调用substream->ops->hw_params(substream, params)。实际上是调用的soc_pcm_hw_params(soc-pcm.c),它会以此调用上面3个hw_params函数。

struct snd_pcm_runtime *runtime中的硬件限制struct snd_pcm_hw_constraints在哪里初始化的呢?在sound/core/pcm_native.c中的函数snd_pcm_hw_constraints_complete初始化,硬件参数限制来自i2s的dma驱动中的struct snd_pcm_hardware xxx_hardware全局变量,比如:

static const struct snd_pcm_hardware idma_hardware = {
 .info =  SNDRV_PCM_INFO_INTERLEAVED |  /* 表示左右声道数据交错存放:左-右-左右.... */
      /*SNDRV_PCM_INFO_NONINTERLEAVED |*/
      SNDRV_PCM_INFO_BLOCK_TRANSFER |
      SNDRV_PCM_INFO_MMAP |
      SNDRV_PCM_INFO_MMAP_VALID |
      SNDRV_PCM_INFO_PAUSE |
      SNDRV_PCM_INFO_RESUME,
     
 .formats = SNDRV_PCM_FMTBIT_S16_LE |
      SNDRV_PCM_FMTBIT_U16_LE,


 .buffer_bytes_max = MAX_IDMA_BUFFER,  //160KB
 .period_bytes_min = 128,
 .period_bytes_max = MAX_IDMA_PERIOD, //128KB
 .periods_min = 1,
 .periods_max = 2,
};

 

alsa的声道处理:

不过,如果硬件只支持双声道的话,播放单声道文件时,播放器会从软件上处理成双声道(复制一份声道数据作为另外一个声道数据)。

aplay在playback_go-->pcm_write-->remap_data函数中处理声道数据功能,因为CONFIG_SUPPORT_CHMAP这个宏默认是定义的。

在aplay中一个sample表示1个声道数据,而一个frame表示所有声道数据集合,以16位,双声道硬件,则:

bits_per_sample = snd_pcm_format_physical_width(hwparams.format) ; /* 16bit */

bits_per_frame =bits_per_sample * hwparams.channels;  /* 2x16 = 32bit */

分析一下这个函数:

static u_char *remap_data(u_char *data, size_t count)
{
 static u_char *tmp, *src, *dst; /* 静态数据指针 */
 static size_t tmp_size;  /* 静态变量 */
 size_t sample_bytes = bits_per_sample / 8; /* 2个字节 */
 size_t step = bits_per_frame / 8; /* 4个字节 */
 size_t chunk_bytes;
 unsigned int ch, i;

 if (!hw_map)
  return data;

 chunk_bytes = count * bits_per_frame / 8; /* 实际硬件需要的字节数 */
 if (tmp_size < chunk_bytes) { /* 这段代码的意思只要处理的count不变,就可以不释放这个临时buffer指针 */
  free(tmp);
  tmp = malloc(chunk_bytes);
  if (!tmp) {
   error(_("not enough memory"));
   exit(1);
  }
  tmp_size = count;
 }

 src = data;
 dst = tmp;
 for (i = 0; i < count; i++) {
  for (ch = 0; ch < hwparams.channels; ch++) {  /* 根据硬件的声道数构造数据 */
   memcpy(dst, src + sample_bytes * hw_map[ch],
          sample_bytes);
   dst += sample_bytes;
  }
  src += step;
 }
 return tmp;
}

 不过这个函数只有当设置channel map(--chmap)选项才会调用。

alsa采样率的处理:

当音频文件的16KHZ时,而硬件只支持44.1KHZ时,软件应该会做插值,转成44.1KHZ播放,但是目前还没有发现alsa中插值算法的地方。不过,当使用这个选项时--disable-resample 会导致你播放一个硬件不支持的采样率文件时会报警告采样率不匹配,同时声音也不正常。

经过对alsa-utils和alsa-lib跟踪,具体可以export  LIBASOUND_DEBUG=1,并采用aplay  -v wav16K_sample.wav命令播放16000音频文件会打印一些有用信息:

Playing WAVE 'wav16K_sample.wav' : Signed 16 bit Little Endian, Rate 16000 Hz, Stereo
ALSA ERROR hw_params: set (RATE)
           value = 16000 : Invalid argument
ACCESS:  MMAP_INTERLEAVED RW_INTERLEAVED
FORMAT:  S16_LE
SUBFORMAT:  STD
SAMPLE_BITS: 16
FRAME_BITS: 32
CHANNELS: 2
RATE: 44100
PERIOD_TIME: (725 743039)
PERIOD_SIZE: [32 32768]
PERIOD_BYTES: [128 131072]
PERIODS: [1 4]
BUFFER_TIME: (725 928799)
BUFFER_SIZE: [32 40960]
BUFFER_BYTES: [128 163840]
TICK_TIME: ALL
ALSA ERROR hw_params: set (RATE)
           value = 32000 : Invalid argument
ACCESS:  MMAP_INTERLEAVED RW_INTERLEAVED
FORMAT:  S16_LE
SUBFORMAT:  STD
SAMPLE_BITS: 16
FRAME_BITS: 32
CHANNELS: 2
RATE: 44100
PERIOD_TIME: (725 743039)
PERIOD_SIZE: [32 32768]
PERIOD_BYTES: [128 131072]
PERIODS: [1 4]
BUFFER_TIME: (725 928799)
BUFFER_SIZE: [32 40960]
BUFFER_BYTES: [128 163840]
TICK_TIME: ALL
Plug PCM: Rate conversion PCM (44100, sformat=S16_LE)     
Converter: linear-interpolation
Protocol version: 10002
Its setup is:
  stream       : PLAYBACK
  access       : RW_INTERLEAVED
  format       : S16_LE
  subformat    : STD
  channels     : 2
  rate         : 16000
  exact rate   : 16000 (16000/1)
  msbits       : 16
  buffer_size  : 8000
  period_size  : 2000
  period_time  : 125011
  tstamp_mode  : NONE
  period_step  : 1
  avail_min    : 2000
  period_event : 0
  start_threshold  : 8000
  stop_threshold   : 8000
  silence_threshold: 0
  silence_size : 0
  boundary     : 524288000
Slave: Hardware PCM card 0 'altera-ch7033' device 0 subdevice 0
Its setup is:
  stream       : PLAYBACK
  access       : MMAP_INTERLEAVED
  format       : S16_LE
  subformat    : STD
  channels     : 2
  rate         : 44100
  exact rate   : 44100 (44100/1)
  msbits       : 16
  buffer_size  : 22052
  period_size  : 5513
  period_time  : 125011
  tstamp_mode  : NONE
  period_step  : 1
  avail_min    : 5513
  period_event : 0
  start_threshold  : 22052
  stop_threshold   : 22052
  silence_threshold: 0
  silence_size : 0
  boundary     : 1445199872
  appl_ptr     : 0
  hw_ptr       : 0

这个时候pcm->fast_ops=snd_pcm_rate_fast_ops,调用的是pcm_rate.c中的函数。resample算法就是在pcm_rate_linear.c中实现的,该线性插值算法经过优化后可读性比较差,注释也很少,高手就是懒啊,时间有限我没有进一步详细研究了,对此原理和算法有研究的同学感谢共享一下。另外值得一提的是pcm_rate.c是做采样率转换的插件,alsa实现很多插件以扩展alsa功能,一般是以pcm_xxx.c文件存在。

而aplay --disable-resample -v wav16K_sample.wav则:

Playing WAVE 'wav16_sample.wav' : Signed 16 bit Little Endian, Rate 16000 Hz, Stereo
Warning: rate is not accurate (requested = 16000Hz, got = 44100Hz)
         please, try the plug plugin (-Dplug:default)
Plug PCM: Hardware PCM card 0 'altera-ch7033' device 0 subdevice 0
Its setup is:
  stream       : PLAYBACK
  access       : RW_INTERLEAVED
  format       : S16_LE
  subformat    : STD
  channels     : 2
  rate         : 44100
  exact rate   : 44100 (44100/1)
  msbits       : 16
  buffer_size  : 22050
  period_size  : 5513
  period_time  : 125011
  tstamp_mode  : NONE
  period_step  : 1
  avail_min    : 5513
  period_event : 0
  start_threshold  : 22050
  stop_threshold   : 22050
  silence_threshold: 0
  silence_size : 0
  boundary     : 1445068800
  appl_ptr     : 0
  hw_ptr       : 0

这个时候pcm->fast_ops=snd_pcm_hw_fast_ops,调用的pcm_hw.c中的函数。

从上面的函数指针挂接可以看到snd_pcm_open函数会根据open_mode从alsa的配置文件alsa.conf中获取合适的句柄。

下面分析一下如何open各pcm的open函数:

snd_pcm_open--->snd_config_update

                             ---->snd_pcm_open_noupdate-->snd_pcm_open_conf-->snd_dlobj_cache_get--->dlopen()

                                                                                                                                                                              --->dlsym()

即根据动态库的名称以及函数名称动态解析出该函数的执行地址,然后再通过这个函数执行地址调用这个函数。

我们可以使用strace来观察系统调用mmap2会把/lib/libasound.so.2的alsa库被动态内存哪个地址,然后把pcm->fast_ops打印出来,然后计算符号的相对地址。

比如:

...... 
open("/lib/libasound.so.2", O_RDONLY|O_CLOEXEC) = 3             //这里打开alsa库,文件描述符3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0(\0\1\0\0\0\260\337\1\0004\0\0\0"..., 512) = 512
lseek(3, 786340, SEEK_SET)              = 786340
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 1280) = 1280
lseek(3, 785660, SEEK_SET)              = 785660
read(3, "A6\0\0\0aeabi\0\1,\0\0\0\0057-A\0\6\n\7A\10\1\t\2\n\4\22"..., 55) = 55
fstat64(3, {st_mode=S_IFREG|0755, st_size=1060045, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x76f28000
mmap2(NULL, 818560, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x76e3a000                //这里映射alsa库到用户空间
mprotect(0x76ef6000, 32768, PROT_NONE)  = 0
mmap2(0x76efe000, 16384, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE,3, 0xbc) = 0x76efe000  //这里映射alsa库到用户空间
close(3)                              = 0
......

而打印出来的pcm->fast_ops=0x76f000d4,这个地址在0x76e3a000地址空间范围。

相对地址:0x76f000d4 - 0x76e3a000 = 0x000c60d4,根据这个偏移地址找到alsa动态链接库里的符号表中的符号,即我们要找的实际调用的函数地址。
xuewt@xuewt-desktop:~/share/alsa-port/alsa-lib-1.0.28/__install/lib$ nm libasound.so.2.0.0 |  grep 000c60d4
000c60d4 d $d
000c60d4 d snd_pcm_rate_fast_ops

可以找到具体调用的是哪个ops了。


 

alsa规则的研究:

规则初始化则在函数snd_pcm_hw_constraints_init中。如果想观察各规则运行情况,以及struct snd_pcm_hw_params中的interval数组如何设置的,可以在pcm_native.c中定义RUELS_DEBUG宏,并echo 6 4 1 7 > /proc/sys/kernel/printk改变内核打印级别。

 ¶ms->intervals[SNDRV_PCM_HW_PARAM_FIRST_INTERVAL...SNDRV_PCM_HW_PARAM_LAST_INTERVAL],这个数组的各元素都是和一定规则联系起来的。

 

#define SNDRV_PCM_HW_PARAM_ACCESS 0 /* Access type */
#define SNDRV_PCM_HW_PARAM_FORMAT 1 /* Format */
#define SNDRV_PCM_HW_PARAM_SUBFORMAT 2 /* Subformat */
#define SNDRV_PCM_HW_PARAM_FIRST_MASK SNDRV_PCM_HW_PARAM_ACCESS
#define SNDRV_PCM_HW_PARAM_LAST_MASK SNDRV_PCM_HW_PARAM_SUBFORMAT

#define SNDRV_PCM_HW_PARAM_SAMPLE_BITS 8 /* Bits per sample */
#define SNDRV_PCM_HW_PARAM_FRAME_BITS 9 /* Bits per frame */
#define SNDRV_PCM_HW_PARAM_CHANNELS 10 /* Channels */
#define SNDRV_PCM_HW_PARAM_RATE  11 /* Approx rate */
#define SNDRV_PCM_HW_PARAM_PERIOD_TIME 12 /* Approx distance between
       * interrupts in us
       */
#define SNDRV_PCM_HW_PARAM_PERIOD_SIZE 13 /* Approx frames between
       * interrupts
       */
#define SNDRV_PCM_HW_PARAM_PERIOD_BYTES 14 /* Approx bytes between
       * interrupts
       */
#define SNDRV_PCM_HW_PARAM_PERIODS 15 /* Approx interrupts per
       * buffer
       */
#define SNDRV_PCM_HW_PARAM_BUFFER_TIME 16 /* Approx duration of buffer
       * in us
       */
#define SNDRV_PCM_HW_PARAM_BUFFER_SIZE 17 /* Size of buffer in frames */
#define SNDRV_PCM_HW_PARAM_BUFFER_BYTES 18 /* Size of buffer in bytes */
#define SNDRV_PCM_HW_PARAM_TICK_TIME 19 /* Approx tick duration in us */
#define SNDRV_PCM_HW_PARAM_FIRST_INTERVAL SNDRV_PCM_HW_PARAM_SAMPLE_BITS
#define SNDRV_PCM_HW_PARAM_LAST_INTERVAL SNDRV_PCM_HW_PARAM_TICK_TIME

 以上的宏可以看到param分为两种,一种为snd mask(SNDRV_PCM_HW_PARAM_FIRST_MASK--->SNDRV_PCM_HW_PARAM_LAST_MASK),范围为0-->2,也有一个mask数组对应params->masks[...]。函数:

static inline struct snd_mask *hw_param_mask(struct snd_pcm_hw_params *params,
         snd_pcm_hw_param_t var)
{
 return ¶ms->masks[var - SNDRV_PCM_HW_PARAM_FIRST_MASK];
}

可以获取这个masks数组。
另外一种为interval数组,范围SNDRV_PCM_HW_PARAM_FIRST_INTERVAL--->SNDRV_PCM_HW_PARAM_LAST_INTERVAL,范围为8-->19,对应为Inerval数组,函数:

 static inline struct snd_interval *hw_param_interval(struct snd_pcm_hw_params *params,
          snd_pcm_hw_param_t var)
{
 return ¶ms->intervals[var - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL];
}

可以获取这个intervals数组。

我们分析各个规则时,最后都会调用intervals数组

int snd_interval_refine(struct snd_interval *i, const struct snd_interval *v)
{
 int changed = 0;
  if (snd_BUG_ON(snd_interval_empty(i)))
  return -EINVAL;
 if (i->min < v->min) {
  i->min = v->min;
  i->openmin = v->openmin;
  changed = 1;
 } else if (i->min == v->min && !i->openmin && v->openmin) {
  i->openmin = 1;
  changed = 1;
 }
 if (i->max > v->max) {
  i->max = v->max;
  i->openmax = v->openmax;
  changed = 1;
 } else if (i->max == v->max && !i->openmax && v->openmax) {
  i->openmax = 1;
  changed = 1;
 }
 if (!i->integer && v->integer) {
  i->integer = 1;
  changed = 1;
 }
 if (i->integer) {
  if (i->openmin) {
   i->min++;
   i->openmin = 0;
  }
  if (i->openmax) {
   i->max--;
   i->openmax = 0;
  }
 } else if (!i->openmin && !i->openmax && i->min == i->max)
  i->integer = 1;
 if (snd_interval_checkempty(i)) {
  snd_interval_none(i);
  return -EINVAL;
 }
 return changed;
 }

其中i是由上层封装下来的参数,比如aplay:set_params中

snd_pcm_hw_params_t *params;

snd_pcm_hw_params_alloca(¶ms);

而v则上经过规则计算得到的参数合理范围,上层设置的值必须通过这个范围检测,不能超过这个范围。

在驱动中定义了snd_pcm_hw_constraints_init和snd_pcm_hw_constraints_complete两个函数,大概有22个规则检测(rules 0... rules 21)

I) BUFFER_SIZE(SNDRV_PCM_HW_PARAM_BUFFER_SIZE)分析一下:

它总共有3个规则:

1)SNDRV_PCM_HW_PARAM_PERIOD_SIZE*SNDRV_PCM_HW_PARAM_PERIODS = SNDRV_PCM_HW_PARAM_BUFFER_SIZE(单位:帧)

     这个规则受限于对应的dma驱动中的struct snd_pcm_hardware idma_hardware结构体成员period_bytes_min和period_bytes_max。

    Rule 13 [80449178]: BUFFER_SIZE = [0 4294967295]

    a->min= 32, a->max=32768, b->min=1,b->max=2
    c->min= 32, c->max=65536

    snd_interval_refine(676):i->min= 0, i->max=4294967295, v->min=32,v->max=65536

    snd_interval_refine(715): i->min= 32, i->max=65536
     -> [32 65536]

 

2)(SNDRV_PCM_HW_PARAM_BUFFER_BYTES*8)/SNDRV_PCM_HW_PARAM_FRAME_BITS = SNDRV_PCM_HW_PARAM_BUFFER_SIZE(单位:帧)

    这个规则上下限对应的dma驱动中的struct snd_pcm_hardware idma_hardware结构体成员buffer_bytes_max。

Rule 14 [80449100]: BUFFER_SIZE = [32 65536]                         

a->min= 128, a->max=163840, b->min=32,b->max=32, k = 8       //c->min=a->min*8/b->max = 128*8/32=32  c->max=a->max*8/b->min=163840*8/32=40960=40K frames
c->min= 32, c->max=40960

 snd_interval_refine(676):i->min= 32, i->max=65536, v->min=32,v->max=40960

 snd_interval_refine(715): i->min= 32, i->max=40960
 -> [32 40960]

 

3)(SNDRV_PCM_HW_PARAM_BUFFER_TIME/1000000)*SNDRV_PCM_HW_PARAM_RATE = SNDRV_PCM_HW_PARAM_BUFFER_SIZE(单位:帧)

同一个参数必须满足所有规则限制,比如SNDRV_PCM_HW_PARAM_BUFFER_SIZE就必须满足这3条规则限制。所以最后BUFFER_SIZE为[32 40960]

 

II) PERIOD_SIZE(SNDRV_PCM_HW_PARAM_PERIOD_SIZE):

也有3个规则:

1)SNDRV_PCM_HW_PARAM_BUFFER_SIZE/SNDRV_PCM_HW_PARAM_PERIODS = SNDRV_PCM_HW_PARAM_PERIOD_SIZE(单位:帧)

2)(SNDRV_PCM_HW_PARAM_PERIOD_BYTES*8)/SNDRV_PCM_HW_PARAM_FRAME_BITS = SNDRV_PCM_HW_PARAM_PERIOD_SIZE(单位:帧)

      这个规则上下限对应的dma驱动中的struct snd_pcm_hardware idma_hardware结构体成员period_bytes_min和period_bytes_max。

     Rule 11 [80449100]: PERIOD_SIZE = [0 4294967295] -> [32 32768]

     a->min= 128, a->max=131072, b->min=32,b->max=32, k = 8             
     c->min= 32, c->max=32768                                              //c->min = a->min*8/b->max = 128*8/32=32 帧 c->max = a->max*8/b->min = 131072*8/32=32768 帧

     snd_interval_refine(676):i->min= 0, i->max=4294967295, v->min=32,v->max=32768

     snd_interval_refine(715): i->min= 32, i->max=32768
 -> [32 32768]

 

3)(SNDRV_PCM_HW_PARAM_PERIOD_TIME/1000000)*SNDRV_PCM_HW_PARAM_RATE = SNDRV_PCM_HW_PARAM_PERIOD_SIZE  其中 SNDRV_PCM_HW_PARAM_PERIOD_TIME/1000000是把微秒转成秒,然后再乘以速度得到以帧为单位的数据块大小 (单位:帧)


III) PERIOD_BYTES(SNDRV_PCM_HW_PARAM_PERIOD_BYTES):

1个规则:

1)SNDRV_PCM_HW_PARAM_PERIOD_SIZE*(SNDRV_PCM_HW_PARAM_FRAME_BITS/8)= SNDRV_PCM_HW_PARAM_PERIOD_BYTES    SNDRV_PCM_HW_PARAM_FRAME_BITS/8 表示一帧的字节数

Rule 16 [80449088]: PERIOD_BYTES = [128 131072]
a->min= 32, a->max=32768, b->min=32,b->max=32
c->min= 128, c->max=131072

 snd_interval_refine(676):i->min= 128, i->max=131072, v->min=128,v->max=131072

 snd_interval_refine(715): i->min= 128, i->max=131072
 -> [128 131072]                就是dma中硬件设定的128B和128KB上下限。

 

IV) BUFFER_BYTES(SNDRV_PCM_HW_PARAM_BUFFER_BYTES):

1)SNDRV_PCM_HW_PARAM_BUFFER_SIZE*(SNDRV_PCM_HW_PARAM_FRAME_BITS/8)= SNDRV_PCM_HW_PARAM_BUFFER_BYTES

Rule 17 [80449088]: BUFFER_BYTES = [128 163840]                   

a->min= 32, a->max=40960, b->min=32,b->max=32
c->min= 128, c->max=163840

 snd_interval_refine(676):i->min= 128, i->max=163840, v->min=128,v->max=163840

 snd_interval_refine(715): i->min= 128, i->max=163840
 -> [128 163840]                   就是dma中硬件设定的128B和160KB上下限。

 

V) PERIOD_TIME(SNDRV_PCM_HW_PARAM_PERIOD_TIME):

1)(SNDRV_PCM_HW_PARAM_PERIOD_SIZE/SNDRV_PCM_HW_PARAM_RATE)*1000000  最后单位为微秒  【公式:大小(帧数)/速度=时间】

Rule 18 [80449100]: PERIOD_TIME = [0 4294967295]
a->min= 32, a->max=32768, b->min=44100,b->max=44100, k = 1000000
c->min= 725, c->max=743039

 snd_interval_refine(676):i->min= 0, i->max=4294967295, v->min=725,v->max=743039

 snd_interval_refine(715): i->min= 725, i->max=743039
 -> (725 743039)

 

VI) BUFFER_TIME(SNDRV_PCM_HW_PARAM_BUFFER_TIME):

1)(SNDRV_PCM_HW_PARAM_BUFFER_SIZE/SNDRV_PCM_HW_PARAM_RATE)*1000000 = SNDRV_PCM_HW_PARAM_BUFFER_TIME us

Rule 19 [80449100]: BUFFER_TIME = [0 4294967295]
a->min= 32, a->max=40960, b->min=44100,b->max=44100, k = 1000000
c->min= 725, c->max=928799

 snd_interval_refine(676):i->min= 0, i->max=4294967295, v->min=725,v->max=928799

 snd_interval_refine(715): i->min= 725, i->max=928799
 -> (725 928799)

 

VII) PERIODS(SNDRV_PCM_HW_PARAM_PERIODS)

1)SNDRV_PCM_HW_PARAM_BUFFER_SIZE/SNDRV_PCM_HW_PARAM_PERIOD_SIZE = SNDRV_PCM_HW_PARAM_PERIODS

 

VIII) FRAME_BITS(SNDRV_PCM_HW_PARAM_FRAME_BITS):

3个规则:

1)SNDRV_PCM_HW_PARAM_SAMPLE_BITS * SNDRV_PCM_HW_PARAM_CHANNELS = SNDRV_PCM_HW_PARAM_FRAME_BITS

2)SNDRV_PCM_HW_PARAM_PERIOD_BYTES*8/SNDRV_PCM_HW_PARAM_PERIOD_SIZE = SNDRV_PCM_HW_PARAM_FRAME_BITS

3) SNDRV_PCM_HW_PARAM_BUFFER_BYTES*8/SNDRV_PCM_HW_PARAM_BUFFER_SIZE = SNDRV_PCM_HW_PARAM_FRAME_BITS

 

IX) CHANNELS(SNDRV_PCM_HW_PARAM_CHANNELS):

1) SNDRV_PCM_HW_PARAM_FRAME_BITS/SNDRV_PCM_HW_PARAM_SAMPLE_BITS = 2

 

X) RATE(SNDRV_PCM_HW_PARAM_RATE):

1)SNDRV_PCM_HW_PARAM_PERIOD_SIZE/(SNDRV_PCM_HW_PARAM_PERIOD_TIME/1000000) = SNDRV_PCM_HW_PARAM_RATE   【公式:大小(帧数)/时间=速率】

2)SNDRV_PCM_HW_PARAM_BUFFER_SIZE/(SNDRV_PCM_HW_PARAM_BUFFER_TIME/1000000) = SNDRV_PCM_HW_PARAM_RATE 

 

XI) SAMPLE_BITS(SNDRV_PCM_HW_PARAM_SAMPLE_BITS):

1)最简单是根据音频数据的格式SNDRV_PCM_FORMAT_S16_LE 则返回16

2)SNDRV_PCM_HW_PARAM_FRAME_BITS/SNDRV_PCM_HW_PARAM_CHANNELS = SNDRV_PCM_HW_PARAM_SAMPLE_BITS

规则总结:

1)period_time,buffer_time单位为us,而period_size,buffer_size单位则为帧。size和time转换关系: buffer_time = (buffer_size/rate)*1000000

2)buffer_size =  period_size*periods

3)  速度*时间 = 帧数, 比如44100HZ采样率表示1秒钟时间采样44100个帧(包括左右声道数据)。

 

当一个声卡活动时,数据总是连续地在硬件缓存区和应用程序缓存区间传输。但是也有例外。在录音例子中,如果应用程序读取数据不够快,循环缓存区将会被新的数据覆盖。这种数据的丢失被称为overrun.在回放例子中,如果应用程序写入数据到缓存区中的速度不够快,缓存区将会"饿死"。这样的错误被称为"underrun"。在ALSA文档中,有时将这两种情形统称为"XRUN"。适当地设计应用程序可以最小化XRUN并且可以从中恢复过来。

如果我们发现播放时偶尔有underrun错误时,可以调整period_size(period_time)以及buffer_size(buffer_time),另外periods周期数也很重要,它影响使用的buffer_size大小,因为buffer_size是periods*period_size。aplay上层默认会获取最大的buffer_time,如果buffer_time>500000则取buffer_time=500000,然后period_time则用这个最大buffer_time的1/4作为period_time。aplay中的chunk_size就是period_size大小,单位为帧。

上述分析具有通用性,具体平台只是分析手段。

你可能感兴趣的:(kernel)