aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/sound/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/sound/README.md')
-rw-r--r--desktop/modules/sound/README.md159
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.