多个 rockit 进程同时播放,第二个直接报
Device or resource busy?TDM 8 通道声卡,想把 4 路麦 + 2 路回采参考 + 喇叭拆开用,不知道怎么配?
asound.conf 改完没生效,也不知道哪里错了?
做 Rockchip 音频开发,这些问题大概率都遇到过。本文用一篇的篇幅,把 asound.conf 从插件原理、TDM 分轨、采样率转换到上板部署全部讲透——全部来自真实工程,可直接抄作业。
上篇《RockChip Linux SDK ALSA 配置实战》讲了 UCM2(PulseAudio/PipeWire 用的设备切换),这篇讲 asound.conf(alsa-lib 的 PCM 插件链),两者是不同层面。
先看 ALSA 的三层结构:
┌─────────────────────────────────────────┐│ 应用层(aplay / rockit / 你的进程) │├─────────────────────────────────────────┤│ alsa-lib(libasound.so) ← asound.conf ││ 解析 PCM 名 → 构造插件链 │├─────────────────────────────────────────┤│ 内核驱动(snd-soc-rockchip-xxx)→ 硬件 │└─────────────────────────────────────────┘关键点:asound.conf 不改变硬件能力,只决定"应用看到的 PCM 如何被组装"。
当应用 open("default") 时,alsa-lib 读配置文件,把 default 这个名字解析成一条插件链,最终落到内核的 hw:X,Y 上:
应用 ⇄ plug(格式转换) ⇄ softvol(音量) ⇄ dmix(混音) ⇄ hw:0,0(内核)每一层插件对上层是一个虚拟 PCM,对下层用一个 slave PCM。你写的所有配置,本质就是在拼这条链。
配置文件放在哪?
/usr/share/alsa/alsa.conf ← alsa-lib 全局默认(别动) └─ /etc/asound.conf ← 系统级,板端生产环境就维护这个 └─ ~/.asoundrc ← 用户级,开发调试临时用板端只管 /etc/asound.conf。
ALSA 有十几个插件,但 Rockchip 音频工程里反复出现的就是这十个。记住它们,90% 的配置都能搞定。
最底层的插件,直接打开内核 PCM,不做任何转换。应用给的格式必须和硬件一致,否则报错。
pcm.hw_primary { type hw card rockchiprk809co}生产环境很少直接用 hw,一般套一层 plug 做格式兼容。
最常用的入口插件。应用给任意格式(S16/S24/任意采样率/任意声道),plug 自动协商转成 slave 能接受的格式。
pcm.speaker { type plug slave.pcm "playback_dshare"}上层应用只需要 open("speaker"),不用关心底下硬件要什么格式。
这是客户最常需要的能力。
没有 dmix 时,硬件 PCM 被第一个应用独占,第二个应用 open 直接失败:
终端1: aplay test.wav ← 成功,独占硬件终端2: aplay test2.wav ← 失败:Device or resource busydmix 通过共享内存 + 定时器,把多个客户端的 PCM 数据相加混音后一次性写给硬件:
对讲(rockit 线程) ─┐提示音(rockit 线程) ─┤报警音(rockit 线程) ─┼─→ "default" ─→ softvol ─→ dmix(混音) ─→ hw:0,0背景音乐(rockit 线程)┘所有客户端都 open 同一个 default,dmix 在底层把声音叠到一起。配置如下:
pcm.dmixer { type dmix ipc_key 5978290 # 共享内存标识,必须全系统唯一 ipc_key_add_uid true slowptr 0 slave { pcm "hw:0,0" channels 2 rate 48000 period_size 768 buffer_size 3072 } bindings { 0 0; 1 1 }}验证混音是否生效:直接用两个 aplay 同时播放,两路声音能叠加听到就说明通了,之后再接 rockit 进程。
关于 rockit:Rockchip IPC(网络摄像机)SDK 的多媒体框架,每路音频流(对讲、提示音、报警音)通常由独立的 rockit 线程处理。这些线程都 open 同一个
default,靠 dmix 自动混音。
dmix 的录音对偶。多个应用同时录音,alsa-lib 在软件层做分发。参数和 dmix 完全一致,只是数据方向反过来:
pcm.dsnooper { type dsnoop ipc_key 5978291 # 和 dmix 用不同的 key ipc_key_add_uid yes slave { pcm "hw:0,0" channels 2 rate 48000 } bindings { 0 0; 1 1 }}dshare 是 dmix 的"分轨"版本。不做混音,而是按 bindings 把不同客户端分配到硬件的不同通道。
pcm.playback_dshare { type dshare ipc_key 5678293 slowptr 0 slave { pcm "hw:0,0" channels 2 rate 48000 } bindings.0 0 # 客户端 ch0 → 硬件 ch0 bindings.1 1 # 客户端 ch1 → 硬件 ch1}这个最小示例和 dsnoop 长得很像,区别在于 dshare 是播放侧分轨,dsnoop 是录音侧分轨。通道数多了以后(TDM8/16),bindings 的分轨能力才真正发挥出来——第三章实战详解。
用 ttable(传输矩阵)把输入通道按增益系数混合/重定向到输出通道。常用于"立体声混到单声道""左右声道各开独立音量"等。
# 把两路输入混成单声道后送到输出 ch1(右声道)pcm.play_chn1 { type route slave { pcm "dmixer" channels 2 } ttable {0.1 0.5 # 输入0 → 输出1,增益 0.51.1 0.5 # 输入1 → 输出1,增益 0.5 }}ttable 格式是 <输入>.<输出> <增益>,增益范围 0.0~1.0。route 是唯一能改增益的通道操作插件,分通道独立音量就靠它 + softvol 组合实现。
把一个或多个 slave PCM 的通道按任意顺序组合/重排成一个新的虚拟 PCM。核心能力是重排和合并,不改增益、不做混音。
最常见的用法:麦阵通道重排。 硬件 TDM 的物理通道顺序和 3A 算法期望的顺序不一致时,用 multi 重排:
pcm.cap_multi { type multi slaves.a.pcm "hw:0,0" slaves.a.channels 4 bindings.0.slave a # 客户端 ch0 → slave a 的 ch0 bindings.0.channel 0 bindings.1.slave a # 客户端 ch1 → slave a 的 ch2(重排!) bindings.1.channel 2 bindings.2.slave a # 客户端 ch2 → slave a 的 ch1(重排!) bindings.2.channel 1}注意 multi 的 bindings 格式和 dshare/dsnoop 完全不同:每个客户端通道要同时指定 .slave(取自哪个 slave)和 .channel(取自该 slave 的哪个通道)。
multi 还能跨设备合并——把两个独立立体声 PCM 合并成一个 4 通道虚拟 PCM,这是 dshare/route 都做不到的独有能力。
在 PCM 链上插入软件音量控制,并在 ctl 层创建一个 named 音量控件(出现在 amixer / alsamixer 里)。硬件没有合适音量控件时靠它。
pcm.softvol_ply { type softvol slave.pcm "hooks_ply" control { name "MasterP Volume" card 0 } min_dB -40.0 max_dB 0.0 resolution 101 # 100 档 + 静音,配合 0~100 百分比较友好}关于 max_dB:
0.0,避免软件放大导致削波max_dB 设正值(如 20.0)做软件增益补偿PCM 被打开时,自动设置一组 mixer 控件到指定值。用于切换 codec 路径(扬声器/耳机切换、麦克风选择)。
pcm.hooks_ply { type hooks slave.pcm "dmixer" hooks.0 { type ctl_elems hook_args [{ name "Playback Path" preserve true # 关闭后恢复原值,避免路径残留 value "SPK" lock false }] }}关键点:
value "SPK" 是 codec 的枚举控件值,需查具体 codec 的 mixer 定义(常见值:SPK/HP/SPK_HP/Main Mic)preserve true:PCM 关闭后恢复原值让 default 这个名字同时支持播放和录音,各走各的链路:
pcm.!default { type asym playback.pcm "plug_ply"# aplay -D default 走这条(softvol → hooks → dmix → 喇叭) capture.pcm "plug_cap"# arecord -D default 走这条(softvol → hooks → dsnoop → 麦克风)}不用 asym 的话,!default 只能定义成一个方向的 PCM——定义成 dmix 则 arecord -D default 失败,定义成 dsnoop 则 aplay -D default 失败。应用就得自己区分播放用哪个名字、录音用哪个名字。
套了 asym 后,上层应用无脑用 default,alsa-lib 自动按方向选链路。生产配置几乎都这么写。
dmix/dshare/route/multi 都涉及通道操作,最容易搞混。一张表理清:
| dmix | ||||
| dshare | ||||
| route | 是 | |||
| multi | 是 |
简单说:混音用 dmix,分轨用 dshare,要改增益用 route,要重排或跨设备合并用 multi。
这是 Rockchip 音频工程里最有技术含量的部分。TDM 多通道硬件,怎么把通道拆给不同用途?有三个典型场景,按复杂度递进。
硬件是 4 通道,前两声道接麦阵,后两声道接 linein,要拆给不同应用录音。
用 dsnoop(录音侧的分轨用 dsnoop,不是 dshare——dshare 是播放侧的):
pcm_slave.ins { pcm "hw:0,0" channels 4 rate 16000 format S16_LE period_size 320 buffer_size 1280}# mic 用前两声道pcm.pcmin1_dsnoop { type dsnoop ipc_key 5978291 # 同一物理设备的多个 dsnoop 共用 key slowptr 0 slave ins bindings.0 0 # 客户端 ch0 → 硬件 ch0 bindings.1 1 # 客户端 ch1 → 硬件 ch1}# linein 用后两声道pcm.pcmin2_dsnoop { type dsnoop ipc_key 5978291 # 同一物理设备,共用同一 key slowptr 0 slave ins bindings.0 2 # 客户端 ch0 → 硬件 ch2 bindings.1 3 # 客户端 ch1 → 硬件 ch3}pcm.micin { type plug; slave.pcm "pcmin1_dsnoop" }pcm.linein { type plug; slave.pcm "pcmin2_dsnoop" }关键点:两个 dsnoop 共用同一个 ipc_key 和 pcm_slave.ins,物理设备只开一次,靠 bindings 把不同通道分给不同虚拟 PCM。
这是最经典的 IPC 场景:TDM8 录音放音共用,播放只要 2ch 喇叭,录音要 4MIC + 2REF(AEC 回采参考)。
TDM8 (8通道)├── 播放(TX)方向:dshare 占 ch0/1(喇叭),其余留空└── 录音(RX)方向:dsnoop 占 ch0~5(4MIC + 2REF)播放用 dshare,录音用 dsnoop,两者共用同一个 pcm_slave(定义总通道数 8):
pcm_slave.tdm8 { pcm "hw:0,0" channels 8 # TDM8 共 8 通道 rate 48000 format S16_LE period_size 960 buffer_size 3840}# 播放(TX)只占 ch0/1(喇叭)pcm.playback_dshare { type dshare ipc_key 5678293# dshare 用 5678293 slowptr 0 slave tdm8 bindings.0 0 bindings.1 1}# 录音(RX)占 4MIC + 2REF,共 6 通道pcm.capture_dsnoop { type dsnoop ipc_key 5678294# dsnoop 用不同 key(不同共享实例) slowptr 0 slave tdm8 # 共用同一 slave,物理设备只开一次 bindings.0 0 # MIC0 bindings.1 1 # MIC1 bindings.2 2 # MIC2 bindings.3 3 # MIC3 bindings.4 4 # REF0(AEC 参考,回采播放左声道) bindings.5 5 # REF1(AEC 参考,回采播放右声道)}pcm.speaker { type plug; slave.pcm "playback_dshare" }pcm.mic_ref { type plug; slave.pcm "capture_dsnoop" }几个关键点:
8 通道 = 4 个立体声对,每路播完全不同的内容。这是 dshare 的看家本领:
# 4 路独立立体声,分别走 ch0/1、ch2/3、ch4/5、ch6/7pcm.play1 { type dshare; ipc_key 5678293; slowptr 0; slave tdm8; bindings.0 0; bindings.1 1 }pcm.play2 { type dshare; ipc_key 5678293; slowptr 0; slave tdm8; bindings.0 2; bindings.1 3 }pcm.play3 { type dshare; ipc_key 5678293; slowptr 0; slave tdm8; bindings.0 4; bindings.1 5 }pcm.play4 { type dshare; ipc_key 5678293; slowptr 0; slave tdm8; bindings.0 6; bindings.1 7 }应用 A open play1、应用 B open play2……各走各的通道对,输出完全不同的声音。这是 dmix 做不到的——dmix 会把所有输入混在一起。
这里 4 个 dshare 共用同一个 ipc_key,因为它们共享同一个物理设备的同一块缓冲,靠 bindings 区分各自通道。
这是个容易被忽略的性能问题。看每个 sample 的实际操作:
| dshare / dsnoop | 零拷贝 | ||
| dmix |
真实案例:16 通道声卡从 route+dmix 改成 dshare 分轨后,CPU 占用从 97% 降到 1~3%。
常见错误:在 dmix 上叠 route 来分轨——先把每路扩成 6/8 通道,再全部 mix 回去,白白做了乘法和加法。正确做法是直接用 dshare 分轨,零拷贝跳过所有算术。
应用给的采样率(如 44.1kHz 音乐)和硬件支持的(如 48kHz)不一致时,必须转换。这部分最容易踩坑。
plug 在应用和 slave 之间协商:format(S16/S24/...)、channels、rate。只要有一个不一致,就插入转换。
采样率转换用哪个算法?由配置文件最顶部的这行决定:
defaults.pcm.rate_converter "speexrate"Rockchip 工程普遍设为 speexrate。
linear | |||
speexrate | alsa-plugins | ||
speexrate_best | |||
samplerate_best |
这是最阴的坑:alsa-lib 本身只内置
linear转换器,speexrate/samplerate来自独立的 alsa-plugins 包。如果系统没装 alsa-plugins,你写的defaults.pcm.rate_converter "speexrate"会静默 fallback 到 linear,不报错。
表现是采样率转换质量明显下降(高频失真),但日志里看不到任何提示。
怎么确认?
ls /usr/lib/alsa-lib/ | grep -E "speex|samplerate"没输出就是没装。buildroot 工程确认 defconfig 里有:
BR2_PACKAGE_ALSA_PLUGINS=y如果你同时在维护 Android 和 Linux 工程,会发现:Android 用 C 头文件描述 codec 路径,Linux 用 asound.conf 的 hooks 描述。两者本质相同——都是"在打开 PCM 时设置一组 mixer 控件"。
Android HAL 的路由表长这样(codec_config/config.h):
struct config_control {const char *ctl_name; // mixer 控件名const char *str_val; // 枚举控件的字符串值const int int_val[2]; // int 控件的左/右值};struct config_route {const int sound_card; // 声卡号const int devices; // 设备位图const struct config_control *controls;const unsigned controls_count;};从它生成 asound.conf 的 5 步法:
(sound_card, devices) 组合 → 决定几张卡。蓝牙等外设走独立声卡speaker_normal → hooks_speaker)int_val 与 str_val → int 用数字,str 用字符串!default → 指向最常用的播放/录音 PCM举个真实例子。rk730_config.h 里主麦克风的路径:
// HAL 描述main_mic_capture: ADCSel Mux = 1(left) MIC2 Boost Volume = 3翻译成 asound.conf:
pcm.hooks_main_mic { type hooks slave.pcm "dsnooper" hooks.0 { type ctl_elems hook_args [ { name "ADCSel Mux"; preserve true; value 1; lock false } { name "MIC2 Boost Volume"; preserve true; value 3; lock false } ] }}对照规则:
.ctl_name + .int_val = {N} | {name "...", value N} |
.ctl_name + .str_val = "X" | {name "...", value "X"} |
.ctl_name + .int_val = {L, R} | {name "...", value.0 L, value.1 R} |
蓝牙 route 特殊处理:蓝牙走独立声卡(
card btsco),不通过主卡的 mixer 控件。翻译时单独定义pcm.hw_bt { type hw; card btsco },不和主路径混用 dmix。
写完配置不等于能用。以下是 Rockchip 平台上出现频率最高的五个坑,每一个都附排查方法。
现象:
ALSA lib pcm_direct.c:(snd_pcm_direct_initialize_poll_fd) unable to open timer 'hw:CLASS=3,SCLASS=0,CARD=0,DEV=0,SUBDEV=1'arecord: audio open error: No such file or directory根因:dmix/dsnoop/dshare 需要 ALSA timer 做多进程同步。报错里的 CLASS=3 是 PCM timer。检查 /dev/snd/ 发现没有 timer 设备——kernel 没开 CONFIG_SND_PCM_TIMER。
部分嵌入式 SDK 为了省 ~20KB 内存关掉了这个选项。
解决:kernel defconfig 开启:
CONFIG_SND_PCM_TIMER=y # 直接修复项,补回 PCM timerCONFIG_SND_HRTIMER=y # 推荐后备,全局精确时钟CONFIG_HIGH_RES_TIMERS=y # SND_HRTIMER 的依赖验证:
ls /dev/snd/timer # 应出现该设备节点arecord -D plug:main_mic -d 2 test.wav # 不再报 timer 错误现象:主卡 dshare 正常,蓝牙那路 dshare 打不开或行为异常。
根因:ipc_key 相同 → 两个 dshare attach 到同一块共享内存,按第一个 opener 的硬件参数协作。跨物理设备(主卡 vs btsco)共用 key,第二个会用到错误的 DMA buffer 参数。
记住:dshare "多实例可共用同一 key"的前提是同一物理设备。跨卡必须用不同 key。
排查:
ipcs -m # 查看所有共享内存段ipcs -m | grep 0x56 # 按 ipc_key 的十六进制过滤解决:每个物理设备的 dshare/dsnoop 用独立 ipc_key。推荐取值:dmix 从 5978290 起、dsnoop 从 5978291 起、dshare 从 5678293 起,同类型递增分配。
现象:PCM 能播放,但 Playback Path 控件没被设置,声音从错误的通道出来。
最常见原因:softvol 绕过了 hooks。错误写法:
pcm.softvol_ply { slave.pcm "dmixer" # ← 错!跳过了 hooks_ply}正确链路应该是 softvol → hooks → dmix,hooks 夹在中间。softvol 直接接 dmix,hooks_ply 永远不会被打开,Playback Path 控件不会被设置。
pcm.softvol_ply { slave.pcm "hooks_ply" # ← 对,经过 hooks 再到 dmix}其他原因:ctl_name 拼错(codec 没这个控件);枚举控件 value 写成了数字(必须用字符串)。
现象:amixer -c 0 contents 里找不到 softvol 定义的那个 Master Volume 控件。
根因:softvol 在 PCM 首次被打开时才注册 ctl。改完配置后没人打开过 PCM,控件就不存在。
解决:
alsa restore # 或手动触发一次 openaplay -d 1 /dev/zero # 触发 PCM openamixer -c 0 contents # 再查,控件出现了看到 bindings.0 0 / bindings.1 0(两路绑同通道)要区分两种场景:
bindings.0 0 / bindings.1 1配置写完、踩坑填完,最后一步:让调好的音量重启不丢。
softvol 创建的音量控件、codec 的 mixer 设置(Playback Path、MIC2 Boost Volume)都存在运行时内存里,重启恢复默认值,产线/用户调好的参数全丢。
Rockchip 用两个 state 文件,兼顾"出厂默认"和"用户自定义":
/userdata/asound.state | ||
/etc/asound.state |
/userdata 分区恢复出厂才清空,平时调的音量不丢;/etc 是只读 rootfs,存出厂默认。
开机时由 init 脚本 /etc/init.d/S49alsa 自动 restore:
#!/bin/shcase "$1" in start)if [ -f "/userdata/asound.state" ]; then alsactl restore -f /userdata/asound.stateelse alsactl restore -f /etc/asound.state # 首次开机用出厂默认fi ;; stop) alsactl store -f /userdata/asound.state # 关机存盘 ;;esac完整生命周期:
烧录固件 → 首次开机(无 /userdata/asound.state) → S49alsa 用 /etc/asound.state 出厂默认恢复 → 产线/用户调音 → 关机时 S49alsa stop 存到 /userdata/asound.state → 之后每次开机都用 /userdata/asound.state 恢复
S49是 init 脚本序号,ALSA restore 要在文件系统挂载后、音频服务启动前,放 S40~S50 区间合适。
一篇文章讲完了 asound.conf 从写到部署的全流程,记住这几个核心:
pcm_slave 定义总通道数,靠 bindings 各占不同通道CONFIG_SND_PCM_TIMER,dmix/dsnoop 全罢工本文所有示例均来自 Rockchip 真实音频工程,配置经多平台验证可直接参考复用。