Linux蓝牙之BlueALSA(轻量蓝牙音频后端)构建配置与使用

前言

前面有简单介绍 BlueALSA(轻量蓝牙音频后端):

目前没有现成的安装包,所以需要自己构建,今天记录下 BlueALSA 构建配置与使用,以下都已通过我的 AI Agent 团队验证测试。

目标环境:Linux Desktop,aarch64,kernel 5.4.18,BlueZ 5.62
交付版本:bluez-alsa v5.0.0 + sbc 2.2 + fdk-aac 2.0.2 + ldacBT 2.0.2.3 + libopenaptx 0.2.0 + liblc3 1.1.1
安装前缀:/usr/local(不动系统 /usr,BlueZ 与 PulseAudio 不受影响)


构建与安装:一切皆可选

BlueALSA 的构建系统充分体现了其“最小化”的设计目标。根据 INSTALL.md,它的许多特性都是可选的,只有在配置构建时显式请求才会被包含,因此 configure 选项非常多。

核心依赖

构建 BlueALSA 本身需要以下基础库:

  • alsa-lib >= 1.0.27
  • bluez >= 5.51
  • glib >= 2.58.2(需要 GIO 支持)
  • sbc >= 1.5

安装依赖

sudo apt-get install -y \
    git pkg-config autoconf automake libtool \
    libasound2-dev libglib2.0-dev libdbus-1-dev libbluetooth-dev libbsd-dev \
    libsbc-dev libspandsp-dev libreadline-dev libsystemd-dev libudev-dev \
    meson ninja-build cmake g++
sudo dpkg --configure -a          # 清掉半配置状态
包 用途
libasound2-dev ALSA 插件与 PCM 接口(必需)
libglib2.0-dev / libdbus-1-dev GLib 事件循环与 D-Bus 服务(必需)
libbluetooth-dev BlueZ 头文件,hci.h / sdp_lib.h(必需)
libsbc-dev SBC 编解码(必需,但版本不够,见 §2)
libbsd-dev strlcpy 等 BSD 扩展(必需)
libspandsp-dev mSBC 解码的丢包隐藏(PLC,codec-msbc.c 里的 plc_init/plc_free;启用 --enable-msbc 时必需)
libreadline-dev bluealsactl 交互补全
libsystemd-dev sd_notify 就绪通知(启用 --enable-systemd 时必需)
meson ninja-build 构建 liblc3
cmake g++ 构建 fdk-aac / ldacBT(fdk-aac 是 C++ 工程)

验证解析到的版本(v5.0.0 的下限):

pkg-config --modversion alsa glib-2.0 dbus-1 bluez    # 1.2.2 / 2.64.6 / 1.12.16 / 5.62

编译与构建

编译 sbc 2.2

bluez-alsa v5.0.0 要求 sbc ≥ 1.5,发行版源里只有 1.4,缺 sbc_reinit_a2dp 等符号。
从 kernel.org 取 sbc 2.2,装到 /usr/local 与系统 1.4 隔离:

git clone --depth 1 git://git.kernel.org/pub/scm/bluetooth/sbc.git
cd sbc && git checkout 2.2

./configure --prefix=/usr/local --disable-tools --disable-tester
make -j"$(nproc)"
sudo make install
sudo ldconfig

**不要装到 --prefix=/usr**:那样会覆盖 /usr/include/sbc/sbc.h 与 /usr/lib/libsbc.so,
但 ld.so 缓存里 multiarch 目录的 1.4 仍优先,链接期报
undefined reference to 'sbc_reinit_a2dp'。装 /usr/local 后系统 1.4 原样保留,
BlueZ / PulseAudio 继续用它,互不干扰。

验证:

PKG_CONFIG_PATH=/usr/local/lib/pkgconfig pkg-config --modversion sbc   # 2.2
grep -c sbc_reinit_a2dp /usr/local/include/sbc/sbc.h                   # 1

编译四个 codec 库(可选,用于 AAC / LDAC / aptX / LC3)

全部装到 /usr/local。只需要 SBC + mSBC 的话可以跳过本节。

库 版本 构建系统 命令要点
fdk-aac 2.0.2 cmake cmake -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_BUILD_TYPE=Release ..
ldacBT 2.0.2.3 cmake 先把 libldac 源码填入 ldacBT-2.0.2.3/libldac/;产物仅 libldacBT_enc/abr
libopenaptx 0.2.0 make make -j8 && sudo make install
liblc3 1.1.1 meson meson setup build --prefix=/usr/local -Dtools=true && ninja -C build

-Dtools **默认是 false**(见 meson_options.txt),必须显式写 -Dtools=true
才会编出 elc3 / dlc3 这两个 LC3 编解码自检工具(§3 末尾的 roundtrip 验证靠它们)。

ldacBT 的 CMakeLists 只声明了 LDAC_SOFT_FLOAT 一个 option,没有 LDAC_DECODE;
传 -DLDAC_DECODE=ON 只会变成一个无人读取的 UNINITIALIZED 缓存项,静默无效。
解码方向不存在的原因见下方说明。

liblc3 源码 tag 是 1.1.1,但 meson 工程版本号为 1.1.0,故 lc3.pc 报 lc3 1.1.0。

每个库装完执行 sudo ldconfig。验证:

export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/local/lib/aarch64-linux-gnu/pkgconfig
for m in fdk-aac ldacBT-enc ldacBT-abr libopenaptx lc3; do
    printf '%-14s %s\n' "$m" "$(pkg-config --modversion $m)"
done
# fdk-aac 2.0.2 / ldacBT-enc 2.0.2.3 / ldacBT-abr 2.0.2.3 / libopenaptx 0.2.0 / lc3 1.1.0

LDAC 只有编码方向:ldacBT-dec(解码库)Sony 未公开源码,开源 libldac 只含
encoder + ABR。因此 A2DP sink 收 LDAC 无解码,LDAC 只能用于 source(发送)方向。
configure 会自动检测:找不到 dec 就不定义 HAVE_LDAC_DECODE,不注册 LDAC sink 端点。

编译安装 bluez-alsa v5.0.0

v5.0.0 的 tarball 不含 configure 脚本,必须先 autoreconf:

tar xzf bluez-alsa-5.0.0.tar.gz && cd bluez-alsa-5.0.0
autoreconf --install --force

export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/local/lib/aarch64-linux-gnu/pkgconfig:/usr/lib/pkgconfig
export LDFLAGS="-L/usr/local/lib -Wl,-rpath,/usr/local/lib"
pkg-config --modversion sbc          # 必须打印 2.2,否则回到 §2

./configure \
    --prefix=/usr/local \
    --sysconfdir=/etc \
    --enable-msbc \
    --enable-rfcomm \
    --enable-a2dpconf \
    --enable-hcitop \
    --enable-ofono \
    --enable-systemd \
    --enable-aac \
    --enable-ldac \
    --enable-aptx \
    --enable-aptx-hd \
    --enable-lc3-swb \
    --with-libopenaptx \
    --with-systemdsystemunitdir=/lib/systemd/system \
    --with-systemdbluealsadargs="-S -p a2dp-source -p a2dp-sink -p hfp-ag" \
    --disable-test

make -j"$(nproc)"
sudo make install
sudo ldconfig
选项 作用
--enable-msbc HFP 宽带语音(依赖 spandsp)
--enable-rfcomm 编出 bluealsa-rfcomm,HFP 的 AT 命令通道
--enable-a2dpconf / --enable-hcitop 编出两个独立工具 a2dpconf(A2DP 编解码参数调优)与 hcitop(HCI 流量 top),源码在 utils/a2dpconf.c、utils/hcitop.c。不是 bluealsactl 的子命令
--enable-ofono 支持 ofono 后端(本机未装 ofono,实际走 native HFP-AG)
--enable-systemd sd_notify + 生成 bluealsa.service
--enable-aac/ldac/aptx/aptx-hd/lc3-swb 对应 codec;缺库则该 codec 静默不编入
--with-libopenaptx aptX 用开源实现(替代 Qualcomm 闭源库)
--with-systemdbluealsadargs 写进 unit 的默认启动参数

产物:

/usr/local/bin/{bluealsad,bluealsactl,bluealsa-aplay,bluealsa-rfcomm,a2dpconf,hcitop}
/usr/lib/aarch64-linux-gnu/alsa-lib/libasound_module_{pcm,ctl}_bluealsa.so
/usr/local/share/alsa/alsa.conf.d/20-bluealsa.conf
/usr/share/dbus-1/system.d/org.bluealsa.conf
/lib/systemd/system/{bluealsa,bluealsa-aplay}.service

a2dpconf / hcitop 由 --enable-a2dpconf / --enable-hcitop 产出,同装 /usr/local/bin,
卸载时一并删。

ALSA 插件不在 /usr/local 而在系统 multiarch 目录:configure.ac:390 的
--with-alsaplugindir 未传参时默认展开为 $(pkg-config --variable=libdir alsa)/alsa-lib
= /usr/lib/aarch64-linux-gnu/alsa-lib(实测吻合)。这是刻意行为——ALSA 只在该目录找插件,
装到 /usr/local 反而加载不到。而 20-bluealsa.conf 走 --prefix,落在
/usr/local/share/alsa/alsa.conf.d/,故 §5.1 必须建软链到 /etc/alsa/conf.d/ 才会被读取
(/etc/alsa/conf.d/ 里发行版自带的几项也都是指向 /usr/share/alsa/alsa.conf.d/ 的软链)。


配置:以 ALSA 为中心

基础配置:ALSA 插件注册

最简单的使用方式是,在 ALSA 配置文件(如 ~/.asoundrc 或 /etc/asound.conf)中定义一个 PCM 设备。
例如,使用预定义的 bluealsa PCM,它是一个 plug 类型设备,会自动处理音频格式转换:

pcm.bluealsa {
    type bluealsa
    DEV=01:23:45:67:89:AB  # 目标设备MAC地址
    PROFILE=a2dp            # 配置文件:a2dp 或 sco
    CODEC=aac               # 可选,强制使用特定编解码器
}

该 PCM 支持的关键参数包括:

  • DEV:蓝牙设备地址。默认值 00:00:00:00:00:00 表示选择最近连接该配置文件的设备。
  • PROFILE:a2dp(高质量音频)或 sco(用于 HFP/HSP,支持麦克风)。默认是 a2dp。
  • CODEC:指定编解码器,会覆盖自动协商的结果。默认 unchanged 表示使用现有设置。
  • VOL:初始音量。

简单操作可以使用复用自带的配置:

sudo mkdir -p /etc/alsa/conf.d
sudo ln -sf /usr/local/share/alsa/alsa.conf.d/20-bluealsa.conf /etc/alsa/conf.d/20-bluealsa.conf

之后,应用程序即可通过 bluealsa 这个 PCM 设备名来播放或录制音频,比如aplay -L | grep bluealsa 能看到该设备,-D bluealsa:... 可直接用。

启动守护进程

通常,bluealsad 由 systemd 等系统服务管理。你也可以手动启动它,并指定要启用的配置文件,例如:bluealsa -p a2dp -p hfp。

开启全部 codec:

--with-systemdbluealsadargs 写入的默认参数不含 --all-codecs,
v5.0.0 里非强制 codec 需要显式开启才会注册端点。用 dropin 覆盖:

sudo systemctl daemon-reload
sudo systemctl edit bluealsa      # 写入下面两行
[Service]
ExecStart=
ExecStart=/usr/local/bin/bluealsad -S --all-codecs -p a2dp-source -p a2dp-sink -p hfp-ag

ExecStart= 空行是必需的——先清空再赋值,否则 systemd 报重复定义。

sudo systemctl enable --now bluealsa
systemctl is-active bluealsa      # active

编解码器协商与限制

当设备连接时,BlueALSA 会自动协商可用的最佳编解码器。CODEC 参数允许你强制指定,但需注意:

  • 如果指定的编解码器不可用,插件会发出警告并回退到默认值。
  • 对于 HFP,BlueALSA 不支持从 HFP-HF 节点更改编解码器,只有 HFP-AG 节点可以更改。如果搭配 oFono 使用,该参数会失效。
  • 对于 A2DP,可以在编解码器名称后附加一个十六进制配置字符串来精确控制,例如 CODEC=SBC:FC450240 可以限制 SBC 只使用单声道或双声道模式。

PulseAudio冲突问题:释放 HFP UUID(关键,否则 HFP 注册失败)

PulseAudio 的蓝牙模块会与 bluealsad 抢注同一个 HFP UUID,症状是
UUID already registered in BlueZ / Couldn't register hands-free profile。
注释掉 /etc/pulse/default.pa 里的蓝牙发现模块:

sudo sed -i 's/^load-module module-bluetooth-discover/#load-module module-bluetooth-discover/' /etc/pulse/default.pa
grep -n 'module-bluetooth-discover' /etc/pulse/default.pa   # 行首应有 #

发行版里这行通常形如 load-module module-bluetooth-discover enable_msbc=false,
sed 的前缀匹配会保留后面的参数,不受影响。

module-bluez5-discover 不需要单独注释——/etc/pulse/default.pa 里没有它的独立行,
它是被 module-bluetooth-discover 内部加载的,注释前者即可一并停掉。

机器上可能有两个 PulseAudio 实例(用户级 + 由 ToDesk 之类服务拉起的 root 级),
二者都读同一份 /etc/pulse/default.pa,所以改这一处即可覆盖。
但已经在跑的 root 实例不会自动重载配置,需要让它重启(或直接 kill,
拉起它的服务会按新配置重拉)才真正释放 UUID。


使用

实用工具示例

  • **bluealsa-aplay**:此命令可将来自蓝牙麦克风的音频流实时播放到本地声卡上。最简单的用法是直接运行 bluealsa-aplay,它会捕获所有已连接蓝牙设备的音频并播放到默认的 ALSA 设备。
  • **bluealsactl**:用于通过 D-Bus 与 bluealsad 守护进程交互,可用于查询状态、管理连接等。

连接设备

bluetoothctl
  scan on
  pair <MAC>          # 首次
  trust <MAC>         # 建议,便于自动回连
  connect <MAC>
  quit

查看状态与 PCM

bluealsactl status          # 服务、适配器、各 profile 支持的 codec
bluealsactl list-pcms       # 当前已连接设备对应的 PCM 路径

status 实测输出:

Service: org.bluealsa
Version: v5.0.0
Adapters: hci0
Profiles:
  A2DP-source : SBC AAC aptX aptX-HD LDAC
  A2DP-sink   : SBC AAC aptX aptX-HD
  HFP-AG      : CVSD mSBC LC3-SWB

播放与录音

# A2DP 播放(source 方向)
aplay -D bluealsa:DEV=<MAC>,PROFILE=a2dp test.wav

# HFP 录音(AG 方向,麦克风;mSBC 不可用时先降级:bluealsactl codec <PCM> CVSD)
arecord -D bluealsa:DEV=<MAC>,PROFILE=sco -f S16_LE -r 8000 -c 1 rec.wav

PROFILE 只接受三个值(src/asound/bluealsa-pcm.c: str2profile):
a2dp / asha / sco,写 hfp 无效。方向(source/sink)不用指定——
插件按 capture/playback 自动选择对应 PCM。

运行时调参

PCM 的 D-Bus 对象路径由 src/ba-transport-pcm.c:99 拼接,形如:

/org/bluealsa/<hci>/dev_<MAC>/<profile段>/<source|sink>

例如 /org/bluealsa/hci0/dev_C4_60_0A_B8_DD_D8/a2dpsrc/sink。

profile 段取值(ba-transport-pcm.c:45-70 的 transport_get_dbus_path_type(),注意是缩写):

profile 路径段
A2DP source / sink a2dpsrc / **a2dpsnk**(不是 a2dpsink)
ASHA source / sink ashasrc / ashasnk
HFP AG / HF hfpag / hfphf
HSP AG / HS hspag / hsphs

最后一段是 PCM 的方向,指 PCM 对应用户侧的 capture 还是 playback,不是蓝牙空口方向,也与 profile 名字面不对应(ba-transport-pcm.h:21-26:MODE_SOURCE = capturing,MODE_SINK = playing;分配处 ba-transport.c:442-446、652-658):

  • A2DP source profile(本机发流给耳机):主通道 PCM 是给 aplay 用的→ a2dpsrc/sink(playback=MODE_SINK);back-channel(FastStream 双向时的收流)是 a2dpsrc/source
  • HFP-AG:扬声器(耳机出声,本机 playback)→ hfpag/sink;麦克风(耳机收音,本机 capture)→ hfpag/source——所以 arecord 麦克风录音操作的是 hfpag/source
  • 同一 profile 的两条 PCM:音量各自独立,但 codec 是共享的(见下方说明)

音量 vs codec 的作用域不同(容易误判,已对源码核实):

  • 音量是 PCM 级:struct ba_transport_pcm 自带 volume[8] 数组(ba-transport-pcm.h:144),
    所以 hfpag/sink(扬声器)与 hfpag/source(麦克风)的音量分别可调,bluealsactl volume 各管各的。
  • codec 是 transport 级,不是 PCM 级:属性 getter ba_variant_new_pcm_codec(pcm) 第一行就取
    const struct ba_transport *t = pcm->t,再调 ba_transport_get_codec(t)(bluealsa-dbus.c:281-284、
    ba-transport.c:1077);SelectCodec 方法同样落到 ba_transport_select_codec_sco(t, codec_id) /
    ba_transport_select_codec_a2dp(t, ...),且共用一把 t->codec_select_client_mtx
    (bluealsa-dbus.c:770 lock / 881 走 a2dp / 899 走 sco / 915 unlock,四行实测吻合;
    bluealsa_pcm_select_codec() 入口在 :755)。
  • 结构体层面更直接:struct ba_transport_pcm 的全部字段里没有任何 codec 身份字段
    (只有 codec_delay_dms / reported_codec_delay_dms 这类延迟量),而
    struct ba_transport 独占 uint32_t codec_id 与保护它的 codec_id_mtx
    (ba-transport.h:106-108)+ codec_select_client_mtx(:111)。
    即 codec 只有一份、挂在 transport 上,PCM 结构体里根本没有可各自保存的位置。
  • 变更会广播给两条 PCM:codec 一改,代码显式对两条都发 UPDATE_CODEC 通知——
    sco.c:242-252 对 pcm_spk 与 pcm_mic 各调一次 bluealsa_dbus_pcm_update(... | BA_DBUS_PCM_UPDATE_CODEC | ...);ba-rfcomm.c:1242-1246(SLC 建链时已选定 codec)
    同样两条都发。这解释了为什么改一次 codec,bluealsactl info 两边一起变。

同一 profile 的两条 PCM 是同一个 transport 的成员(A2DP: pcm 与 pcm_bc,
ba-transport.h:162;HFP: pcm_spk 与 pcm_mic,:209-210),
因此对任一条 PCM 设 codec 会同时影响另一条——bluealsactl codec <PCM> CVSD 之后,
扬声器和麦克风两条路径一起降到 CVSD,不是只降你指定的那条。

实践中先用 list-pcms 取到确切路径再操作:

命令 作用
bluealsactl list-services 列出所有 BlueALSA D-Bus 服务名(多实例/-B 后缀时用)
bluealsactl info <PCM> 查看 codec / 采样率 / 声道 / 延迟 / 音量
bluealsactl codec <PCM> [CODEC] 查询或切换 codec(如降级到 CVSD)
bluealsactl volume <PCM> [L] [R] 查询或设置音量
bluealsactl mute <PCM> 静音开关
bluealsactl client-delay <PCM> 客户端缓冲延迟
bluealsactl soft-volume <PCM> 软音量控制
bluealsactl monitor 实时打印 D-Bus 信号(调试用)
bluealsactl open <PCM> 通过 stdin/stdout 传原始 PCM

已知限制

mSBC / LC3-SWB 需要内核补丁

HFP 的 mSBC 与 LC3-SWB 在本机上无法出数据,dmesg 报:

Bluetooth: hci0: Device does not support ALT setting 6
Bluetooth: hci0: urb submission failed (90)      # 90 = EMSGSIZE

根因是内核 btusb 无 alt6 时 new_alts 留在 0,切到零带宽端点。
不是硬件限制,需要打上上游的修复补丁。

codec 限制

  • AAC / aptX / LDAC 这些 codec 都需要设备支持。
  • LDAC 仅 source 方向,sink 方向缺 Sony 未开放的解码库。

卸载

sudo systemctl disable --now bluealsa
sudo rm -f /etc/alsa/conf.d/20-bluealsa.conf
sudo rm -f /etc/systemd/system/bluealsa.service.d/all-codecs.conf
sudo rm -f /lib/systemd/system/bluealsa{,-aplay}.service
sudo rm -f /usr/share/dbus-1/system.d/org.bluealsa.conf
sudo rm -f /usr/local/bin/bluealsa{d,ctl,-aplay,-rfcomm} /usr/local/bin/{a2dpconf,hcitop}
sudo rm -f /usr/lib/aarch64-linux-gnu/alsa-lib/libasound_module_{pcm,ctl}_bluealsa.*
sudo rm -f /usr/local/lib/libsbc.so*                                  # 系统 1.4 在 /lib/aarch64-linux-gnu/,不受影响
sudo rm -rf /usr/local/include/sbc /usr/local/lib/pkgconfig/sbc.pc
# 若按 §3 装过四个 codec 库,一并卸掉(注意 liblc3 在 multiarch 目录,与其余三个不同)
sudo rm -f /usr/local/lib/libfdk-aac.* /usr/local/lib/pkgconfig/fdk-aac.pc
sudo rm -f /usr/local/lib/libopenaptx.* /usr/local/lib/pkgconfig/libopenaptx.pc
sudo rm -f /usr/local/lib/libldacBT_* /usr/local/lib/pkgconfig/ldacBT-*.pc
sudo rm -f /usr/local/lib/aarch64-linux-gnu/liblc3.so* \
           /usr/local/lib/aarch64-linux-gnu/pkgconfig/lc3.pc \
           /usr/local/bin/{elc3,dlc3,openaptxenc,openaptxdec}
sudo systemctl daemon-reload && sudo ldconfig

恢复 PulseAudio 蓝牙:去掉 /etc/pulse/default.pa 中那行的 # 前缀后重启 PA。