PulseAudio

dongwang 发布于 10 天前 20 次阅读


前言

官方网站:
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 去那里查找即可,这样改动成本最低,也最安全。