Linux ALSA 音频系统:逻辑设备篇

2024-02-17 07:32

本文主要是介绍Linux ALSA 音频系统:逻辑设备篇,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

https://blog.csdn.net/zyuanyun/article/details/59180272#t6

6. 声卡和 PCM 设备的建立过程

前面几章分析了 Codec、Platform、Machine 驱动的组成部分及其注册过程,这三者都是物理设备相关的,大家应该对音频物理链路有了一定的认知。接着分析音频驱动的中间层,由于这些并不是真正的物理设备,故我们称之为逻辑设备。

PCM 逻辑设备,我们又习惯称之为 PCM 中间层或 pcm native,起着承上启下的作用:往上是与用户态接口的交互,实现音频数据在用户态和内核态之间的拷贝;往下是触发 codec、platform、machine 的操作函数,实现音频数据在 dma_buffer <-> cpu_dai <-> codec 之间的传输。后面章节将会详细分析这个过程,这里还是先从声卡的注册谈起。

//
// 声明:本文由 http://blog.csdn.net/zyuanyun 原创,转载请注明出处,谢谢!
//

声卡驱动中,一般挂载着多个逻辑设备,看看我们计算机的声卡驱动有几个逻辑设备:

$ cat /proc/asound/devices
  1:        : sequencer
  2: [ 0- 7]: digital audio playback
  3: [ 0- 3]: digital audio playback
  4: [ 0- 2]: digital audio capture
  5: [ 0- 0]: digital audio playback
  6: [ 0- 0]: digital audio capture
  7: [ 0- 3]: hardware dependent
  8: [ 0- 0]: hardware dependent
  9: [ 0]   : control
 33:        : timer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

Device     Description
digital audio playback     用于回放的 PCM 设备
digital audio capture     用于录制的 PCM 设备
control     用于声卡控制的 CTL 设备,如通路控制、音量调整等
timer     定时器设备
sequencer     音序器设备

嵌入式系统中,通常我们更关心 PCM 和 CTL 这两种设备。

设备节点如下:

$ ll /dev/snd
drwxr-xr-x   3 root root      260 Feb 26 13:59 ./
drwxr-xr-x  16 root root     4300 Mar  6 17:07 ../
drwxr-xr-x   2 root root       60 Feb 26 13:59 by-path/
crw-rw---T+  1 root audio 116,  9 Feb 26 13:59 controlC0
crw-rw---T+  1 root audio 116,  8 Feb 26 13:59 hwC0D0
crw-rw---T+  1 root audio 116,  7 Feb 26 13:59 hwC0D3
crw-rw---T+  1 root audio 116,  6 Feb 26 13:59 pcmC0D0c
crw-rw---T+  1 root audio 116,  5 Mar  6 19:08 pcmC0D0p
crw-rw---T+  1 root audio 116,  4 Feb 26 13:59 pcmC0D2c
crw-rw---T+  1 root audio 116,  3 Feb 26 13:59 pcmC0D3p
crw-rw---T+  1 root audio 116,  2 Feb 26 13:59 pcmC0D7p
crw-rw---T+  1 root audio 116,  1 Feb 26 13:59 seq
crw-rw---T+  1 root audio 116, 33 Feb 26 13:59 timer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

可以看到这些设备节点的 Major=116,Minor 则与 /proc/asound/devices 所列的对应起来,都是字符设备。上层可以通过 open/close/read/write/ioctl 等系统调用来操作声卡设备,这和其他字符设备类似,但一般情况下我们会使用已封装好的用户接口库如 tinyalsa、alsa-lib。
6.1. 声卡结构概述

回顾下 ASoC 是如何注册声卡的,详细请参考章节 5. ASoC machine driver,这里仅简单陈述下:

    Machine 驱动初始化时,.name = "soc-audio" 的 platform_device 与 platform_driver 匹配成功,触发 soc_probe() 调用;
    继而调用 snd_soc_register_card():
        为每个音频物理链路找到对应的 codec、codec_dai、cpu_dai、platform 设备实例,完成 dai_link 的绑定;
        调用 snd_card_create() 创建声卡;
        依次回调 cpu_dai、codec、platform 的 probe() 函数,完成物理设备的初始化;
    随后调用 soc_new_pcm():
        设置 pcm native 中要使用的 pcm 操作函数,这些函数用于驱动音频物理设备,包括 machine、codec_dai、cpu_dai、platform;
        调用 snd_pcm_new() 创建 pcm 逻辑设备,回放子流和录制子流都在这里创建;
        回调 platform 驱动的 pcm_new(),完成音频 dma 设备初始化和 dma buffer 内存分配;
    最后调用 snd_card_register() 注册声卡。

关于音频物理设备部分(Codec/Platform/Machine)不再累述,下面详细分析声卡和 PCM 逻辑设备的注册过程。

上面提到声卡驱动上挂着多个逻辑子设备,有 pcm 音频数据流、control 混音器、midi 迷笛、timer 定时器、sequencer 音序器等。

                  +-----------+
                  | snd_card  |
                  +-----------+
                    |   |   |
        +-----------+   |   +------------+
        |               |                |
+-----------+    +-----------+    +-----------+
 |  snd_pcm  |    |snd_control|    | snd_timer |    ...
 +-----------+    +-----------+    +-----------+

    1
    2
    3
    4
    5
    6
    7
    8
    9

这些与声音相关的逻辑设备都在结构体 snd_card 管理之下,可以说 snd_card 是 alsa 中最顶层的结构。我们再看看 alsa 声卡驱动的大致结构图(不是严格的 UML 类图,有结构体定义、模块关系、函数调用,方便标示结构模块的层次及关系):

snd_card

snd_cards:记录着所注册的声卡实例,每个声卡实例有着各自的逻辑设备,如 PCM 设备、CTL 设备、MIDI 设备等,并一一记录到 snd_card 的 devices 链表上
snd_minors:记录着所有逻辑设备的上下文信息,它是声卡逻辑设备与系统调用 API 之间的桥梁;每个 snd_minor 在逻辑设备注册时被填充,在逻辑设备使用时就可以从该结构体中得到相应的信息(主要是系统调用函数集 file_operations)
6.2. 声卡的创建

声卡实例通过函数 snd_card_create() 来创建,其函数原型:

/**
 *  snd_card_create - create and initialize a soundcard structure
 *  @idx: card index (address) [0 ... (SNDRV_CARDS-1)]
 *  @xid: card identification (ASCII string)
 *  @module: top level module for locking
 *  @extra_size: allocate this extra size after the main soundcard structure
 *  @card_ret: the pointer to store the created card instance
 *
 *  Creates and initializes a soundcard structure.
 *
 *  The function allocates snd_card instance via kzalloc with the given
 *  space for the driver to use freely.  The allocated struct is stored
 *  in the given card_ret pointer.
 *
 *  Returns zero if successful or a negative error code.
 */
int snd_card_create(int idx, const char *xid,
            struct module *module, int extra_size,
            struct snd_card **card_ret)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

注释非常详细,简单说下:

    idx:声卡的编号,如为 -1,则由系统自动分配
    xid:声卡标识符,如为 NULL,则以 snd_card 的 shortname 或 longname 代替
    card_ret:返回所创建的声卡实例的指针

如下是我的计算机的声卡信息:

$ cat /proc/asound/cards
 0 [PCH            ]: HDA-Intel - HDA Intel PCH
                      HDA Intel PCH at 0xf7c30000 irq 47

    1
    2
    3

    number:0
    id:PCH
    shortname:HDA Intel PCH
    longname:HDA Intel PCH at 0xf7c30000 irq 47

shortname、longname 常用于打印信息,上面的声卡信息是通过如下函数打印出来的:

static void snd_card_info_read(struct snd_info_entry *entry,
                   struct snd_info_buffer *buffer)
{
    int idx, count;
    struct snd_card *card;

    for (idx = count = 0; idx < SNDRV_CARDS; idx++) {
        mutex_lock(&snd_card_mutex);
        if ((card = snd_cards[idx]) != NULL) {
            count++;
            snd_iprintf(buffer, "%2i [%-15s]: %s - %s\n",
                    idx,
                    card->id,
                    card->driver,
                    card->shortname);
            snd_iprintf(buffer, "                      %s\n",
                    card->longname);
        }
        mutex_unlock(&snd_card_mutex);
    }
    if (!count)
        snd_iprintf(buffer, "--- no soundcards ---\n");
}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

6.3. 逻辑设备的创建

当声卡实例建立后,接着可以创建声卡下面的各个逻辑设备了。每个逻辑设备创建时,都会调用 snd_device_new() 生成一个 snd_device 实例,并把该实例挂到声卡 snd_card 的 devices 链表上。alsa 驱动为各种逻辑设备提供了创建接口,如下:
Device     Interface
PCM     snd_pcm_new()
CONTROL     snd_ctl_create()
MIDI     snd_rawmidi_new()
TIMER     snd_timer_new()
SEQUENCER     snd_seq_device_new()
JACK     snd_jack_new()

这些接口的一般过程如下:

int snd_xxx_new()
{
    // 这些接口供逻辑设备注册时回调
    static struct snd_device_ops ops = {
        .dev_free = snd_xxx_dev_free,
        .dev_register = snd_xxx_dev_register,
        .dev_disconnect = snd_xxx_dev_disconnect,
    };
    
    // 逻辑设备实例初始化
    
    // 新建一个设备实例 snd_device,挂到 snd_card 的 devices 链表上,把该逻辑设备纳入声卡的管理当中,SNDRV_DEV_xxx 是逻辑设备的类型
    return snd_device_new(card, SNDRV_DEV_xxx, card, &ops);
}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

其中 snd_device_ops 是声卡逻辑设备的注册函数集,dev_register() 回调尤其重要,它在声卡注册时被调用,用于建立系统的设备节点,/dev/snd/ 目录的设备节点都是在这里创建的,通过这些设备节点可系统调用 open/release/read/write/ioctl… 访问操作该逻辑设备。

例如 snd_ctl_dev_register():

// CTL 设备的系统调用接口
static const struct file_operations snd_ctl_f_ops =
{
    .owner =    THIS_MODULE,
    .read =     snd_ctl_read,
    .open =     snd_ctl_open,
    .release =  snd_ctl_release,
    .llseek =   no_llseek,
    .poll =     snd_ctl_poll,
    .unlocked_ioctl =   snd_ctl_ioctl,
    .compat_ioctl = snd_ctl_ioctl_compat,
    .fasync =   snd_ctl_fasync,
};

/*
 * registration of the control device
 */
static int snd_ctl_dev_register(struct snd_device *device)
{
    struct snd_card *card = device->device_data;
    int err, cardnum;
    char name[16];

    if (snd_BUG_ON(!card))
        return -ENXIO;
    cardnum = card->number;
    if (snd_BUG_ON(cardnum < 0 || cardnum >= SNDRV_CARDS))
        return -ENXIO;
    sprintf(name, "controlC%i", cardnum);
    if ((err = snd_register_device(SNDRV_DEVICE_TYPE_CONTROL, card, -1,
                       &snd_ctl_f_ops, card, name)) < 0)
        return err;
    return 0;
}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34

事实是调用 snd_register_device_for_dev ():

    分配并初始化一个 snd_minor 实例;
    保存该 snd_minor 实例到 snd_minors 数组中;
    调用 device_create() 生成设备文件节点。

/**
 * snd_register_device_for_dev - Register the ALSA device file for the card
 * @type: the device type, SNDRV_DEVICE_TYPE_XXX
 * @card: the card instance
 * @dev: the device index
 * @f_ops: the file operations
 * @private_data: user pointer for f_ops->open()
 * @name: the device file name
 * @device: the &struct device to link this new device to
 *
 * Registers an ALSA device file for the given card.
 * The operators have to be set in reg parameter.
 *
 * Returns zero if successful, or a negative error code on failure.
 */
int snd_register_device_for_dev(int type, struct snd_card *card, int dev,
                const struct file_operations *f_ops,
                void *private_data,
                const char *name, struct device *device)
{
    int minor;
    struct snd_minor *preg;

    if (snd_BUG_ON(!name))
        return -EINVAL;
    preg = kmalloc(sizeof *preg, GFP_KERNEL);
    if (preg == NULL)
        return -ENOMEM;
    preg->type = type;
    preg->card = card ? card->number : -1;
    preg->device = dev;
    preg->f_ops = f_ops;
    preg->private_data = private_data;
    mutex_lock(&sound_mutex);
#ifdef CONFIG_SND_DYNAMIC_MINORS
    minor = snd_find_free_minor(type);
#else
    minor = snd_kernel_minor(type, card, dev);
    if (minor >= 0 && snd_minors[minor])
        minor = -EBUSY;
#endif
    if (minor < 0) {
        mutex_unlock(&sound_mutex);
        kfree(preg);
        return minor;
    }
    snd_minors[minor] = preg;
    preg->dev = device_create(sound_class, device, MKDEV(major, minor),
                  private_data, "%s", name);
    if (IS_ERR(preg->dev)) {
        snd_minors[minor] = NULL;
        mutex_unlock(&sound_mutex);
        minor = PTR_ERR(preg->dev);
        kfree(preg);
        return minor;
    }

    mutex_unlock(&sound_mutex);
    return 0;
}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60

上面过程是声卡注册时才被回调的。
6.4. 声卡的注册

当声卡下的所有逻辑设备都已经准备就绪后,就可以调用 snd_card_register() 注册声卡了:

    创建声卡的 sysfs 设备;
    调用 snd_device_register_all() 注册所有挂在该声卡下的逻辑设备;
    建立 proc 信息文件和 sysfs 属性文件。

/**
 *  snd_card_register - register the soundcard
 *  @card: soundcard structure
 *
 *  This function registers all the devices assigned to the soundcard.
 *  Until calling this, the ALSA control interface is blocked from the
 *  external accesses.  Thus, you should call this function at the end
 *  of the initialization of the card.
 *
 *  Returns zero otherwise a negative error code if the registration failed.
 */
int snd_card_register(struct snd_card *card)
{
    int err;

    if (snd_BUG_ON(!card))
        return -EINVAL;

    // 创建 sysfs 设备,声卡的 class 将会出现在 /sys/class/sound/ 下面
    if (!card->card_dev) {
        card->card_dev = device_create(sound_class, card->dev,
                           MKDEV(0, 0), card,
                           "card%i", card->number);
        if (IS_ERR(card->card_dev))
            card->card_dev = NULL;
    }

    // 遍历挂在该声卡的所有逻辑设备,回调各 snd_device 的 ops->dev_register() 完成各逻辑设备的注册
    if ((err = snd_device_register_all(card)) < 0)
        return err;
    mutex_lock(&snd_card_mutex);
    if (snd_cards[card->number]) {
        /* already registered */
        mutex_unlock(&snd_card_mutex);
        return 0;
    }
    if (*card->id) {
        /* make a unique id name from the given string */
        char tmpid[sizeof(card->id)];
        memcpy(tmpid, card->id, sizeof(card->id));
        snd_card_set_id_no_lock(card, tmpid, tmpid);
    } else {
        /* create an id from either shortname or longname */
        const char *src;
        src = *card->shortname ? card->shortname : card->longname;
        snd_card_set_id_no_lock(card, src,
                    retrieve_id_from_card_name(src));
    }
    snd_cards[card->number] = card; // 把该声卡实例保存到 snd_cards 数组中
    mutex_unlock(&snd_card_mutex);
    // 声卡相关信息,见:/proc/asound/card0
    init_info_for_card(card);
#if defined(CONFIG_SND_MIXER_OSS) || defined(CONFIG_SND_MIXER_OSS_MODULE)
    if (snd_mixer_oss_notify_callback)
        snd_mixer_oss_notify_callback(card, SND_MIXER_OSS_NOTIFY_REGISTER);
#endif
    // 声卡的 sysfs 属性节点
    if (card->card_dev) {
        err = device_create_file(card->card_dev, &card_id_attrs);
        if (err < 0)
            return err;
        err = device_create_file(card->card_dev, &card_number_attrs);
        if (err < 0)
            return err;
    }

    return 0;
}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68

至此完成了声卡及声卡下的所有逻辑设备的注册,用户态可以通过系统调用来访问这些设备了。
6.5. PCM 设备的创建

最后我们简单描述下 PCM 设备的建立过程:

snd_soc_instantiate_card

snd_pcm_set_ops:设置 PCM 设备的操作接口,设置完成后,在 PCM 设备层即可访问操作底层音频物理设备。
snd_pcm_new:

    创建一个 PCM 设备实例 snd_pcm;
    创建 playback stream 和 capture stream,旗下的 substream 也同时建立;
    调用 snd_device_new() 把 PCM 设备挂到声卡的 devices 链表上。

static int _snd_pcm_new(struct snd_card *card, const char *id, int device,
        int playback_count, int capture_count, bool internal,
        struct snd_pcm **rpcm)
{
    struct snd_pcm *pcm;
    int err;
    static struct snd_device_ops ops = {
        .dev_free = snd_pcm_dev_free,
        .dev_register = snd_pcm_dev_register,
        .dev_disconnect = snd_pcm_dev_disconnect,
    };

    if (snd_BUG_ON(!card))
        return -ENXIO;
    if (rpcm)
        *rpcm = NULL;
    pcm = kzalloc(sizeof(*pcm), GFP_KERNEL);
    if (pcm == NULL) {
        snd_printk(KERN_ERR "Cannot allocate PCM\n");
        return -ENOMEM;
    }
    pcm->card = card;
    pcm->device = device;
    pcm->internal = internal;
    if (id)
        strlcpy(pcm->id, id, sizeof(pcm->id));
    if ((err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_PLAYBACK, playback_count)) < 0) {
        snd_pcm_free(pcm);
        return err;
    }
    if ((err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_CAPTURE, capture_count)) < 0) {
        snd_pcm_free(pcm);
        return err;
    }
    mutex_init(&pcm->open_mutex);
    init_waitqueue_head(&pcm->open_wait);
    if ((err = snd_device_new(card, SNDRV_DEV_PCM, pcm, &ops)) < 0) {
        snd_pcm_free(pcm);
        return err;
    }
    if (rpcm)
        *rpcm = pcm;
    return 0;
}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44

我们再看看 PCM 设备的系统调用:

const struct file_operations snd_pcm_f_ops[2] = {
    {
        .owner =        THIS_MODULE,
        .write =        snd_pcm_write,
        .aio_write =        snd_pcm_aio_write,
        .open =         snd_pcm_playback_open,
        .release =      snd_pcm_release,
        .llseek =       no_llseek,
        .poll =         snd_pcm_playback_poll,
        .unlocked_ioctl =   snd_pcm_playback_ioctl,
        .compat_ioctl =     snd_pcm_ioctl_compat,
        .mmap =         snd_pcm_mmap,
        .fasync =       snd_pcm_fasync,
        .get_unmapped_area =    snd_pcm_get_unmapped_area,
    },
    {
        .owner =        THIS_MODULE,
        .read =         snd_pcm_read,
        .aio_read =     snd_pcm_aio_read,
        .open =         snd_pcm_capture_open,
        .release =      snd_pcm_release,
        .llseek =       no_llseek,
        .poll =         snd_pcm_capture_poll,
        .unlocked_ioctl =   snd_pcm_capture_ioctl,
        .compat_ioctl =     snd_pcm_ioctl_compat,
        .mmap =         snd_pcm_mmap,
        .fasync =       snd_pcm_fasync,
        .get_unmapped_area =    snd_pcm_get_unmapped_area,
    }
};

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30

snd_pcm_f_ops 作为 snd_register_device_for_dev() 的参数被传入,并被记录在 snd_minors[minor] 中的字段 f_ops 中。snd_pcm_f_ops[0] 是回放使用的系统调用接口,snd_pcm_f_ops[1] 是录制使用的系统调用接口。
7. Frame & Period

后面章节将分析 dma buffer 的管理,其中细节需要对音频数据相关概念有一定的了解。因此本章说明下音频数据中的几个重要概念:

    Sample:样本长度,音频数据最基本的单位,常见的有 8 位和 16 位;
    Channel:声道数,分为单声道 mono 和立体声 stereo;
    Frame:帧,构成一个完整的声音单元,所谓的声音单元是指一个采样样本,Frame = Sample * 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 数据所需的硬件中断次数。

下面一张图直观的表示 buffer/period/frame/sample 之间的关系:

      read/write pointer
               |
               v
+-----------------------------+--------------+--------------+
|              |              |              |              |buffer = 4 periods
+--------------+--------------+--------------+--------------+
                     ^
                     |
               +---+---+------+
               |   |   |  ... |period = 1024 frames
               +---+---+------+
                     ^
                     |
                   +---+
                   |L|R|frame = 2 samples (left + right)
                   +---+

                        sample = 2 bytes (16bit)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

这个 buffer 中有 4 个 period,每当 DMA 搬运完一个 period 的数据就会出生一次中断,因此搬运这个 buffer 中的数据将产生 4 次中断。ALSA 为什么这样做?因为数据缓存区可能很大,一次传输可能会导致不可接受的延迟;为了解决这个问题,alsa 把缓存区拆分成多个周期,以周期为单元传输数据。
7.1. Frames & Periods

敏感的读者会察觉到 period 和 buffer size 在 PCM 数据搬运中扮演着非常重要的角色。下面引用两段来自 alsa 官网对 Period 的详细解释:

Period

    The interval between interrupts from the hardware. This defines the input latency, since the CPU will not have any idea that there is data waiting until the audio interface interrupts it.
    The audio interface has a “pointer” that marks the current position for read/write in its h/w buffer. The pointer circles around the buffer as long as the interface is running.
    Typically, there are an integral number of periods per traversal of the h/w buffer, but not always. There is at least one card (ymfpci) that generates interrupts at a fixed rate indepedent of the buffer size (which can be changed), resulting in some “odd” effects compared to more traditional designs.
    Note: h/w generally defines the interrupt in frames, though not always.
    Alsa’s period size setting will affect how much work the CPU does. if you set the period size low, there will be more interrupts and the work that is done every interrupt will be done more often. So, if you don’t care about low latency, set the period size large as possible and you’ll have more CPU cycles for other things. The defaults that ALSA provides are in the middle of the range, typically.
    (from an old AlsaDevel thread[1], quoting Paul Davis)
    Retrieved from “http://alsa.opensrc.org/Period”

FramesPeriods

    A frame is equivalent of one sample being played, irrespective of the number of channels or the number of bits. e.g.

    1 frame of a Stereo 48khz 16bit PCM stream is 4 bytes.
    1 frame of a 5.1 48khz 16bit PCM stream is 12 bytes.
    A period is the number of frames in between each hardware interrupt. The poll() will return once a period.
    The buffer is a ring buffer. The buffer size always has to be greater than one period size. Commonly this is 2*period size, but some hardware can do 8 periods per buffer. It is also possible for the buffer size to not be an integer multiple of the period size.
    Now, if the hardware has been set to 48000Hz , 2 periods, of 1024 frames each, making a buffer size of 2048 frames. The hardware will interrupt 2 times per buffer. ALSA will endeavor to keep the buffer as full as possible. Once the first period of samples has been played, the third period of samples is transfered into the space the first one occupied while the second period of samples is being played. (normal ring buffer behaviour).

Additional example
Here is an alternative example for the above discussion.
Say we want to work with a stereo, 16-bit, 44.1 KHz stream, one-way (meaning, either in playback or in capture direction). Then we have:

    ‘stereo’ = number of channels: 2
    1 analog sample is represented with 16 bits = 2 bytes
    1 frame represents 1 analog sample from all channels; here we have 2 channels, and so: 1 frame = (num_channels) * (1 sample in bytes) = (2 channels) * (2 bytes (16 bits) per sample) = 4 bytes (32 bits)
    To sustain 2x 44.1 KHz analog rate - the system must be capable of data transfer rate, in Bytes/sec: Bps_rate = (num_channels) * (1 sample in bytes) * (analog_rate) = (1 frame) * (analog_rate) = ( 2 channels ) * (2 bytes/sample) * (44100 samples/sec) = 2244100 = 176400 Bytes/sec

Now, if ALSA would interrupt each second, asking for bytes - we’d need to have 176400 bytes ready for it (at end of each second), in order to sustain analog 16-bit stereo @ 44.1Khz.

    If it would interrupt each half a second, correspondingly for the same stream we’d need 176400/2 = 88200 bytes ready, at each interrupt;
    if the interrupt hits each 100 ms, we’d need to have 176400*(0.1/1) = 17640 bytes ready, at each interrupt.

We can control when this PCM interrupt is generated, by setting a period size, which is set in frames.

    Thus, if we set 16-bit stereo @ 44.1Khz, and the period_size to 4410 frames => (for 16-bit stereo @ 44.1Khz, 1 frame equals 4 bytes - so 4410 frames equal 4410*4 = 17640 bytes) => an interrupt will be generated each 17640 bytes - that is, each 100 ms.
    Correspondingly, buffer_size should be at least 2period_size = 24410 = 8820 frames (or 8820*4 = 35280 bytes).

It seems (writing-an-alsa-driver.pdf), however, that it is the ALSA runtime that decides on the actual buffer_size and period_size, depending on: the requested number of channels, and their respective properties (rate and sampling resolution) - as well as the parameters set in the snd_pcm_hardware structure (in the driver).
Also, the following quote may be relevant, from “(alsa-devel) Questions about writing a new ALSA driver for a very limitted device”:

            The “frame” represents the unit, 1 frame = # channels x sample_bytes.
            In your case, 1 frame corresponds to 2 channels x 16 bits = 4 bytes.

            The periods is the number of periods in a ring-buffer. In OSS, called
            as “fragments”.

            So,

                buffer_size = period_size * periods
                period_bytes = period_size * bytes_per_frame
                bytes_per_frame = channels * bytes_per_sample

        I still don’t understand what ‘period_size’ and a ‘period’ is?

The “period” defines the frequency to update the status, usually via the invokation of interrupts. The “period_size” defines the frame sizes corresponding to the “period time”. This term corresponds to the “fragment size” on OSS. On major sound hardwares, a ring-buffer is divided to several parts and an irq is issued on each boundary. The period_size defines the size of this chunk.

On some hardwares, the irq is controlled on the basis of a timer. In this case, the period is defined as the timer frequency to invoke an irq.

这里不做翻译了,简单说下 Frame 和 Period 要点:

    Frame:帧,构成一个完整的声音单元,它的大小等于 sample_bits * channels;
    Peroid:周期大小,即每次 dma 运输处理音频数据的帧数。如果周期大小设定得较大,则单次处理的数据较多,这意味着单位时间内硬件中断的次数较少,CPU 也就有更多时间处理其他任务,功耗也更低,但这样也带来一个显著的弊端——数据处理的时延会增大。

再说说 period bytes,对于 dma 处理来说,它直接关心的是数据大小,而非 period_size(一个周期的帧数),有个转换关系:period_bytes = period_size * sample_bits * channels / 8

由于 I2S 总线采样率是稳定的,我们可以计算 I2S 传输一个周期的数据所需的时间:transfer_time = 1 * period_size / sample_rate, in second

例如 period_size = 1024,sample_rate = 48KHz ,那么一个周期数据的传输时间是: 1 * 1024 / 48000 = 21.3 (ms)。
7.2. hrtimer 模拟 PCM 周期中断

在 4.2.1. pcm operations 章节中,我们提到:每次 dma 传输完成一个周期的数据传输后,都要调用 snd_pcm_period_elapsed() 告知 pcm native 一个周期的数据已经传送到 FIFO 上了,然后再次调用 dma 传输音频数据…如此循环。

但有些 Platform 可能由于设计如此或设计缺陷,dma 传输完一个周期的数据不会产生硬件中断。这样系统如何知道什么时候传输完一个周期的数据了呢?在上个章节的最后,我们提到 I2S 总线传输一个周期的数据所需的时间,这其实也是 dma 搬运一个周期的数据所需的时间,这很容易理解:I2S FIFO 消耗完一个周期的数据,dma 才接着搬运一个周期的数据到 I2S FIFO。

因此我们可以用定时器来模拟这种硬件中断:

    触发dma搬运数据时,启动定时器开始计时;
    当定时到 1 * period_size / sample_rate,这时 I2S 已传输完一个周期的音频数据了,进入定时器中断处理:调用 snd_pcm_period_elapsed() 告知 pcm native 一个周期的数据已经处理完毕了,同时准备下一次的数据搬运;
    继续执行步骤 1…

为了更好保证数据传输的实时性,建议采用高精度定时器 hrtimer。

作者见过至少两家芯片在传输音频数据时需要用定时器模拟周期中断,一是 MTK 的智能手机处理器,二是 Freescale 的 i.MX 系列处理器。后者已经合入 Linux 内核代码,具体见:sound/soc/imx/imx-pcm-fiq.c,这里简略分析:

// 定时器中断处理例程
static enum hrtimer_restart snd_hrtimer_callback(struct hrtimer *hrt)
{
    ...

    /* If we've transferred at least a period then report it and
     * reset our poll time */
    if (delta >= iprtd->period) {
        snd_pcm_period_elapsed(substream); // 告知 pcm native 一个周期的数据已经处理完毕
        iprtd->last_offset = iprtd->offset;
    }

    hrtimer_forward_now(hrt, ns_to_ktime(iprtd->poll_time_ns)); // 重新计时,poll_time_ns:I2S 传输一个周期的数据所需的时间
    return HRTIMER_RESTART;
}

// hw_params 回调,数据传输开始前,先设置 dma 传输参数
static int snd_imx_pcm_hw_params(struct snd_pcm_substream *substream,
                struct snd_pcm_hw_params *params)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    struct imx_pcm_runtime_data *iprtd = runtime->private_data;

    iprtd->size = params_buffer_bytes(params);    // dma 缓冲区大小
    iprtd->periods = params_periods(params);      // 周期数
    iprtd->period = params_period_bytes(params) ; // 周期大小
    iprtd->offset = 0;
    iprtd->last_offset = 0;
    iprtd->poll_time_ns = 1000000000 / params_rate(params) *
                params_period_size(params);       // 计算 I2S 传输一个周期的数据所需的时间
    snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer); // 设置 dma 缓冲区

    return 0;
}

// trigger 回调,触发 dma 传输或停止
static int snd_imx_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    struct imx_pcm_runtime_data *iprtd = runtime->private_data;

    switch (cmd) {
    case SNDRV_PCM_TRIGGER_START:
    case SNDRV_PCM_TRIGGER_RESUME:
    case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
        atomic_set(&iprtd->running, 1);
        hrtimer_start(&iprtd->hrt, ns_to_ktime(iprtd->poll_time_ns), // 准备传输数据,启动定时器,开始计时
              HRTIMER_MODE_REL);
        if (++fiq_enable == 1)
            enable_fiq(imx_pcm_fiq); // 开始 dma 传输

        break;

    case SNDRV_PCM_TRIGGER_STOP:
    case SNDRV_PCM_TRIGGER_SUSPEND:
    case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
        atomic_set(&iprtd->running, 0);

        if (--fiq_enable == 0)
            disable_fiq(imx_pcm_fiq); // 停止 dma 传输

        break;
    default:
        return -EINVAL;
    }

    return 0;
}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69

– to be continued…
---------------------
版权声明:本文为CSDN博主「zyuanyun」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zyuanyun/article/details/59180272

这篇关于Linux ALSA 音频系统:逻辑设备篇的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/717097

相关文章

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Linux服务器Java启动脚本

Linux服务器Java启动脚本 1、初版2、优化版本3、常用脚本仓库 本文章介绍了如何在Linux服务器上执行Java并启动jar包, 通常我们会使用nohup直接启动,但是还是需要手动停止然后再次启动, 那如何更优雅的在服务器上启动jar包呢,让我们一起探讨一下吧。 1、初版 第一个版本是常用的做法,直接使用nohup后台启动jar包, 并将日志输出到当前文件夹n

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级

【Linux】应用层http协议

一、HTTP协议 1.1 简要介绍一下HTTP        我们在网络的应用层中可以自己定义协议,但是,已经有大佬定义了一些现成的,非常好用的应用层协议,供我们直接使用,HTTP(超文本传输协议)就是其中之一。        在互联网世界中,HTTP(超文本传输协议)是一个至关重要的协议,他定义了客户端(如浏览器)与服务器之间如何进行通信,以交换或者传输超文本(比如HTML文档)。

如何编写Linux PCIe设备驱动器 之二

如何编写Linux PCIe设备驱动器 之二 功能(capability)集功能(capability)APIs通过pci_bus_read_config完成功能存取功能APIs参数pos常量值PCI功能结构 PCI功能IDMSI功能电源功率管理功能 功能(capability)集 功能(capability)APIs int pcie_capability_read_wo