Compare commits

...
13 Commits
11 changed files with 443 additions and 127 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
ko_fi: felitendo
@@ -0,0 +1,35 @@
name: 💡 Feature request
description: An idea to make bt-volume-step better.
labels:
- enhancement
body:
- type: checkboxes
id: checks
attributes:
label: Before you start
options:
- label: I searched the issues and this is not requested yet.
required: true
- type: textarea
id: problem
attributes:
label: What problem would this solve?
description: What are you trying to do, and what gets in the way?
validations:
required: true
- type: textarea
id: idea
attributes:
label: What would you like?
description: How it could work. A rough idea is fine.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Other ways you thought of
- type: textarea
id: more
attributes:
label: Anything else?
description: Mockups, screenshots, or other tools that do it well.
+81
View File
@@ -0,0 +1,81 @@
name: 🐛 Bug report
description: Something does not work as it should.
labels:
- bug
body:
- type: markdown
attributes:
value: Thanks for taking the time! The more you fill in, the faster it can be fixed.
- type: checkboxes
id: checks
attributes:
label: Before you start
options:
- label: I searched the issues and this is not reported yet.
required: true
- label: I use the latest version.
required: true
- type: textarea
id: what
attributes:
label: What happened?
description: What did you do, and what went wrong?
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
placeholder: |
1.
2.
3.
- type: textarea
id: expected
attributes:
label: What did you expect?
validations:
required: true
- type: input
id: device
attributes:
label: Bluetooth device
description: The model of your headphones or speaker.
placeholder: Sony WH-1000XM4
- type: input
id: version
attributes:
label: Version
description: The output of `bt-volume-step --version`.
placeholder: bt-volume-step 1.0.0
validations:
required: true
- type: dropdown
id: install
attributes:
label: How did you install it?
options:
- AUR
- Built from source
- Other
validations:
required: true
- type: input
id: system
attributes:
label: System
description: Distribution, desktop and PipeWire version.
placeholder: CachyOS, KDE Plasma 6.4, PipeWire 1.4
validations:
required: true
- type: textarea
id: logs
attributes:
label: Status and logs
description: The output of `bt-volume-step --show` and `journalctl --user -b -u bt-volume-step`.
render: shell
- type: textarea
id: more
attributes:
label: Anything else?
description: Screenshots, videos or anything else that could help.
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
#
# Builds the GitHub release notes for a tag, the way big projects such as
# Immich lay them out: the hand-written entry from CHANGELOG.md (a welcome,
# the highlights), a support section, then every commit since the last
# release, sorted by kind, and a link to the full changelog.
#
# .github/release-notes.sh v1.2.0 the notes
# .github/release-notes.sh --title v1.2.0 the title (checks the entry exists)
#
# Commit authors come from the GitHub API through gh. Without it the list
# goes without them.
set -euo pipefail
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
title=0
if [[ ${1:-} == --title ]]; then
title=1
shift
fi
tag="${1:?usage: release-notes.sh [--title] vX.Y.Z}"
if ! grep -qx "## $tag" CHANGELOG.md; then
echo "CHANGELOG.md has no entry for $tag. Add \"## $tag\" first." >&2
exit 1
fi
if (( title )); then
printf '%s\n' "$tag"
exit 0
fi
repo="${GITHUB_REPOSITORY:-$(git remote get-url origin | sed -E 's#^.*github\.com[:/]##; s#\.git$##')}"
name="${repo#*/}"
# The entry: up to the next one, without the date line (GitHub shows the
# date), one heading level up, outside code blocks.
entry="$(awk -v h="## $tag" '
$0 == h { on = 1; next }
on && /^## / { exit }
!on { next }
/^```/ { code = !code }
!code && /^_[0-9]{4}-[0-9]{2}-[0-9]{2}_$/ { next }
!code && /^###/ { sub(/^#/, "") }
{ print }
' CHANGELOG.md | sed -e '/./,$!d')"
# The commits: from the last release, or from the start. A tag that does not
# exist yet is a preview of what HEAD would become.
if git rev-parse -q --verify "refs/tags/$tag" > /dev/null; then
target="$tag"
prev="$(git describe --tags --abbrev=0 --match 'v[0-9]*' "$tag^" 2>/dev/null || true)"
else
target="$(git rev-parse HEAD)"
prev="$(git describe --tags --abbrev=0 --match 'v[0-9]*' HEAD 2>/dev/null || true)"
fi
declare -A login=()
if command -v gh > /dev/null; then
if [[ -n $prev ]]; then
api=(api "repos/$repo/compare/$prev...$target" --jq '.commits[] | [.sha, (.author.login // "")] | @tsv')
else
api=(api --paginate "repos/$repo/commits?sha=$target&per_page=100" --jq '.[] | [.sha, (.author.login // "")] | @tsv')
fi
while IFS=$'\t' read -r sha who; do
[[ -n $who ]] && login[$sha]="$who"
done < <(gh "${api[@]}" 2>/dev/null || true)
fi
declare -A list=()
count_maint=0
while IFS=$'\t' read -r sha subject body; do
# Version bumps say nothing a reader needs.
[[ $subject =~ ^(chore:\ )?($name\ )?v?[0-9]+\.[0-9]+\.[0-9]+$ ]] && continue
case "$subject" in
*!:*) kind=breaking ;;
feat:* | feat\(*) kind=feat ;;
fix:* | fix\(*) kind=fix ;;
perf:* | perf\(*) kind=enh ;;
docs:* | docs\(*) kind=docs ;;
chore* | ci:* | ci\(* | build* | refactor* | test* | style*) kind=maint ;;
# Older commits without a prefix, sorted by what they say.
*README* | *readme* | *Readme*) kind=docs ;;
Add\ * | Put\ * | Introduce\ *) kind=feat ;;
Fix\ * | Repair\ * | Recover\ * | Stop\ *) kind=fix ;;
*\ *) kind=enh ;;
*) kind=maint ;;
esac
[[ $body == *"BREAKING CHANGE"* ]] && kind=breaking
[[ $subject == "Initial commit" ]] && kind=maint
line="* $subject"
[[ -n ${login[$sha]:-} ]] && line+=" by @${login[$sha]}"
line+=" in https://github.com/$repo/commit/$sha"
list[$kind]+="$line"$'\n'
[[ $kind == maint ]] && count_maint=$((count_maint + 1))
done < <(git log --reverse --no-merges --format='%H%x09%s%x09%b%x1e' "${prev:+$prev..}$target" | tr '\n\036' ' \n' | sed 's/^ //')
changes=''
for pair in "breaking:🚨 Breaking Changes" "feat:🚀 Features" "enh:🌟 Enhancements" "fix:🐛 Bug fixes" "docs:📚 Documentation"; do
kind="${pair%%:*}"
[[ -n ${list[$kind]:-} ]] || continue
changes+="### ${pair#*:}"$'\n'"${list[$kind]}"
done
if [[ -n ${list[maint]:-} ]]; then
changes+=$'\n'"<details><summary>🧰 Maintenance ($count_maint)</summary>"$'\n\n'"${list[maint]}"$'\n'"</details>"$'\n'
fi
if [[ -n $prev ]]; then
full="**Full Changelog**: https://github.com/$repo/compare/$prev...$tag"
else
full="**Full Changelog**: https://github.com/$repo/commits/$tag"
fi
# A release with highlights is a big one: it gets a heading and the support
# section, as in Immich. A patch is a sentence or two and the list.
if grep -q '^## Highlights$' <<< "$entry"; then
cat <<EOF
# 🚀 $name $tag
$entry
## ☕ Support $name
If $name is useful to you, you can buy me a coffee. It keeps these tools going. Found a bug or have an idea? Tell me in the [issues](https://github.com/$repo/issues).
<a href="https://ko-fi.com/felitendo"><img src="https://storage.ko-fi.com/cdn/kofi5.png?v=6" alt="Buy me a coffee on Ko-fi" height="48"></a>
----
EOF
else
printf '%s\n\n' "$entry"
fi
printf "## What's Changed\n%s\n\n%s\n" "$changes" "$full"
+21
View File
@@ -0,0 +1,21 @@
# Changelog
What each release brings, newest first. The [GitHub releases](https://github.com/LoonixTools/bt-volume-step/releases) add every commit that went into it.
## v1.0.0
_2026-08-16_
Welcome to the very first release of bt-volume-step! It gives your Bluetooth headphones and speakers clean volume steps.
AirPods Pro only know 16 volume steps, so every swipe moves the volume by 6.25 %, and your desktop walks through numbers like these:
```
6 · 13 · 19 · 25 · 31 · 38 · 44 · 50 · 56 · 63 · 69 · 75 · 81 · 88 · 94 · 100
```
### Highlights
- One swipe, one clean step
- Learns every device on its own
- Follows Plasma
+73
View File
@@ -0,0 +1,73 @@
# CLAUDE.md
## Style
- Use simple English: short sentences, common words.
- Avoid em dashes (—). Do not just swap them for "-" either. Rewrite the sentence instead, for
example with a comma, a colon, brackets or two sentences.
## Commits
- Do not commit or push before I have checked the changes locally and said they are fine. Then
commit and push. Several attempts at the same thing make one commit, and attempts that did not
work make none. Different things done in one session get a commit each. This also goes for
releases and tags.
## Releases
Releases look like the ones of big projects such as Immich. `.github/release-notes.sh <tag>` builds
the notes: the entry from `CHANGELOG.md` (welcome and highlights), a support section, every commit
since the last release sorted by its prefix (`feat`, `fix`, `docs`, ...) with author and link, and
the full changelog link. So commit subjects end up in public: keep them clear.
1. Read `git log <last tag>..HEAD` and pick the version: only fixes → patch, something new → minor,
something that breaks or needs the user to act → major.
2. Bump the version and add the entry at the top of `CHANGELOG.md`, in one commit (`chore: 1.4.0`).
3. Push, tag and create the release:
```bash
git tag v1.4.0 && git push origin main v1.4.0
gh release create v1.4.0 --title v1.4.0 --notes "$(.github/release-notes.sh v1.4.0)"
```
4. PKGBUILDS picks up the new release for the AUR on its own.
A minor or major release (has `### Highlights`, gets a heading and the support section):
```markdown
## v1.4.0
_2026-09-24_
Welcome to bt-volume-step `v1.4.0`! One or two sentences on what this release is about.
<p align="center">
<img width="480" alt="What the picture shows" src="https://raw.githubusercontent.com/LoonixTools/bt-volume-step/v1.4.0/<path>">
</p>
### 🚨 Breaking changes
- Only if there are any: what changed, and what the user has to do.
### Highlights
- First highlight, a few words
- Second highlight
```
A picture under the welcome is optional. To show the menu or another screen of the program, use a
real screenshot of it running in Konsole, in English. Never a text copy of the screen. The same goes
for the README. The highlights stay a plain list: no heading or text per highlight, the list of
commits explains the rest.
A patch release is just a sentence or two, for example: "A small patch. The menu no longer closes
when you press Enter." The list of commits follows on its own.
- Write for users: what they notice, not how the code does it. Friendly and simple.
- Commands and settings they type go in backticks, buttons and labels in bold.
- To change an old release: edit its entry, commit, then
`gh release edit <tag> --title <tag> --notes "$(.github/release-notes.sh <tag>)"`.
## README
- New UI (a window, a screen, a menu, a setting, a notification) gets a screenshot in the README,
next to the text about it. Like for releases: a real screenshot of it running, in English.
- When a screen changes, take its screenshot again. The README never shows an old one.
+58 -120
View File
@@ -1,159 +1,97 @@
# bt-volume-step
<p align="center">
<img width="200" src="res/bt-volume-step.svg" alt="bt-volume-step">
</p>
Fixed volume steps for Bluetooth audio devices on PipeWire.
<h1 align="center">bt-volume-step</h1>
## The problem
<h3 align="center">Clean volume steps for Bluetooth headphones and speakers.</h3>
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
<p align="center">
One press or swipe on your device becomes one clean step on your volume slider.
</p>
```
6 · 13 · 19 · 25 · 31 · 38 · 44 · 50 · 56 · 63 · 69 · 75 · 81 · 88 · 94 · 100
```
<h5 align="center">
<a href="#install">Install</a> |
<a href="#how-to-use">How to use</a> |
<a href="https://github.com/LoonixTools/bt-volume-step/issues">Report a bug</a>
</h5>
instead of clean multiples of five. A speaker's volume buttons do the same
thing with whatever grid that speaker happens to use.
<p align="center">
<a href="https://ko-fi.com/felitendo"><img src="https://storage.ko-fi.com/cdn/kofi5.png?v=6" alt="Buy me a coffee on Ko-fi" height="48"></a>
</p>
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.
| | Volume after each swipe on AirPods Pro |
|---|---|
| Before | 6 · 13 · 19 · 25 · 31 · 38 · 44 · 50 ... |
| After | 5 · 10 · 15 · 20 · 25 · 30 · 35 · 40 ... |
## 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
## Install
```bash
yay -S bt-volume-step
```
### From source
```bash
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:
```bash
systemctl --user enable --now bt-volume-step
```
That is the whole setup. Connect a Bluetooth device and use its volume
control.
Needs PipeWire and Python 3.9 or newer.
## Calibration
## How to use
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:
Connect your device and press volume up three or four times. Now it knows the device and every
step is clean.
```bash
bt-volume-step --show
bt-volume-step --show # what it measured
bt-volume-step --reset # forget it
```
```
file: /home/you/.local/state/bt-volume-step/devsteps.json
AirPods Pro (30_0E_43_04_52_19) = 6.25 % (16 steps)
```
## More
Discard a measurement (all devices, or one MAC):
<details>
<summary>How it works</summary>
```bash
bt-volume-step --reset
bt-volume-step --reset 30_0E_43_04_52_19
```
Many Bluetooth devices only know a few volume steps (AirPods Pro: 16), and they send the volume as a
number, not as "up" or "down". bt-volume-step only looks at which way the volume moved and sets a
clean step of its own. The device takes that new volume without a word, so nothing fights back.
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.
It only steps in once it has seen the same jump three times. Until then it just watches.
## Configuration
</details>
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 %.
<details>
<summary>Settings</summary>
Everything can be overridden through the environment — put these in a drop-in
(`systemctl --user edit bt-volume-step`):
On KDE Plasma the step comes from *System Settings → Audio → Volume step*, elsewhere it is 5 %.
To change more, use `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) |
| `BT_VOL_STEP` | Fixed step in percent |
| `BT_VOL_DEVSTEP` | Fixed device step, skips measuring |
| `BT_VOL_MAC` | Only this device (MAC with `_` instead of `:`) |
| `BT_VOL_MAX` | Highest volume in percent (default 100) |
The daemon handles several connected Bluetooth devices at once, keeping state
and calibration per device.
</details>
## Localisation
<details>
<summary>Limits</summary>
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.
- A volume change from somewhere else that happens to match one or two device steps can be read
as a swipe.
- Devices that report their volume back are not supported. Set a volume and check after a few
seconds with `pactl get-sink-volume`: if it moved on its own, this tool is not for that device.
## Limitations
</details>
**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:
```bash
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
<details>
<summary>Build from source</summary>
```bash
sudo make install
make check
```
Covers the decision logic and the measurement, including simulated devices
with 8, 16 and 32 steps.
`make install PREFIX=$HOME/.local` works without root.
## License
</details>
BSD 3-Clause. See [LICENSE](LICENSE).
BSD 3-Clause.
+6 -6
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2026, Felitendo
"""bt-volume-step - fixed volume steps for Bluetooth audio devices.
"""bt-volume-step: fixed volume steps for Bluetooth audio devices.
Many Bluetooth devices carry a coarse internal volume grid. AirPods Pro, for
example, expose only 16 AVRCP steps, so every swipe on the stem moves the
@@ -72,8 +72,8 @@ TRANSLATIONS = {
"known device steps:": "bekannte Gerätestufen:",
"watching {device} at {volume:.1f} %":
"beobachte {device} bei {volume:.1f} %",
"{device}: measured device step = {step:.2f} % ({count} steps) - active from now on":
"{device}: Gerätestufe gemessen = {step:.2f} % ({count} Stufen) – ab jetzt aktiv",
"{device}: measured device step = {step:.2f} % ({count} steps), active from now on":
"{device}: Gerätestufe gemessen = {step:.2f} % ({count} Stufen), ab jetzt aktiv",
"{device}: {last:.1f} % -> device reported {cur:.1f} % -> set {target:.1f} %":
"{device}: {last:.1f} % -> Gerät meldete {cur:.1f} % -> gesetzt {target:.1f} %",
"file: {path}": "Datei: {path}",
@@ -141,7 +141,7 @@ def log(msg):
def half_up(x):
"""Round half away from zero instead of Python's round-half-to-even.
Otherwise snap(65, 10) would be 60 while snap(75, 10) is 80 - that is,
Otherwise snap(65, 10) would be 60 while snap(75, 10) is 80. That is
unpredictable for values sitting exactly between two grid points.
"""
return math.floor(x + 0.5)
@@ -284,7 +284,7 @@ def calibrate(delta, samples, plasma_step):
A volume button press on the device always produces the same jump. Deltas
that match the desktop's own step size most likely come from the keyboard
and are skipped - otherwise the daemon would calibrate itself onto them.
and are skipped. Otherwise the daemon would calibrate itself onto them.
"""
mag = abs(delta)
if not DEV_MIN <= mag <= DEV_MAX:
@@ -387,7 +387,7 @@ def run():
entry["step"] = round(found, 3)
save_store(store)
log(_("{device}: measured device step = {step:.2f} % "
"({count} steps) - active from now on")
"({count} steps), active from now on")
.format(device=device, step=found,
count=round(100 / found)))
st["last"] = cur
+30
View File
@@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="13.0 67.5 486.1 486.1" width="512" height="512">
<defs>
<linearGradient id="bo-cup" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#5fd0ff"/><stop offset="1" stop-color="#0c62a0"/></linearGradient>
<linearGradient id="bo-bar" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#5fd0ff"/><stop offset="1" stop-color="#1383bd"/></linearGradient>
<linearGradient id="bo-band" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#a9bcd0"/><stop offset="1" stop-color="#56697e"/></linearGradient>
<linearGradient id="bo-pad" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#1f2c3b"/><stop offset="1" stop-color="#3a4b5e"/></linearGradient>
<linearGradient id="bo-paper" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#dfe7f0"/></linearGradient>
<filter id="bo-soft" x="-20%" y="-20%" width="140%" height="150%"><feDropShadow dx="0" dy="10" stdDeviation="12" flood-color="#04213a" flood-opacity=".35"/></filter>
</defs>
<g filter="url(#bo-soft)">
<!-- headband -->
<path d="M118 292 C 118 150, 394 150, 394 292" fill="none" stroke="url(#bo-band)" stroke-width="36" stroke-linecap="round"/>
<!-- ear pads, facing inwards -->
<rect x="150" y="286" width="40" height="132" rx="20" fill="url(#bo-pad)"/>
<rect x="322" y="286" width="40" height="132" rx="20" fill="url(#bo-pad)"/>
<!-- cups -->
<rect x="66" y="270" width="104" height="164" rx="46" fill="url(#bo-cup)"/>
<rect x="342" y="270" width="104" height="164" rx="46" fill="url(#bo-cup)"/>
<path d="M88 312 C 88 298, 94 288, 106 284" fill="none" stroke="#fff" stroke-opacity=".28" stroke-width="7" stroke-linecap="round"/>
<path d="M364 312 C 364 298, 370 288, 382 284" fill="none" stroke="#fff" stroke-opacity=".28" stroke-width="7" stroke-linecap="round"/>
<path d="M98.8 337.6 L135.6 366.4 L118.0 380.8 L118.0 323.2 L135.6 337.6 L98.8 366.4" fill="none" stroke="#fff" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"/>
<!-- fixed steps -->
<rect x="212" y="366" width="24" height="44" rx="8" fill="url(#bo-bar)"/>
<rect x="244" y="340" width="24" height="70" rx="8" fill="url(#bo-bar)"/>
<rect x="276" y="314" width="24" height="96" rx="8" fill="url(#bo-bar)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
[Unit]
Description=Fixed volume steps for Bluetooth audio devices
Documentation=https://github.com/Felitendo/bt-volume-step
Documentation=https://github.com/LoonixTools/bt-volume-step
After=pipewire-pulse.service
Wants=pipewire-pulse.service