- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| README.md | ||
| volume-daemon.py | ||
volume-daemon
Event-driven управление громкостью PipeWire/PulseAudio для Sway + Waybar.
Демон запускается один раз за сессию и живёт постоянно. Горячие клавиши не запускают скрипт заново, а посылают уже работающему процессу realtime-сигналы, поэтому реакция мгновенная даже при удержании клавиши. Изменения громкости, сделанные другими клиентами (wiremix, микшер, само приложение), приходят напрямую через libpulse и сразу отражаются в баре.
Что он делает
- Меняет громкость и mute стандартного sink'а, mute стандартного source'а (микрофона).
- Пишет состояние одной JSON-строкой в файл, который читает модуль Waybar, и толкает бары сигналом.
- Показывает OSD через
dunstify(громкость с прогресс-баром, микрофон, смена устройства вывода). - При смене устройства вывода ставит на паузу все MPRIS-плееры через
playerctl. Отключение наушников иначе перебрасывает звук на встроенные динамики посреди трека на той громкости, которая там оказалась. - Следит за собственной живучестью: модуль бара при каждом тике проверяет, жив ли демон, и поднимает его, если тот упал.
Зависимости
| Python | 3.9+ (используется from __future__ import annotations) |
python3-pulsectl |
обязательно, без него скрипт выходит с кодом 1 |
PipeWire с pipewire-pulse или PulseAudio |
обязательно |
dunstify |
опционально — без него просто не будет OSD |
playerctl |
опционально — без него не будет паузы при смене вывода |
| Linux | обязательно: используются prctl, /proc, flock, sigtimedwait |
Отсутствие dunstify и playerctl проверяется по PATH при старте, ошибок это
не вызывает.
Режимы запуска
volume-daemon.py # демон (то, что запускает sway)
volume-daemon.py bar # модуль Waybar: читает файл состояния + watchdog
volume-daemon.py render # разово посчитать и напечатать состояние
volume-daemon.py --check # диагностика
Повторный запуск демона безопасен: блокировка через flock не даёт подняться
второму экземпляру, лишний процесс просто выходит с кодом 0.
Сигналы
Управляющие сигналы шлются самому демону. Имя процесса выставляется через
prctl, поэтому pkill -x попадает именно в него.
| Сигнал | Действие |
|---|---|
SIGRTMIN+1 |
громче |
SIGRTMIN+2 |
тише |
SIGRTMIN+3 |
mute вывода |
SIGRTMIN+4 |
mute микрофона |
SIGRTMIN+5 |
показать текущую громкость |
Обратно демон шлёт барам SIGRTMIN+12 — это "signal": 12 в конфиге Waybar.
Сигналы блокируются маской до импорта pulsectl и до создания потоков, а
читаются через sigtimedwait(). Благодаря этому каждое нажатие при автоповторе
обрабатывается отдельно, а не схлопывается штатным коалесцированием сигналов в
Python.
Настройка Sway
Демон запускает sway, а не Waybar. Модуль Waybar инстанцируется по одному на
каждый монитор, так что запуск из бара дал бы несколько демонов, и все они
реагировали бы на один и тот же pkill.
exec_always ~/.config/sway/scripts/volume-daemon.py
bindsym --locked XF86AudioRaiseVolume exec pkill -x -RTMIN+1 volume-daemon
bindsym --locked XF86AudioLowerVolume exec pkill -x -RTMIN+2 volume-daemon
bindsym --locked XF86AudioMute exec pkill -x -RTMIN+3 volume-daemon
bindsym --locked XF86AudioMicMute exec pkill -x -RTMIN+4 volume-daemon
Настройка Waybar
"custom/volume": {
"exec": "~/.config/sway/scripts/volume-daemon.py bar",
"return-type": "json",
"interval": 30,
"signal": 12,
"on-click": "pkill -x -RTMIN+3 volume-daemon",
"on-scroll-up": "pkill -x -RTMIN+1 volume-daemon",
"on-scroll-down": "pkill -x -RTMIN+2 volume-daemon"
}
interval намеренно большой: обновления приходят сигналом, а тик нужен только
как дешёвый watchdog для демона.
CSS-классы в выводе: on, muted, overamplified (выше 100%), off (звук
недоступен), error (упал рендер).
Файлы
| Путь | Назначение |
|---|---|
$XDG_RUNTIME_DIR/waybar-state/volume.json |
состояние для бара, пишется атомарно через os.replace |
$XDG_RUNTIME_DIR/xff-volume-daemon.lock |
блокировка единственного экземпляра и она же проба живости |
$XDG_RUNTIME_DIR/xff-logs/volume.log |
трейсбеки упавшего рендера |
Если XDG_RUNTIME_DIR не задан, используется /tmp. Всё лежит в tmpfs и
исчезает при перезагрузке; лог обнуляется по достижении 256 КБ.
Параметры внутри скрипта
Правятся константами в начале файла.
| Константа | По умолчанию | Смысл |
|---|---|---|
VOLUME_STEP |
0.05 |
шаг на одно нажатие |
VOLUME_LIMIT |
1.0 |
потолок; поднять выше 1.0, чтобы разрешить усиление |
SIGNAL_DEBOUNCE |
0.3 |
не чаще одного сигнала барам за это время |
PAUSE_ON_OUTPUT_CHANGE |
True |
пауза плееров при смене устройства вывода |
WAYBAR_SIGNAL |
12 |
номер сигнала модуля, должен совпадать с конфигом бара |
NOTIFY_*_TIMEOUT |
1500 / 5000 мс |
время жизни OSD |
ICON_* |
Nerd Font | иконки для 0 / <34% / <67% / выше |
Debounce касается только цифры в баре: OSD рисуется напрямую из демона и остаётся мгновенным.
Отказоустойчивость
- Потеря соединения с pulse не роняет демон: поток событий переподключается раз в секунду, ошибка логируется однократно, а не на каждой попытке.
- Любое исключение в пути рендера бара перехватывается: на stdout всегда уходит
разбираемый JSON, трейсбек уходит в лог. Пустой stdout при
"return-type": "json"в зависимости от версии Waybar означает потерянное обновление или падение всего бара. - Сигнал бару не отправляется, пока тот не установил обработчик (проверяется по
SigCgtв/proc/<pid>/status). Действие по умолчанию дляSIGRTMIN+n— убить процесс, а sway поднимает демон раньше, чем Waybar успевает инициализировать GTK.
Диагностика
$ volume-daemon.py --check
state file: /run/user/1000/waybar-state/volume.json
daemon: running
waybar pids: [1234, 1235]
signal: SIGRTMIN+12
pause on output change: enabled (playerctl found)
Логи демона идут в stderr. Если он запущен из sway, смотреть в журнале сессии.