本文主要是介绍Linux 平台 PulseAudio 音频播放数据通路 I,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
Linux 内核中,音频子系统由 ALSA 框架实现,用户空间应用程序通过 ALSA 框架向 devtmpfs 虚拟文件系统,即 /dev/snd 目录下导出的一组紧密相关的设备文件,如 controlC0、pcmC0D0c 和 pcmC0D0p 等与 Linux 内核音频子系统交互,包括传递数据,及对硬件设备层的音频数据通路进行控制。
ALSA 项目,在用户空间提供了 alsa-lib 库,以方便应用程序访问音频设备。alsa-lib 提供了一组音频设备领域的标准 API,其实现中通过 open()
、read()
、write()
、ioctl()
、mmap()
和 poll()
等标准文件操作访问音频设备文件,只是执行这些文件操作时传递的参数,依循 ALSA 标准的定义,包括命令码和各个参数的数据结构等。
在 Linux 平台,播放及录制音频数据,最终都需要通过 Linux 内核的 ALSA 接口实现。一般的 Linux 操作系统发行版,用户空间库用 alsa-lib,但如 Android 等则用 alsa-lib 的精简版实现 tinyalsa。PulseAudio 作为常用于 PC 平台 Linux 发行版的音频服务系统,通过 alsa-lib 访问 Linux 内核音频子系统。
PulseAudio 服务在 Ubuntu Linux 系统中是一个用户服务,通常在登入非 root 用户时启动,它由几个部分组成(通过 pulseaudio 音频服务 apt 包安装的文件可以看出来):
- pulseaudio 音频服务可执行程序,位于 /usr/bin/pulseaudio。
- pulseaudio 音频服务配置文件,位于 /etc/pulse/daemon.conf;另外,各个用户可以在 ~/.config/pulse/daemon.conf 定义自己的配置文件,且分别位于 ~/.config/pulse/daemon.conf.d/ 和 /etc/pulse/daemon.conf.d/ 目录下的配置文件也会被用到。PulseAudio 音频服务在启动时会读取这些配置文件中的配置指令,通过这些配置文件可以配置默认使用的采样率、通道数和采样格式等参数。更多详细信息可以通过
man pulse-daemon.conf
查看。 - PulseAudio 音频服务启动配置脚本,为 /etc/pulse/default.pa 和 /etc/pulse/system.pa。PulseAudio 音频服务在启动时解释一个配置脚本,这些配置脚本主要用于定义要加载的模块。各个用户可以在 ~/.config/pulse/default.pa 定义自己的配置脚本。PulseAudio 音频服务从这些配置脚本中选择一个来用:当 PulseAudio 音频服务以用户模式运行,且 ~/.config/pulse/default.pa 文件存在时,则使用这个文件;当 PulseAudio 音频服务以用户模式运行,且 ~/.config/pulse/default.pa 文件不存在时,使用 /etc/pulse/default.pa;当 PulseAudio 音频服务以系统服务运行时,使用 /etc/pulse/system.pa。更多详细信息可以通过
man default.pa
查看。 - pulseaudio 核心库,位于 /usr/lib/x86_64-linux-gnu/pulseaudio/libpulsecore-13.99.so。
- 插件模块,位于 /usr/lib/pulse-13.99.1/modules/ 目录下,包括 module-alsa-card.so、module-alsa-sink.so、module-alsa-source.so、module-echo-cancel.so、module-http-protocol-unix.so 和 module-udev-detect.so 等众多用于实现不同功能的插件模块。
- udev 规则文件,位于 /lib/udev/rules.d/90-pulseaudio.rules。
- alsa-mixer 配置文件,位于 /usr/share/pulseaudio/alsa-mixer/ 目录下。
- PulseAudio 音频服务 systemd 配置文件,为 /usr/lib/systemd/user/pulseaudio.service 和 /usr/lib/systemd/user/pulseaudio.socket。
- ALSA 配置文件,为 /usr/share/alsa/alsa.conf.d/pulse.conf、/usr/share/alsa/pulse-alsa.conf 和 /etc/alsa/conf.d/99-pulse.conf。
- bash-completion 脚本,位于 /usr/share/bash-completion/completions/ 目录下。
- D-Bus 配置文件,为 /etc/dbus-1/system.d/pulseaudio-system.conf,及其它若干配置文件。
以用户模式启动的 PulseAudio 音频服务,在启动时解释配置脚本,默认为 /etc/pulse/default.pa,来加载模块。与 ALSA 相关的模块主要有如下这些:
- module-alsa-sink.so:用来为 PulseAudio 音频服务创建并添加一个 alsa-sink。
- module-alsa-source.so:用来为 PulseAudio 音频服务创建并添加一个 alsa-source。
- module-alsa-card.so:用来管理 ALSA 声卡,会根据需要配置 alsa-mixer、创建并添加 alsa-sink,及创建并添加 alsa-source 等。
- module-udev-detect.so:通过 udev 和 inotify 机制监控系统中音频设备的状态变化,如音频设备的插入和拔出等,并根据具体情况加载或卸载 module-alsa-card.so 等模块。
比较新版本的 PulseAudio 音频服务,其 /etc/pulse/default.pa 配置脚本,一般不会直接加载 module-alsa-sink.so、module-alsa-source.so 和 module-alsa-card.so 等模块,而是加载 module-udev-detect.so 模块,并由它根据需要创建 alsa-sink 和 alsa-source。
这些模块其关系大概如下图:
音频设备状态变化探测
音频设备状态变化探测在 module-udev-detect.so 模块中,通过 udev 和 inotify 机制实现。音频设备状态变化探测可以从两个过程来看,一是在模块加载时,创建 udev
及 udev_monitor
对象,和 inotify 对象,设置监听回调,并启动监听;二是在监听到有事件发生时,处理事件,特别是在发现有设备插入时加载 module-alsa-card.so 模块。
module-udev-detect.so 模块加载时,执行模块初始化函数 (位于 pulseaudio/src/modules/module-udev-detect.c) pa__init()
:
static int setup_inotify(struct userdata *u) {int r;if (u->inotify_fd >= 0)return 0;if ((u->inotify_fd = inotify_init1(IN_CLOEXEC|IN_NONBLOCK)) < 0) {pa_log("inotify_init1() failed: %s", pa_cstrerror(errno));return -1;}r = inotify_add_watch(u->inotify_fd, "/dev/snd", IN_ATTRIB|IN_CLOSE_WRITE|IN_DELETE_SELF|IN_MOVE_SELF);if (r < 0) {int saved_errno = errno;pa_close(u->inotify_fd);u->inotify_fd = -1;if (saved_errno == ENOENT) {pa_log_debug("/dev/snd/ is apparently not existing yet, retrying to create inotify watch later.");return 0;}if (saved_errno == ENOSPC) {pa_log("You apparently ran out of inotify watches, probably because Tracker/Beagle took them all away. ""I wished people would do their homework first and fix inotify before using it for watching whole ""directory trees which is something the current inotify is certainly not useful for. ""Please make sure to drop the Tracker/Beagle guys a line complaining about their broken use of inotify.");return 0;}pa_log("inotify_add_watch() failed: %s", pa_cstrerror(saved_errno));return -1;}pa_assert_se(u->inotify_io = u->core->mainloop->io_new(u->core->mainloop, u->inotify_fd, PA_IO_EVENT_INPUT, inotify_cb, u));return 0;
}int pa__init(pa_module *m) {struct userdata *u = NULL;pa_modargs *ma;struct udev_enumerate *enumerate = NULL;struct udev_list_entry *item = NULL, *first = NULL;int fd;bool use_tsched = true, fixed_latency_range = false, ignore_dB = false, deferred_volume = m->core->deferred_volume;bool use_ucm = true;bool avoid_resampling;pa_assert(m);if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {pa_log("Failed to parse module arguments");goto fail;}m->userdata = u = pa_xnew0(struct userdata, 1);u->core = m->core;u->devices = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) device_free);u->inotify_fd = -1;if (pa_modargs_get_value_boolean(ma, "tsched", &use_tsched) < 0) {pa_log("Failed to parse tsched= argument.");goto fail;}u->use_tsched = use_tsched;if (pa_modargs_get_value(ma, "tsched_buffer_size", NULL)) {if (pa_modargs_get_value_u32(ma, "tsched_buffer_size", &u->tsched_buffer_size) < 0) {pa_log("Failed to parse tsched_buffer_size= argument.");goto fail;}u->tsched_buffer_size_valid = true;}if (pa_modargs_get_value_boolean(ma, "fixed_latency_range", &fixed_latency_range) < 0) {pa_log("Failed to parse fixed_latency_range= argument.");goto fail;}u->fixed_latency_range = fixed_latency_range;if (pa_modargs_get_value_boolean(ma, "ignore_dB", &ignore_dB) < 0) {pa_log("Failed to parse ignore_dB= argument.");goto fail;}u->ignore_dB = ignore_dB;if (pa_modargs_get_value_boolean(ma, "deferred_volume", &deferred_volume) < 0) {pa_log("Failed to parse deferred_volume= argument.");goto fail;}u->deferred_volume = deferred_volume;if (pa_modargs_get_value_boolean(ma, "use_ucm", &use_ucm) < 0) {pa_log("Failed to parse use_ucm= argument.");goto fail;}u->use_ucm = use_ucm;avoid_resampling = m->core->avoid_resampling;if (pa_modargs_get_value_boolean(ma, "avoid_resampling", &avoid_resampling) < 0) {pa_log("Failed to parse avoid_resampling= argument.");goto fail;}u->avoid_resampling = avoid_resampling;if (!(u->udev = udev_new())) {pa_log("Failed to initialize udev library.");goto fail;}if (setup_inotify(u) < 0)goto fail;if (!(u->monitor = udev_monitor_new_from_netlink(u->udev, "udev"))) {pa_log("Failed to initialize monitor.");goto fail;}if (udev_monitor_filter_add_match_subsystem_devtype(u->monitor, "sound", NULL) < 0) {pa_log("Failed to subscribe to sound devices.");goto fail;}errno = 0;if (udev_monitor_enable_receiving(u->monitor) < 0) {pa_log("Failed to enable monitor: %s", pa_cstrerror(errno));if (errno == EPERM)pa_log_info("Most likely your kernel is simply too old and ""allows only privileged processes to listen to device events. ""Please upgrade your kernel to at least 2.6.30.");goto fail;}if ((fd = udev_monitor_get_fd(u->monitor)) < 0) {pa_log("Failed to get udev monitor fd.");goto fail;}pa_assert_se(u->udev_io = u->core->mainloop->io_new(u->core->mainloop, fd, PA_IO_EVENT_INPUT, monitor_cb, u));if (!(enumerate = udev_enumerate_new(u->udev))) {pa_log("Failed to initialize udev enumerator.");goto fail;}if (udev_enumerate_add_match_subsystem(enumerate, "sound") < 0) {pa_log("Failed to match to subsystem.");goto fail;}if (udev_enumerate_scan_devices(enumerate) < 0) {pa_log("Failed to scan for devices.");goto fail;}first = udev_enumerate_get_list_entry(enumerate);udev_list_entry_foreach(item, first)process_path(u, udev_list_entry_get_name(item));udev_enumerate_unref(enumerate);pa_log_info("Found %u cards.", pa_hashmap_size(u->devices));pa_modargs_free(ma);return 0;fail:if (enumerate)udev_enumerate_unref(enumerate);if (ma)pa_modargs_free(ma);pa__done(m);return -1;
}
module-udev-detect.so 模块的 pa__init()
函数执行过程大体如下:
- 创建 module-udev-detect.so 模块的私有数据结构对象,并解析传进来的模块参数,这包括
tsched
、tsched_buffer_size
、fixed_latency_range
、ignore_dB
、deferred_volume
、use_ucm
和avoid_resampling
。module-udev-detect.so 模块本身并不需要这些参数,这些参数基本上会在加载 module-alsa-card.so 模块时被透传出去。module-udev-detect.so 模块的私有数据结构维护一个以声卡设备的路径为健的声卡设备哈希表。 - 创建
udev
对象。 - 创建、配置 inotify 对象,并启动监听。inotify 对象在用户空间用文件描述符表示,它被配置为监听 /dev/snd 目录。inotify 的文件描述符被添加进 IO 事件监听文件描述符集合中,事件处理回调为
inotify_cb()
函数。 - 创建
udev_monitor
对象,配置监听 sound 类型的设备,并启用监听。获得udev_monitor
对象的文件描述符,并将其加入 IO 事件监听文件描述符集合中,事件处理回调为monitor_cb()
函数。 - 通过 udev 的接口扫描、遍历并处理已有的 sound 类型设备路径。通过 udev 的接口获得的所有音频设备路径可能像下面这样,既有文件,也有目录:
/sys/devices/pci0000:00/0000:00:05.0/sound/card0
/sys/devices/pci0000:00/0000:00:05.0/sound/card0/pcmC0D0c
/sys/devices/pci0000:00/0000:00:05.0/sound/card0/pcmC0D0p
/sys/devices/pci0000:00/0000:00:05.0/sound/card0/pcmC0D1c
/sys/devices/pci0000:00/0000:00:05.0/sound/card0/controlC0
/sys/devices/virtual/sound/seq
/sys/devices/virtual/sound/timer
音频设备路径由 process_path()
函数处理。process_path()
函数定义如下:
static const char *path_get_card_id(const char *path) {const char *e;if (!path)return NULL;if (!(e = strrchr(path, '/')))return NULL;if (!pa_startswith(e, "/card"))return NULL;return e + 5;
}. . . . . .
static void process_path(struct userdata *u, const char *path) {struct udev_device *dev;if (!path_get_card_id(path))return;if (!(dev = udev_device_new_from_syspath(u->udev, path))) {pa_log("Failed to get udev device object from udev.");return;}process_device(u, dev);udev_device_unref(dev);
}
process_path()
函数首先会尝试从路径中提取声卡 ID,只有成功时,才会实际做进一步处理。udev 只会处理 /sys/devices/pci0000:00/0000:00:05.0/sound/card0 类型的路径。process_path()
函数从音频设备路径创建 udev_device
对象,并通过 process_device()
函数做进一步的处理。
inotiy 的事件处理回调函数 inotify_cb()
,其定义为:
static bool pcm_node_belongs_to_device(struct device *d,const char *node) {char *cd;bool b;cd = pa_sprintf_malloc("pcmC%sD", path_get_card_id(d->path));b = pa_startswith(node, cd);pa_xfree(cd);return b;
}static bool control_node_belongs_to_device(struct device *d,const char *node) {char *cd;bool b;cd = pa_sprintf_malloc("controlC%s", path_get_card_id(d->path));b = pa_streq(node, cd);pa_xfree(cd);return b;
}static void inotify_cb(pa_mainloop_api*a,pa_io_event* e,int fd,pa_io_event_flags_t events,void *userdata) {struct {struct inotify_event e;char name[NAME_MAX];} buf;struct userdata *u = userdata;static int type = 0;bool deleted = false;struct device *d;void *state;for (;;) {ssize_t r;struct inotify_event *event;pa_zero(buf);if ((r = pa_read(fd, &buf, sizeof(buf), &type)) <= 0) {if (r < 0 && errno == EAGAIN)break;pa_log("read() from inotify failed: %s", r < 0 ? pa_cstrerror(errno) : "EOF");goto fail;}event = &buf.e;while (r > 0) {size_t len;if ((size_t) r < sizeof(struct inotify_event)) {pa_log("read() too short.");goto fail;}len = sizeof(struct inotify_event) + event->len;if ((size_t) r < len) {pa_log("Payload missing.");goto fail;}/* From udev we get the guarantee that the control* device's ACL is changed last. To avoid races when ACLs* are changed we hence watch only the control device */if (((event->mask & IN_ATTRIB) && pa_startswith(event->name, "controlC")))PA_HASHMAP_FOREACH(d, u->devices, state)if (control_node_belongs_to_device(d, event->name))d->need_verify = true;/* ALSA doesn't really give us any guarantee on the closing* order, so let's simply hope */if (((event->mask & IN_CLOSE_WRITE) && pa_startswith(event->name, "pcmC")))PA_HASHMAP_FOREACH(d, u->devices, state)if (pcm_node_belongs_to_device(d, event->name))d->need_verify = true;/* /dev/snd/ might have been removed */if ((event->mask & (IN_DELETE_SELF|IN_MOVE_SELF)))deleted = true;event = (struct inotify_event*) ((uint8_t*) event + len);r -= len;}}PA_HASHMAP_FOREACH(d, u->devices, state)if (d->need_verify) {d->need_verify = false;verify_access(u, d);}if (!deleted)return;fail:if (u->inotify_io) {a->io_free(u->inotify_io);u->inotify_io = NULL;}if (u->inotify_fd >= 0) {pa_close(u->inotify_fd);u->inotify_fd = -1;}
}
inotify 事件中包含具体发生状态变化的设备文件的路径,module-udev-detect.so 模块关注 /dev/snd 目录下 control 和 pcm 文件的状态变化。module-udev-detect.so 模块发现 control 设备文件的属性发生变化,或 pcm 设备文件被关闭时,则查找该设备文件所属的已经创建的声卡设备,并为声卡设备执行 verify_access()
。此外,module-udev-detect.so 模块还通过 inotify 机制关注 /dev/snd 目录是否被移除,当这个目录被移除时,释放 inotify 监听相关资源。
udev 的事件处理回调函数 monitor_cb()
,其定义为:
static void card_changed(struct userdata *u, struct udev_device *dev) {struct device *d;const char *path;const char *t;char *n;pa_strbuf *args_buf;pa_assert(u);pa_assert(dev);/* Maybe /dev/snd is now available? */setup_inotify(u);path = udev_device_get_devpath(dev);if ((d = pa_hashmap_get(u->devices, path))) {verify_access(u, d);return;}d = pa_xnew0(struct device, 1);d->path = pa_xstrdup(path);d->module = PA_INVALID_INDEX;PA_INIT_RATELIMIT(d->ratelimit, 10*PA_USEC_PER_SEC, 5);if (!(t = udev_device_get_property_value(dev, "PULSE_NAME")))if (!(t = udev_device_get_property_value(dev, "ID_ID")))if (!(t = udev_device_get_property_value(dev, "ID_PATH")))t = path_get_card_id(path);n = pa_namereg_make_valid_name(t);d->card_name = pa_sprintf_malloc("alsa_card.%s", n);args_buf = pa_strbuf_new();pa_strbuf_printf(args_buf,"device_id=\"%s\" ""name=\"%s\" ""card_name=\"%s\" ""namereg_fail=false ""tsched=%s ""fixed_latency_range=%s ""ignore_dB=%s ""deferred_volume=%s ""use_ucm=%s ""avoid_resampling=%s ""card_properties=\"module-udev-detect.discovered=1\"",path_get_card_id(path),n,d->card_name,pa_yes_no(u->use_tsched),pa_yes_no(u->fixed_latency_range),pa_yes_no(u->ignore_dB),pa_yes_no(u->deferred_volume),pa_yes_no(u->use_ucm),pa_yes_no(u->avoid_resampling));pa_xfree(n);if (u->tsched_buffer_size_valid)pa_strbuf_printf(args_buf, " tsched_buffer_size=%" PRIu32, u->tsched_buffer_size);d->args = pa_strbuf_to_string_free(args_buf);pa_hashmap_put(u->devices, d->path, d);verify_access(u, d);
}static void remove_card(struct userdata *u, struct udev_device *dev) {struct device *d;pa_assert(u);pa_assert(dev);if (!(d = pa_hashmap_remove(u->devices, udev_device_get_devpath(dev))))return;pa_log_info("Card %s removed.", d->path);if (d->module != PA_INVALID_INDEX)pa_module_unload_request_by_index(u->core, d->module, true);device_free(d);
}static void process_device(struct userdata *u, struct udev_device *dev) {const char *action, *ff;pa_assert(u);pa_assert(dev);if (udev_device_get_property_value(dev, "PULSE_IGNORE")) {pa_log_debug("Ignoring %s, because marked so.", udev_device_get_devpath(dev));return;}if ((ff = udev_device_get_property_value(dev, "SOUND_CLASS")) &&pa_streq(ff, "modem")) {pa_log_debug("Ignoring %s, because it is a modem.", udev_device_get_devpath(dev));return;}action = udev_device_get_action(dev);if (action && pa_streq(action, "remove"))remove_card(u, dev);else if ((!action || pa_streq(action, "change")) && udev_device_get_property_value(dev, "SOUND_INITIALIZED"))card_changed(u, dev);/* For an explanation why we don't look for 'add' events here* have a look into /lib/udev/rules.d/78-sound-card.rules! */
}. . . . . .
static void monitor_cb(pa_mainloop_api*a,pa_io_event* e,int fd,pa_io_event_flags_t events,void *userdata) {struct userdata *u = userdata;struct udev_device *dev;pa_assert(a);if (!(dev = udev_monitor_receive_device(u->monitor))) {pa_log("Failed to get udev device object from monitor.");goto fail;}if (!path_get_card_id(udev_device_get_devpath(dev))) {udev_device_unref(dev);return;}process_device(u, dev);udev_device_unref(dev);return;fail:a->io_free(u->udev_io);u->udev_io = NULL;
}
monitor_cb()
函数可以从 udev_monitor
对象获得状态发生改变的设备路径的 udev_device
对象,过滤掉非声卡设备路径,并通过 process_device()
函数做进一步的处理,这与在模块加载函数中执行的 process_path()
函数一样。
在 process_path()
函数中,首先过滤掉需要忽略,或类型为 modem 的声卡设备,之后根据设备事件的类型分别处理。如果设备被移除,为设备卸载 module-alsa-card.so 模块。如果是添加设备或设备状态改变,则通过 card_changed()
函数处理设备状态改变。这又分为两种情况,一是设备状态改变,即已经为设备路径添加过声卡设备,此时为该声卡设备执行 verify_access()
;二是添加设备,此时为设备路径创建声卡设备表示,即 struct device
对象,并添加进声卡设备哈希表,为声卡设备构造声卡名称,优先基于 udev_device
对象的多个设备属性进行,都失败时,基于声卡 ID 进行,为 module-alsa-card.so 模块加载构造模块参数,并为该声卡设备执行 verify_access()
。
verify_access()
函数验证声卡设备的可访问性,其定义为:
static char *card_get_sysattr(const char *card_idx, const char *name) {struct udev *udev;struct udev_device *card = NULL;char *t, *r = NULL;const char *v;pa_assert(card_idx);pa_assert(name);if (!(udev = udev_new())) {pa_log_error("Failed to allocate udev context.");goto finish;}t = pa_sprintf_malloc("/sys/class/sound/card%s", card_idx);card = udev_device_new_from_syspath(udev, t);pa_xfree(t);if (!card) {pa_log_error("Failed to get card object.");goto finish;}if ((v = udev_device_get_sysattr_value(card, name)) && *v)r = pa_xstrdup(v);finish:if (card)udev_device_unref(card);if (udev)udev_unref(udev);return r;
}static bool pcm_is_modem(const char *card_idx, const char *pcm) {char *sysfs_path, *pcm_class;bool is_modem;pa_assert(card_idx);pa_assert(pcm);/* Check /sys/class/sound/card.../pcmC...../pcm_class. An HDA* modem can be used simultaneously with generic* playback/record. */sysfs_path = pa_sprintf_malloc("pcmC%sD%s/pcm_class", card_idx, pcm);pcm_class = card_get_sysattr(card_idx, sysfs_path);is_modem = pcm_class && pa_streq(pcm_class, "modem");pa_xfree(pcm_class);pa_xfree(sysfs_path);return is_modem;
}static bool is_card_busy(const char *id) {char *card_path = NULL, *pcm_path = NULL, *sub_status = NULL;DIR *card_dir = NULL, *pcm_dir = NULL;FILE *status_file = NULL;struct dirent *de;bool busy = false;pa_assert(id);/* This simply uses /proc/asound/card.../pcm.../sub.../status to* check whether there is still a process using this audio device. */card_path = pa_sprintf_malloc("/proc/asound/card%s", id);if (!(card_dir = opendir(card_path))) {pa_log_warn("Failed to open %s: %s", card_path, pa_cstrerror(errno));goto fail;}for (;;) {errno = 0;de = readdir(card_dir);if (!de && errno) {pa_log_warn("readdir() failed: %s", pa_cstrerror(errno));goto fail;}if (!de)break;if (!pa_startswith(de->d_name, "pcm"))continue;if (pcm_is_modem(id, de->d_name + 3))continue;pa_xfree(pcm_path);pcm_path = pa_sprintf_malloc("%s/%s", card_path, de->d_name);if (pcm_dir)closedir(pcm_dir);if (!(pcm_dir = opendir(pcm_path))) {pa_log_warn("Failed to open %s: %s", pcm_path, pa_cstrerror(errno));continue;}for (;;) {char line[32];errno = 0;de = readdir(pcm_dir);if (!de && errno) {pa_log_warn("readdir() failed: %s", pa_cstrerror(errno));goto fail;}if (!de)break;if (!pa_startswith(de->d_name, "sub"))continue;pa_xfree(sub_status);sub_status = pa_sprintf_malloc("%s/%s/status", pcm_path, de->d_name);if (status_file)fclose(status_file);if (!(status_file = pa_fopen_cloexec(sub_status, "r"))) {pa_log_warn("Failed to open %s: %s", sub_status, pa_cstrerror(errno));continue;}if (!(fgets(line, sizeof(line)-1, status_file))) {pa_log_warn("Failed to read from %s: %s", sub_status, pa_cstrerror(errno));continue;}if (!pa_streq(line, "closed\n")) {busy = true;break;}}}fail:pa_xfree(card_path);pa_xfree(pcm_path);pa_xfree(sub_status);if (card_dir)closedir(card_dir);if (pcm_dir)closedir(pcm_dir);if (status_file)fclose(status_file);return busy;
}static void verify_access(struct userdata *u, struct device *d) {char *cd;pa_card *card;bool accessible;pa_assert(u);pa_assert(d);if (d->ignore)return;cd = pa_sprintf_malloc("/dev/snd/controlC%s", path_get_card_id(d->path));accessible = access(cd, R_OK|W_OK) >= 0;pa_log_debug("%s is accessible: %s", cd, pa_yes_no(accessible));pa_xfree(cd);if (d->module == PA_INVALID_INDEX) {/* If we are not loaded, try to load */if (accessible) {pa_module *m;bool busy;/* Check if any of the PCM devices that belong to this* card are currently busy. If they are, don't try to load* right now, to make sure the probing phase can* successfully complete. When the current user of the* device closes it we will get another notification via* inotify and can then recheck. */busy = is_card_busy(path_get_card_id(d->path));pa_log_debug("%s is busy: %s", d->path, pa_yes_no(busy));if (!busy) {/* So, why do we rate limit here? It's certainly ugly,* but there seems to be no other way. Problem is* this: if we are unable to configure/probe an audio* device after opening it we will close it again and* the module initialization will fail. This will then* cause an inotify event on the device node which* will be forwarded to us. We then try to reopen the* audio device again, practically entering a busy* loop.** A clean fix would be if we would be able to ignore* our own inotify close events. However, inotify* lacks such functionality. Also, during probing of* the device we cannot really distinguish between* other processes causing EBUSY or ourselves, which* means we have no way to figure out if the probing* during opening was canceled by a "try again"* failure or a "fatal" failure. */if (pa_ratelimit_test(&d->ratelimit, PA_LOG_DEBUG)) {int err;pa_log_debug("Loading module-alsa-card with arguments '%s'", d->args);err = pa_module_load(&m, u->core, "module-alsa-card", d->args);if (m) {d->module = m->index;pa_log_info("Card %s (%s) module loaded.", d->path, d->card_name);} else if (err == -PA_ERR_NOENTITY) {pa_log_info("Card %s (%s) module skipped.", d->path, d->card_name);d->ignore = true;} else {pa_log_info("Card %s (%s) failed to load module.", d->path, d->card_name);}} elsepa_log_warn("Tried to configure %s (%s) more often than %u times in %llus",d->path,d->card_name,d->ratelimit.burst,(long long unsigned) (d->ratelimit.interval / PA_USEC_PER_SEC));}}} else {/* If we are already loaded update suspend status with* accessible boolean */if ((card = pa_namereg_get(u->core, d->card_name, PA_NAMEREG_CARD))) {pa_log_debug("%s all sinks and sources of card %s.", accessible ? "Resuming" : "Suspending", d->card_name);pa_card_suspend(card, !accessible, PA_SUSPEND_SESSION);}}
}
在之前没有给声卡加载过 module-alsa-card.so 模块,声卡的 control 设备文件可访问,且声卡不处于繁忙状态时,为声卡加载 module-alsa-card.so 模块。
判断声卡是否繁忙,通过查看声卡各个 pcm 设备的状态进行。proc 文件系统中,声卡的文件大概有类似如下这些:
$ find /proc/asound/card0/
/proc/asound/card0/
/proc/asound/card0/id
/proc/asound/card0/pcm0c
/proc/asound/card0/pcm0c/info
/proc/asound/card0/pcm0c/sub0
/proc/asound/card0/pcm0c/sub0/info
/proc/asound/card0/pcm0c/sub0/status
/proc/asound/card0/pcm0c/sub0/prealloc
/proc/asound/card0/pcm0c/sub0/hw_params
/proc/asound/card0/pcm0c/sub0/sw_params
/proc/asound/card0/pcm0c/sub0/prealloc_max
/proc/asound/card0/pcm0p
/proc/asound/card0/pcm0p/info
/proc/asound/card0/pcm0p/sub0
/proc/asound/card0/pcm0p/sub0/info
/proc/asound/card0/pcm0p/sub0/status
/proc/asound/card0/pcm0p/sub0/prealloc
/proc/asound/card0/pcm0p/sub0/hw_params
/proc/asound/card0/pcm0p/sub0/sw_params
/proc/asound/card0/pcm0p/sub0/prealloc_max
/proc/asound/card0/pcm1c
/proc/asound/card0/pcm1c/info
/proc/asound/card0/pcm1c/sub0
/proc/asound/card0/pcm1c/sub0/info
/proc/asound/card0/pcm1c/sub0/status
/proc/asound/card0/pcm1c/sub0/prealloc
/proc/asound/card0/pcm1c/sub0/hw_params
/proc/asound/card0/pcm1c/sub0/sw_params
/proc/asound/card0/pcm1c/sub0/prealloc_max
/proc/asound/card0/intel8x0
/proc/asound/card0/codec97#0
/proc/asound/card0/codec97#0/ac97#0-0
/proc/asound/card0/codec97#0/ac97#0-0+regs
module-udev-detect.so 模块通过读取 proc 的 pcm 的文件来判断声卡是否处于繁忙状态。
总结一下,module-udev-detect.so 模块的工作过程大概如下图所示:
inotify 和 udev 探测的事件有什么区别?为什么需要两种机制都用上?相同的事件是否有可能会被重复探测到,即 inotify 和 udev 机制分别探测到并上报一次?
module-udev-detect.so 模块配置 inotify 时,/dev/snd 目录可能还没有创建,则 inotify 可能配置失败,而 udev 机制则不依赖 /dev/snd 目录。
inotify 机制,用来跟踪 udev 已经添加的声卡设备,及其 PCM 设备的状态变化。
管理 ALSA 声卡
PulseAudio 音频服务,一般通过 module-udev-detect.so 模块为各个声卡设备加载 module-alsa-card.so 模块实例,以创建用于管理声卡的各种对象。管理 ALSA 声卡,主要指的是管理声卡的 profile、port 和 jack,及为 profile 创建 alsa-sink 和 slas-source。alsa-sink 和 slas-source 负责与 Linux 音频设备进行数据交换,分别用来向音频设备写入音频数据来播放,及从音频设备读取音频数据来录音。
PulseAudio 音频服务,主要用 pa_card
对象来管理声卡,这个结构定义 (位于 pulseaudio/src/pulsecore/card.h) 如下:
struct pa_card {uint32_t index;pa_core *core;char *name;pa_proplist *proplist;pa_module *module;char *driver;pa_idxset *sinks;pa_idxset *sources;pa_hashmap *profiles;pa_card_profile *active_profile;pa_hashmap *ports;pa_device_port *preferred_input_port;pa_device_port *preferred_output_port;bool save_profile:1;bool profile_is_sticky:1;pa_suspend_cause_t suspend_cause;bool linked;void *userdata;int (*set_profile)(pa_card *c, pa_card_profile *profile);
};
module-udev-detect.so 模块为每个声卡实例,加载一次 module-alsa-card.so 模块。module-alsa-card.so 模块为声卡创建并向 pulseaudio 核心添加 pa_card
对象。module-alsa-card.so 模块定义了模块私有数据结构,如:
struct userdata {pa_core *core;pa_module *module;char *device_id;int alsa_card_index;pa_hashmap *mixers;pa_hashmap *jacks;pa_card *card;pa_modargs *modargs;pa_alsa_profile_set *profile_set;/* ucm stuffs */bool use_ucm;pa_alsa_ucm_config ucm;};
module-alsa-card.so 模块加载时,执行模块初始化函数 (位于 pulseaudio/src/modules/alsa/module-alsa-card.c) pa__init()
,这个函数定义如下:
int pa__init(pa_module *m) {pa_card_new_data data;bool ignore_dB = false;struct userdata *u;pa_reserve_wrapper *reserve = NULL;const char *description;const char *profile_str = NULL;char *fn = NULL;char *udev_args = NULL;bool namereg_fail = false;int err = -PA_MODULE_ERR_UNSPECIFIED, rval;pa_alsa_refcnt_inc();pa_assert(m);m->userdata = u = pa_xnew0(struct userdata, 1);u->core = m->core;u->module = m;u->use_ucm = true;u->ucm.core = m->core;u->mixers = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func,pa_xfree, (pa_free_cb_t) pa_alsa_mixer_free);u->ucm.mixers = u->mixers; /* alias */if (!(u->modargs = pa_modargs_new(m->argument, valid_modargs))) {pa_log("Failed to parse module arguments.");goto fail;}u->device_id = pa_xstrdup(pa_modargs_get_value(u->modargs, "device_id", DEFAULT_DEVICE_ID));if ((u->alsa_card_index = snd_card_get_index(u->device_id)) < 0) {pa_log("Card '%s' doesn't exist: %s", u->device_id, pa_alsa_strerror(u->alsa_card_index));goto fail;}#ifdef HAVE_UDEVudev_args = pa_udev_get_property(u->alsa_card_index, PULSE_MODARGS);
#endifif (udev_args) {bool udev_modargs_success = true;pa_modargs *temp_ma = pa_modargs_new(udev_args, valid_modargs);if (temp_ma) {/* do not try to replace device_id */if (pa_modargs_remove_key(temp_ma, "device_id") == 0) {pa_log_warn("Unexpected 'device_id' module argument override ignored from udev " PULSE_MODARGS "='%s'", udev_args);}/* Implement modargs override by copying original module arguments* over udev entry arguments ignoring duplicates. */if (pa_modargs_merge_missing(temp_ma, u->modargs, valid_modargs) == 0) {/* swap module arguments */pa_modargs *old_ma = u->modargs;u->modargs = temp_ma;temp_ma = old_ma;pa_log_info("Applied module arguments override from udev " PULSE_MODARGS "='%s'", udev_args);} else {pa_log("Failed to apply module arguments override from udev " PULSE_MODARGS "='%s'", udev_args);udev_modargs_success = false;}pa_modargs_free(temp_ma);} else {pa_log("Failed to parse module arguments from udev " PULSE_MODARGS "='%s'", udev_args);udev_modargs_success = false;}pa_xfree(udev_args);if (!udev_modargs_success)goto fail;}if (pa_modargs_get_value_boolean(u->modargs, "ignore_dB", &ignore_dB) < 0) {pa_log("Failed to parse ignore_dB argument.");goto fail;}if (!pa_in_system_mode()) {char *rname;if ((rname = pa_alsa_get_reserve_name(u->device_id))) {reserve = pa_reserve_wrapper_get(m->core, rname);pa_xfree(rname);if (!reserve)goto fail;}}if (pa_modargs_get_value_boolean(u->modargs, "use_ucm", &u->use_ucm) < 0) {pa_log("Failed to parse use_ucm argument.");goto fail;}/* Force ALSA to reread its configuration. This matters if our device* was hot-plugged after ALSA has already read its configuration - see* https://bugs.freedesktop.org/show_bug.cgi?id=54029*/snd_config_update_free_global();rval = u->use_ucm ? pa_alsa_ucm_query_profiles(&u->ucm, u->alsa_card_index) : -1;if (rval == -PA_ALSA_ERR_UCM_LINKED) {err = -PA_MODULE_ERR_SKIP;goto fail;}if (rval == 0) {pa_log_info("Found UCM profiles");u->profile_set = pa_alsa_ucm_add_profile_set(&u->ucm, &u->core->default_channel_map);/* hook start of sink input/source output to enable modifiers *//* A little bit later than module-role-cork */pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SINK_INPUT_PUT], PA_HOOK_LATE+10,(pa_hook_cb_t) sink_input_put_hook_callback, u);pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SOURCE_OUTPUT_PUT], PA_HOOK_LATE+10,(pa_hook_cb_t) source_output_put_hook_callback, u);/* hook end of sink input/source output to disable modifiers *//* A little bit later than module-role-cork */pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SINK_INPUT_UNLINK], PA_HOOK_LATE+10,(pa_hook_cb_t) sink_input_unlink_hook_callback, u);pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_SOURCE_OUTPUT_UNLINK], PA_HOOK_LATE+10,(pa_hook_cb_t) source_output_unlink_hook_callback, u);}else {u->use_ucm = false;
#ifdef HAVE_UDEVfn = pa_udev_get_property(u->alsa_card_index, "PULSE_PROFILE_SET");
#endifif (pa_modargs_get_value(u->modargs, "profile_set", NULL)) {pa_xfree(fn);fn = pa_xstrdup(pa_modargs_get_value(u->modargs, "profile_set", NULL));}u->profile_set = pa_alsa_profile_set_new(fn, &u->core->default_channel_map);pa_xfree(fn);}if (!u->profile_set)goto fail;u->profile_set->ignore_dB = ignore_dB;pa_alsa_profile_set_probe(u->profile_set, u->mixers, u->device_id, &m->core->default_sample_spec, m->core->default_n_fragments, m->core->default_fragment_size_msec);pa_alsa_profile_set_dump(u->profile_set);pa_card_new_data_init(&data);data.driver = __FILE__;data.module = m;pa_alsa_init_proplist_card(m->core, data.proplist, u->alsa_card_index);pa_proplist_sets(data.proplist, PA_PROP_DEVICE_STRING, u->device_id);pa_alsa_init_description(data.proplist, NULL);set_card_name(&data, u->modargs, u->device_id);/* We need to give pa_modargs_get_value_boolean() a pointer to a local* variable instead of using &data.namereg_fail directly, because* data.namereg_fail is a bitfield and taking the address of a bitfield* variable is impossible. */namereg_fail = data.namereg_fail;if (pa_modargs_get_value_boolean(u->modargs, "namereg_fail", &namereg_fail) < 0) {pa_log("Failed to parse namereg_fail argument.");pa_card_new_data_done(&data);goto fail;}data.namereg_fail = namereg_fail;if (reserve)if ((description = pa_proplist_gets(data.proplist, PA_PROP_DEVICE_DESCRIPTION)))pa_reserve_wrapper_set_application_device_name(reserve, description);add_profiles(u, data.profiles, data.ports);if (pa_hashmap_isempty(data.profiles)) {pa_log("Failed to find a working profile.");pa_card_new_data_done(&data);goto fail;}add_disabled_profile(data.profiles);prune_singleton_availability_groups(data.ports);if (pa_modargs_get_proplist(u->modargs, "card_properties", data.proplist, PA_UPDATE_REPLACE) < 0) {pa_log("Invalid properties");pa_card_new_data_done(&data);goto fail;}/* The Intel HDMI LPE driver needs some special handling. When the HDMI* cable is not plugged in, trying to play audio doesn't work. Any written* audio is immediately discarded and an underrun is reported, and that* results in an infinite loop of "fill buffer, handle underrun". To work* around this issue, the suspend_when_unavailable flag is used to stop* playback when the HDMI cable is unplugged. */if (!u->use_ucm &&pa_safe_streq(pa_proplist_gets(data.proplist, "alsa.driver_name"), "snd_hdmi_lpe_audio")) {pa_device_port *port;void *state;PA_HASHMAP_FOREACH(port, data.ports, state) {pa_alsa_port_data *port_data;port_data = PA_DEVICE_PORT_DATA(port);port_data->suspend_when_unavailable = true;}}u->card = pa_card_new(m->core, &data);pa_card_new_data_done(&data);if (!u->card)goto fail;u->card->userdata = u;u->card->set_profile = card_set_profile;pa_module_hook_connect(m, &m->core->hooks[PA_CORE_HOOK_CARD_SUSPEND_CHANGED], PA_HOOK_NORMAL,(pa_hook_cb_t) card_suspend_changed, u);init_jacks(u);pa_card_choose_initial_profile(u->card);/* If the "profile" modarg is given, we have to override whatever the usual* policy chose in pa_card_choose_initial_profile(). */profile_str = pa_modargs_get_value(u->modargs, "profile", NULL);if (profile_str) {pa_card_profile *profile;profile = pa_hashmap_get(u->card->profiles, profile_str);if (!profile) {pa_log("No such profile: %s", profile_str);goto fail;}pa_card_set_profile(u->card, profile, false);}pa_card_put(u->card);init_profile(u);init_eld_ctls(u);/* Remove all probe only mixers */if (u->mixers) {const char *devname;pa_alsa_mixer *pm;void *state;PA_HASHMAP_FOREACH_KV(devname, pm, u->mixers, state)if (pm->used_for_probe_only)pa_hashmap_remove_and_free(u->mixers, devname);}if (reserve)pa_reserve_wrapper_unref(reserve);if (!pa_hashmap_isempty(u->profile_set->decibel_fixes))pa_log_warn("Card %s uses decibel fixes (i.e. overrides the decibel information for some alsa volume elements). ""Please note that this feature is meant just as a help for figuring out the correct decibel values. ""PulseAudio is not the correct place to maintain the decibel mappings! The fixed decibel values ""should be sent to ALSA developers so that they can fix the driver. If it turns out that this feature ""is abused (i.e. fixes are not pushed to ALSA), the decibel fix feature may be removed in some future ""PulseAudio version.", u->card->name);return 0;fail:if (reserve)pa_reserve_wrapper_unref(reserve);pa__done(m);return err;
}int pa__get_n_used(pa_module *m) {struct userdata *u;int n = 0;uint32_t idx;pa_sink *sink;pa_source *source;pa_assert(m);pa_assert_se(u = m->userdata);pa_assert(u->card);PA_IDXSET_FOREACH(sink, u->card->sinks, idx)n += pa_sink_linked_by(sink);PA_IDXSET_FOREACH(source, u->card->sources, idx)n += pa_source_linked_by(source);return n;
}
module-alsa-card.so 模块加载时:
-
解析传进来的模块参数,如 device_id、ignore_dB 和 use_ucm 等。module-alsa-card.so 模块的参数有两个来源,一是加载时 module-udev-detect.so 模块传入的,二是 UDEV 机制可用时,通过 UDEV 获取的设备的参数,这两种参数会合并。
-
加载 profile 集合。module-alsa-card.so 模块首先尝试加载声卡的 ALSA UCM 配置,声卡的 ALSA UCM 配置由
pa_alsa_ucm_config
结构体描述 (定义位于 pulseaudio/src/modules/alsa/alsa-ucm.h):
/* UCM - Use Case Manager is available on some audio cards */struct pa_alsa_ucm_device {PA_LLIST_FIELDS(pa_alsa_ucm_device);pa_proplist *proplist;pa_device_port_type_t type;unsigned playback_priority;unsigned capture_priority;unsigned playback_rate;unsigned capture_rate;unsigned playback_channels;unsigned capture_channels;/* These may be different per verb, so we store this as a hashmap of verb -> volume_control. We might eventually want to* make this a hashmap of verb -> per-verb-device-properties-struct. */pa_hashmap *playback_volumes;pa_hashmap *capture_volumes;pa_alsa_mapping *playback_mapping;pa_alsa_mapping *capture_mapping;pa_idxset *conflicting_devices;pa_idxset *supported_devices;/* One device may be part of multiple ports, since each device has* a dedicated port, and in addition to that we sometimes generate ports* that represent combinations of devices. */pa_dynarray *ucm_ports; /* struct ucm_port */pa_alsa_jack *jack;pa_dynarray *hw_mute_jacks; /* pa_alsa_jack */pa_available_t available;char *eld_mixer_device_name;int eld_device;
};. . . . . .struct pa_alsa_ucm_modifier {PA_LLIST_FIELDS(pa_alsa_ucm_modifier);pa_proplist *proplist;pa_idxset *conflicting_devices;pa_idxset *supported_devices;pa_direction_t action_direction;char *media_role;/* Non-NULL if the modifier has its own PlaybackPCM/CapturePCM */pa_alsa_mapping *playback_mapping;pa_alsa_mapping *capture_mapping;/* Count how many role matched streams are running */int enabled_counter;
};struct pa_alsa_ucm_verb {PA_LLIST_FIELDS(pa_alsa_ucm_verb);pa_proplist *proplist;unsigned priority;PA_LLIST_HEAD(pa_alsa_ucm_device, devices);PA_LLIST_HEAD(pa_alsa_ucm_modifier, modifiers);
};struct pa_alsa_ucm_config {pa_core *core;snd_use_case_mgr_t *ucm_mgr;pa_alsa_ucm_verb *active_verb;char *alib_prefix;pa_hashmap *mixers;PA_LLIST_HEAD(pa_alsa_ucm_verb, verbs);PA_LLIST_HEAD(pa_alsa_jack, jacks);
};
pa_alsa_ucm_config
相关的结构大概如下图:
加载声卡的 ALSA UCM 配置成功,则为其创建 profile 集合,这通过 pa_alsa_ucm_add_profile_set()
函数完成。加载声卡的 ALSA UCM 配置失败,则由 pulseaudio 的 conf 配置创建 profile 集合。加载 module-alsa-card.so 模块时,可以通过 profile_set 参数传入期望使用的 profile 集合配置文件名。profile 集合配置通过 pa_alsa_profile_set_new()
加载,该函数定义 (位于 pulseaudio/src/modules/alsa/alsa-mixer.c) 如下:
static char *get_data_path(const char *data_dir, const char *data_type, const char *fname) {char *result;char *dir;char *data_home;pa_dynarray *data_dirs;if (data_dir) {result = pa_maybe_prefix_path(fname, data_dir);if (access(result, R_OK) == 0)return result;elsepa_xfree(result);}#ifdef HAVE_RUNNING_FROM_BUILD_TREEif (pa_run_from_build_tree()) {dir = pa_sprintf_malloc(PA_SRCDIR "/modules/alsa/mixer/%s/", data_type);result = pa_maybe_prefix_path(fname, dir);pa_xfree(dir);if (access(result, R_OK) == 0)return result;elsepa_xfree(result);}
#endifif (pa_get_data_home_dir(&data_home) == 0) {dir = pa_sprintf_malloc("%s" PA_PATH_SEP "alsa-mixer" PA_PATH_SEP "%s", data_home, data_type);pa_xfree(data_home);result = pa_maybe_prefix_path(fname, dir);pa_xfree(dir);if (access(result, R_OK) == 0)return result;elsepa_xfree(result);}if (pa_get_data_dirs(&data_dirs) == 0) {int idx;const char *n;PA_DYNARRAY_FOREACH(n, data_dirs, idx) {dir = pa_sprintf_malloc("%s" PA_PATH_SEP "alsa-mixer" PA_PATH_SEP "%s", n, data_type);result = pa_maybe_prefix_path(fname, dir);pa_xfree(dir);if (access(result, R_OK) == 0) {pa_dynarray_free(data_dirs);return result;}else {pa_xfree(result);}}pa_dynarray_free(data_dirs);}dir = pa_sprintf_malloc(PA_ALSA_DATA_DIR PA_PATH_SEP "%s", data_type);result = pa_maybe_prefix_path(fname, dir);pa_xfree(dir);return result;
}. . . . . .
pa_alsa_profile_set* pa_alsa_profile_set_new(const char *fname, const pa_channel_map *bonus) {pa_alsa_profile_set *ps;pa_alsa_profile *p;pa_alsa_mapping *m;pa_alsa_decibel_fix *db_fix;char *fn;int r;void *state;static pa_config_item items[] = {/* [General] */{ "auto-profiles", pa_config_parse_bool, NULL, "General" },/* [Mapping ...] */{ "device-strings", mapping_parse_device_strings, NULL, NULL },{ "channel-map", mapping_parse_channel_map, NULL, NULL },{ "paths-input", mapping_parse_paths, NULL, NULL },{ "paths-output", mapping_parse_paths, NULL, NULL },{ "element-input", mapping_parse_element, NULL, NULL },{ "element-output", mapping_parse_element, NULL, NULL },{ "direction", mapping_parse_direction, NULL, NULL },{ "exact-channels", mapping_parse_exact_channels, NULL, NULL },{ "intended-roles", mapping_parse_intended_roles, NULL, NULL },/* Shared by [Mapping ...] and [Profile ...] */{ "description", mapping_parse_description, NULL, NULL },{ "description-key", mapping_parse_description_key,NULL, NULL },{ "priority", mapping_parse_priority, NULL, NULL },{ "fallback", mapping_parse_fallback, NULL, NULL },/* [Profile ...] */{ "input-mappings", profile_parse_mappings, NULL, NULL },{ "output-mappings", profile_parse_mappings, NULL, NULL },{ "skip-probe", profile_parse_skip_probe, NULL, NULL },/* [DecibelFix ...] */{ "db-values", decibel_fix_parse_db_values, NULL, NULL },{ NULL, NULL, NULL, NULL }};ps = pa_xnew0(pa_alsa_profile_set, 1);ps->mappings = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) mapping_free);ps->profiles = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) profile_free);ps->decibel_fixes = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) decibel_fix_free);ps->input_paths = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) pa_alsa_path_free);ps->output_paths = pa_hashmap_new_full(pa_idxset_string_hash_func, pa_idxset_string_compare_func, NULL, (pa_free_cb_t) pa_alsa_path_free);items[0].data = &ps->auto_profiles;if (!fname)fname = "default.conf";fn = get_data_path(NULL, "profile-sets", fname);pa_log_info("Loading profile set: %s", fn);r = pa_config_parse(fn, NULL, items, NULL, false, ps);pa_xfree(fn);if (r < 0)goto fail;PA_HASHMAP_FOREACH(m, ps->mappings, state)if (mapping_verify(m, bonus) < 0)goto fail;if (ps->auto_profiles)profile_set_add_auto(ps);PA_HASHMAP_FOREACH(p, ps->profiles, state)if (profile_verify(p) < 0)goto fail;PA_HASHMAP_FOREACH(db_fix, ps->decibel_fixes, state)if (decibel_fix_verify(db_fix) < 0)goto fail;return ps;fail:pa_alsa_profile_set_free(ps);return NULL;
}
没有通过 module-alsa-card.so 模块的 profile_set 参数传入期望使用的 profile 集合配置文件名时,默认使用 default.conf 作为文件名。配置文件所在的目录则按照一定的优先级依次查找:
一、由 PulseAudio 数据主目录获得:
(1). XDG_DATA_HOME 环境变量存在且为绝对路径,则 PulseAudio 数据主目录路径为 ${XDG_DATA_HOME}/pulseaudio;
(2). 依次尝试从 HOME 和 USERPROFILE 环境变量获得用户主目录路径, PulseAudio 数据主目录路径为 ${user_home}/.local/share/pulseaudio;
PulseAudio 数据主目录获得成功时,配置文件所在的目录为 ${data_home}/alsa-mixer/profile-sets,如 ~/.local/share/pulseaudio/alsa-mixer/profile-sets
二、由系统数据目录获得:
(1). XDG_DATA_DIRS 环境变量存在时,它所包含的目录路径集合作为候选系统数据目录集,否则 “/usr/local/share/:/usr/share/” 作为候选系统数据目录集,系统数据目录集中的绝对路径为系统数据目录;
系统数据目录获得成功时,配置文件所在的目录为 ${data_dir}/alsa-mixer/profile-sets,如 /usr/share/pulseaudio/alsa-mixer/profile-sets。
三、ALSA 数据目录下的 /usr/local/share/pulseaudio/alsa-mixer/profile-sets。
默认的 profile 集合配置文件常为 /usr/share/pulseaudio/alsa-mixer/profile-sets/default.conf。
-
加载 profile 集合之后,调用
pa_alsa_profile_set_probe()
函数执行 profile 集合的探测。探测即检查获得的 profile 对应的各个音频设备是否可访问,设备以pa_alsa_mapping
对象描述,并为各个设备创建以pa_alsa_path_set
描述的路径集合。 -
为声卡创建
pa_card
对象。使用类似于 Builder 模式的方式创建pa_card
对象,首先创建pa_card_new_data
对象,随后基于pa_card_new_data
对象创建pa_card
对象。pa_card_new_data
结构定义 (位于 pulseaudio/src/pulsecore/card.h) 如下:
typedef struct pa_card_new_data {char *name;pa_proplist *proplist;const char *driver;pa_module *module;pa_hashmap *profiles;pa_hashmap *ports;pa_device_port *preferred_input_port;pa_device_port *preferred_output_port;bool namereg_fail:1;
} pa_card_new_data;
创建 pa_card_new_data
对象的过程中,会为其填充声卡相关的各种信息:
(1). ALSA 声卡索引,及从驱动获得的 ALSA 声卡名、ALSA 声卡长名称,ALSA 驱动程序名称等 (通过 pa_alsa_init_proplist_card()
函数)。
(2). 设备字符串 (通过 pa_proplist_sets()
函数)。
(3). 设备描述 (通过 pa_alsa_init_description()
函数)。
(4). 声卡名称 (通过 set_card_name()
函数)。
(5). profiles 和 ports (通过 add_profiles()
函数)。前面通过 UCM 或 profile-set 配置文件获得的以 pa_alsa_profile
描述的 profile 和以 pa_alsa_mapping
描述的设备,被转换为 pa_card_profile
和 pa_device_port
对象。
(6). disabled profile (通过 add_disabled_profile()
函数)。
创建 pa_card
对象通过 pa_card_new()
函数完成。随后 Builder 对象 pa_card_new_data
被释放。
- 为声卡对象
pa_card
设置set_profile
回调为card_set_profile()
函数,这个函数定义如下:
static int card_set_profile(pa_card *c, pa_card_profile *new_profile) {struct userdata *u;struct profile_data *nd, *od;uint32_t idx;pa_alsa_mapping *am;pa_queue *sink_inputs = NULL, *source_outputs = NULL;int ret = 0;pa_assert(c);pa_assert(new_profile);pa_assert_se(u = c->userdata);nd = PA_CARD_PROFILE_DATA(new_profile);od = PA_CARD_PROFILE_DATA(c->active_profile);if (od->profile && od->profile->output_mappings)PA_IDXSET_FOREACH(am, od->profile->output_mappings, idx) {if (!am->sink)continue;if (nd->profile &&nd->profile->output_mappings &&pa_idxset_get_by_data(nd->profile->output_mappings, am, NULL))continue;sink_inputs = pa_sink_move_all_start(am->sink, sink_inputs);pa_alsa_sink_free(am->sink);am->sink = NULL;}if (od->profile && od->profile->input_mappings)PA_IDXSET_FOREACH(am, od->profile->input_mappings, idx) {if (!am->source)continue;if (nd->profile &&nd->profile->input_mappings &&pa_idxset_get_by_data(nd->profile->input_mappings, am, NULL))continue;source_outputs = pa_source_move_all_start(am->source, source_outputs);pa_alsa_source_free(am->source);am->source = NULL;}/* if UCM is available for this card then update the verb */if (u->use_ucm) {if (pa_alsa_ucm_set_profile(&u->ucm, c, nd->profile, od->profile) < 0) {ret = -1;goto finish;}}if (nd->profile && nd->profile->output_mappings)PA_IDXSET_FOREACH(am, nd->profile->output_mappings, idx) {if (!am->sink)am->sink = pa_alsa_sink_new(c->module, u->modargs, __FILE__, c, am);if (sink_inputs && am->sink) {pa_sink_move_all_finish(am->sink, sink_inputs, false);sink_inputs = NULL;}}if (nd->profile && nd->profile->input_mappings)PA_IDXSET_FOREACH(am, nd->profile->input_mappings, idx) {if (!am->source)am->source = pa_alsa_source_new(c->module, u->modargs, __FILE__, c, am);if (source_outputs && am->source) {pa_source_move_all_finish(am->source, source_outputs, false);source_outputs = NULL;}}finish:if (sink_inputs)pa_sink_move_all_fail(sink_inputs);if (source_outputs)pa_source_move_all_fail(source_outputs);return ret;
}
声卡对象 pa_card
的 set_profile
回调在为声卡切换配置时会被调用,如在 设置 里为输出设备切换 配置。card_set_profile()
函数保存老的配置的 sink 的 sink_inputs,并释放老的 sink,保存老的配置的 source 的 source_outputs,并释放老的 source;如果使用了 UCM,则为 UCM 设置 profile;打开新的配置的 alsa sink 和 alsa source,并将保存的 sink_inputs 和 source_outputs 迁移过来。
- 通过
init_jacks()
函数初始化耳机插孔,这个函数定义如下:
static int report_jack_state(snd_mixer_elem_t *melem, unsigned int mask) {struct userdata *u = snd_mixer_elem_get_callback_private(melem);snd_hctl_elem_t **_elem = snd_mixer_elem_get_private(melem), *elem;snd_ctl_elem_value_t *elem_value;bool plugged_in;void *state;pa_alsa_jack *jack;struct temp_port_avail *tp, *tports;pa_card_profile *profile;pa_available_t active_available = PA_AVAILABLE_UNKNOWN;pa_assert(u);pa_assert(_elem);elem = *_elem;/* Changing the jack state may cause a port change, and a port change will* make the sink or source change the mixer settings. If there are multiple* users having pulseaudio running, the mixer changes done by inactive* users may mess up the volume settings for the active users, because when* the inactive users change the mixer settings, those changes are picked* up by the active user's pulseaudio instance and the changes are* interpreted as if the active user changed the settings manually e.g.* with alsamixer. Even single-user systems suffer from this, because gdm* runs its own pulseaudio instance.** We rerun this function when being unsuspended to catch up on jack state* changes */if (u->card->suspend_cause & PA_SUSPEND_SESSION)return 0;if (mask == SND_CTL_EVENT_MASK_REMOVE)return 0;snd_ctl_elem_value_alloca(&elem_value);if (snd_hctl_elem_read(elem, elem_value) < 0) {pa_log_warn("Failed to read jack detection from '%s'", pa_strnull(snd_hctl_elem_get_name(elem)));return 0;}plugged_in = !!snd_ctl_elem_value_get_boolean(elem_value, 0);pa_log_debug("Jack '%s' is now %s", pa_strnull(snd_hctl_elem_get_name(elem)), plugged_in ? "plugged in" : "unplugged");tports = tp = pa_xnew0(struct temp_port_avail, pa_hashmap_size(u->jacks)+1);PA_HASHMAP_FOREACH(jack, u->jacks, state)if (jack->melem == melem) {pa_alsa_jack_set_plugged_in(jack, plugged_in);if (u->use_ucm) {/* When using UCM, pa_alsa_jack_set_plugged_in() maps the jack* state to port availability. */continue;}/* When not using UCM, we have to do the jack state -> port* availability mapping ourselves. */pa_assert_se(tp->port = jack->path->port);tp->avail = calc_port_state(tp->port, u);tp++;}/* Report available ports before unavailable ones: in case port 1 becomes available when port 2 becomes unavailable,this prevents an unnecessary switch port 1 -> port 3 -> port 2 */for (tp = tports; tp->port; tp++)if (tp->avail != PA_AVAILABLE_NO)pa_device_port_set_available(tp->port, tp->avail);for (tp = tports; tp->port; tp++)if (tp->avail == PA_AVAILABLE_NO)pa_device_port_set_available(tp->port, tp->avail);for (tp = tports; tp->port; tp++) {pa_alsa_port_data *data;pa_sink *sink;uint32_t idx;data = PA_DEVICE_PORT_DATA(tp->port);if (!data->suspend_when_unavailable)continue;PA_IDXSET_FOREACH(sink, u->core->sinks, idx) {if (sink->active_port == tp->port)pa_sink_suspend(sink, tp->avail == PA_AVAILABLE_NO, PA_SUSPEND_UNAVAILABLE);}}/* Update profile availabilities. Ideally we would mark all profiles* unavailable that contain unavailable devices. We can't currently do that* in all cases, because if there are multiple sinks in a profile, and the* profile contains a mix of available and unavailable ports, we don't know* how the ports are distributed between the different sinks. It's possible* that some sinks contain only unavailable ports, in which case we should* mark the profile as unavailable, but it's also possible that all sinks* contain at least one available port, in which case we should mark the* profile as available. Until the data structures are improved so that we* can distinguish between these two cases, we mark the problematic cases* as available (well, "unknown" to be precise, but there's little* practical difference).** A profile will be marked unavailable:* only contains output ports and all ports are unavailable* only contains input ports and all ports are unavailable* contains both input and output ports and all ports are unavailable** A profile will be awarded priority bonus:* only contains output ports and at least one port is available* only contains input ports and at least one port is available* contains both output and input ports and at least one output port* and one input port are available** The rest profiles will not be marked unavailable and will not be* awarded priority bonus** If there are no output ports at all, but the profile contains at least* one sink, then the output is considered to be available. */if (u->card->active_profile)active_available = u->card->active_profile->available;PA_HASHMAP_FOREACH(profile, u->card->profiles, state) {pa_device_port *port;void *state2;bool has_input_port = false;bool has_output_port = false;bool found_available_input_port = false;bool found_available_output_port = false;pa_available_t available = PA_AVAILABLE_UNKNOWN;profile->priority &= ~PROFILE_PRIO_BONUS;PA_HASHMAP_FOREACH(port, u->card->ports, state2) {if (!pa_hashmap_get(port->profiles, profile->name))continue;if (port->direction == PA_DIRECTION_INPUT) {has_input_port = true;if (port->available != PA_AVAILABLE_NO)found_available_input_port = true;} else {has_output_port = true;if (port->available != PA_AVAILABLE_NO)found_available_output_port = true;}}if ((has_input_port && found_available_input_port && !has_output_port) ||(has_output_port && found_available_output_port && !has_input_port) ||(has_input_port && found_available_input_port && has_output_port && found_available_output_port))profile->priority |= PROFILE_PRIO_BONUS;if ((has_input_port && !found_available_input_port && has_output_port && !found_available_output_port) ||(has_input_port && !found_available_input_port && !has_output_port) ||(has_output_port && !found_available_output_port && !has_input_port))available = PA_AVAILABLE_NO;/* We want to update the active profile's status last, so logic that* may change the active profile based on profile availability status* has an updated view of all profiles' availabilities. */if (profile == u->card->active_profile)active_available = available;elsepa_card_profile_set_available(profile, available);}if (u->card->active_profile)pa_card_profile_set_available(u->card->active_profile, active_available);pa_xfree(tports);return 0;
}. . . . . .
static void init_jacks(struct userdata *u) {void *state;pa_alsa_path* path;pa_alsa_jack* jack;char buf[64];u->jacks = pa_hashmap_new(pa_idxset_trivial_hash_func, pa_idxset_trivial_compare_func);if (u->use_ucm) {PA_LLIST_FOREACH(jack, u->ucm.jacks)if (jack->has_control)pa_hashmap_put(u->jacks, jack, jack);} else {/* See if we have any jacks */if (u->profile_set->output_paths)PA_HASHMAP_FOREACH(path, u->profile_set->output_paths, state)PA_LLIST_FOREACH(jack, path->jacks)if (jack->has_control)pa_hashmap_put(u->jacks, jack, jack);if (u->profile_set->input_paths)PA_HASHMAP_FOREACH(path, u->profile_set->input_paths, state)PA_LLIST_FOREACH(jack, path->jacks)if (jack->has_control)pa_hashmap_put(u->jacks, jack, jack);}pa_log_debug("Found %d jacks.", pa_hashmap_size(u->jacks));if (pa_hashmap_size(u->jacks) == 0)return;PA_HASHMAP_FOREACH(jack, u->jacks, state) {if (!jack->mixer_device_name) {jack->mixer_handle = pa_alsa_open_mixer(u->mixers, u->alsa_card_index, false);if (!jack->mixer_handle) {pa_log("Failed to open mixer for card %d for jack detection", u->alsa_card_index);continue;}} else {jack->mixer_handle = pa_alsa_open_mixer_by_name(u->mixers, jack->mixer_device_name, false);if (!jack->mixer_handle) {pa_log("Failed to open mixer '%s' for jack detection", jack->mixer_device_name);continue;}}pa_alsa_mixer_set_fdlist(u->mixers, jack->mixer_handle, u->core->mainloop);jack->melem = pa_alsa_mixer_find_card(jack->mixer_handle, &jack->alsa_id, 0);if (!jack->melem) {pa_alsa_mixer_id_to_string(buf, sizeof(buf), &jack->alsa_id);pa_log_warn("Jack %s seems to have disappeared.", buf);pa_alsa_jack_set_has_control(jack, false);continue;}snd_mixer_elem_set_callback(jack->melem, report_jack_state);snd_mixer_elem_set_callback_private(jack->melem, u);report_jack_state(jack->melem, 0);}
}
init_jacks()
函数将通过 UCM 或 profile-set 配置文件获得的 pa_alsa_jack
保存起来,为这些 jacks 打开 mixer 设备,为这些 jacks 注册状态回调函数,并报告各个 jack 的状态。
- 通过
pa_card_choose_initial_profile()
函数为声卡选择合适的 profile。pa_card_choose_initial_profile()
函数定义 (位于 pulseaudio/src/pulsecore/card.c) 如下:
void pa_card_choose_initial_profile(pa_card *card) {pa_card_profile *profile;void *state;pa_card_profile *best = NULL;pa_assert(card);/* By default, pick the highest priority profile that is not unavailable,* or if all profiles are unavailable, pick the profile with the highest* priority regardless of its availability. */pa_log_debug("Looking for initial profile for card %s", card->name);PA_HASHMAP_FOREACH(profile, card->profiles, state) {pa_log_debug("%s availability %s", profile->name, pa_available_to_string(profile->available));if (profile->available == PA_AVAILABLE_NO)continue;if (!best || profile->priority > best->priority)best = profile;}if (!best) {PA_HASHMAP_FOREACH(profile, card->profiles, state) {if (!best || profile->priority > best->priority)best = profile;}}pa_assert(best);card->active_profile = best;card->save_profile = false;card->profile_is_sticky = false;pa_log_info("%s: active_profile: %s", card->name, card->active_profile->name);/* Let policy modules override the default. */pa_hook_fire(&card->core->hooks[PA_CORE_HOOK_CARD_CHOOSE_INITIAL_PROFILE], card);
}
默认情况下,从非 unavailable 的 profile 中选择优先级最高的 profile。如果所有的 profile 都是 unavailable,则选择优先级最高的 profile,而不再关注它的 availability。
-
如果加载 module-alsa-card 模块时,传入了 profile 参数,则尝试为声卡设置 profile 参数指定的 profile。
-
通过
pa_card_put()
函数将创建的pa_card
声卡对象,放进全局的声卡对象集合中。 -
调用
init_profile()
函数初始化 profile,这个函数定义如下:
static void init_profile(struct userdata *u) {uint32_t idx;pa_alsa_mapping *am;struct profile_data *d;pa_alsa_ucm_config *ucm = &u->ucm;pa_assert(u);d = PA_CARD_PROFILE_DATA(u->card->active_profile);if (d->profile && u->use_ucm) {/* Set initial verb */if (pa_alsa_ucm_set_profile(ucm, u->card, d->profile, NULL) < 0) {pa_log("Failed to set ucm profile %s", d->profile->name);return;}}if (d->profile && d->profile->output_mappings)PA_IDXSET_FOREACH(am, d->profile->output_mappings, idx)am->sink = pa_alsa_sink_new(u->module, u->modargs, __FILE__, u->card, am);if (d->profile && d->profile->input_mappings)PA_IDXSET_FOREACH(am, d->profile->input_mappings, idx)am->source = pa_alsa_source_new(u->module, u->modargs, __FILE__, u->card, am);
}
如果使用了 UCM,则首先为 UCM 设置 profile。创建 alsa sink 或 alsa source。
- 调用
init_eld_ctls()
函数初始化 eld 控制,这个函数定义如下:
static int hdmi_eld_changed(snd_mixer_elem_t *melem, unsigned int mask) {struct userdata *u = snd_mixer_elem_get_callback_private(melem);snd_hctl_elem_t **_elem = snd_mixer_elem_get_private(melem), *elem;int device;const char *old_monitor_name;pa_device_port *p;pa_hdmi_eld eld;bool changed = false;pa_assert(u);pa_assert(_elem);elem = *_elem;device = snd_hctl_elem_get_device(elem);if (mask == SND_CTL_EVENT_MASK_REMOVE)return 0;p = find_port_with_eld_device(u, device);if (p == NULL) {pa_log_error("Invalid device changed in ALSA: %d", device);return 0;}if (pa_alsa_get_hdmi_eld(elem, &eld) < 0)memset(&eld, 0, sizeof(eld));old_monitor_name = pa_proplist_gets(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME);if (eld.monitor_name[0] == '\0') {changed |= old_monitor_name != NULL;pa_proplist_unset(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME);} else {changed |= (old_monitor_name == NULL) || (strcmp(old_monitor_name, eld.monitor_name) != 0);pa_proplist_sets(p->proplist, PA_PROP_DEVICE_PRODUCT_NAME, eld.monitor_name);}if (changed && mask != 0)pa_subscription_post(u->core, PA_SUBSCRIPTION_EVENT_CARD|PA_SUBSCRIPTION_EVENT_CHANGE, u->card->index);return 0;
}static void init_eld_ctls(struct userdata *u) {void *state;pa_device_port *port;/* The code in this function expects ports to have a pa_alsa_port_data* struct as their data, but in UCM mode ports don't have any data. Hence,* the ELD controls can't currently be used in UCM mode. */PA_HASHMAP_FOREACH(port, u->card->ports, state) {snd_mixer_t *mixer_handle;snd_mixer_elem_t* melem;int device;if (u->use_ucm) {pa_alsa_ucm_port_data *data = PA_DEVICE_PORT_DATA(port);device = data->eld_device;if (device < 0 || !data->eld_mixer_device_name)continue;mixer_handle = pa_alsa_open_mixer_by_name(u->mixers, data->eld_mixer_device_name, true);} else {pa_alsa_port_data *data = PA_DEVICE_PORT_DATA(port);pa_assert(data->path);device = data->path->eld_device;if (device < 0)continue;mixer_handle = pa_alsa_open_mixer(u->mixers, u->alsa_card_index, true);}if (!mixer_handle)continue;melem = pa_alsa_mixer_find_pcm(mixer_handle, "ELD", device);if (melem) {pa_alsa_mixer_set_fdlist(u->mixers, mixer_handle, u->core->mainloop);snd_mixer_elem_set_callback(melem, hdmi_eld_changed);snd_mixer_elem_set_callback_private(melem, u);hdmi_eld_changed(melem, 0);pa_log_info("ELD device found for port %s (%d).", port->name, device);}elsepa_log_debug("No ELD device found for port %s (%d).", port->name, device);}
}
init_eld_ctls()
函数为各个 ELD 设备打开 mixer 设备,为这些 ELD mixer ctrl 注册状态回调函数,并报告各个 ELD mixer ctrl 的状态。
这篇关于Linux 平台 PulseAudio 音频播放数据通路 I的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!