diff options
Diffstat (limited to 'desktop/modules/sound/README.md')
| -rw-r--r-- | desktop/modules/sound/README.md | 159 |
1 files changed, 159 insertions, 0 deletions
diff --git a/desktop/modules/sound/README.md b/desktop/modules/sound/README.md new file mode 100644 index 0000000..f0a8313 --- /dev/null +++ b/desktop/modules/sound/README.md @@ -0,0 +1,159 @@ +# volume-osd + +An on-screen display for volume, covering both output (speakers) and input +(microphone). It appears at the bottom of the screen when the level or mute +state changes, and fades out 1.5 seconds later. + + ┌────────────────────────────────────┐ + │ ▪ Outside World ⏮ ⏸ ⏭ │ + │ Sunbeam │ + │ ──────────────────────────────── │ + │ 🔊 Output 75% │ + │ ████████████████░░░░░░░░░░ │ + └────────────────────────────────────┘ + + (the track row only exists while a player does) + +## Running it + + qs -p . + +From Hyprland, to start it with the session: + + exec-once = qs -p ~/Programming/GIT/quickshell/volume-osd + +## Now playing + +When an MPRIS player is running, a track row sits above the volume bar: album +art, title, artist, and prev/play/next. With no player the panel is exactly +the volume OSD, at its original size. A track change or a play/pause shows the +panel too, so the row is not something you only see by touching the volume. + +Hovering the panel freezes its fade so the buttons can be clicked; moving away +starts the countdown again. Without a hover it behaves exactly as it did before +there was anything clickable on it. It still takes no keyboard focus. + +Two things about MPRIS that are not obvious: + +**Proxies publish duplicates.** playerctld proxies whichever player is +active and republishes it under `org.mpris.MediaPlayer2.playerctld`. With a +browser, plasma-browser-integration does the same, so one Navidrome tab was +live on three bus names at once. `Player.qml` drops both proxies and talks to +the browser's own entry. None of the three exists until playback starts, and +an open but silent tab publishes nothing at all. The plasma name is a browser +thing only: Feishin, playing from the same server, publishes just its own +entry and playerctld, so dropping it costs a native player nothing. + +**Album art can be a reused temp path.** Audacious extracts embedded art to a +file in its cache and rewrites that same path on each track, so the URL repeats +while the image behind it changes. The source carries the track title as a +cache buster and `cache: false`, or the previous track's cover stays on screen. + +Verified against audacious, Feishin, and Firefox playing Navidrome. Feishin is +the reference case, the only player here that fills the metadata in properly: +a real `mpris:artUrl`, `xesam:artist`, and a clean `xesam:title`. It is also +the only one whose art is a remote HTTP URL rather than a local file, so the +cover depends on reaching the Navidrome host. + +The same server through Firefox gives much less, which is a browser limit and +not something this component can fix. Navidrome's web player sets no +`mpris:artUrl` at all, so the row renders with no cover, and it packs +everything into `xesam:title` ("Roxanne - The Police - Navidrome") leaving +`xesam:artist` empty. Neither is worked around, a title like that cannot be +split back apart without guessing where a real dash ends. + +Signal publishes no MPRIS bus at all: playing an attachment claims no +`org.mpris.MediaPlayer2.*` name, only its tray `StatusNotifierItem`, so the +track row stays hidden and there is nothing here to fix. Any player that never +publishes is invisible to this component by construction. + +## No keybinds to change + +The OSD watches PipeWire rather than being triggered by a hotkey, so existing +volume binds keep working untouched: + + bind = , XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ + +Because the source of truth is PipeWire and not the keypress, the OSD also +appears for volume changed from anywhere else: pavucontrol, a per-application +slider, or another machine's remote control. + +One widget serves both directions. Whichever device changed last is the one +displayed, with a speaker icon for output and a microphone for input. + +Icons are Nerd Font glyphs (speaker and microphone), so the font stack needs a +Nerd Font available for fallback. Inconsolata Nerd Font Mono, which +unified-desktop-theme already installs, covers them. + +## Frosted glass + +The panel is drawn translucent (65% over the Macchiato base) and the blur +behind it comes from the compositor, not from QML. Hyprland blurs a layer +surface only when a rule says to, matched on the namespace this window sets +(`quickshell-volume-osd`): + + hl.layer_rule({ + name = "blur-volume-osd", + match = { namespace = "^(quickshell-volume-osd)$" }, + blur = true, + xray = false, + ignore_alpha = 0.1, + }) + +`xray = false` frosts the windows actually behind the OSD rather than jumping +straight to the wallpaper. `ignore_alpha = 0.1` leaves near-transparent pixels +unblurred, which keeps the rounded corners from picking up a halo. + +Doing it this way costs nothing in the shell: no `MultiEffect`, no live +blur pass in QML, no offscreen buffer. Without the rule the OSD still works, +it just renders flat translucent instead of frosted. + +## Theme + +`Theme.qml` holds no palette of its own beyond a fallback. The colours come +from `~/.cache/wal/udt-palette.qml`, which `udt-accent` generates on every +wallpaper change from unified-desktop-theme's `palette.rasi`, carrying the +whole Macchiato palette plus the accent snapped from the wallpaper. + +That file is watched, so editing the palette in unified-desktop-theme and +regenerating recolours a running OSD with no restart. Where the file does not +exist, the hardcoded defaults in `Theme.qml` apply, which is what keeps this +directory runnable on a machine without unified-desktop-theme. + +## Two details worth knowing + +**A node's volume arrives before it is ready.** When a `PwNode` binds, its +volume populates and emits a change signal, and that happens while `ready` is +still false. Those first signals are state being read, not the user turning a +knob, so `show()` checks `ready` and ignores them, which is why there is no +OSD at login. + +That one check is the whole guard, and it is tempting to add a second. An +earlier version also swallowed the first change per node, on the assumption +that the startup values arrived *after* ready. They do not, so the extra guard +ate the user's first keypress instead: the OSD only appeared from the second +change onward. If this symptom comes back, trace the signal order before +adding a filter. + +**`PwObjectTracker` is not optional.** PipeWire node properties are only kept +current while something binds the node. Without the tracker the volume reads +once and then goes stale, which looks like an OSD that displays a number from +several changes ago. + +**A config with no visible window exits.** The OSD is hidden most of the time, +so it holds itself open with a 1x1 transparent window with an empty mask, +which is click-through and draws nothing. Without it the shell loads, reports +no error, and quits, and the symptom is a keybind that appears to do nothing +or a panel that never paints. This was removed once during development after +misreading a process check, and the bug came straight back. + +**A Row sizes to its children, not to its parent.** The track row's text +column originally had a fixed width, and art + text + buttons + spacing came +to 356px inside a 328px content box, so the `next` button hung over the panel +edge. The column now takes whatever the art and transport buttons leave, which +holds at any panel width. Fixed widths inside a Row are worth distrusting. + +## Volume above 100% + +PipeWire allows volume over 1.0. The percentage is reported as-is, so it can +read above 100%, while the bar stops at full rather than overflowing its track. |
