Skip to content

Instantly share code, notes, and snippets.

@jpasquier
Last active October 5, 2025 13:23
Show Gist options
  • Save jpasquier/65e95707089f79d9406fa8e7f9e96eb0 to your computer and use it in GitHub Desktop.
Save jpasquier/65e95707089f79d9406fa8e7f9e96eb0 to your computer and use it in GitHub Desktop.

PipeWire BlueZ5 AAC Codec Builder (Debian Trixie)

This gist provides a script to build and package the AAC Bluetooth codec plugin (libspa-codec-bluez5-aac.so) for PipeWire on Debian trixie.

Debian does not ship AAC support in PipeWire by default (due to licensing), but the library can be built from source if you have libfdk-aac available. This script automates the whole process using Docker and produces a .deb that installs the codec in the correct multiarch path.


Features

  • Detects the installed PipeWire/SPA version automatically (libspa-0.2-bluetooth).
  • Downloads the matching upstream tarball from Debian sources.
  • Builds only the AAC codec inside a clean debian:trixie-slim Docker image.
  • Packages the resulting .so into a minimal .deb.
  • Ensures correct multiarch triplet path (/usr/lib/x86_64-linux-gnu/spa-0.2/bluez5/ on amd64, /usr/lib/aarch64-linux-gnu/... on arm64, etc.).

Requirements

On the host system:

  • Debian trixie (or derivative)
  • docker
  • dpkg-deb
  • fakeroot
  • dpkg-architecture

Install requirements:

sudo apt install docker.io dpkg-dev fakeroot

Make sure Docker is running:

sudo systemctl start docker

Usage

  1. Clone the gist:

    gh gist clone <gist-id>
    cd <gist-id>
  2. Run the build script:

    ./build-libspa-codec-bluez5-aac.sh
  3. Install the generated package:

    sudo apt install ./libspa-codec-bluez5-aac_<version>_amd64.deb
  4. Restart services (or reboot the system):

    systemctl --user restart pipewire pipewire-pulse wireplumber
    sudo systemctl restart bluetooth

License

This gist is WTFPL-licensed. AAC support uses FDK AAC, which has its own licensing terms.

Source: StackExchange

#!/bin/bash
set -euo pipefail
# ── Preconditions ────────────────────────────────────────────────────────────
need() { command -v "$1" >/dev/null 2>&1 || { echo "$1 is required." >&2; exit 1; }; }
need docker
need dpkg-deb
need fakeroot
need dpkg-architecture
if ! docker info > /dev/null 2>&1; then
echo "This script uses docker, and it isn't running - please start docker and try again!" >&2
exit 1
fi
# ── Detect versions from the system ──────────────────────────────────────────
# Prefer the installed libspa-0.2-bluetooth version; fallback to pipewire if needed.
DEB_VER="$(dpkg-query -W -f='${Version}\n' libspa-0.2-bluetooth 2>/dev/null || true)"
if [ -z "${DEB_VER}" ] || [ "${DEB_VER}" = "unknown" ]; then
DEB_VER="$(dpkg-query -W -f='${Version}\n' pipewire 2>/dev/null || true)"
fi
if [ -z "${DEB_VER}" ] || [ "${DEB_VER}" = "unknown" ]; then
echo "Could not detect installed PipeWire/SPA version (libspa-0.2-bluetooth or pipewire). Install PipeWire first." >&2
exit 1
fi
# Upstream tarball version (strip Debian revision like '-1', '-1+b1', etc.)
UPSTREAM_VER="${DEB_VER%%-*}"
# Some Debian versions may not have a hyphen; ensure UPSTREAM_VER non-empty.
if [ -z "${UPSTREAM_VER}" ]; then
UPSTREAM_VER="${DEB_VER}"
fi
# ── Setup ────────────────────────────────────────────────────────────────────
CURRENT_DIR="$(pwd)"
BUILD_DIR="$(mktemp -d "${TMPDIR:-/tmp}/build-pipewire-aac.XXXXXXXX")"
trap 'rm -rf "$BUILD_DIR"' EXIT
IMAGE_NAME="pipewire-aac"
CONTAINER_NAME="pipewire-aac"
# Architecture / multiarch triplet
ARCH="$(dpkg --print-architecture 2>/dev/null || echo amd64)"
TRIPLET="$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null || gcc -dumpmachine || echo "${ARCH}-linux-gnu")"
echo "Detected libspa/pipewire version:"
echo " Debian package version: ${DEB_VER}"
echo " Upstream (tarball) ver: ${UPSTREAM_VER}"
echo
# ── Dockerfile ───────────────────────────────────────────────────────────────
# Keep variables inside the Dockerfile literal (use ARG/ENV inside Dockerfile).
cat > "$BUILD_DIR/Dockerfile" << 'EOL'
FROM debian:trixie-slim
ARG PIPEWIRE_VER
ENV PIPEWIRE_VER=${PIPEWIRE_VER}
ENV DEBIAN_FRONTEND=noninteractive
ENV USER=pipewire
# Enable contrib + non-free (+ non-free-firmware) for trixie
RUN printf "deb http://deb.debian.org/debian trixie main contrib non-free non-free-firmware\n\
deb http://deb.debian.org/debian trixie-updates main contrib non-free non-free-firmware\n\
deb http://security.debian.org/debian-security trixie-security main contrib non-free non-free-firmware\n" > /etc/apt/sources.list
# Update and install necessary dependencies (build + fetch tools)
RUN apt-get update && apt-get install -y \
build-essential \
meson ninja-build pkgconf \
libbluetooth-dev \
libasound2-dev \
libdbus-1-dev \
libglib2.0-dev \
libudev-dev \
libsbc-dev \
libopus-dev \
liblc3-dev \
libldacbt-enc-dev \
libfdk-aac-dev \
git \
wget \
python3 \
&& rm -rf /var/lib/apt/lists/*
# Create a non-root user and a drop location for the built .so
RUN useradd -ms /bin/bash "$USER" \
&& mkdir /pipewire-aac \
&& chown "$USER":"$USER" /pipewire-aac
USER $USER
WORKDIR /home/$USER
# Fetch the exact upstream PipeWire tarball
RUN mkdir -p /home/$USER/build/pipewire-aac
WORKDIR /home/$USER/build/pipewire-aac
RUN wget -O pipewire_${PIPEWIRE_VER}.orig.tar.bz2 \
"https://deb.debian.org/debian/pool/main/p/pipewire/pipewire_${PIPEWIRE_VER}.orig.tar.bz2"
RUN tar xf pipewire_${PIPEWIRE_VER}.orig.tar.bz2
WORKDIR /home/$USER/build/pipewire-aac/pipewire-${PIPEWIRE_VER}
# Configure only the BlueZ5 AAC codec and build
RUN meson setup build \
-Dbluez5=enabled \
-Dbluez5-codec-aac=enabled
RUN ninja -C build
# Copy the built library to the shared location (robust to path changes)
RUN bash -lc 'set -euo pipefail; \
f=$(find build -type f -name libspa-codec-bluez5-aac.so | head -n1); \
if [ -z "$f" ]; then \
echo "libspa-codec-bluez5-aac.so not found under build/"; \
echo "Available bluez5 codec artifacts:"; \
find build -type f -name "libspa-codec-bluez5-*.so" -print || true; \
exit 1; \
fi; \
install -Dm644 "$f" /pipewire-aac/libspa-codec-bluez5-aac.so; \
ls -l /pipewire-aac/libspa-codec-bluez5-aac.so'
EOL
# ── Build and extract ────────────────────────────────────────────────────────
docker build --network=host --build-arg "PIPEWIRE_VER=${UPSTREAM_VER}" -t "$IMAGE_NAME" "$BUILD_DIR"
# Create a container and run it (it will exit immediately, which is fine for docker cp)
docker run --name "$CONTAINER_NAME" "$IMAGE_NAME" || true
# Copy the binary from the container to the BUILD_DIR
docker cp "$CONTAINER_NAME":/pipewire-aac/libspa-codec-bluez5-aac.so "$BUILD_DIR/"
# Stop and remove the container; remove the image
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
docker image rm "$IMAGE_NAME" >/dev/null 2>&1 || true
# Ensure the .so exists
if [ ! -f "$BUILD_DIR/libspa-codec-bluez5-aac.so" ]; then
echo "Failed to extract libspa-codec-bluez5-aac.so from the container." >&2
exit 1
fi
# Fix ownership if run with sudo
if [ "${SUDO_USER:-}" != "" ] && [ "$EUID" -eq 0 ]; then
chown "$SUDO_USER:$SUDO_USER" "$BUILD_DIR/libspa-codec-bluez5-aac.so"
fi
# ── Package .deb ─────────────────────────────────────────────────────────────
PKG_NAME="libspa-codec-bluez5-aac_${UPSTREAM_VER}_${ARCH}"
PKG_DIR="$BUILD_DIR/$PKG_NAME"
mkdir -p "$PKG_DIR/DEBIAN" \
"$PKG_DIR/usr/lib/$TRIPLET/spa-0.2/bluez5"
# Place the built .so in the Debian-expected location
cp "$BUILD_DIR/libspa-codec-bluez5-aac.so" \
"$PKG_DIR/usr/lib/$TRIPLET/spa-0.2/bluez5/libspa-codec-bluez5-aac.so"
# Control file
# - Package version = upstream (e.g., 1.4.2) — it's the plugin's build version.
# - Depends on libspa-0.2-bluetooth (>= DEB_VER) to match the exact ABI we built against.
cat > "$PKG_DIR/DEBIAN/control" <<CTRL
Package: libspa-codec-bluez5-aac
Version: ${UPSTREAM_VER}
Section: libs
Priority: optional
Architecture: ${ARCH}
Depends: libfdk-aac2t64 (>= 2.0.3), libspa-0.2-bluetooth (>= ${DEB_VER})
Maintainer: Your Name <[email protected]>
Description: AAC codec plugin for PipeWire BlueZ5 (built from PipeWire ${UPSTREAM_VER})
Installs /usr/lib/${TRIPLET}/spa-0.2/bluez5/libspa-codec-bluez5-aac.so
CTRL
# Build the package
fakeroot dpkg-deb --build "$PKG_DIR" >/dev/null
# Move final .deb to the original working directory
FINAL_DEB="${PKG_DIR}.deb"
mv -f "$FINAL_DEB" "$CURRENT_DIR/"
# Fix ownership of the .deb if run with sudo
if [ "${SUDO_USER:-}" != "" ] && [ "$EUID" -eq 0 ]; then
chown "$SUDO_USER:$SUDO_USER" "$CURRENT_DIR/$(basename "$FINAL_DEB")"
fi
echo
echo "✅ Built: $(basename "$FINAL_DEB")"
echo " Location: $CURRENT_DIR/$(basename "$FINAL_DEB")"
echo " To install: sudo apt install ./$(basename "$FINAL_DEB")"
@jpasquier
Copy link
Author

Hello, the script only builds a Debian package. When the package is built, you still have to install it with sudo apt install ./libspa-codec-bluez5-aac_0.3.65_amd64.deb. After that, you're done.

@BugZappa
Copy link

Has this been tested on Debian 13 (Trixie)? I followed a different guide to install the aac.so file but since upgrading from 12 to 13 i have yet been able to get the aac codec working again.

@BugZappa
Copy link

BugZappa commented Sep 29, 2025

i chose to be the guinea pig, appears i had already tried your script, no luck it seems.

user@debian:~/Downloads/bluez5$ sudo ./build-aac.sh 
--2025-09-28 18:47:12--  https://ppa.launchpadcontent.net/aglasgall/pipewire-extra-bt-codecs/ubuntu/pool/main/p/pipewire/libspa-0.2-bluetooth_0.3.65-4%7Eglasgall1_amd64.deb
Resolving ppa.launchpadcontent.net (ppa.launchpadcontent.net)... 185.125.190.80, 2620:2d:4000:1::81
Connecting to ppa.launchpadcontent.net (ppa.launchpadcontent.net)|185.125.190.80|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 341944 (334K) [application/x-debian-package]
Saving to: ‘libspa-0.2-bluetooth_0.3.65-4~glasgall1_amd64.deb’

libspa-0.2-bluetooth_0.3.65-4~glasgall1_amd64.deb 100%[==========================================================================================================>] 333.93K   724KB/s    in 0.5s    

2025-09-28 18:47:14 (724 KB/s) - ‘libspa-0.2-bluetooth_0.3.65-4~glasgall1_amd64.deb’ saved [341944/341944]

dpkg-deb: building package 'libspa-codec-bluez5-aac' in 'libspa-codec-bluez5-aac_0.3.65_amd64.deb'.
user@debian:~/Downloads/bluez5$ ls
total 40
drwxrwxr-x  3 user user  4096 Sep 28 18:47 .
drwxr-xr-x 16 user user 12288 Sep 28 18:37 ..
drwxr-xr-x 24 user user  4096 Sep 28 12:26 bluez-5.66
-rwxrwxr-x  1 user user  1118 Sep 28 18:46 build-aac.sh
-rw-r--r--  1 root   root   12632 Sep 28 18:47 libspa-codec-bluez5-aac_0.3.65_amd64.deb
user@debian:~/Downloads/bluez5$ sudo apt install libsp
Display all 120 possibilities? (y or n)^C
user@debian:~/Downloads/bluez5$ ^C
user@debian:~/Downloads/bluez5$ sudo apt install libspa
libspa-0.2-bluetooth     libspa-0.2-libcamera     libspandsp2t64           libsparsehash-dev        libspatial4j-0.4-java    libspatialaudio-dev      libspatialindex-dev
libspa-0.2-dev           libspa-0.2-modules       libspandsp-dev           libsparskit2.0           libspatial4j-java        libspatialindex8         libspatialite8t64
libspa-0.2-jack          libspa-codec-bluez5-aac  libspandsp-doc           libsparskit-dev          libspatialaudio0t64      libspatialindex-c8       libspatialite-dev
user@debian:~/Downloads/bluez5$ sudo apt install ./libspa-codec-bluez5-aac_0.3.65_amd64.deb 
Note, selecting 'libspa-codec-bluez5-aac' instead of './libspa-codec-bluez5-aac_0.3.65_amd64.deb'
libspa-codec-bluez5-aac is already the newest version (0.3.65).
Summary:
  Upgrading: 0, Installing: 0, Removing: 0, Not Upgrading: 0
user@debian:~/Downloads/bluez5$ sudo apt reinstall ./libspa-codec-bluez5-aac_0.3.65_amd64.deb 
Note, selecting 'libspa-codec-bluez5-aac' instead of './libspa-codec-bluez5-aac_0.3.65_amd64.deb'
Summary:
  Upgrading: 0, Installing: 0, Reinstalling: 1, Removing: 0, Not Upgrading: 0
  Download size: 0 B / 12.6 kB
  Space needed: 0 B / 55.3 GB available

Get:1 /home/user/Downloads/bluez5/libspa-codec-bluez5-aac_0.3.65_amd64.deb libspa-codec-bluez5-aac amd64 0.3.65 [12.6 kB]
(Reading database ... 475985 files and directories currently installed.)
Preparing to unpack .../libspa-codec-bluez5-aac_0.3.65_amd64.deb ...
Unpacking libspa-codec-bluez5-aac:amd64 (0.3.65) over (0.3.65) ...
Setting up libspa-codec-bluez5-aac:amd64 (0.3.65) ...
Notice: Download is performed unsandboxed as root as file '/home/user/Downloads/bluez5/libspa-codec-bluez5-aac_0.3.65_amd64.deb' couldn't be accessed by user '_apt'. - pkgAcquire::Run (13: Permission denied)
image

@jpasquier
Copy link
Author

I suppose that the library has been updated upstream. Somebody proposed an updated version based on my gist: https://blog.fernvenue.com/archives/airpods-pro-2-on-debian/. Thanks to him/her.

@ibgp2
Copy link

ibgp2 commented Oct 2, 2025

I face the same issue as @BugZappa; the AAC codec doesn't show.

I suppose that the library has been updated upstream. Somebody proposed an updated version based on my gist: https://blog.fernvenue.com/archives/airpods-pro-2-on-debian/. Thanks to him/her.

@jpasquier There is no update in this blog with respect your gist. Moreover, the author should have reported the commands evoked by @tlk which would have been better, e.g., if you plan to update your gist.

For now, I guess we have to follow these steps. If someone can detail those steps to rebuild libspa-codec-bluez5-aac*.deb from scratch under Debian, it would be helpful.

@jpasquier
Copy link
Author

I’ve updated the gist. On Debian trixie the PipeWire version is 1.4.2, which I couldn’t find prebuilt on Ubuntu, so it needs to be built manually. To avoid cluttering the host system with build dependencies, I’m using a Docker image for the build. Tested and working on my setup.
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment