A DIY project for turning ordinary passive speakers into battery-powered wireless mono speakers.
Each speaker is a self-contained unit consisting of:
- a tool battery for portable power
- a TPA3116D2 amplifier
- a Bluetooth audio receiver
- a buck converter to provide 5 V to the Bluetooth board
- a passive speaker
The two speakers can be connected to a Raspberry Pi and used for simultaneous playback. The Raspberry Pi acts as the central audio hub, while the individual speaker units remain simple and independent.
- Raspberry Pi 4B + microSD card
- 2× DollaTek DC 12V–24V TPA3116D2 100W Mono Audio Amplifier boards
- 2× Heemol VHM-314 Bluetooth 5.0 audio receiver boards
- 2× LM2596 DC-DC step-down regulator modules
- 2× tool batteries
- I used generic unbranded "Makita-style" 18–21 V batteries, 4 Ah
- 2× matching DC adapters are used to connect the batteries
- 1× 3.5 mm stereo mini-jack cable, cut in half
- 2× 10 kΩ 0.25 W resistors
- Wire
The two speaker units are electrically independent. Each one has its own battery, amplifier, Bluetooth receiver and buck converter.
Each speaker converts a normal passive speaker into a portable Bluetooth speaker.
The VHM-314 Bluetooth receiver provides a stereo audio output, while the TPA3116D2 amplifier is being used as a mono amplifier. The left and right channels therefore need to be combined into one mono signal.
Two 10 kΩ resistors are used for this purpose. The resistors prevent the left and right outputs of the Bluetooth board from being directly connected to each other.
VHM-314 Bluetooth board
(3.5 mm stereo output)
│
├── L ──[10 kΩ]──┐
│ │
│ ├──► Amp "+" (IN)
│ │
├── R ──[10 kΩ]──┘
│
└── GND ─────────────► Amp "−" (IN)
The two resistors form a simple passive stereo-to-mono mixer.
Do not simply connect L and R together. The resistor on each channel keeps the two outputs isolated from one another while allowing their signals to be combined at the amplifier input.
18–21 V "Makita-style" tool battery
│
├────────────────────────────► TPA3116D2 amplifier
│ VIN+ / VIN−
│
└──► LM2596 buck converter
│
└──► 5.0 V ──► VHM-314 Bluetooth board
The TPA3116D2 amplifier can accept the battery voltage directly because its specified supply range is 12–24 V.
The Bluetooth receiver requires a lower voltage, so the battery voltage is reduced to 5 V using an LM2596 buck converter.
Important: set the LM2596 output to 5.0 V before connecting the Bluetooth board.
The following connections are for one speaker. Build the same circuit a second time for the other speaker.
| From | To | Notes |
|---|---|---|
| Battery + / − | LM2596 IN+ / IN− | Raw 18–21 V battery voltage |
| Battery + / − | Amp VIN+ / VIN− | Direct connection; amplifier accepts 12–24 V |
| LM2596 OUT+ / OUT− | BT board 5V / GND | Adjust LM2596 to 5.0 V first |
| BT board L out | 10 kΩ resistor #1 | Other resistor leg goes to amplifier "+" input |
| BT board R out | 10 kΩ resistor #2 | Other resistor leg goes to the same amplifier "+" input |
| BT board GND | Amp "−" input | Direct connection |
| Amp speaker out + / − | Speaker | Normal two-wire speaker connection |
The Raspberry Pi is used as the central audio hub. It handles Bluetooth connections and audio routing between the two wireless speakers.
Flash Ubuntu 26.04 LTS Server to a microSD card using Raspberry Pi Imager.
During installation, enable:
- SSH
- Wi-Fi
Boot the Raspberry Pi and find its local IP address.
Then connect over SSH:
ssh username@<raspberry-pi-ip>For example:
ssh pi@192.168.1.102sudo apt update
sudo apt upgrade -yThis project uses PipeWire for audio routing.
PipeWire sits between applications and the physical/Bluetooth audio devices. It is useful here because the Raspberry Pi needs to manage multiple independent Bluetooth audio outputs.
pactl info | grep "Server Name"dpkg -l | grep pipewiresystemctl --user list-units | grep -i 'pulse\|pipewire\|wireplumber'sudo apt-get update
sudo apt-get install -y pipewire pipewire-audio-client-libraries pipewire-pulse pipewire-jack pipewire-alsa libspa-0.2-bluetooth wireplumberAlso make sure the Bluetooth integration package is installed:
sudo apt-get install -y libspa-0.2-bluetooth pipewire-audiosystemctl --user enable pipewire pipewire-pulse wireplumber
systemctl --user start pipewire pipewire-pulse wireplumberVerify that PipeWire is now providing the audio server:
pactl info | grep "Server Name"You should see something similar to:
Server Name: PulseAudio (on PipeWire ...)
pw-cli infoList PipeWire audio nodes:
pw-cli list-objects | grep -i "PipeWire:Interface:Node"Or use WirePlumber's more user-friendly interface:
wpctl statusTo see the audio devices:
wpctl status | grep -A 20 "Audio"Bluetooth is handled by BlueZ, the standard Linux Bluetooth protocol stack. PipeWire/WirePlumber then handles the audio side of the Bluetooth connection.
sudo apt install bluetooth bluez bluez-tools -yInstall rfkill as well:
sudo apt install rfkill -yCheck the BlueZ version:
bluetoothctl --versionCheck whether a Bluetooth adapter is present:
lsusb | grep -i bluetooth
lspci | grep -i bluetoothCheck whether the kernel recognizes the adapter:
hciconfig -aOr use the newer BlueZ interface:
bluetoothctl showCheck loaded Bluetooth kernel modules:
lsmod | grep bluetoothView the adapter version:
hciconfig hci0 versionBecause the Raspberry Pi is running Ubuntu Server without a desktop environment, some Bluetooth/PipeWire components may need additional configuration.
Make sure the Bluetooth PipeWire integration package is installed:
sudo apt install libspa-0.2-bluetoothYou can confirm that it is actually installed rather than only having the PipeWire/WirePlumber core packages.
This allows the user's systemd user services to continue running without an active graphical login session.
Replace <your-user> with the Linux user that will run the audio system:
sudo loginctl enable-linger <your-user>Then, while logged in as that user:
systemctl --user enable --now pipewire pipewire-pulse wireplumberEdit:
/usr/share/wireplumber/wireplumber.conf
Add/modify:
monitor.bluez.seat-monitoring = disabled
wireplumber.profiles = {
main = {
monitor.bluez.seat-monitoring = disabled
}
}
This is intended to prevent WirePlumber from depending on desktop-session seat monitoring, which can otherwise interfere with Bluetooth audio on a headless server.
Find the path of this file with the following command:
find / -xdev \( -path /proc -o -path /sys \) -prune -o -iname 'wireplumber.conf*' -print 2>/dev/null
bluetoothctl is the main command-line tool used to manage Bluetooth devices.
Start it with:
bluetoothctlInside the interactive shell:
[bluetooth]# show
[bluetooth]# power on
[bluetooth]# agent on
[bluetooth]# default-agent
[bluetooth]# discoverable on
[bluetooth]# discoverable-timeout 120
[bluetooth]# pairable on
Exit with:
[bluetooth]# quit
To connect a speaker:
bluetoothctlThen:
[bluetooth]# power on
[bluetooth]# scan on
[bluetooth]# pair XX:XX:XX:XX:XX:XX
[bluetooth]# trust XX:XX:XX:XX:XX:XX
[bluetooth]# connect XX:XX:XX:XX:XX:XX
Repeat the pairing and connection process for the second speaker.
Once both speakers are connected, check the available sinks:
pactl list short sinksYou should see Bluetooth sinks similar to:
bluez_output.52_58_0D_19_0A_4B.1
bluez_output.63_5E_53_8E_2B_06.1
Create a combined sink:
pactl load-module module-combine-sink \
sink_name=both_speakers \
slaves=bluez_output.52_58_0D_19_0A_4B.1,bluez_output.63_5E_53_8E_2B_06.1Set it as the default output:
pactl set-default-sink both_speakersAt this point, normal PulseAudio-compatible applications can send audio to both_speakers, causing the same audio stream to be sent to both Bluetooth speakers.
If an application tries to use ALSA directly, it can be useful to route the ALSA default device through PipeWire.
Create or edit:
~/.asoundrc
Or configure it system-wide in:
/etc/asound.conf
Use:
pcm.!default {
type pipewire
}
ctl.!default {
type pipewire
}
This allows ALSA applications to use PipeWire as their audio backend.
Using the PulseAudio-compatible interface:
paplay /usr/share/sounds/alsa/Front_Center.wavUsing PipeWire directly:
pw-play /usr/share/sounds/alsa/Front_Center.wavThe second command is useful when testing PipeWire itself because it talks directly to the PipeWire audio system.
The basic architecture is:
┌──────────────────────┐
│ Raspberry Pi 4B │
│ │
│ Ubuntu Server │
│ BlueZ │
│ PipeWire │
│ WirePlumber │
└──────────┬───────────┘
│
Bluetooth │ Bluetooth
┌───────┴───────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Speaker 1 │ │ Speaker 2 │
│ │ │ │
│ BT receiver │ │ BT receiver │
│ ↓ │ │ ↓ │
│ TPA3116D2 │ │ TPA3116D2 │
│ ↓ │ │ ↓ │
│ Speaker │ │ Speaker │
└─────────────┘ └─────────────┘
│ │
Battery Battery
The Raspberry Pi is therefore the central controller. The speaker units do not need to run Linux or perform any audio processing themselves; they only need to receive Bluetooth audio and amplify it.
This README currently documents the working hardware setup and the basic Ubuntu/PipeWire/Bluetooth configuration.
You can now go ahead and use this in combination with uxplay (linux airplay server) or cliamp (terminal winamp style media player) to enjoy some music with your wireless bluetooth system!
The next logical steps for the project are:
- automate Bluetooth speaker connection
- automatically create the combined audio sink
- investigate synchronization between the Bluetooth speakers
- measure the latency of each speaker
- add per-speaker delay compensation
- automate calibration from the Raspberry Pi
- eventually turn the setup into a proper multi-speaker wireless surround system
https://oneuptime.com/blog/post/2026-03-02-set-up-bluetooth-ubuntu-server/
https://oneuptime.com/blog/post/2026-03-02-how-to-set-up-pipewire-as-audio-server-on-ubuntu/
https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/bluetooth.html
https://deepwiki.com/antimof/UxPlay/2-installation-and-setup
sudo apt install uxplay
sudo apt install gstreamer*
Copy service file to user directory
mkdir -p ~/.config/systemd/user
cp /usr/share/doc/uxplay/systemd/uxplay.service ~/.config/systemd/user/
Start/stop service
systemctl --user start uxplay
systemctl --user stop uxplay
Enable autostart on login
systemctl --user enable uxplay
Check status
systemctl --user status uxplay

