Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3592f71bef | ||
|
|
15906915a7 | ||
|
|
2351d9bea9 | ||
|
|
3244030012 | ||
|
|
7abaa25355 | ||
|
|
cb5996ae72 | ||
|
|
bf4c172ac0 | ||
|
|
9f07150643 | ||
|
|
a8c59024b7 | ||
|
|
499666005f | ||
|
|
32e43db062 | ||
|
|
ad17493f5f | ||
|
|
060af7fbae |
No files matched your search
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
Executable
+136
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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
|
<p align="center">
|
||||||
only 16 AVRCP steps, so every swipe on the stem moves the system volume by
|
One press or swipe on your device becomes one clean step on your volume slider.
|
||||||
6.25 %, and the on-screen display walks through
|
</p>
|
||||||
|
|
||||||
```
|
<h5 align="center">
|
||||||
6 · 13 · 19 · 25 · 31 · 38 · 44 · 50 · 56 · 63 · 69 · 75 · 81 · 88 · 94 · 100
|
<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
|
<p align="center">
|
||||||
thing with whatever grid that speaker happens to use.
|
<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
|
| | Volume after each swipe on AirPods Pro |
|
||||||
exactly 5 %. The grid lives in the device firmware, and it cannot be changed:
|
|---|---|
|
||||||
the device sends *absolute* volume values over AVRCP, not increments.
|
| Before | 6 · 13 · 19 · 25 · 31 · 38 · 44 · 50 ... |
|
||||||
|
| After | 5 · 10 · 15 · 20 · 25 · 30 · 35 · 40 ... |
|
||||||
|
|
||||||
## What this does
|
## Install
|
||||||
|
|
||||||
`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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yay -S bt-volume-step
|
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
|
systemctl --user enable --now bt-volume-step
|
||||||
```
|
```
|
||||||
|
|
||||||
That is the whole setup. Connect a Bluetooth device and use its volume
|
Needs PipeWire and Python 3.9 or newer.
|
||||||
control.
|
|
||||||
|
|
||||||
## Calibration
|
## How to use
|
||||||
|
|
||||||
The daemon measures each device's grid on its own. **While a device's grid is
|
Connect your device and press volume up three or four times. Now it knows the device and every
|
||||||
unknown it does not intervene at all** — it only watches. Once the same jump
|
step is clean.
|
||||||
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:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bt-volume-step --show
|
bt-volume-step --show # what it measured
|
||||||
|
bt-volume-step --reset # forget it
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
## More
|
||||||
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):
|
<details>
|
||||||
|
<summary>How it works</summary>
|
||||||
|
|
||||||
```bash
|
Many Bluetooth devices only know a few volume steps (AirPods Pro: 16), and they send the volume as a
|
||||||
bt-volume-step --reset
|
number, not as "up" or "down". bt-volume-step only looks at which way the volume moved and sets a
|
||||||
bt-volume-step --reset 30_0E_43_04_52_19
|
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
|
It only steps in once it has seen the same jump three times. Until then it just watches.
|
||||||
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
|
</details>
|
||||||
|
|
||||||
On KDE Plasma the step size comes from *System Settings → Audio → Volume step*
|
<details>
|
||||||
(`plasmaparc [General] VolumeStep`, read through `kreadconfig6` so KDE's
|
<summary>Settings</summary>
|
||||||
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
|
On KDE Plasma the step comes from *System Settings → Audio → Volume step*, elsewhere it is 5 %.
|
||||||
(`systemctl --user edit bt-volume-step`):
|
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_STEP` | Fixed step in percent |
|
||||||
| `BT_VOL_DEVSTEP` | fixed device grid in percent; skips measurement |
|
| `BT_VOL_DEVSTEP` | Fixed device step, skips measuring |
|
||||||
| `BT_VOL_MAC` | only watch this device (MAC with `_` instead of `:`) |
|
| `BT_VOL_MAC` | Only this device (MAC with `_` instead of `:`) |
|
||||||
| `BT_VOL_MAX` | upper limit in percent (default 100) |
|
| `BT_VOL_MAX` | Highest volume in percent (default 100) |
|
||||||
|
|
||||||
The daemon handles several connected Bluetooth devices at once, keeping state
|
</details>
|
||||||
and calibration per device.
|
|
||||||
|
|
||||||
## Localisation
|
<details>
|
||||||
|
<summary>Limits</summary>
|
||||||
|
|
||||||
Messages follow `LC_ALL` / `LC_MESSAGES` / `LANG`. English and German are
|
- A volume change from somewhere else that happens to match one or two device steps can be read
|
||||||
included; other languages fall back to English. To add one, extend
|
as a swipe.
|
||||||
`TRANSLATIONS` in the script — English strings are the keys.
|
- 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
|
<details>
|
||||||
information about where a change came from, so a jump that happens to match
|
<summary>Build from source</summary>
|
||||||
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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
sudo make install
|
||||||
make check
|
make check
|
||||||
```
|
```
|
||||||
|
|
||||||
Covers the decision logic and the measurement, including simulated devices
|
`make install PREFIX=$HOME/.local` works without root.
|
||||||
with 8, 16 and 32 steps.
|
|
||||||
|
|
||||||
## License
|
</details>
|
||||||
|
|
||||||
BSD 3-Clause. See [LICENSE](LICENSE).
|
BSD 3-Clause.
|
||||||
+6
-6
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
# Copyright (c) 2026, Felitendo
|
# 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
|
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
|
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:",
|
"known device steps:": "bekannte Gerätestufen:",
|
||||||
"watching {device} at {volume:.1f} %":
|
"watching {device} at {volume:.1f} %":
|
||||||
"beobachte {device} bei {volume:.1f} %",
|
"beobachte {device} bei {volume:.1f} %",
|
||||||
"{device}: measured device step = {step:.2f} % ({count} steps) - active from now on":
|
"{device}: measured device step = {step:.2f} % ({count} steps), active from now on":
|
||||||
"{device}: Gerätestufe gemessen = {step:.2f} % ({count} Stufen) – ab jetzt aktiv",
|
"{device}: Gerätestufe gemessen = {step:.2f} % ({count} Stufen), ab jetzt aktiv",
|
||||||
"{device}: {last:.1f} % -> device reported {cur:.1f} % -> set {target:.1f} %":
|
"{device}: {last:.1f} % -> device reported {cur:.1f} % -> set {target:.1f} %":
|
||||||
"{device}: {last:.1f} % -> Gerät meldete {cur:.1f} % -> gesetzt {target:.1f} %",
|
"{device}: {last:.1f} % -> Gerät meldete {cur:.1f} % -> gesetzt {target:.1f} %",
|
||||||
"file: {path}": "Datei: {path}",
|
"file: {path}": "Datei: {path}",
|
||||||
@@ -141,7 +141,7 @@ def log(msg):
|
|||||||
def half_up(x):
|
def half_up(x):
|
||||||
"""Round half away from zero instead of Python's round-half-to-even.
|
"""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.
|
unpredictable for values sitting exactly between two grid points.
|
||||||
"""
|
"""
|
||||||
return math.floor(x + 0.5)
|
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
|
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
|
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)
|
mag = abs(delta)
|
||||||
if not DEV_MIN <= mag <= DEV_MAX:
|
if not DEV_MIN <= mag <= DEV_MAX:
|
||||||
@@ -387,7 +387,7 @@ def run():
|
|||||||
entry["step"] = round(found, 3)
|
entry["step"] = round(found, 3)
|
||||||
save_store(store)
|
save_store(store)
|
||||||
log(_("{device}: measured device step = {step:.2f} % "
|
log(_("{device}: measured device step = {step:.2f} % "
|
||||||
"({count} steps) - active from now on")
|
"({count} steps), active from now on")
|
||||||
.format(device=device, step=found,
|
.format(device=device, step=found,
|
||||||
count=round(100 / found)))
|
count=round(100 / found)))
|
||||||
st["last"] = cur
|
st["last"] = cur
|
||||||
|
|||||||
@@ -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,6 +1,6 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Fixed volume steps for Bluetooth audio devices
|
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
|
After=pipewire-pulse.service
|
||||||
Wants=pipewire-pulse.service
|
Wants=pipewire-pulse.service
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user