Linux 平台 PulseAudio 音频播放数据通路 I

Linux 内核中,音频子系统由 ALSA 框架实现,用户空间应用程序通过 ALSA 框架向 devtmpfs 虚拟文件系统,即 /dev/snd 目录下导出的一组紧密相关的设备文件,如 controlC0pcmC0D0cpcmC0D0p 等与 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 包安装的文件可以看出来):

  1. pulseaudio 音频服务可执行程序,位于 /usr/bin/pulseaudio
  2. pulseaudio 音频服务配置文件,位于 /etc/pulse/daemon.conf;另外,各个用户可以在 ~/.config/pulse/daemon.conf 定义自己的配置文件,且分别位于 ~/.config/pulse/daemon.conf.d//etc/pulse/daemon.conf.d/ 目录下的配置文件也会被用到。PulseAudio 音频服务在启动时会读取这些配置文件中的配置指令,通过这些配置文件可以配置默认使用的采样率、通道数和采样格式等参数。更多详细信息可以通过 man pulse-daemon.conf 查看。
  3. 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 查看。
  4. pulseaudio 核心库,位于 /usr/lib/x86_64-linux-gnu/pulseaudio/libpulsecore-13.99.so
  5. 插件模块,位于 /usr/lib/pulse-13.99.1/modules/ 目录下,包括 module-alsa-card.somodule-alsa-sink.somodule-alsa-source.somodule-echo-cancel.somodule-http-protocol-unix.somodule-udev-detect.so 等众多用于实现不同功能的插件模块。
  6. udev 规则文件,位于 /lib/udev/rules.d/90-pulseaudio.rules
  7. alsa-mixer 配置文件,位于 /usr/share/pulseaudio/alsa-mixer/ 目录下。
  8. PulseAudio 音频服务 systemd 配置文件,为 /usr/lib/systemd/user/pulseaudio.service/usr/lib/systemd/user/pulseaudio.socket
  9. ALSA 配置文件,为 /usr/share/alsa/alsa.conf.d/pulse.conf/usr/share/alsa/pulse-alsa.conf/etc/alsa/conf.d/99-pulse.conf
  10. bash-completion 脚本,位于 /usr/share/bash-completion/completions/ 目录下。
  11. 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.somodule-alsa-source.somodule-alsa-card.so 等模块,而是加载 module-udev-detect.so 模块,并由它根据需要创建 alsa-sink 和 alsa-source。

这些模块其关系大概如下图:
请添加图片描述

音频设备状态变化探测

音频设备状态变化探测在 module-udev-detect.so 模块中,通过 udev 和 inotify 机制实现。音频设备状态变化探测可以从两个过程来看,一是在模块加载时,创建 udevudev_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() 函数执行过程大体如下:

  1. 创建 module-udev-detect.so 模块的私有数据结构对象,并解析传进来的模块参数,这包括 tschedtsched_buffer_sizefixed_latency_rangeignore_dBdeferred_volumeuse_ucmavoid_resamplingmodule-udev-detect.so 模块本身并不需要这些参数,这些参数基本上会在加载 module-alsa-card.so 模块时被透传出去。module-udev-detect.so 模块的私有数据结构维护一个以声卡设备的路径为健的声卡设备哈希表。
  2. 创建 udev 对象。
  3. 创建、配置 inotify 对象,并启动监听。inotify 对象在用户空间用文件描述符表示,它被配置为监听 /dev/snd 目录。inotify 的文件描述符被添加进 IO 事件监听文件描述符集合中,事件处理回调为 inotify_cb() 函数。
  4. 创建 udev_monitor 对象,配置监听 sound 类型的设备,并启用监听。获得 udev_monitor 对象的文件描述符,并将其加入 IO 事件监听文件描述符集合中,事件处理回调为 monitor_cb() 函数。
  5. 通过 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 does
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值