> 技术文档 > PipeWire 音频设计与实现分析一——介绍

PipeWire 音频设计与实现分析一——介绍

PipeWire 是一个基于图的媒体处理引擎,一个可以运行多媒体节点图的媒体服务器,是 Linux 的音频/视频总线,它管理 Linux 系统中,不同应用程序对音频和视频设备的共享访问。它提供了一个本地客户端音频 API,但也提供兼容层来支持使用常见的音频 API,包括 ALSA、PulseAudio 和 JACK。

在实现上,PipeWire 主要分为几部分:

  • PipeWire 主守护进程 pipewire
  • PipeWire 的本地 API 客户端库
  • PipeWire 的 PulseAudio 兼容层,主要是 pipewire-pulse 守护进程
  • PipeWire 的 JACK 兼容层,主要是一个 shell 脚本和几个 JACK 兼容客户端库
  • PipeWire 的 ALSA 兼容层,主要是 PipeWire 的 ALSA 插件
  • PipeWire 的媒体会话管理器,目前主要有两个实现,一个是 pipewire-media-session,另一个是 wireplumber

PipeWire 在媒体节点图的处理方面,采用机制与策略分离的设计。PipeWire 主守护进程运行媒体节点图,是机制而不是策略。外部媒体会话管理器构建它们自己的音视频流水线,然后让 pipewire 守护进程执行最终的结果。这包括监听系统中设备状态的变化,并根据需要改变音视频流水线。

媒体会话管理器不属于 pipewire 项目本身,它们是单独的项目,独立的应用程序,它们通过 pipewire 的本地客户端 API 库与 PipeWire 主守护进程通信。媒体会话管理器目前的两个实现中,pipewire-media-session 的源码位于 media-session,WirePlumber 的源码位于 wireplumber,在 GitHub 上有 wireplumber 项目的镜像。PipeWire 社区有逐渐将媒体会话管理器从 pipewire-media-session 切换到 WirePlumber 的趋势,pipewire-media-session 的代码已经很少更新 (2025-03-09),甚至没有做 GitHub 的镜像。PipeWire 相关各部分结构如下图:
PipeWire 音频设计与实现分析一——介绍
PipeWire 中,大部分功能通过模块和插件实现,主守护进程 pipewire 和它的 PulseAudio 兼容层 pipewire-pulse 尽管运行在不同的进程中,但共用相同的可执行文件,这两个守护进程通过加载不同的配置文件,进而加载不同的模块和插件,来运行不同的逻辑,执行不同的功能,如:

$ ls -al /usr/bin/pipewire-pulse lrwxrwxrwx 1 root root 8 11月 9 2022 /usr/bin/pipewire-pulse -> pipewire

在 Ubuntu 操作系统平台上,pipewire 守护进程的配置文件为 /usr/share/pipewire/pipewire.conf,pipewire-pulse 的配置文件为 /usr/share/pipewire/pipewire-pulse.conf。媒体会话管理器同样有它的配置文件,pipewire-media-session 的配置文件为 /usr/share/pipewire/media-session.d/media-session.conf,此外,它还有用于监视 ALSA 音频设备、蓝牙音频设备和 V4L2 视频设备的配置文件 /usr/share/pipewire/media-session.d/alsa-monitor.conf/usr/share/pipewire/media-session.d/bluez-monitor.conf/usr/share/pipewire/media-session.d/v4l2-monitor.conf

本文分析应用程序接入 PulseAudio 本地 API 客户端库或 PipeWire 本地 API 客户端库的场景下,PipeWire 音频部分的设计和实现,这主要包括 PipeWire 的本地 API 客户端库、PipeWire 主守护进程 pipewire、PipeWire 的 PulseAudio 兼容层 pipewire-pulse 和媒体会话管理器 pipewire-media-sessionpipewire-media-session 主要与比较老的 pipewire 版本一起使用,尽管它有逐渐被替代的趋势,但它的实现逻辑更容易理解一点,这里仍然以它为例,来看 PipeWire 的会话管理器的实现。

代码获取及编译调试运行

安装 PipeWire 的首选方法是通过 Linux 系统发行版的包管理系统,比如通过 apt/dpkg 为 Debian/Ubuntu 等系统安装,通过 yum/rpm 为 Fedora/CentOS 等系统安装,然而,我们为了方便代码分析调试,从代码编译运行。

获取代码的方法有多种,从 pipewire 和 pipewire-media-session 的源码仓库,自然可以通过 git 命令获得源码。

如果是 Ubuntu 系统,可以通过 apt 命令获得源码,具体方法如下:

  1. 编辑 /etc/apt/sources.list 文件,取消注释那些 deb src 行,随后执行 sudo apt-get update 命令更新包仓库信息。
  2. 执行 sudo apt build-dep pipewiresudo apt build-dep pipewire-media-session 命令分别安装编译 pipewire 和 pipewire-media-session 所需的所有依赖包。
  3. 执行 apt source pipewireapt source pipewire-media-session 命令分别获得 pipewire 和 pipewire-media-session 的源码及其 debian 编译配置文件。
  4. 执行 cd pipewire-0.3.48 && dpkg-buildpackage -b -uc -nc 命令以 dpkg-buildpackage 的方式编译 pipewire 的源码,这将生成多个 .deb 文件。可以以同样的方式编译 pipewire-media-session 的源码。其中 dpkg-buildpackage-nc 参数表示不清理之前编译结果 (或者 --no-clean)。

dpkg-buildpackage 构建工具,封装 pipewire 本身的构建工具,编译生成正式版的 deb 包。对于调试分析,更方便的是,直接使用 pipewire 本身的构建工具编译生成 debug 版的二进制文件。PipeWire 使用称为 Meson 的构建工具作为它的构建过程的基础。这个工具与 Autotools 和 CMake 有点像。Meson 生成构建配置文件,喂给更底层的构建工具 Ninja,后者大概与我们更熟悉的 GNU Make 工作在相同的抽象层次上。

Meson 使用一个用户指定的构建目录,Meson 生成的所有文件都会放在这个目录中。本文中,构建目录称为 builddir

对于构建依赖,可以通过各个发行版的包管理工具安装。切换到 PipeWire 源码目录,并为 Ninja 生成构建配置文件:

$ cd pipewire-0.3.48$ meson setup builddir

上面的命令完成之后,可以看一下配置的构建选项:

$ meson configure builddir Core properties: Source dir /media/data/develop/pipewire_0.3.48/pipewire-0.3.48 Build dir /media/data/develop/pipewire_0.3.48/pipewire-0.3.48/builddirMain project options: Core options Current Value Possible Values Description ------------ ------------- --------------- ----------- auto_features auto [enabled, disabled, auto] Override value of all \'auto\' features backend ninja  [ninja, vs, vs2010, vs2012, vs2013, Backend to use vs2015, vs2017, vs2019, vs2022, xcode] buildtype debugoptimized [plain, debug, debugoptimized, Build type to use release, minsize, custom]  cmake_prefix_path  [] List of additional prefixes for cmake to search debug true [true, false] Debug default_library  shared [shared, static, both] Default library type force_fallback_for  [] Force fallback for those subprojects install_umask 0022 [preserve, 0000-0777]  Default umask to apply on permissions of installed files layout mirror [mirror, flat] Build directory layout optimization 2  [0, g, 1, 2, 3, s]  Optimization level pkg_config_path  [] List of additional paths for pkg-config to search strip false  [true, false] Strip targets on install unity off [on, off, subprojects] Unity build unity_size 4  >=2 Unity block size warning_level 3  [0, 1, 2, 3] Compiler warning level to use werror false  [true, false] Treat warnings as errors wrap_mode default [default, nofallback, nodownload, Wrap mode forcefallback, nopromote]  Backend options  Current Value Possible Values Description ---------------  ------------- --------------- ----------- backend_max_links  0  >=0 Maximum number of linker processes to run or 0 for no limit Base options Current Value Possible Values Description ------------ ------------- --------------- ----------- b_asneeded true [true, false] Use -Wl,--as-needed when linking b_colorout always [auto, always, never]  Use colored output b_coverage false  [true, false] Enable coverage tracking. b_lto false  [true, false] Use link time optimization b_lto_threads 0 Use multiple threads for Link Time Optimization b_lundef true [true, false] Use -Wl,--no-undefined when linking b_ndebug false  [true, false, if-release] Disable asserts b_pch true [true, false] Use precompiled headers b_pgo off [off, generate, use]  Use profile guided optimization b_pie true [true, false] Build executables as position independent b_sanitize none [none, address, thread, undefined, Code sanitizer to use memory, address,undefined]  b_staticpic true [true, false] Build static libraries as position independent Compiler options  Current Value Possible Values Description ----------------  ------------- --------------- ----------- c_args [] Extra arguments passed to the c compiler c_link_args [] Extra arguments passed to the c linker c_std gnu99  [none, c89, c99, c11, c17, c18, c2x, C language standard to use gnu89, gnu99, gnu11, gnu17, gnu18, gnu2x]  cpp_args [] Extra arguments passed to the cpp compiler cpp_debugstl false  [true, false] STL debug mode cpp_eh default [none, default, a, s, sc] C++ exception handling type. cpp_link_args [] Extra arguments passed to the cpp linker cpp_rtti true [true, false] Enable RTTI cpp_std c++17  [none, c++98, c++03, c++11, c++14, C++ language standard to use c++17, c++1z, c++2a, c++20,gnu++03, gnu++11, gnu++14, gnu++17, gnu++1z, gnu++2a, gnu++20]  Directories Current Value Possible Values Description ----------- ------------- --------------- ----------- bindir bin Executable directory datadir share Data file directory includedir include Header file directory infodir share/infoInfo page directory libdir lib/aarch64-linux-gnu  Library directory libexecdir libexec Library executable directory localedir share/locale  Locale data directory localstatedir /var/localLocalstate data directory mandir share/man Manual page directory prefix /usr/localInstallation prefix sbindir sbin System executable directory sharedstatedir/var/local/lib  Architecture-independent data directory sysconfdir etc Sysconf data directory Testing options  Current Value Possible Values Description ---------------  ------------- --------------- ----------- errorlogs true [true, false] Whether to print the logs from failing tests stdsplit true [true, false] Split stdout and stderr in test logs Project options  Current Value Possible Values Description ---------------  ------------- --------------- ----------- alsa auto [enabled, disabled, auto] Enable alsa spa plugin integration audioconvert enabled [enabled, disabled, auto] Enable audioconvert spa plugin integration audiomixer enabled [enabled, disabled, auto] Enable audiomixer spa plugin integration audiotestsrc enabled [enabled, disabled, auto] Enable audiotestsrc spa plugin integration avahi auto [enabled, disabled, auto] Enable code that depends on avahi bluez5 auto [enabled, disabled, auto] Enable bluez5 spa plugin integration bluez5-backend-hfp-native  enabled [enabled, disabled, auto] Enable HFP in native backend in bluez5 spa plugin bluez5-backend-hsp-native  enabled [enabled, disabled, auto] Enable HSP in native backend in bluez5 spa plugin bluez5-backend-hsphfpd  enabled [enabled, disabled, auto] Enable hsphfpd backend in bluez5 spa plugin (no dependency on hsphfpd) bluez5-backend-ofono enabled [enabled, disabled, auto] Enable oFono HFP backend in bluez5 spa plugin (no dependency on oFono) bluez5-codec-aac  auto [enabled, disabled, auto] Enable Fraunhofer FDK AAC open source codec implementation bluez5-codec-aptx  auto [enabled, disabled, auto] Enable AptX Qualcomm open source codec implementation bluez5-codec-ldac  auto [enabled, disabled, auto] Enable LDAC Sony open source codec implementation control enabled [enabled, disabled, auto] Enable control spa plugin integration dbus enabled [enabled, disabled, auto] Enable code that depends on dbus docdir  Directory for installing documentation to (defaults to pipewire_datadir/doc/meson.project_name() ) docs disabled [enabled, disabled, auto] Build documentation echo-cancel-webrtc  auto [enabled, disabled, auto] Enable WebRTC-based echo canceller evl  disabled [enabled, disabled, auto] Enable EVL support spa plugin integration examples enabled [enabled, disabled, auto] Build examples ffmpeg disabled [enabled, disabled, auto] Enable ffmpeg spa plugin integration gstreamer auto [enabled, disabled, auto] Build GStreamer plugins gstreamer-device-provider  auto [enabled, disabled, auto] Build GStreamer device provider plugin installed_tests  disabled [enabled, disabled, auto] Install manual and automated test executables jack auto [enabled, disabled, auto] Enable jack spa plugin integration jack-devel false  [true, false] Install jack development files libcamera auto [enabled, disabled, auto] Enable libcamera spa plugin integration libcanberra auto [enabled, disabled, auto] Enable code that depends on libcanberra libjack-path Where to install the libjack.so library libpulse auto [enabled, disabled, auto] Enable code that depends on libpulse libusb auto [enabled, disabled, auto] Enable code that depends on libusb libv4l2-path Where to install the libpw-v4l2.so library lv2  auto [enabled, disabled, auto] Enable loading of LV2 plugins man  auto [enabled, disabled, auto] Build manpages pipewire-alsa auto [enabled, disabled, auto] Enable pipewire-alsa integration pipewire-jack enabled [enabled, disabled, auto] Enable pipewire-jack integration pipewire-v4l2 enabled [enabled, disabled, auto] Enable pipewire-v4l2 integration pw-cat auto [enabled, disabled, auto] Build pw-cat/pw-play/pw-record raop auto [enabled, disabled, auto] Enable module for Remote Audio Output Protocol roc  auto [enabled, disabled, auto] Enable code that depends on roc toolkit sdl2 auto [enabled, disabled, auto] Enable code that depends on SDL 2 session-managers  [media-session]  Session managers to build (can be [] for none or an absolute path) sndfile auto [enabled, disabled, auto] Enable code that depends on libsndfile spa-plugins enabled [enabled, disabled, auto] Enable spa plugins integration support enabled [enabled, disabled, auto] Enable support spa plugin integration systemd auto [enabled, disabled, auto] Enable systemd integration systemd-system-service  disabled [enabled, disabled, auto] Install systemd system service file systemd-system-unit-dir Directory for system systemd units (defaults to /usr/lib/systemd/system) systemd-user-service enabled [enabled, disabled, auto] Install systemd user service file (ignored without systemd) systemd-user-unit-dir Directory for user systemd units (defaults to /usr/lib/systemd/user) test disabled [enabled, disabled, auto] Enable test spa plugin integration tests enabled [enabled, disabled, auto] Build tests udev auto [enabled, disabled, auto] Enable Udev integration udevrulesdir Directory for udev rules (defaults to /lib/udev/rules.d) v4l2 auto [enabled, disabled, auto] Enable v4l2 spa plugin integration videoconvert enabled [enabled, disabled, auto] Enable videoconvert spa plugin integration videotestsrc enabled [enabled, disabled, auto] Enable videotestsrc spa plugin integration volume enabled [enabled, disabled, auto] Enable volume spa plugin integration vulkan disabled [enabled, disabled, auto] Enable vulkan spa plugin integration x11  auto [enabled, disabled, auto] Enable code that depends on X11 x11-xfixes auto [enabled, disabled, auto] Enable code that depends on XFixesSubproject media-session: Core options Current Value Possible Values Description ------------ ------------- --------------- ----------- default_library  shared [shared, static, both] Default library type warning_level 3  [0, 1, 2, 3] Compiler warning level to use werror false  [true, false] Treat warnings as errors Project options  Current Value Possible Values Description ---------------  ------------- --------------- ----------- docdir  Directory for installing documentation to (defaults to pipewire_datadir/doc/meson.project_name() ) docs disabled [enabled, disabled, auto] Build documentation installed_tests  disabled [enabled, disabled, auto] Install manual and automated test executables systemd auto [enabled, disabled, auto] Enable systemd integration systemd-system-service  disabled [enabled, disabled, auto] Install systemd system service file systemd-user-service enabled [enabled, disabled, auto] Install systemd user service file (ignored without systemd) systemd-user-unit-dir Directory for user systemd units (defaults to /usr/lib/systemd/user) tests enabled [enabled, disabled, auto] Build tests with-module-sets  [jack, pulseaudio]  [alsa, jack, pulseaudio]  Extra modules sets to enable on install (see media-session.conf)

定义安装目录前缀:

$ meson configure builddir -Dprefix=/usr # Default: /usr/local

PipeWire 特有的构建选项在 Project options 部分展示,它们在 meson_options.txt 文件中定义。

最后,调用如下命令构建 PipeWire:

$ ninja -C builddir

如果想要在不安装 PipeWire 的情况运行它,可以先运行一个脚本,它会创建一个 PipeWire 可以从构建目录运行的环境,其中 ALSA、PulseAudio 和 JACK 应用程序将自动地使用 PipeWire 仿真库。这个脚本具体的执行方法如下:

$ ./pw-uninstalled.sh -b builddir

pw-uninstalled.sh 脚本内容如下:

#!/usr/bin/env bashset -eSCRIPT_DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" >/dev/null 2>&1 && pwd )\"while getopts \":b:v:\" opt; docase ${opt} inb)BUILDDIR=${OPTARG};;v)VERSION=${OPTARG}echo \"Version: ${VERSION}\";;\\?)echo \"Invalid option: -${OPTARG}\"exit 1;;:)echo \"Option -${OPTARG} requires an argument\"exit 1;;esacdoneif [ -z \"${BUILDDIR}\" ]; thenBUILDDIR=${SCRIPT_DIR}/builddirecho \"Using default build directory: ${BUILDDIR}\"fiif [ ! -d \"${BUILDDIR}\" ]; thenecho \"Invalid build directory: ${BUILDDIR}\"exit 1fi# the config file read by the daemonexport PIPEWIRE_CONFIG_DIR=\"${BUILDDIR}/src/daemon\"# the directory with SPA pluginsexport SPA_PLUGIN_DIR=\"${BUILDDIR}/spa/plugins\"export SPA_DATA_DIR=\"${SCRIPT_DIR}/spa/plugins\"# the directory with pipewire modulesexport PIPEWIRE_MODULE_DIR=\"${BUILDDIR}/src/modules\"export PATH=\"${BUILDDIR}/src/daemon:${BUILDDIR}/src/tools:${BUILDDIR}/src/media-session:${BUILDDIR}/src/examples:${PATH}\"export LD_LIBRARY_PATH=\"${BUILDDIR}/src/pipewire/:${BUILDDIR}/pipewire-jack/src/${LD_LIBRARY_PATH+\":$LD_LIBRARY_PATH\"}\"export GST_PLUGIN_PATH=\"${BUILDDIR}/src/gst/${GST_PLUGIN_PATH+\":${GST_PLUGIN_PATH}\"}\"# the directory with card profiles and pathsexport ACP_PATHS_DIR=\"${SCRIPT_DIR}/spa/plugins/alsa/mixer/paths\"export ACP_PROFILES_DIR=\"${SCRIPT_DIR}/spa/plugins/alsa/mixer/profile-sets\"# ALSA plugin directoryexport ALSA_PLUGIN_DIR=\"${BUILDDIR}/pipewire-alsa/alsa-plugins\"export PW_UNINSTALLED=1export PKG_CONFIG_PATH=\"${BUILDDIR}/meson-uninstalled/:${PKG_CONFIG_PATH}\"if [ -d \"${BUILDDIR}/subprojects/wireplumber\" ]; then# FIXME: find a nice, shell-neutral way to specify a prompt\"${SCRIPT_DIR}\"/subprojects/wireplumber/wp-uninstalled.sh -b\"${BUILDDIR}\"/subprojects/wireplumber \"${SHELL}\"elif [ -d \"${BUILDDIR}/subprojects/media-session\" ]; then# FIXME: find a nice, shell-neutral way to specify a prompt\"${SCRIPT_DIR}\"/subprojects/media-session/media-session-uninstalled.sh -b\"${BUILDDIR}\"/subprojects/media-session \"${SHELL}\"else# FIXME: find a nice, shell-neutral way to specify a prompt${SHELL}fi

pw-uninstalled.sh 脚本接受构建目录 builddir 作为参数,根据构建工具生成的二进制文件的目录结构,导出 PipeWire 各守护进程或客户端应用程序需要用到的环境变量,如 PIPEWIRE_CONFIG_DIRPIPEWIRE_MODULE_DIRSPA_PLUGIN_DIRLD_LIBRARY_PATHSPA_DATA_DIR 等。

pw-uninstalled.sh 脚本有个 bug:当传入的构建目录 builddir 是个相对路径时,各环境变量也会以相对路径的形式导出。在 PipeWire 的代码仓库根目录执行pw-uninstalled.sh 脚本并检查构建目录的有效性,随后基于构建目录路径导出环境变量,切换到构建目录运行 PipeWire 系统音频服务器,会发现各环境变量指向的路径为无效路径。

从构建目录运行 pipewire 的方法如下:

$ cd builddir/$ make run

这将会使用默认配置文件配置和启动 pipewire 守护进程。默认的配置文件也将自动启动 pipewire-media-session,一个默认的示例会话管理器,和 pipewire-pulse,一个 PulseAudio 兼容服务器。

正常安装的 PipeWire,它的 pipewire 守护进程、pipewire-media-session 和 pipewire-pulse 是作为 3 个系统服务,各自由 systemd 独立管理的。从构建目录运行的 PipeWire,pipewire 守护进程的配置文件为 pipewire-0.3.48/builddir/src/daemon/pipewire-uninstalled.conf,其中有如下这样一段配置:

context.exec = [ #{ path =  [ args = \"\" ] } # # Execute the given program with arguments. # # You can optionally start the session manager here, # but it is better to start it as a systemd service. # Run the session manager with -h for options. # { path = \"/media/data/develop/pipewire_0.3.48/pipewire-0.3.48/builddir/subprojects/media-session/media-session-uninstalled.sh\" args = \"pipewire-media-session\" } # # You can optionally start the pulseaudio-server here as well # but it is better to start it as a systemd service. # It can be interesting to start another daemon here that listens # on another address with the -a option (eg. -a tcp:4713). # { path = \"/media/data/develop/pipewire_0.3.48/pipewire-0.3.48/builddir/src/daemon/pipewire\" args = \"-c pipewire-pulse.conf\" }]

pipewire-media-session 和 pipewire-pulse 由 pipewire 守护进程启动。

也可以通过 PIPEWIRE_DEBUG 环境变量启用更多的调试日志,如:

$ cd builddir/$ PIPEWIRE_DEBUG=\"D\" make run

还可以启动 gdb 来调试运行 pipewire,如:

$ cd builddir/$ PIPEWIRE_DEBUG=\"D\" make gdb

如果系统中已经安装了 PipeWire,且以 pipewire-media-session 作为媒体会话管理器,则从构建目录运行之前,可能需要先通过 systemd 停掉 pipewire service/socket,如:

$ systemctl --user stop pipewire.service \\pipewire.socket \\pipewire-media-session.service \\pipewire-pulse.service \\pipewire-pulse.socket

如果以 wireplumber 作为媒体会话管理器,需要将上面命令中的 pipewire-media-session.service 替换为 wireplumber.service

PipeWire 包含一些库和工具,在 builddir 目录中运行如下命令安装所有这些东西到系统的指定目录:

$ sudo meson install

完整安装的 PipeWire 应该有一个 pipewire 进程,一个 pipewire-media-session (或 WirePlumber) 和一个 pipewire-pulse 进程。PipeWire 通常作为一个 systemd 系统服务来启动。PipeWire 守护进程的配置文件位于 /usr/share/pipewire/pipewire.conf,可以参考这个配置文件中的注释来了解更多关于配置选项的内容。通过 systemd 启动 pipewire 守护进程的方法如下:

$ systemctl --user start pipewire.service pipewire.socket

如果没有在 pipewire.conf 配置文件中启动 media-session,也需要像下面这样启动它:

$ systemctl --user start pipewire-media-session.service

要使它在系统启动时自动启动可以执行如下命令:

$ systemctl --user enable pipewire-media-session.service

可以使用 enable --now 来立即启动它。

对于 wireplumber,启动它的方法与 media-session 类似,唯一的区别是 systemd 激活文件需要替换为 wireplumber.service

pipewire-pulse 进程是 PulseAudio 音频服务器协议的重新实现,并作为一个系统服务在 PipeWire 之上运行。它的二进制可执行文件通常位于 /usr/bin/pipewire-pulse。pipewire-pulse 进程可以通过提供的 systemd 激活文件或 PipeWire 守护进程来启动,可参考 /usr/share/pipewire/pipewire.conf 文件。

通过 systemd 启动 pipewire-pulse 服务的方法如下:

$ systemctl --user start pipewire-pulse.service pipewire-pulse.socket

pipewire-pulse 的二进制可执行文件实际为 pipewire 守护进程二进制可执行文件的符号链接。

PipeWire 的 ALSA 插件在 Fedora 发行版中通常安装在 /usr/lib64/alsa-lib/libasound_module_pcm_pipewire.so,在 Ubuntu 发行版中通常安装在 /usr/lib/x86_64-linux-gnu/alsa-lib/libasound_module_pcm_pipewire.so,还有一个配置文件安装在 /usr/share/alsa/alsa.conf.d/50-pipewire.conf,当以下文件位于 /etc/alsa/conf.d/ 目录中时,插件将被 alsa 拾取:

/etc/alsa/conf.d/50-pipewire.conf -> /usr/share/alsa/alsa.conf.d/50-pipewire.conf/etc/alsa/conf.d/99-pipewire-default.conf

通过这样的设置,aplay -L 应该可以列出 pipewire: 设备,它们可以被用作普通的 alsa 设备来播放和录音。

PipeWire 音频服务器主程序

PipeWire 音频服务器守护进程 pipewire 和它的 PulseAudio 兼容层 pipewire-pulse 守护进程共用同一个二进制可执行文件,它们的主程序定义 (位于 pipewire/src/daemon/pipewire.c) 如下:

int main(int argc, char *argv[]){struct pw_context *context = NULL;struct pw_main_loop *loop = NULL;struct pw_properties *properties = NULL;static const struct option long_options[] = {{ \"help\",no_argument,NULL, \'h\' },{ \"version\",no_argument,NULL, \'V\' },{ \"config\",required_argument,NULL, \'c\' },{ \"verbose\",no_argument,NULL, \'v\' },{ NULL, 0, NULL, 0}};int c, res = 0;char path[PATH_MAX];const char *config_name;enum spa_log_level level = pw_log_level;if (setenv(\"PIPEWIRE_INTERNAL\", \"1\", 1) < 0)fprintf(stderr, \"can\'t set PIPEWIRE_INTERNAL env: %m\");snprintf(path, sizeof(path), \"%s.conf\", argv[0]);config_name = basename(path);pw_init(&argc, &argv);while ((c = getopt_long(argc, argv, \"hVc:v\", long_options, NULL)) != -1) {switch (c) {case \'v\':if (level dict);if (loop == NULL) {pw_log_error(\"failed to create main-loop: %m\");res = -errno;goto done;}pw_loop_add_signal(pw_main_loop_get_loop(loop), SIGINT, do_quit, loop);pw_loop_add_signal(pw_main_loop_get_loop(loop), SIGTERM, do_quit, loop);context = pw_context_new(pw_main_loop_get_loop(loop), properties, 0);properties = NULL;if (context == NULL) {pw_log_error(\"failed to create context: %m\");res = -errno;goto done;}pw_log_info(\"start main loop\");pw_main_loop_run(loop);pw_log_info(\"leave main loop\");done:pw_properties_free(properties);if (context)pw_context_destroy(context);if (loop)pw_main_loop_destroy(loop);pw_deinit();return res;}

pipewire 和 pipewire-pulse 主程序的执行过程如下:

  1. 传入可执行程序执行时的命令行参数,调用 pw_init() 函数初始化 PipeWire,初始化一些基本配置和功能组件pw_init() 函数在所有 PipeWire 程序中都会被调用,包括 PipeWire 守护进程及其 PulseAudio 兼容层,PipeWire 客户端程序,PipeWire 媒体会话管理器 pipewire-media-session 和 wireplumber。初始化 PipeWire 的具体含义稍后会更细致地来看。
  2. 解析命令行参数。-v 参数指示 PipeWire 进程在执行时输出更多日志,具体指输出日志等级加 1,而不是输出所有日志。-c 参数用来指定配置文件名,配置文件名默认为可执行文件名加上 .conf 后缀,如 pipewire 的为 pipewire.conf,pipewire-pulse 的为 pipewire-pulse.conf,这个参数指定的配置文件名替换默认配置文件名。
  3. 调用 pw_main_loop_new() 函数创建主循环。以 struct spa_dict 对象的形式传递参数,这里参数主要是配置文件名。为主循环添加 SIGINTSIGTERM 信号的处理程序,收到这两个信号时,PipeWire 进程退出主循环。
  4. 基于主循环和配置信息创建上下文。
  5. 运行主循环。
  6. 主循环结束时清理上下文和主循环等对象。

pw_init() 函数初始化一些基本配置和功能组件,该函数定义 (位于 pipewire/src/pipewire/pipewire.c) 如下:

#define MAX_SUPPORT32#define SUPPORTLIB\"support/libspa-support\"PW_LOG_TOPIC_EXTERN(log_context);#define PW_LOG_TOPIC_DEFAULT log_contextstatic char *prgname;static struct spa_i18n *_pipewire_i18n = NULL;struct plugin {struct spa_list link;char *filename;void *hnd;spa_handle_factory_enum_func_t enum_func;struct spa_list handles;int ref;};struct handle {struct spa_list link;struct plugin *plugin;char *factory_name;int ref;struct spa_handle handle SPA_ALIGNED(8);};struct registry {struct spa_list plugins;};struct support {char **categories;const char *plugin_dir;const char *support_lib;struct registry registry;char *i18n_domain;struct spa_interface i18n_iface;struct spa_support support[MAX_SUPPORT];uint32_t n_support;unsigned int initialized:1;unsigned int in_valgrind:1;unsigned int no_color:1;unsigned int no_config:1;};static pthread_mutex_t init_lock = PTHREAD_MUTEX_INITIALIZER;static pthread_mutex_t support_lock = PTHREAD_MUTEX_INITIALIZER;static struct support global_support; . . . . . . static void init_i18n(struct support *support){/* Load locale from the environment. */setlocale(LC_ALL, \"\");/* Set LC_NUMERIC to C so that floating point strings are consistently * formatted and parsed across locales. */setlocale(LC_NUMERIC, \"C\");bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);bind_textdomain_codeset(GETTEXT_PACKAGE, \"UTF-8\");pw_set_domain(GETTEXT_PACKAGE);} . . . . . .SPA_EXPORTvoid pw_init(int *argc, char **argv[]){const char *str;struct spa_dict_item items[6];uint32_t n_items;struct spa_dict info;struct support *support = &global_support;struct spa_log *log;char level[32];pthread_mutex_lock(&init_lock);if (support->initialized)goto done;pthread_mutex_lock(&support_lock);support->in_valgrind = RUNNING_ON_VALGRIND;if (getenv(\"NO_COLOR\") != NULL)support->no_color = true;if ((str = getenv(\"PIPEWIRE_NO_CONFIG\")) != NULL)support->no_config = pw_properties_parse_bool(str);init_i18n(support);if ((str = getenv(\"SPA_PLUGIN_DIR\")) == NULL)str = PLUGINDIR;support->plugin_dir = str;if ((str = getenv(\"SPA_SUPPORT_LIB\")) == NULL)str = SUPPORTLIB;support->support_lib = str;spa_list_init(&support->registry.plugins);if (pw_log_is_default()) {char *patterns = NULL;n_items = 0;if (!support->no_color)items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_COLORS, \"true\");items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_TIMESTAMP, \"true\");if ((str = getenv(\"PIPEWIRE_LOG_LINE\")) == NULL || spa_atob(str))items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_LINE, \"true\");snprintf(level, sizeof(level), \"%d\", pw_log_level);items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_LEVEL, level);if ((str = getenv(\"PIPEWIRE_LOG\")) != NULL)items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_FILE, str);if ((patterns = parse_pw_debug_env()) != NULL)items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_PATTERNS, patterns);info = SPA_DICT_INIT(items, n_items);log = add_interface(support, SPA_NAME_SUPPORT_LOG, SPA_TYPE_INTERFACE_Log, &info);if (log)pw_log_set(log);#ifdef HAVE_SYSTEMDif ((str = getenv(\"PIPEWIRE_LOG_SYSTEMD\")) == NULL || spa_atob(str)) {log = load_journal_logger(support, &info);if (log)pw_log_set(log);}#endiffree(patterns);} else {support->support[support->n_support++] =SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, pw_log_get());}pw_log_init();n_items = 0;if ((str = getenv(\"PIPEWIRE_CPU\")))items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_CPU_FORCE, str);if ((str = getenv(\"PIPEWIRE_VM\")))items[n_items++] = SPA_DICT_ITEM_INIT(SPA_KEY_CPU_VM_TYPE, str);info = SPA_DICT_INIT(items, n_items);add_interface(support, SPA_NAME_SUPPORT_CPU, SPA_TYPE_INTERFACE_CPU, &info);add_i18n(support);pw_log_info(\"version %s\", pw_get_library_version());support->initialized = true;pthread_mutex_unlock(&support_lock);done:pthread_mutex_unlock(&init_lock);}

PipeWire 程序用 struct support 类型的全局对象 global_support 维护一些基本配置和功能组件,如插件目录、支持库路径、i18n、日志等,以及所有加载的插件,其中所有加载的插件用 struct registry 对象维护,基本功能组件用 struct spa_support 数组维护。

pw_init() 函数初始化 global_support 对象,具体过程如下:

  1. 从环境变量 NO_COLOR、 **PIPEWIRE_NO_CONFIG ** 中获取对应配置,初始化 i18n。从环境变量 SPA_PLUGIN_DIR 获取插件目录,失败时使用默认目录路径,如 /usr/lib/aarch64-linux-gnu/spa-0.2。从环境变量 SPA_SUPPORT_LIB 获取支持库路径,失败时使用默认路径 support/libspa-support。初始化 struct registry

  2. 初始化并添加日志组件。如果全局日志组件为默认日志组件,构造日志组件初始化时的配置参数,除了前面获取的 no_color,还有如下这些配置参数:

    • 是否打印时间戳配置 log.timestamp,为 true
    • 从环境变量 **PIPEWIRE_LOG_LINE ** 中获取的是否打印行号配置 log.line;
    • 打印的最低日志等级配置 log.level
    • 从环境变量 **PIPEWIRE_LOG ** 中获取的是否打印文件配置 log.file
    • 从环境变量 PIPEWIRE_DEBUG 中获取的日志模式配置log.patterns

    之后调用 add_interface() 函数从支持库插件中加载日志组件,并设置全局日志组件为该日志组件。如果环境变量 PIPEWIRE_LOG_SYSTEMD 指示使用 systemd 日志,则调用 load_journal_logger() 函数加载 journal 日志组件替换 支持库插件中加载的日志组件。
    如果全局日志组件不是默认日志组件,则向 global_support 添加全局日志组件为。
    最后,调用 pw_log_init() 函数基于新的全局日志组件,初始化各个日志 topic。
    日志是 PipeWire 配置的第一个组件。

  3. 初始化并添加 CPU 组件。从环境变量 PIPEWIRE_CPUPIPEWIRE_VM 获取配置参数,调用 add_interface() 函数从支持库插件中加载 CPU 组件。

  4. 添加 i18n 组件。

  5. 结束初始化。

global_supportstruct spa_support 数组中维护的这些组件,提供十分基础的全局功能,可以在其它一般插件的实现中使用。全局支持组件主要包括日志、CPU 和 i18n。