Building from Source
Table of contents
- Requirements
- Quick Build
- If You Already Cloned Without
--recursive - Project Structure
- Tests
- Release Packages
- Testing Without Hardware (tools/emu)
- Dependencies
Requirements
- Rust (stable, 1.90 or later) via rustup
- Visual Studio 2022 or later (or its Build Tools) with the “Desktop development with C++” workload: the C libraries (BTstack, the codecs) are compiled with MSVC
- Node.js 22 and pnpm for the UI (
corepack enableprovides pnpm) - Git (for the submodules)
- PowerShell 7 (
pwsh) forbuild.ps1/build.bat(winget install Microsoft.PowerShell) - For ARM64 builds: the Visual Studio components “MSVC v143 - VS 2022 C++ ARM64 build tools” and, on an x64 PC, “C++ Clang Compiler for Windows” (rustls’
ringassembles its ARM64 code with clang;installer/build-release.ps1finds Visual Studio’s clang itself), andrustup target add aarch64-pc-windows-msvc
Quick Build
git clone --recursive https://github.com/SeiyaFunaokaJP/A2DP-Windows-Bridge.git
cd A2DP-Windows-Bridge
pwsh ./build.ps1
Or double-click build.bat in Explorer: it runs build.ps1 and waits for a key at the end (build.bat -Test passes the switches on). build.ps1 first checks that Rust, Node.js, pnpm and the submodules are there, runs pnpm install when node_modules is missing, builds the window’s page and then the three programs into target/release/: a2dpwb-core.exe (the resident core: Bluetooth, audio, tray), a2dpwb.exe (the window) and a2dpwb-driver.exe (the elevated helper that assigns / restores the WinUSB driver). Keep the three in one folder; a2dpwb-core.exe --cli streams without the window.
Switches: -Debug (dev profile, target/debug/), -Test (then the checks under Tests), -Decoder (also the decoder tool), -Emu (also the emulator test), -Release -Arch x64|arm64 (only the release script, see Release Packages).
Step by step (what build.ps1 does; the window’s page must be built before a2dpwb.exe, which embeds it):
cargo build --release -p a2dpwb-core-exe -p a2dpwb-driver
cd app/src/ui
pnpm install --frozen-lockfile
pnpm build
cd ../../..
cargo build --release -p a2dpwb-ui
If You Already Cloned Without --recursive
git submodule update --init --recursive
Project Structure
A2DP-Windows-Bridge/
├── app/
│ ├── src/
│ │ ├── core/ a2dpwb-core.exe: resident core, tray, IPC server, --cli
│ │ ├── ui/ a2dpwb.exe: Tauri 2 + SolidJS (pnpm dev runs it in a browser with a mock core)
│ │ ├── driver/ a2dpwb-driver.exe: WinUSB assign / restore (elevated)
│ │ └── crates/ Libraries: btstack-sys, hci-transport, bt-host, codec, the codec -sys crates,
│ │ pipeline, audio-capture, win-sys, core (a2dpwb-core), ipc, le-audio, lc3-sys
│ ├── lang/ Localization (en.json is the source; tools/check_lang.py checks the others)
│ └── resources/ Icon and manifest
├── Cargo.toml Cargo workspace
├── installer/ Release script, NSIS hooks, updater notes (installer/README.md)
├── compat/ MSVC compatibility headers for the AOSP code
├── docs/ Documentation (this site) and the design document (v2-design.md)
├── extern/ Third-party C libraries (git submodules)
│ ├── btstack/ BTstack Bluetooth stack
│ ├── libldac/ AOSP LDAC encoder
│ ├── libopenaptx/ aptX / aptX HD / aptX LL encoder
│ ├── fdk-aac/ Fraunhofer AAC encoder
│ ├── lhdcv5-enc/ LHDC V5 encoder
│ └── opus/ libopus (Opus encoder)
└── tools/
├── a2dp_decode/ a2dpwb_decode: checks / decodes the media stream in an HCI capture (.pklg)
├── emu/ End-to-end test against a virtual Bluetooth sink (Python)
└── linux_sink/ a2dpwb_sink: measuring A2DP receiver on Linux (Python, Bumble)
Tests
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cd app/src/ui && pnpm exec tsc --noEmit && pnpm exec vitest run
python tools/check_lang.py
Release Packages
pwsh installer/build-release.ps1 -Arch x64
writes the installer (A2DPWB-<version>-win64-setup.exe), the portable zip and the updater manifest to target/dist. Signing the in-app updates and the other details are in installer/README.md.
Testing Without Hardware (tools/emu)
tools/emu streams every codec from A2DPWB to a virtual Bluetooth sink on the same PC, with no adapter or headphones. It runs on Windows only and needs Python 3.11 or later and CMake (bundled with Visual Studio) for the decoder tool.
python tools/emu/setup_env.py # once: creates tools/emu/.venv with pinned packages
cmake -S tools/a2dp_decode -B build/a2dp_decode # once: the decoder tool
cmake --build build/a2dp_decode --config Release
cargo build --release -p a2dpwb-core-exe
python tools/emu/run_test.py # about 35 s for all codecs
How it works:
emu_sink.pyruns two Bumble virtual controllers on an in-process link. One is served as H4 over TCP (127.0.0.1) for A2DPWB. The other is used by a Bumble A2DP sink that offers SBC, AAC, aptX, aptX HD, aptX LL and LDAC, and records its HCI traffic.run_test.pyrunsa2dpwb-core.exe --clifor each codec with development options:--hci-tcp(virtual controller instead of WinUSB),--test-tone(1 kHz left / 1.5 kHz right instead of system audio),--duration,--hci-capture. The environment variableA2DPWB_CONFIG_DIRpoints A2DPWB at a separate config folder, so pairing with the virtual sink never touches your real link keys.- Both HCI captures are then checked with
a2dpwb_decode(the sink side with--received).
A codec passes when all of these hold:
- the requested codec was negotiated
a2dpwb_decodefinds no problem on the sent side or the received side- the sink received every media packet and frame A2DPWB sent, and at least 90% of the requested audio duration was sent
- except LDAC (no open-source decoder): the decoded audio is identical on both sides and is the test tone
| Option | Description |
|---|---|
--codecs sbc aac ... | Test only these codecs (sbc aac aptx aptxhd aptxll ldac, default: all) |
--duration <s> | Seconds of streaming per codec (default 5) |
--max-packet <bytes> | Passed to A2DPWB (max media packet size, see Usage) |
--a2dpwb <exe> | The core to test (default target/release/a2dpwb-core.exe) |
--decoder <exe> | The decoder tool (default build/a2dp_decode/Release/a2dpwb_decode.exe) |
--out <dir> | Output folder (default tools/emu/out) |
For each codec, tools/emu/out/<codec>/ holds the captures (a2dpwb.pklg, sink.pklg), logs, the a2dpwb_decode reports and the decoded WAV files. The exit code is 0 when every codec passed.
The virtual link has no radio: it checks encoding, packetization, AVDTP / L2CAP signalling and interoperability with an independent Bluetooth stack (Bumble), but not radio conditions, timing on real hardware or how real headphones play the audio. The Bumble packages are test tooling only and are not part of A2DPWB (see THIRD_PARTY_LICENSES.md).
emu_vhci.py does the same towards the Linux Bluetooth stack: on Linux it attaches a virtual adapter to BlueZ through /dev/vhci, which the measuring receiver tools/linux_sink then takes like a real adapter - it can be tested without hardware, optionally with lost or held-back media packets (--drop, --stall). See tools/linux_sink/README.md.
Dependencies
The C libraries are git submodules; Rust crates and the UI’s npm packages are fetched by Cargo and pnpm (versions pinned in Cargo.lock and app/src/ui/pnpm-lock.yaml; cargo deny check checks their licences).
| Library | Method | Pinned version | License |
|---|---|---|---|
| BTstack | git submodule | v1.8.1-6-g5bc5cbdbe | BTstack License (BSD-3-Clause-style with a non-commercial clause) |
| Bluedroid SBC codec | encoder ported to Rust; the decoder (tests, tools) from BTstack’s copy | as in BTstack | Apache-2.0 |
| rijndael | bundled in BTstack (3rd-party/rijndael) | as in BTstack | Public domain |
| liblc3 (lc3-google) | bundled in BTstack (3rd-party/lc3-google) | as in BTstack | Apache-2.0 |
| micro-ecc | bundled in BTstack (3rd-party/micro-ecc) | as in BTstack | BSD-2-Clause |
| libldac (AOSP) | git submodule | android-15.0.0_r36-4-geeee1a3 | Apache-2.0 |
| libopenaptx | git submodule | 0.2.0 (do not update) | LGPL-2.1+ |
| fdk-aac | git submodule | v2.0.3-158-gd8e6b1a | FDK AAC License |
| LHDC V5 encoder | git submodule | as pinned | Apache-2.0 |
| libopus | git submodule | v1.5.2 | BSD-3-Clause |
| Tauri, SolidJS, Rust crates | Cargo / pnpm | Cargo.lock, pnpm-lock.yaml | MIT / Apache-2.0 and others (licenses/rust-crates.txt in the release) |
Because BTstack is linked in, any A2DPWB you build may only be used and redistributed for personal, non-commercial purposes, even though the A2DPWB source code itself is dual-licensed under Apache-2.0 or MIT.
See THIRD_PARTY_LICENSES.md for full license details.