前言
官方网站:
https://www.freedesktop.org/wiki/Software/PulseAudio/
源码:
https://gitlab.freedesktop.org/pulseaudio/pulseaudio
音频框架
本文参照的是高通平台pa的架构。
应用层 paplay / app
↓
PA 层 PulseAudio Daemon
↓
高通抽象层 QAL (Qualcomm Audio Layer) ← 这里
↓
图管理层 AGM (Audio Graph Manager)
↓
内核层 ALSA Driver
↓
硬件 Codec / DSP
apps_proc/src/external/pulseaudio/src/modules/qal/module-qal-card/qal-sink.c
QAL (Qualcomm Audio Layer):
管理 stream(音频流的打开、读写、关闭)
管理 session(一次录音/播放会话)
决定走哪个 audio graph(音频图)
AGM (Audio Graph Manager)有两种方式:
libagm.so
agm service
/usr/lib # find / -name "*mixer_plugin*" -o -name "*agm_mixer*" 2>/dev/null | grep -v proc
/usr/lib/libagm_mixer_plugin.so
配置文件
pa的几乎所有配置都遵循一个默认路径
$HOME/.config/pulse
如果设置了配置文件,会优先读取client.conf配置
如果设置了环境变量,环境变量的优先级最高
环境变量配置
| 环境变量 | 作用 |
|---|---|
PULSE_CLIENTCONFIG |
指定 client.conf 文件的完整路径 |
PULSE_COOKIE |
指定认证 cookie 文件路径 |
PULSE_SERVER |
指定要连接的 server 地址,可以是 unix socket 路径或 tcp 地址 |
PULSE_RUNTIME_PATH |
覆盖 PulseAudio 运行时目录,优先级高于 XDG_RUNTIME_DIR |
PULSE_STATE_PATH |
覆盖 PulseAudio 持久化状态目录(默认 sink/source 记忆、音量记忆等) |
XDG_CONFIG_HOME |
用户配置根目录,影响 ~/.config/pulse/ 这一层默认路径 |
XDG_RUNTIME_DIR |
影响 socket、锁文件等运行时状态默认目录 |
| 这 7 个里,PULSE_ 开头的 5 个是 PulseAudio 专用变量,优先级最高,会直接覆盖对应的默认路径逻辑;XDG_ 开头的 2 个是 freedesktop 标准变量,PulseAudio 只在对应 PULSE_* 变量没设置时才会去看它们,属于兜底层。 |
daemon级配置
/etc/pulse/daemon.conf
通常只读(配置只被读取,不被程序写入)
系统集成/打包时预置
全局,所有用户共享同一份
这个是给 pulseaudio server 进程用的,控制采样率、重采样算法、实时调度等
client级配置
路径通常是 /etc/pulse/client.conf(系统级)或 ~/.config/pulse/client.conf(用户级,用户级优先)。控制 client 行为
PULSE_COOKIE指定去哪里查找cookie文件,cookie文件存放位置一般是写在系统配置文件中client.conf中的 cookie-file
和下面环境变量设置作用一样。
export PULSE_COOKIE=/data/audio/pulse-cookie
启动脚本
/etc/pulse/default.pa(用户模式启动)或 /etc/pulse/system.pa(system 模式启动)
这是一个脚本式配置,里面是 load-module 指令,决定 server 启动时加载哪些模块(ALSA sink、UDEV 探测等)。也是 server 端
用户模式登录用户都有自己独立的一份 PulseAudio server 实例,通常在用户第一次尝试连接音频(比如打开个播放器)时,由 autospawn 机制自动拉起,进程以该普通用户的权限运行。
system模式,这是单一系统级实例,以 root 或专门的系统用户(比如 pulse 用户)权限运行,开机时作为系统服务启动,所有登录用户共享这一个 server 实例。
大部分嵌入式音频方案(尤其是单用户、没有多用户登录概念的设备)会选择 system 模式,原因是:
设备通常只有一个"用户"(比如你日志里的 root),用户级隔离没有实际意义
不需要"用户登录才启动音频"这种桌面场景的懒加载逻辑,设备开机就要能出声
Socket 路径固定、可预测,方便多个进程(比如你的 simcom_audio_test 和其他音频相关服务)统一连接同一个已知地址
运行时状态数据
libpulse 客户端库运行时自动创建(很多时候会没权限创建报错,但是不影响,这个目录没有也没事,嵌入式更多不需要认证)
cookie 认证文件、运行时 socket 软链接
每个用户各自独立一份
由c端读取
cookie # 认证凭据
qcm2290-mtp-runtime -> /tmp/pulse-xxx # 运行时 socket 目录的软链接
音频输入
输入
查音频输入源
pactl list sources
音频输出
查询基本信息
pactl info
sink
查看所有sink
(可以去掉short参数,显示每个sink的详细参数,其中包括每个sink设置的音量)
/ # pactl list sinks short
1 low-latency0 module-qal-card.c s16le 2ch 48000Hz SUSPENDED
2 deep-buffer0 module-qal-card.c s16le 2ch 48000Hz SUSPENDED
3 offload0 module-qal-card.c s16le 2ch 48000Hz SUSPENDED
低延迟场景 → low-latency0 (实时音效、提示音)
长音频场景 → deep-buffer0 (音乐播放,省电)
DSP卸载场景 → offload0 (硬件解码,CPU占用极低)
查看默认sink
pactl get-default-sink
或者播放音量的时候查看哪个sink被使用了。
pactl list sink-inputs short
查音频输出源
pactl list sinks
如果不知道具体用的是哪一个,可以使音频处于工作状态然后再用命令查看状态。
查询sink信息
pactl list sinks
音量控制
设置音量
primary是sinks的名字,根据播放音频的时候使用的具体sinks名字调整。
pactl set-sink-volume primary 5%
但是在嵌入式中有可能这样设置并不能改变音量。
/ # pactl list sinks | grep -A 40 "low-latency0" | grep -E "Flags|Volume"
Volume: front-left: 6553 / 10%, front-right: 6553 / 10%
Base Volume: 65536 / 100%
Flags: HARDWARE HW_VOLUME_CTRL LATENCY
Volume: front-left: 65536 / 100%, front-right: 65536 / 100%
Base Volume: 65536 / 100%
Flags: HARDWARE HW_VOLUME_CTRL LATENCY
HW_VOLUME_CTRL表示有其他的在控制音量,pa会把音量控制交给这里,而不是pcm直接处理。所以上面的方法设置音量不会生效。
获取当前音量
pactl get-sink-volume primary
数据流转
格式确认
获取音频文件的格式
D: [pulseaudio] sink-input.c: Negotiated format: pcm, format.sample_format = "\"s16le\"" format.rate = "44100" format.channels = "2" format.channel_map = "\"front-left,front-right\""
重采样
PulseAudio 打开音频设备时通常会选择一个固定的“主采样率”(通常是硬件最优先支持的采样率)
当多个应用同时播放不同采样率的音频时,PulseAudio 需要先对齐到同一个基准采样率(即硬件当前采样率)才能混音。这个对齐过程就是重采样。
重采样可以被避免,当且仅当 流的采样率与硬件当前运行的主采样率完全一致。
默认采样率和备用采样率一样,切换没有意义。只能进行重采样。
sink.c: Default and alternate sample rates are the same, so there is no point in switching.
重采样:
resampler.c: rate 44100 -> 48000 (method speex-float-1)
D: [pulseaudio] resampler.c: rate 44100 -> 48000 (method speex-float-1)
D: [pulseaudio] resampler.c: format s16le -> s16le (intermediate float32le)
D: [pulseaudio] resampler.c: channels 2 -> 2 (resampling 2)
I: [pulseaudio] speex.c: Choosing speex quality setting 1.
内存缓冲
当 PulseAudio 决定把流连接到 low-latency0 sink 时,首先会检查该 sink 的采样率(48000 Hz)与流的采样率(44100 Hz)是否一致。发现不匹配后,立即初始化重采样器及其所需的输入缓冲区。这个缓冲区是 sink-input 对象的一部分,用于给重采样器喂数据。所以它的 memblockq 日志出现在最先。
这个缓冲区的参数来源于客户端的请求(tlength=200ms, minreq=20ms),必须等客户端与 PulseAudio 完成协议协商后才能确定。协商过程(protocol-native.c 中的延迟计算)发生在重采样器初始化之后。因此,协议层缓冲的 memblockq 日志稍晚出现。
第一级:协议层缓冲(客户端 → PulseAudio)
D: [pulseaudio] memblockq.c: memblockq requested: maxlength=4194304, tlength=35280, base=4, prebuf=31756, minreq=3528 maxrewind=0
D: [pulseaudio] memblockq.c: memblockq sanitized: maxlength=4194304, tlength=35280, base=4, prebuf=31756, minreq=3528 maxrewind=0
tlength=35280 字节:对应 200 ms(44100 Hz 立体声 s16le)。期望缓冲水位。
prebuf=31756 字节:对应 180 ms。播放开始前必须累积的数据量。
minreq=3528 字节:对应 20 ms。每次读取的最小数据量。
第二级:重采样器缓冲(位于重采样器输入端)
D: [pulseaudio] memblockq.c: memblockq requested: maxlength=33554432, tlength=0, base=4, prebuf=0, minreq=1 maxrewind=0
D: [pulseaudio] memblockq.c: memblockq sanitized: maxlength=33554432, tlength=33554432, base=4, prebuf=0, minreq=4 maxrewind=0
maxlength=32 MB:巨大容量以容纳重采样所需的大量连续数据。
tlength 从 0 被修正为 32 MB:目标水位拉到最大,期望缓冲始终满载。
prebuf=0:无预缓冲,一旦硬件需要数据可立即从该缓冲取走(即使空)。
minreq=4 字节:一帧(立体声 s16le)为最小处理单位。
延迟
总延迟构成:客户端请求延迟 (160 ms) + 协议开销 (2 × 20 ms) + 硬件固有延迟 (5.33 ms) = 205.33 ms。
I: [pulseaudio] protocol-native.c: Requested tlength=200.00 ms, minreq=20.00 ms
D: [pulseaudio] protocol-native.c: Traditional mode enabled, modifying sink usec only for compat with minreq.
D: [pulseaudio] protocol-native.c: Requested latency=160.00 ms, Received latency=5.33 ms
I: [pulseaudio] protocol-native.c: Final latency 205.33 ms = 160.00 ms + 2*20.00 ms + 5.33 ms
module
module-alsa-sink
流程
可以看到标准的音频流可以任何时候调节音量,就是因为把这个写入到文件中了。无论 stream 开着还是关着,mixer 寄存器都在
apps_proc/src/external/pulseaudio/src/modules/alsa/module-alsa-sink.c
pactl set-sink-volume
↓
PA sink.c 收到请求
↓
module-alsa-sink.c 的 set_volume_cb
↓
写入 ALSA mixer control(独立于stream存在)
↓
mixer control 值永久保存在硬件寄存器里
设计思路
也就是pa的插件同时设计打开两条流,不同的是数据流有音频时候就打开,控制流是在音量改变的时候
标准的alsa是走的两个接口,一个走控制,一个走数据流
但是高通这个就不管控制流了,直走数据流,音量调节直接绑定数据流的stream去调节
/dev/snd/pcmC0D0p ← 音频数据流(PCM数据)
/dev/snd/controlC0 ← 控制流(音量/路由/开关)
pcmC0D0p:
没有播放 → 关闭
播放中 → 打开
播放结束 → 关闭
controlC0:
系统启动 → 打开
调音量 → 读写
系统关闭 → 关闭
→ controlC0 始终存在,和播放状态无关
→ 所以随时可以调音量
音量控制
每次设置音量之前都会先获取mix中音量
pa_sink_set_get_volume_callback(u->sink, sink_get_volume_cb);
pa_sink_set_set_volume_callback(u->sink, sink_set_volume_cb);
可以看到这里去检查mixer中的参数
static void sink_set_volume_cb(pa_sink *s) {
......
pa_assert(u->mixer_path);
pa_assert(u->mixer_handle);
qal-sink
apps_proc/src/external/pulseaudio/src/modules/qal/module-qal-card/qal-sink.c
流程
`高通qal-sink流程
pactl set-sink-volume
↓
PA sink.c 收到请求
↓
qal-sink.c 的 set_volume_cb
↓
调用 qal_stream_set_volume(u->qal_stream)
↓
qal_stream 不存在?→ 直接报错返回
设计思路
高通这样设计可以很明显看出来音量控制是绑定在stream上的,也就是说没有音频流就无法设置音量`
可能是这个下面多个设备接口,只有一个control,没法控制。
/dev/snd # ls
by-path controlC0 pcmC0D0c pcmC0D10c pcmC0D11c pcmC0D12c pcmC0D13c pcmC0D1p pcmC0D2p pcmC0D3c pcmC0D4c pcmC0D5p pcmC0D6p pcmC0D7p pcmC0D8p pcmC0D9c timer
生命周期
连接client
Client authenticated anonymously.
sink 从 SUSPENDED 唤醒
qal-sink.c: opening sink with configuration type=0x1, format=1, sample_rate=48000, channels=2
qal-sink.c: pal sink opened 0x7f78000e60
采样配置
rate 44100 -> 48000 (method speex-float-1)
channels 1 -> 2 (resampling 1)
音频流创建并播放(RUNNING也就是这个时候音量才可以被调节)
sink.c: low-latency0: state: IDLE -> RUNNING
Created input 1 "/data/test.wav" on low-latency0
Final latency 2005.33 ms
播放结束,销毁stream
Freeing input 1 "/data/test.wav"
sink 重新进入 SUSPENDED(空闲超过1s进入SUSPENDED)
qal-sink.c: closing pal sink 0x7f78000e60 ← qal_stream 被销毁
sink.c: low-latency0: state: IDLE -> SUSPENDED
log
查看日志
journalctl -u pulseaudio -f
提高log等级
5是debug级
PULSE_LOG=5 paplay data/test.wav
个人总结
在嵌入式环境中的 PulseAudio 实际开发里,我关注的两个核心方向通常是:音频框架的适配,以及系统权限带来的影响。
嵌入式项目中的音频框架差异较大,各有各的实现方式。以我当前的开发环境为例,Qualcomm 自行封装了一个 module,用来对接其私有音频框架,而不是直接使用上游 PulseAudio 的原生模块。所以首要问题是理清平台音频框架的调用链路,确保 PulseAudio 能正确地挂载输入输出通路。
系统权限方面,问题主要来源于配置文件的资源访问限制。好在嵌入式场景下 PulseAudio 一般不需要严格鉴权,因此权限问题通常不会造成严重影响。更常见的麻烦是只读文件系统导致 PulseAudio 无法创建或访问必要的文件(例如运行时状态、数据库文件等)。我总结的应对思路是:不要轻易去修改系统分区的挂载属性或权限,而是调整配置文件的存放路径,将它放到一个可写的位置,再通过编译时的宏或环境变量,让 PulseAudio 去那里查找即可,这样改动成本最低,也最安全。