linux下alsa音频数据流分析-凯发app官方网站

凯发app官方网站-凯发k8官网下载客户端中心 | | 凯发app官方网站-凯发k8官网下载客户端中心
  • 博客访问: 263919
  • 博文数量: 90
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 665
  • 用 户 组: 普通用户
  • 注册时间: 2018-10-15 14:13
个人简介

搭建一个和linux开发者知识共享和学习的平台

文章分类

全部博文(90)

文章存档

2024年(4)

2023年(24)

2022年(27)

2019年(8)

2018年(27)

相关博文
  • ·
  • ·
  • ·
  • ·
  • ·
  • ·
  • ·
  • ·
  • ·
  • ·

分类: linux

2023-02-10 11:28:57

1. 前言
本文,我们将以回放(playback,播放音频)为例,讲解pcm data是如何从用户空间到内核空间,{banned}{banned}最佳佳后传递到codec。
在linux 音频驱动(一) asoc音频框架简介中,我们给出了回放(playback)pcm数据流示意图:






对于linux来说,由于分为 user space 和kernel space,而且两者之间不能随便互相访问。因此用户如果播放音频,则需要调用copy_from_user()将用户数据从user space拷贝到kernel space (dma buffer)。
dma 负责将dma buffer中的音频数据搬运到i2s tx fifo。
通过i2s总线,将音频数据传送到codec。
codec内部经过dac转换,将模拟信号传到扬声器spk(头戴式耳机hp、耳塞式耳机earp)。
下面基于源码讲解pcm data flow。


2. pcm data flow
内核版本:kernel 版本:3.10
内核源码文件:
         ./kernel-3.10/sound/core/device.c
         ./kernel-3.10/sound/core/init.c
         ./kernel-3.10/sound/core/pcm.c
         ./kernel-3.10/sound/core/pcm_lib.c
         ./kernel-3.10/sound/core/pcm_native.c
         ./kernel-3.10/sound/soc/soc-pcm.c
tinyalsa源码文件:
         ./external/tinyalsa/pcm.c
         ./external/kernel-headers/original/uapi/sound/asound.h


user space


在我的源码包里,用户空间应用程序使用的是 tinyalsa提供的接口write pcm data,即播放音频文件。
write pcm逻辑设备是通过 ioctl() 函数完成的,即应用程序将需要播放的音频数据通过pcm_write() --> ioctl() 传递到内核。


// ./external/kernel-headers/original/uapi/sound/asound.h, line 448
struct snd_xferi {
snd_pcm_sframes_t result;
void __user *buf;
snd_pcm_uframes_t frames;
};
// ./external/tinyalsa/pcm.c, line 483
int pcm_write(struct pcm *pcm, const void *data, unsigned int count)
{
    struct snd_xferi x;
    ......
    x.buf = (void*)data;
    x.frames = count / (pcm->config.channels *
                        pcm_format_to_bits(pcm->config.format) / 8);
    ......
    ioctl(pcm->fd, sndrv_pcm_ioctl_writei_frames, &x);
    ......
}

音频数据中的几个重要概念:
format:样本长度(采样精度 or 采样深度),音频数据{banned}{banned}最佳佳基本的单位,常见的有 8 位和 16 位;
channel:声道数,分为单声道 mono 和立体声stereo;
frame:帧,构成一个完整的声音单元,frame = format * channel;
rate:又称 sample rate:采样率,即每秒的采样次数,针对帧而言;
period size:周期,每次硬件中断处理音频数据的帧数,对于音频设备的数据读写,以此为单位;
buffer size:数据缓冲区大小,这里指runtime 的 buffer size,而不是结构体 snd_pcm_hardware 中定义的buffer_bytes_max;一般来说 buffer_size = period_size * period_count,period_count 相当于处理完一个 buffer 数据所需的硬件中断次数。


为了通过系统调用ioctl()传递音频数据,定义了struct snd_xferi x,x.buf指向本次要播放的音频数据,x.frames表示本次音频数据总共有多少帧(frame)。


kernel space


通过系统调用ioctl()传递数据到内核,在内核空间是pcm逻辑设备对应的snd_pcm_f_ops[0].unlocked_ioctl()。


// ./kernel-3.10/sound/core/pcm_native.c, line 3481
const struct file_operations snd_pcm_f_ops[2] = {
{
.owner =this_module,
.write =snd_pcm_write,
        ......
.unlocked_ioctl =snd_pcm_playback_ioctl,
        ......
},
{
.owner =this_module,
.read =snd_pcm_read,
        ......
.unlocked_ioctl =snd_pcm_capture_ioctl,
        ......
}
};


snd_pcm_playback_ioctl() 直接调用了snd_pcm_playback_ioctl1()。


// ./kernel-3.10/sound/core/pcm_native.c, line 2784
static long snd_pcm_playback_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct snd_pcm_file *pcm_file;
pcm_file = file->private_data;
    ......
return snd_pcm_playback_ioctl1(file, pcm_file->substream, cmd, (void __user *)arg);
}

snd_pcm_playback_ioctl1()函数:
a. 定义struct snd_xferi xferi,为了获取用户空间传来的arg;
b. 调用put_user() 清除snd_xferi.result状态;
c. 调用copy_from_user()将取用户空间的arg拷贝到内核空间,即拷贝音频数据存储空间的指针buf和音频数据帧数frames;
d. 调用snd_pcm_lib_write();
e. 调用put_user() 回填write结果_xferi->result。


// ./kernel-3.10/sound/core/pcm_native.c, line 2624
static int snd_pcm_playback_ioctl1(struct file *file, struct snd_pcm_substream *substream, unsigned int cmd, void __user *arg)
{
    ......
switch (cmd) {
case sndrv_pcm_ioctl_writei_frames:
{
struct snd_xferi xferi;
struct snd_xferi __user *_xferi = arg;
struct snd_pcm_runtime *runtime = substream->runtime;
snd_pcm_sframes_t result;
if (runtime->status->state == sndrv_pcm_state_open)
return -ebadfd;
if (put_user(0, &_xferi->result))
return -efault;
if (copy_from_user(&xferi, _xferi, sizeof(xferi)))
return -efault;
result = snd_pcm_lib_write(substream, xferi.buf, xferi.frames);
__put_user(result, &_xferi->result);
return result < 0 ? result : 0;
}
......
}


snd_pcm_lib_write()做一些参数检查后调用snd_pcm_lib_write1()。注意调用snd_pcm_lib_write1()时传入的{banned}{banned}最佳佳后一个参数snd_pcm_lib_write_transfer,该函数完成音频数据从 kernel space 到 dma buffer 的传输。


// ./kernel-3.10/sound/core/pcm_lib.c, line 2101
snd_pcm_sframes_t snd_pcm_lib_write(struct snd_pcm_substream *substream, const void __user *buf, snd_pcm_uframes_t size)
{
struct snd_pcm_runtime *runtime;
int nonblock;
......
nonblock = !!(substream->f_flags & o_nonblock);
......
return snd_pcm_lib_write1(substream, (unsigned long)buf, size, nonblock, snd_pcm_lib_write_transfer);
}

如下snd_pcm_lib_write1()代码段中,第40行调用transfer(),即调用snd_pcm_lib_write_transfer(),将音频数据从 kernel space 拷贝到 dma buffer。然后,在第49行,调用snd_pcm_start()启动dma传输,将音频数据从 dma buffer 拷贝到 i2s tx fifo。


// ./kernel-3.10/sound/core/pcm_lib.c, line 1985
static snd_pcm_sframes_t snd_pcm_lib_write1(struct snd_pcm_substream *substream, unsigned long data,
    snd_pcm_uframes_t size, int nonblock, transfer_f transfer)
{
struct snd_pcm_runtime *runtime = substream->runtime;
snd_pcm_uframes_t xfer = 0;
snd_pcm_uframes_t offset = 0;
snd_pcm_uframes_t avail;
int err = 0;


if (size == 0)
return 0;
    ......
runtime->twake = runtime->control->avail_min ? : 1;
if (runtime->status->state == sndrv_pcm_state_running)
snd_pcm_update_hw_ptr(substream);
avail = snd_pcm_playback_avail(runtime);
while (size > 0) {
snd_pcm_uframes_t frames, appl_ptr, appl_ofs;
snd_pcm_uframes_t cont;
if (!avail) {
if (nonblock) {
err = -eagain;
goto _end_unlock;
}
runtime->twake = min_t(snd_pcm_uframes_t, size,
runtime->control->avail_min ? : 1);
err = wait_for_avail(substream, &avail);
if (err < 0)
goto _end_unlock;
}
frames = size > avail ? avail : size;
cont = runtime->buffer_size - runtime->control->appl_ptr % runtime->buffer_size;
if (frames > cont)
frames = cont;
......
appl_ptr = runtime->control->appl_ptr;
appl_ofs = appl_ptr % runtime->buffer_size;
snd_pcm_stream_unlock_irq(substream);
err = transfer(substream, appl_ofs, data, offset, frames);  //将音频数据从 kernel space 拷贝到 dma buffer
snd_pcm_stream_lock_irq(substream);
......
offset = frames;
size -= frames;
xfer = frames;
avail -= frames;
if (runtime->status->state == sndrv_pcm_state_prepared &&
    snd_pcm_playback_hw_avail(runtime) >= (snd_pcm_sframes_t)runtime->start_threshold) {
err = snd_pcm_start(substream);  //启动dma传输,将音频数据从 dma buffer 拷贝到 i2s tx fifo
if (err < 0)
goto _end_unlock;
}
}
    ......
}

snd_pcm_lib_write_transfer()中:
a. 如果有设定过substream->ops->copy回调函数,则执行substream->ops->copy()将音频数据从 kernel space 拷贝到 dma buffer。
b. 如果没有设定substream->ops->copy回调函数,则直接调用copy_from_user()将音频数据从 kernel space 拷贝到 dma buffer。


// ./kernel-3.10/sound/core/pcm_lib.c, line 1962
static int snd_pcm_lib_write_transfer(struct snd_pcm_substream *substream, unsigned int hwoff,
      unsigned long data, unsigned int off, snd_pcm_uframes_t frames)
{
struct snd_pcm_runtime *runtime = substream->runtime;
int err;
char __user *buf = (char __user *) data frames_to_bytes(runtime, off);
if (substream->ops->copy) {
if ((err = substream->ops->copy(substream, -1, hwoff, buf, frames)) < 0)
return err;
} else {
char *hwbuf = runtime->dma_area frames_to_bytes(runtime, hwoff);
if (copy_from_user(hwbuf, buf, frames_to_bytes(runtime, frames)))
return -efault;
}
return 0;
}


注:本例中,substream->ops->copy回调函数是在soc_new_pcm()中设置的。在soc_new_pcm()中,如果有设定platform->driver->ops (即pcm dma驱动操作函数集),则pcm逻辑设备的某些操作函数将会被platform->driver->ops中覆盖掉,如下代码段第26行。


// ./kernel-3.10/sound/soc/soc-pcm.c, line 2005
int soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num)
{
    ......
/* asoc pcm operations */
if (rtd->dai_link->dynamic) {
rtd->ops.open= dpcm_fe_dai_open;
rtd->ops.hw_params= dpcm_fe_dai_hw_params;
rtd->ops.prepare= dpcm_fe_dai_prepare;
rtd->ops.trigger= dpcm_fe_dai_trigger;
rtd->ops.hw_free= dpcm_fe_dai_hw_free;
rtd->ops.close= dpcm_fe_dai_close;
rtd->ops.pointer= soc_pcm_pointer;
rtd->ops.ioctl= soc_pcm_ioctl;
} else {
rtd->ops.open= soc_pcm_open;
rtd->ops.hw_params= soc_pcm_hw_params;
rtd->ops.prepare= soc_pcm_prepare;
rtd->ops.trigger= soc_pcm_trigger;
rtd->ops.hw_free= soc_pcm_hw_free;
rtd->ops.close= soc_pcm_close;
rtd->ops.pointer= soc_pcm_pointer;
rtd->ops.ioctl= soc_pcm_ioctl;
}


if (platform->driver->ops) {
rtd->ops.ack= platform->driver->ops->ack;
rtd->ops.copy= platform->driver->ops->copy;
rtd->ops.silence= platform->driver->ops->silence;
rtd->ops.page= platform->driver->ops->page;
rtd->ops.mmap= platform->driver->ops->mmap;
}
    ......
}


现在看一下snd_pcm_start()如何启动dma传输的。
snd_pcm_start()直接调用了snd_pcm_action()。此处要注意snd_pcm_action()的{banned}中国{banned}中国第一个参数snd_pcm_action_start。snd_pcm_action()在多个地方会被调用,要注意其被调用时的{banned}中国{banned}中国第一个参数是什么。


// ./kernel-3.10/sound/core/pcm_native.c, line 891
static struct action_ops snd_pcm_action_start = {
.pre_action = snd_pcm_pre_start,
.do_action = snd_pcm_do_start,
.undo_action = snd_pcm_undo_start,
.post_action = snd_pcm_post_start
};
// ./kernel-3.10/sound/core/pcm_native.c, line 904
int snd_pcm_start(struct snd_pcm_substream *substream)
{
return snd_pcm_action(&snd_pcm_action_start, substream, sndrv_pcm_state_running);
}

snd_pcm_action()中会调用snd_pcm_action_group()或snd_pcm_action_single()。为简化讲解,我们关注snd_pcm_action_single()。snd_pcm_action_single()函数非常简单,就是执行struct action_ops *ops指向的回调函数集,本例中为上文提到的snd_pcm_action_start。


// ./kernel-3.10/sound/core/pcm_native.c, line 785
static int snd_pcm_action(struct action_ops *ops, struct snd_pcm_substream *substream, int state)
{
int res;


if (snd_pcm_stream_linked(substream)) {
if (!spin_trylock(&substream->group->lock)) {
spin_unlock(&substream->self_group.lock);
spin_lock(&substream->group->lock);
spin_lock(&substream->self_group.lock);
}
res = snd_pcm_action_group(ops, substream, state, 1);
spin_unlock(&substream->group->lock);
} else {
res = snd_pcm_action_single(ops, substream, state);
}
return res;
}
// ./kernel-3.10/sound/core/pcm_native.c, line 765
static int snd_pcm_action_single(struct action_ops *ops,
struct snd_pcm_substream *substream,
int state)
{
int res;

res = ops->pre_action(substream, state);
if (res < 0)
return res;
res = ops->do_action(substream, state);
if (res == 0)
ops->post_action(substream, state);
else if (ops->undo_action)
ops->undo_action(substream, state);
return res;
}


我们重点看一下ops->do_action(),即snd_pcm_do_start()。在前面soc_new_pcm()代码段中,我们看到substream->ops->trigger()被设定为soc_pcm_trigger(),该函数依次调用codec_dai driver的trigger函数、pcm_dma的trigger函数、cpu_dai driver的trigger函数。


// ./kernel-3.10/sound/core/pcm_native.c, line 862
static int snd_pcm_do_start(struct snd_pcm_substream *substream, int state)
{
if (substream->runtime->trigger_master != substream)
return 0;
return substream->ops->trigger(substream, sndrv_pcm_trigger_start);
}

// ./kernel-3.10/sound/core/soc-pcm.c, line 609
static int soc_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
    struct snd_soc_pcm_runtime *rtd = substream->private_data;
struct snd_soc_platform *platform = rtd->platform;
struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
struct snd_soc_dai *codec_dai = rtd->codec_dai;
int ret;


if (codec_dai->driver->ops->trigger) {
ret = codec_dai->driver->ops->trigger(substream, cmd, codec_dai);  //调用codec_dai driver的trigger函数 (可选的)
if (ret < 0)
return ret;
}


if (platform->driver->ops && platform->driver->ops->trigger) {
ret = platform->driver->ops->trigger(substream, cmd);  //调用pcm_dma的trigger函数
if (ret < 0)
return ret;
}


if (cpu_dai->driver->ops->trigger) {
ret = cpu_dai->driver->ops->trigger(substream, cmd, cpu_dai);  //调用cpu_dai driver的trigger函数 (可选的)
if (ret < 0)
return ret;
}
return 0;
}


问题:为什么执行trigger函数的顺序是codec_dai --> pcm_dma --> cpu_dai 呢 ?是否可以调整顺序 ?


思考:
是可以调整的。(下面的思考是我个人想法,其实我倾向于相信这部分的设计是大神有意为之的,可能我还没理解吧。)
首先,codec_dai和cpu_dai的trigger函数是可选的。查看struct snd_soc_dai_ops结构体的源码,可以看到其中pcm operation函数集注释说“alsa pcm audio operations - all optional.”。
其次,假设codec_dai和cpu_dai的trigger都有定义,个人认为相对理想的顺序应该是codec_dai --> cpu_dai --> pcm_dma。因为,当我们启动dma将音频数据拷贝到cpu_dai(i2s tx buffer)时,如果该硬件还没有ready,那dma搬过去的数据可能会丢失。同理,如果cpu_dai开始传数据了,而codec_dai(i2s rx buffer)没有ready,那数据也有可能丢失。理论上应该是在启动dma之前,数据流下一接收装置应该已经ready。所以,我才会说相对理想的trigger顺序应该是codec_dai --> cpu_dai --> pcm_dma。
(我目前使用的mtk平台没有定义codec_dai和cpu_dai的trigger函数,没有办法研究该顺序是否有意义。有条件的读者可以调整顺序,看是否会对音频播放造成问题。)


下面介绍trigger函数的内容不同的soc平台会有差异,本例基于mtk平台的源码分析。
本例中,codec_dai driver的trigger函数、pcm_dma的trigger函数、cpu_dai driver的trigger函数分别如下:


codec_dai driver的trigger函数:mt6323_codec_trigger(),虽然该函数有定义,但是该函数没有做任何事情。


// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_codec_63xx.c, line 1070
static struct snd_soc_dai_driver mtk_6331_dai_codecs[] =
{
    {
        .name = mt_soc_codec_txdai_name,
        .ops = &mt6323_aif1_dai_ops,
        ......
    },
    ......
}
// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_codec_63xx.c, line 1063
static const struct snd_soc_dai_ops mt6323_aif1_dai_ops =
{
    .startup    = mt63xx_codec_startup,
    .prepare   = mt63xx_codec_prepare,
    .trigger     = mt6323_codec_trigger,
};
// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_codec_63xx.c, line 1048
static int mt6323_codec_trigger(struct snd_pcm_substream *substream , int command , struct snd_soc_dai *daiport)
{
    switch (command)
    {
        case sndrv_pcm_trigger_start:
        case sndrv_pcm_trigger_resume:
        case sndrv_pcm_trigger_stop:
        case sndrv_pcm_trigger_suspend:
            break;
    }


    return 0;
}


pcm_dma的trigger函数:mtk_pcm_i2s0dl1_trigger()


// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_pcm_dl1_i2s0dl1.c, line 730
static struct snd_soc_platform_driver mtk_i2s0dl1_soc_platform =
{
    .ops        = &mtk_i2s0dl1_ops,
    .pcm_new    = mtk_asoc_pcm_i2s0dl1_new,
    .probe      = mtk_afe_i2s0dl1_probe,
};
// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_pcm_dl1_i2s0dl1.c, line 715
static struct snd_pcm_ops mtk_i2s0dl1_ops =
{
    ......
    .trigger =  mtk_pcm_i2s0dl1_trigger,
    .pointer =  mtk_pcm_i2s0dl1_pointer,
    .copy =     mtk_pcm_i2s0dl1_copy,
    ......
};
// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_pcm_dl1_i2s0dl1.c, line 529
static int mtk_pcm_i2s0dl1_trigger(struct snd_pcm_substream *substream, int cmd)
{
    //printk("mtk_pcm_i2s0dl1_trigger cmd = %d\n", cmd);


    switch (cmd)
    {
        case sndrv_pcm_trigger_start:
        case sndrv_pcm_trigger_resume:
            return mtk_pcm_i2s0dl1_start(substream);  // 启动 dma 传输
        case sndrv_pcm_trigger_stop:
        case sndrv_pcm_trigger_suspend:
            return mtk_pcm_i2s0dl1_stop(substream);  // 停止 dma 传输
    }
    return -einval;
}
// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_pcm_dl1_i2s0dl1.c, line 493
static int mtk_pcm_i2s0dl1_start(struct snd_pcm_substream *substream)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    printk("%s\n", __func__);
    // here start digital part


    setconnection(soc_aud_intercon_connection, soc_aud_interconnectioninput_i05, soc_aud_interconnectionoutput_o00);
    setconnection(soc_aud_intercon_connection, soc_aud_interconnectioninput_i06, soc_aud_interconnectionoutput_o01);
    setconnection(soc_aud_intercon_connection, soc_aud_interconnectioninput_i05, soc_aud_interconnectionoutput_o03);
    setconnection(soc_aud_intercon_connection, soc_aud_interconnectioninput_i06, soc_aud_interconnectionoutput_o04);


    setirqenable(soc_aud_irq_mcu_mode_irq1_mcu_mode, true);


    setsamplerate(soc_aud_digital_block_mem_dl1, runtime->rate);
    setchannels(soc_aud_digital_block_mem_dl1, runtime->channels);
    setmemorypathenable(soc_aud_digital_block_mem_dl1, true);


    enableafe(true);
    ......
    return 0;
}


cpu_dai driver的trigger函数:本例中没有实现该函数


// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_dai_stub.c, line 98
static struct snd_soc_dai_driver mtk_dai_stub_dai[] =
{
    {
        ......
        .name = mt_soc_dl1dai_name,
        .ops = &mtk_dai_stub_ops,
    },
    ......
}
// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_dai_stub.c, line 93
static struct snd_soc_dai_ops mtk_dai_stub_ops =
{
    .startup    = multimedia_startup,
};

3. 总结
回放(playback)pcm数据流示意图:




{banned}{banned}最佳佳后简单总结一下pcm write时的数据传递:
  i.  应用程序调用tinyalsa提供的接口pcm_write()-->ioctl()将需要回放的音频数据指针和帧数传递给内核。
 ii.  内核在snd_pcm_lib_write_transfer()函数中使用copy_from_user()将音频数据从user space拷贝到kernel space,即从应用程序的buffer拷贝到dma buffer。
iii.  内核在snd_pcm_start()中启动dma传输,将音频数据从dma buffer拷贝到i2s tx fifo。(实质上是通过pcm_dma的trigger函数来做的。)
————————————————
凯发k8官网下载客户端中心的版权声明:本文为csdn博主「_modest_」的原创文章,遵循cc 4.0 by-sa凯发k8官网下载客户端中心的版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yangjizhen1533/article/details/114576914
阅读(1217) | 评论(0) | 转发(0) |
0

上一篇:

下一篇:

给主人留下些什么吧!~~
")); function link(t){ var href= $(t).attr('href'); href ="?url=" encodeuricomponent(location.href); $(t).attr('href',href); //setcookie("returnouturl", location.href, 60, "/"); }
网站地图