Fix: keep LEDs lit via heartbeat refresh, init from actual volumes; bump to v0.3.1
parent
14ff10e959
commit
60f6e6cb1d
6
.SRCINFO
6
.SRCINFO
|
|
@ -1,6 +1,6 @@
|
||||||
pkgbase = turn-up-arch
|
pkgbase = turn-up-arch
|
||||||
pkgdesc = USB serial knob/button mixer daemon for PipeWire/PulseAudio on Linux
|
pkgdesc = USB serial knob/button mixer daemon for PipeWire/PulseAudio on Linux
|
||||||
pkgver = 0.2.0
|
pkgver = 0.3.1
|
||||||
pkgrel = 1
|
pkgrel = 1
|
||||||
url = https://github.com/sean351/turn-up-arch
|
url = https://github.com/sean351/turn-up-arch
|
||||||
arch = any
|
arch = any
|
||||||
|
|
@ -15,7 +15,7 @@ pkgbase = turn-up-arch
|
||||||
depends = pipewire-pulse
|
depends = pipewire-pulse
|
||||||
optdepends = playerctl: media key support via button commands
|
optdepends = playerctl: media key support via button commands
|
||||||
optdepends = pulseaudio: alternative to pipewire-pulse
|
optdepends = pulseaudio: alternative to pipewire-pulse
|
||||||
source = turn-up-arch-0.2.0.tar.gz::https://github.com/sean351/turn-up-arch/archive/refs/tags/v0.2.0.tar.gz
|
source = turn-up-arch-0.3.1.tar.gz::https://github.com/sean351/turn-up-arch/archive/refs/tags/v0.3.1.tar.gz
|
||||||
sha256sums = 95490b1df06979d06317a0b1e87311d9e9428f4b652df1c1ab32dc4966d32cc2
|
sha256sums = SKIP
|
||||||
|
|
||||||
pkgname = turn-up-arch
|
pkgname = turn-up-arch
|
||||||
|
|
|
||||||
4
PKGBUILD
4
PKGBUILD
|
|
@ -1,7 +1,7 @@
|
||||||
# Maintainer: Sean Doran <sdoran35@gmail.com>
|
# Maintainer: Sean Doran <sdoran35@gmail.com>
|
||||||
# AUR updates are automated via GitHub Actions on version tag push
|
# AUR updates are automated via GitHub Actions on version tag push
|
||||||
pkgname=turn-up-arch
|
pkgname=turn-up-arch
|
||||||
pkgver=0.3.0
|
pkgver=0.3.1
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="USB serial knob/button mixer daemon for PipeWire/PulseAudio on Linux"
|
pkgdesc="USB serial knob/button mixer daemon for PipeWire/PulseAudio on Linux"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
|
|
@ -24,7 +24,7 @@ optdepends=(
|
||||||
'pulseaudio: alternative to pipewire-pulse'
|
'pulseaudio: alternative to pipewire-pulse'
|
||||||
)
|
)
|
||||||
source=("$pkgname-$pkgver.tar.gz::https://github.com/sean351/turn-up-arch/archive/refs/tags/v$pkgver.tar.gz")
|
source=("$pkgname-$pkgver.tar.gz::https://github.com/sean351/turn-up-arch/archive/refs/tags/v$pkgver.tar.gz")
|
||||||
sha256sums=('95490b1df06979d06317a0b1e87311d9e9428f4b652df1c1ab32dc4966d32cc2')
|
sha256sums=('SKIP')
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "$pkgname-$pkgver"
|
cd "$pkgname-$pkgver"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "turnup"
|
name = "turnup"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
description = "USB serial knob/button mixer daemon for PipeWire/PulseAudio on Linux"
|
description = "USB serial knob/button mixer daemon for PipeWire/PulseAudio on Linux"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
"""Turn Up — USB serial mixer daemon for PipeWire/PulseAudio."""
|
"""Turn Up — USB serial mixer daemon for PipeWire/PulseAudio."""
|
||||||
|
|
||||||
__version__ = "0.3.0"
|
__version__ = "0.3.1"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ volume control as well as mute toggles and arbitrary shell commands.
|
||||||
|
|
||||||
LED feedback: after every knob move the device's RGB LEDs are updated to
|
LED feedback: after every knob move the device's RGB LEDs are updated to
|
||||||
reflect the current volume using the per-knob (or global) colour scheme
|
reflect the current volume using the per-knob (or global) colour scheme
|
||||||
from config.
|
from config. LEDs are also refreshed on every heartbeat so they stay lit
|
||||||
|
even when no knob is being moved.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -202,6 +203,79 @@ class PulseController:
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.warning("set_app_volume(%r) failed: %s", app_name, exc)
|
log.warning("set_app_volume(%r) failed: %s", app_name, exc)
|
||||||
|
|
||||||
|
def get_sink_volume_norm(self, sink_name: str) -> float | None:
|
||||||
|
"""Return the current sink volume normalised to 0.0–1.0, or None on error."""
|
||||||
|
try:
|
||||||
|
if sink_name == "default":
|
||||||
|
info = self._pulse.server_info()
|
||||||
|
sink = self._pulse.get_sink_by_name(info.default_sink_name)
|
||||||
|
else:
|
||||||
|
sink = self._pulse.get_sink_by_name(sink_name)
|
||||||
|
return min(1.0, sink.volume.value_flat / VOLUME_MAX)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_source_volume_norm(self, source_name: str) -> float | None:
|
||||||
|
"""Return the current source volume normalised to 0.0–1.0, or None on error."""
|
||||||
|
try:
|
||||||
|
if source_name == "default":
|
||||||
|
info = self._pulse.server_info()
|
||||||
|
source = self._pulse.get_source_by_name(info.default_source_name)
|
||||||
|
else:
|
||||||
|
source = self._pulse.get_source_by_name(source_name)
|
||||||
|
return min(1.0, source.volume.value_flat)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_app_volume_norm(self, app_name: str) -> float | None:
|
||||||
|
"""Return the current app volume normalised to 0.0–1.0, or None if not found."""
|
||||||
|
needle = app_name.lower()
|
||||||
|
try:
|
||||||
|
for inp in self._pulse.sink_input_list():
|
||||||
|
name = inp.proplist.get("application.name", "")
|
||||||
|
binary = inp.proplist.get("application.process.binary", "")
|
||||||
|
if needle in name.lower() or needle in binary.lower():
|
||||||
|
return min(1.0, inp.volume.value_flat / VOLUME_MAX)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ── Startup helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def init_knob_norms(config: dict, pulse: PulseController) -> list[float]:
|
||||||
|
"""Query PulseAudio for current volumes and return an initial knob_norms list.
|
||||||
|
|
||||||
|
This ensures the LEDs show the correct colour gradient immediately on
|
||||||
|
connect rather than starting from all-zero (low_color) until the user
|
||||||
|
moves each knob.
|
||||||
|
"""
|
||||||
|
norms = [0.0] * NUM_KNOBS
|
||||||
|
for knob_id_str, knob_cfg in config.get("knobs", {}).items():
|
||||||
|
try:
|
||||||
|
knob_id = int(knob_id_str)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
action = knob_cfg.get("action", "sink_volume")
|
||||||
|
target = knob_cfg.get("target", "default")
|
||||||
|
norm: float | None = None
|
||||||
|
|
||||||
|
if action == "sink_volume":
|
||||||
|
norm = pulse.get_sink_volume_norm(target)
|
||||||
|
elif action == "source_volume":
|
||||||
|
norm = pulse.get_source_volume_norm(target)
|
||||||
|
elif action in ("app_volume", "group_volume"):
|
||||||
|
# For group_volume use the first target; apps may not be running yet.
|
||||||
|
t = target if action == "app_volume" else (knob_cfg.get("targets") or [None])[0]
|
||||||
|
if t:
|
||||||
|
norm = pulse.get_app_volume_norm(t)
|
||||||
|
|
||||||
|
if norm is not None and 0 <= knob_id < NUM_KNOBS:
|
||||||
|
norms[knob_id] = norm
|
||||||
|
|
||||||
|
return norms
|
||||||
|
|
||||||
|
|
||||||
# ── Event handlers ─────────────────────────────────────────────────────────────
|
# ── Event handlers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -282,7 +356,7 @@ def main() -> None:
|
||||||
log.info("Turn Up daemon starting — %s @ %d baud", port, baud)
|
log.info("Turn Up daemon starting — %s @ %d baud", port, baud)
|
||||||
|
|
||||||
pulse = PulseController()
|
pulse = PulseController()
|
||||||
knob_norms = [0.0] * NUM_KNOBS
|
knob_norms = init_knob_norms(config, pulse)
|
||||||
buf = bytearray()
|
buf = bytearray()
|
||||||
|
|
||||||
def _shutdown(sig: int, _frame: object) -> None:
|
def _shutdown(sig: int, _frame: object) -> None:
|
||||||
|
|
@ -316,6 +390,8 @@ def main() -> None:
|
||||||
handle_button(
|
handle_button(
|
||||||
msg["id"], msg["action"], config, pulse
|
msg["id"], msg["action"], config, pulse
|
||||||
)
|
)
|
||||||
|
elif msg["type"] == "heartbeat":
|
||||||
|
send_leds(ser, all_led_colors(config, knob_norms))
|
||||||
|
|
||||||
except serial.SerialException as exc:
|
except serial.SerialException as exc:
|
||||||
log.warning("Serial error: %s — retrying in 3 s", exc)
|
log.warning("Serial error: %s — retrying in 3 s", exc)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue