欢迎访问 生活随笔!

生活随笔

当前位置: 首页 > 运维知识 > linux >内容正文

linux

Linux ALSA音频系统:soundcard

发布时间:2024/3/7 linux 37 豆豆
生活随笔 收集整理的这篇文章主要介绍了 Linux ALSA音频系统:soundcard 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

8.1声卡和PCM设备的建立过程

 前面分析了codec,platform,machine驱动的组成部分及其注册过程,这三者都是物理设备相关的。

     pcm逻辑设备,我们习惯称之为PCM中间层或pcm native,起着承上启下的作用:往上是与用户态接口的交互,实现音频数据在用户和内核态之间的拷贝;往下是触发codec,platform,machine的操作函数,实现音频数据在dma_buffer<->cpu_dai<->codec之间的传输。

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

$ cat /proc/asound/devices 2: [ 0] : control3: [ 0- 0]: digital audio playback4: [ 0- 0]: digital audio capture5: [ 0- 3]: digital audio playback6: [ 0- 7]: digital audio playback7: [ 0- 8]: digital audio playback8: [ 0- 0]: hardware dependent9: [ 0- 3]: hardware dependent33: : timer

 

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

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

$ ll /dev/snd/ total 0 drwxr-xr-x 2 root root 60 7月 14 22:19 by-path crw-rw----+ 1 root audio 116, 2 7月 14 22:19 controlC0 crw-rw----+ 1 root audio 116, 8 7月 14 22:19 hwC0D0 crw-rw----+ 1 root audio 116, 9 7月 14 22:19 hwC0D3 crw-rw----+ 1 root audio 116, 4 7月 15 13:43 pcmC0D0c crw-rw----+ 1 root audio 116, 3 7月 15 15:35 pcmC0D0p crw-rw----+ 1 root audio 116, 5 7月 14 22:20 pcmC0D3p crw-rw----+ 1 root audio 116, 6 7月 14 22:20 pcmC0D7p crw-rw----+ 1 root audio 116, 7 7月 14 22:20 pcmC0D8p crw-rw----+ 1 root audio 116, 1 7月 14 22:19 seq crw-rw----+ 1 root audio 116, 33 7月 14 22:19 timer

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

8.2声卡结构概述

  回顾下ASoC是如何注册声卡的,这里仅简单陈述下:

  • 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()注册声卡。

    下面详细分析声卡和PCM逻辑设备的注册过程。

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

     

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

    这些与声音相关的逻辑设备都在结构体snd_card管理之下,可以说snd_card是alsa中最顶层的结构。我们在看看alsa声卡驱动的大致结构图。

     

         

    snd_cards:记录着所注册的声卡实例,每个声卡实例有着各自的逻辑设备,如PCM设备,CTL设备,MIDI设备等,并一一记录到snd_card的device链表上

    snd_minors:记录着所有逻辑设备的上下文信息,它是声卡逻辑设备与系统调用api之间的桥梁;每个snd_minor在逻辑设备注册时被填充,在逻辑设备使用时就可以从该结构中的到相应的信息(主要是系统调用函数集file_operations)

    8.3声卡的创建

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

    /*** snd_card_new - create and initialize a soundcard structure* @parent: the parent device object* @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.** Return: Zero if successful or a negative error code.*/int snd_card_new(struct device *parent, int idx, const char *xid, struct module *module, int extra_size,struct snd_card **card_ret)

    主要参数说明:

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

    如下是我ubuntu16.04的声卡信息:

    $ cat /proc/asound/cards0 [PCH ]: HDA-Intel - HDA Intel PCHHDA Intel PCH at 0xf2530000 irq 31
    • 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");}

    8.4逻辑设备的创建

    当声卡实例建立后,接着可以创建声卡下面的各个逻辑设备看。每个逻辑设备创建式,都会调用snd_device_new()生成一个snd_device实例,并把该实例挂到声卡snd_card的devices链表上。alsa驱动为各种逻辑设备提供了创建接口,如下:

    DeviceInterface
    PCMsnd_pcm_new()
    CONTROLsnd_ctl_create()
    MIDIsnd_rawmidi_new()
    TIMERsnd_timer_new()
    SEQUENCERsnd_seq_device_new()
    JACKsnd_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是逻辑设备的类型snd_device_new(card, SNDRV_DEV_XXX, xxx, &ops); }

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

    snd_ctl_dev_register():

    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;return snd_register_device(SNDRV_DEVICE_TYPE_CONTROL, card, -1,&snd_ctl_f_ops, card, &card->ctl_dev);} /*** snd_register_device - 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()* @device: the device to register** Registers an ALSA device file for the given card.* The operators have to be set in reg parameter.** Return: Zero if successful, or a negative error code on failure.*/int snd_register_device(int type, struct snd_card *card, int dev,const struct file_operations *f_ops,void *private_data, struct device *device){int minor;int err = 0;struct snd_minor *preg; if (snd_BUG_ON(!device))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;preg->card_ptr = card;mutex_lock(&sound_mutex);minor = snd_find_free_minor(type, card, dev);if (minor < 0) {err = minor;goto error;}preg->dev = device;device->devt = MKDEV(major, minor);err = device_add(device);if (err < 0)goto error;snd_minors[minor] = preg;error:mutex_unlock(&sound_mutex);if (err < 0)kfree(preg);return err;}

    从snd_ctl_dev_register函数中可以看到:

    • 分配并初始化一个snd_minor实例,
    • 分配设备节点
    • 保存snd_minor实例到snd_minors数组中
    1016 /**1017 * device_add - add device to device hierarchy. 1018 * @dev: device.1019 *1020 * This is part 2 of device_register(), though may be called1021 * separately _iff_ device_initialize() has been called separately.1022 *1023 * This adds @dev to the kobject hierarchy via kobject_add(), adds it1024 * to the global and sibling lists for the device, then1025 * adds it to the other relevant subsystems of the driver model.1026 *1027 * Do not call this routine or device_register() more than once for1028 * any device structure. The driver model core is not designed to work1029 * with devices that get unregistered and then spring back to life.1030 * (Among other things, it's very hard to guarantee that all references1031 * to the previous incarnation of @dev have been dropped.) Allocate1032 * and register a fresh new struct device instead.1033 *1034 * NOTE: _Never_ directly free @dev after calling this function, even1035 * if it returned an error! Always use put_device() to give up your1036 * reference instead.1037 */1038 int device_add(struct device *dev)1039 {1040 struct device *parent = NULL;1041 struct kobject *kobj;1042 struct class_interface *class_intf;1043 int error = -EINVAL;1044 struct kobject *glue_dir = NULL;1045 1046 dev = get_device(dev);1047 if (!dev)1048 goto done; 1050 if (!dev->p) {1051 error = device_private_init(dev);1052 if (error)1053 goto done;1054 }1055 1056 /*1057 * for statically allocated devices, which should all be converted1058 * some day, we need to initialize the name. We prevent reading back1059 * the name, and force the use of dev_name()1060 */1061 if (dev->init_name) {1062 dev_set_name(dev, "%s", dev->init_name);1063 dev->init_name = NULL;1064 }1065 1066 /* subsystems can specify simple device enumeration */1067 if (!dev_name(dev) && dev->bus && dev->bus->dev_name)1068 dev_set_name(dev, "%s%u", dev->bus->dev_name, dev->id);1069 1070 if (!dev_name(dev)) {1071 error = -EINVAL;1072 goto name_error;1073 }1074 1075 pr_debug("device: '%s': %s\n", dev_name(dev), __func__);1076 1077 parent = get_device(dev->parent);1078 kobj = get_device_parent(dev, parent);1079 if (kobj)1080 dev->kobj.parent = kobj; 1081 1082 /* use parent numa_node */1083 if (parent && (dev_to_node(dev) == NUMA_NO_NODE))1084 set_dev_node(dev, dev_to_node(parent));1085 1086 /* first, register with generic layer. */1087 /* we require the name to be set before, and pass NULL */1088 error = kobject_add(&dev->kobj, dev->kobj.parent, NULL);1089 if (error) {1090 glue_dir = get_glue_dir(dev);1091 goto Error;1092 }1093 1094 /* notify platform of device entry */1095 if (platform_notify)1096 platform_notify(dev);1097 1098 error = device_create_file(dev, &dev_attr_uevent);1099 if (error)1100 goto attrError;1101 1102 error = device_add_class_symlinks(dev);1103 if (error)1104 goto SymlinkError;1105 error = device_add_attrs(dev);1106 if (error)1107 goto AttrsError;1108 error = bus_add_device(dev);1109 if (error)1110 goto BusError;1111 error = dpm_sysfs_add(dev);1112 if (error)goto DPMError;device_pm_add(dev); if (MAJOR(dev->devt)) {error = device_create_file(dev, &dev_attr_dev);if (error)goto DevAttrError;error = device_create_sys_dev_entry(dev);if (error)goto SysEntryError;devtmpfs_create_node(dev);}/* Notify clients of device addition. This call must come* after dpm_sysfs_add() and before kobject_uevent().*/if (dev->bus)blocking_notifier_call_chain(&dev->bus->p->bus_notifier,BUS_NOTIFY_ADD_DEVICE, dev); kobject_uevent(&dev->kobj, KOBJ_ADD);bus_probe_device(dev); if (parent)klist_add_tail(&dev->p->knode_parent,&parent->p->klist_children);if (dev->class) {mutex_lock(&dev->class->p->mutex);/* tie the class to the device */klist_add_tail(&dev->knode_class,&dev->class->p->klist_devices);/* notify any interfaces that the device is here */list_for_each_entry(class_intf,&dev->class->p->interfaces, node)if (class_intf->add_dev)class_intf->add_dev(dev, class_intf);mutex_unlock(&dev->class->p->mutex);}done:put_device(dev);return error;SysEntryError:if (MAJOR(dev->devt))device_remove_file(dev, &dev_attr_dev);DevAttrError:device_pm_remove(dev);dpm_sysfs_remove(dev);DPMError:bus_remove_device(dev);BusError:device_remove_attrs(dev);AttrsError:device_remove_class_symlinks(dev);SymlinkError:device_remove_file(dev, &dev_attr_uevent);attrError:kobject_uevent(&dev->kobj, KOBJ_REMOVE);glue_dir = get_glue_dir(dev);kobject_del(&dev->kobj);Error:cleanup_glue_dir(dev, glue_dir);put_device(parent);name_error:kfree(dev->p);dev->p = NULL;goto done;}

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

    8.5声卡的注册

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

    • 创建声卡的sysfs设备;
    • 调用snd_device_register_all()注册所有挂在该声卡下的逻辑设备;
    • 建立proc信息文件和sysfs属性文件。
    724 /**725 * snd_card_register - register the soundcard726 * @card: soundcard structure727 *728 * This function registers all the devices assigned to the soundcard.729 * Until calling this, the ALSA control interface is blocked from the730 * external accesses. Thus, you should call this function at the end731 * of the initialization of the card.732 *733 * Return: Zero otherwise a negative error code if the registration failed.734 */735 int snd_card_register(struct snd_card *card)736 {737 int err;738 739 if (snd_BUG_ON(!card))740 return -EINVAL;741    //创建sysfs设备,声卡的class将会出现在/sys/class/sound下面742 if (!card->registered) {743 err = device_add(&card->card_dev);744 if (err < 0)745 return err;746 card->registered = true;747 }748 //遍历挂在该声卡的所有逻辑设备,回调各snd_device的ops->dev_register()完成各逻辑设备的注册749 if ((err = snd_device_register_all(card)) < 0)750 return err;751 mutex_lock(&snd_card_mutex);752 if (snd_cards[card->number]) {753 /* already registered */754 mutex_unlock(&snd_card_mutex);755 return snd_info_card_register(card); /* register pending info */756 }757 if (*card->id) {758 /* make a unique id name from the given string */759 char tmpid[sizeof(card->id)];760 memcpy(tmpid, card->id, sizeof(card->id));761 snd_card_set_id_no_lock(card, tmpid, tmpid);762 } else {763 /* create an id from either shortname or longname */764 const char *src;765 src = *card->shortname ? card->shortname : card->longname;766 snd_card_set_id_no_lock(card, src,767 retrieve_id_from_card_name(src)); 768 }769 snd_cards[card->number] = card;//把该声卡实例保存到snd_cards数组中770 mutex_unlock(&snd_card_mutex);//声卡相关信息,见:/proc/asound/card0771 init_info_for_card(card);772 #if IS_ENABLED(CONFIG_SND_MIXER_OSS)773 if (snd_mixer_oss_notify_callback)774 snd_mixer_oss_notify_callback(card, SND_MIXER_OSS_NOTIFY_REGISTER);775 #endif776 return 0;777 }

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

    8.6PCM设备的创建

     

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

    snd_pcm_new:

    • 创建一个PCM设备实例snd_pcm;
    • 创建playback stream和capture stream,旗下的substream也同时建立;
    • 调用snd_device_new()把pcm设备挂到声卡的devices链表上。
    /**800 * snd_pcm_new - create a new PCM instance801 * @card: the card instance802 * @id: the id string803 * @device: the device index (zero based)804 * @playback_count: the number of substreams for playback805 * @capture_count: the number of substreams for capture806 * @rpcm: the pointer to store the new pcm instance807 *808 * Creates a new PCM instance.809 *810 * The pcm operators have to be set afterwards to the new instance811 * via snd_pcm_set_ops().812 *813 * Return: Zero if successful, or a negative error code on failure.814 */815 int snd_pcm_new(struct snd_card *card, const char *id, int device,816 int playback_count, int capture_count, struct snd_pcm **rpcm)817 {818 return _snd_pcm_new(card, id, device, playback_count, capture_count,819 false, rpcm);820 } 755 static int _snd_pcm_new(struct snd_card *card, const char *id, int device,756 int playback_count, int capture_count, bool internal,757 struct snd_pcm **rpcm)758 {759 struct snd_pcm *pcm;760 int err;761 static struct snd_device_ops ops = {762 .dev_free = snd_pcm_dev_free,763 .dev_register = snd_pcm_dev_register,764 .dev_disconnect = snd_pcm_dev_disconnect,765 };766 767 if (snd_BUG_ON(!card))768 return -ENXIO;769 if (rpcm)770 *rpcm = NULL;771 pcm = kzalloc(sizeof(*pcm), GFP_KERNEL);772 if (!pcm)773 return -ENOMEM;774 pcm->card = card;775 pcm->device = device;776 pcm->internal = internal;777 mutex_init(&pcm->open_mutex);778 init_waitqueue_head(&pcm->open_wait);779 INIT_LIST_HEAD(&pcm->list);780 if (id)781 strlcpy(pcm->id, id, sizeof(pcm->id));782 if ((err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_PLAYBACK, playback_count)) < 0) {783 snd_pcm_free(pcm);784 return err;785 }786 if ((err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_CAPTURE, capture_count)) < 0) {787 snd_pcm_free(pcm);788 return err;789 }790 if ((err = snd_device_new(card, SNDRV_DEV_PCM, pcm, &ops)) < 0) {791 snd_pcm_free(pcm);792 return err;793 }794 if (rpcm)795 *rpcm = pcm;796 return 0; 797 }

    在看看pcm设备的系统调用:

    const struct file_operations snd_pcm_f_ops[2] = {3681 {3682 .owner = THIS_MODULE,3683 .write = snd_pcm_write,3684 .write_iter = snd_pcm_writev,3685 .open = snd_pcm_playback_open,3686 .release = snd_pcm_release,3687 .llseek = no_llseek,3688 .poll = snd_pcm_playback_poll,3689 .unlocked_ioctl = snd_pcm_playback_ioctl,3690 .compat_ioctl = snd_pcm_ioctl_compat,3691 .mmap = snd_pcm_mmap,3692 .fasync = snd_pcm_fasync,3693 .get_unmapped_area = snd_pcm_get_unmapped_area,3694 },3695 {3696 .owner = THIS_MODULE,3697 .read = snd_pcm_read,3698 .read_iter = snd_pcm_readv,3699 .open = snd_pcm_capture_open,3700 .release = snd_pcm_release,3701 .llseek = no_llseek,3702 .poll = snd_pcm_capture_poll,3703 .unlocked_ioctl = snd_pcm_capture_ioctl,3704 .compat_ioctl = snd_pcm_ioctl_compat,3705 .mmap = snd_pcm_mmap,3706 .fasync = snd_pcm_fasync,3707 .get_unmapped_area = snd_pcm_get_unmapped_area,3708 }3709 };

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

    9.Frame && Period

    音频数据中的几个重要概念:

    • Sample:样本长度,音频数据最基本的单位,常见的有8bit和16bit;
    • 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之间的关系:

    这个buffer中有4个period,每当DMA搬运完一个period的数据就会出生一次中断,因此搬运这个buffer中的数据将产生4次中断。

    ALSA为什么这样做?因为数据缓冲区可能很大,一次传输可能会导致不可接收的延迟;为了解决这个问题,alsa把缓冲区拆分成多个周期,以周期为单元传输数据。

    9.1frames&periods

    alsa官网对periods的解释:https://www.alsa-project.org/main/index.php/FramesPeriods

    • Frame:帧,构成一个完整的声音单元,它的大小等于sample_bits * channels;
    • Period:周期大小,即每次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)

     

     

    总结

    以上是生活随笔为你收集整理的Linux ALSA音频系统:soundcard的全部内容,希望文章能够帮你解决所遇到的问题。

    如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。