Files
bt-volume-step/README.md
T

5.5 KiB

bt-volume-step

bt-volume-step

Fixed volume steps for Bluetooth audio devices on PipeWire.

One press or swipe on your earbuds or speaker becomes one clean step on your volume slider.

How to use | Install | Report a bug

Buy Me A Coffee

The problem

Many Bluetooth devices carry a coarse internal volume grid. AirPods Pro expose only 16 AVRCP steps, so every swipe on the stem moves the system volume by 6.25 %, and the on-screen display walks through

6 · 13 · 19 · 25 · 31 · 38 · 44 · 50 · 56 · 63 · 69 · 75 · 81 · 88 · 94 · 100

instead of clean multiples of five. A speaker's volume buttons do the same thing with whatever grid that speaker happens to use.

This is not a desktop misconfiguration. KDE's own volume step is already exactly 5 %. The grid lives in the device firmware, and it cannot be changed: the device sends absolute volume values over AVRCP, not increments.

What this does

bt-volume-step watches for device-initiated volume changes, takes only their direction into account, and applies a clean step of its own instead. One press or swipe on the device becomes one step on your desktop's grid.

It works because such devices adopt a volume written back over AVRCP silently, without reporting it, so there is no feedback loop. As a side effect the device's internal position follows the corrected value, which keeps it from running into the end of its own scale.

Requirements

  • PipeWire with pactl (libpulse)
  • Python 3.9 or newer
  • Optional: kreadconfig6 (KDE Plasma) to follow the desktop's own step size
  • Optional: bluez-utils for device names in --show

Installation

Arch Linux

yay -S bt-volume-step

From source

sudo make install

make install honours PREFIX and DESTDIR; for a per-user install use make install PREFIX=$HOME/.local.

Usage

Enable it for your user session:

systemctl --user enable --now bt-volume-step

That is the whole setup. Connect a Bluetooth device and use its volume control.

Calibration

The daemon measures each device's grid on its own. While a device's grid is unknown it does not intervene at all, it only watches. Once the same jump has repeated three times, that jump is accepted as the device step and remembered, and corrections start from then on. In practice: press volume-up three or four times on a new device and it is set up.

Show what has been measured:

bt-volume-step --show
file: /home/you/.local/state/bt-volume-step/devsteps.json
  AirPods Pro (30_0E_43_04_52_19) = 6.25 % (16 steps)

Discard a measurement (all devices, or one MAC):

bt-volume-step --reset
bt-volume-step --reset 30_0E_43_04_52_19

To keep the daemon from calibrating itself onto keyboard input, jumps that match the desktop's own step size are excluded from measurement, as are jumps below 1.5 % or above 20 %. A device whose grid happens to equal the desktop step therefore never calibrates, and is left alone.

Configuration

On KDE Plasma the step size comes from System Settings → Audio → Volume step (plasmaparc [General] VolumeStep, read through kreadconfig6 so KDE's configuration cascade applies). Changes take effect within two seconds, with no restart. On other desktops, or without kreadconfig6, it falls back to 5 %.

Everything can be overridden through the environment. Put these in a drop-in (systemctl --user edit bt-volume-step):

Variable Effect
BT_VOL_STEP fixed step size in percent; ignores the desktop setting
BT_VOL_DEVSTEP fixed device grid in percent; skips measurement
BT_VOL_MAC only watch this device (MAC with _ instead of :)
BT_VOL_MAX upper limit in percent (default 100)

The daemon handles several connected Bluetooth devices at once, keeping state and calibration per device.

Localisation

Messages follow LC_ALL / LC_MESSAGES / LANG. English and German are included; other languages fall back to English. To add one, extend TRANSLATIONS in the script. English strings are the keys.

Limitations

Foreign volume changes can be misread. PipeWire events carry no information about where a change came from, so a jump that happens to match one or two device steps is indistinguishable from a button press. The daemon accepts at most two steps per event to keep that window narrow, but it cannot close it entirely.

Devices that report their volume back are not supported. The approach assumes a device accepts a written volume silently. One that echoes a snapped value back would oscillate. To check, set a volume and watch it for a few seconds:

pactl set-sink-volume bluez_output.XX_XX_XX_XX_XX_XX.1 40%
sleep 5 && pactl get-sink-volume bluez_output.XX_XX_XX_XX_XX_XX.1

If the value drifts on its own, this tool is the wrong approach; disabling hardware volume (bluez5.enable-hw-volume = false in WirePlumber) is the alternative, at the cost of the device's own control.

Tests

make check

Covers the decision logic and the measurement, including simulated devices with 8, 16 and 32 steps.

License

BSD 3-Clause. See LICENSE.