#+TITLE: Audio Panel — the pulsemixer console #+AUTHOR: Craig Jennings #+DATE: 2026-07-03 #+TODO: TODO | DONE #+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED * IMPLEMENTED Status :PROPERTIES: :ID: 71f556c6-ee02-47cc-a3be-68c8289380f3 :END: - [2026-07-03 Fri] IMPLEMENTED — built in a no-approvals speedrun in the dotfiles repo (branch panel-bugfixing): engine (pactl), presenter, GTK panel, PTT arming, bar indicator, and the bar/keybind wiring, across four commits 65e5bb0..9601420. 102 unit tests + a passing AT-SPI smoke on velox. All five Decisions below resolved. Live-eyeball validation (visual polish, PTT-in-a-meeting, fader feel, the master-mute hardware key) is the one open follow-up, tracked as a manual-testing task in todo.org. - [2026-07-03 Fri] DRAFT — stub from the todo.org task "Audio panel spec" (roam ask 2026-07-02) plus the 2026-07-03 waybar/sound design discussion. Written to iterate alongside the prototype (=working/sound-panel/sound-panel-prototype.html=). Spine is present; the Decisions and Design detail get filled in as we go. * Metadata | Field | Value | |--------+---------------------------------------------------| | Status | implemented | |--------+---------------------------------------------------| | Owner | Craig Jennings | |--------+---------------------------------------------------| | Repo | dotfiles | |--------+---------------------------------------------------| | Kin | net panel + bt panel (architecture + aesthetic | | | donors), desktop-settings panel (sibling) | |--------+---------------------------------------------------| * Problem Audio control today is the pyprland audio scratchpad (Super+A) — a floating pulsemixer TUI — plus scattered bar affordances: =pulseaudio= (volume, click to mute sink), =pulseaudio#mic= (mic glyph + mic-toggle), Super+M audio-cycle ring, Super+Shift+A mic-toggle. There's no single glanceable surface that shows every sink and source, lets you set the default output/input, and carries the meeting-grade mic controls Craig wants (a clean muted mode and a hold-to-talk mode). The net + bluetooth panels set the pattern for exactly this shape; audio is the third instrument in the family. * Goals 1. One panel, opened from the bar's sound glyph, exposing the full pulsemixer surface: every sink and source, per-device volume, per-device mute, and switching the default output and input. 2. Replace the pyprland audio scratchpad (Super+A) as the primary audio UI. 3. Mic modes for meetings: *live*, *muted*, and *push-to-talk* (mic stays muted except while Space is held, releasing re-mutes). 4. A *master quick-mute* — one action mutes all output — reachable from the faceplate and a keybind. 5. Instrument-console aesthetic and architecture consistent with net + bt: same faceplate, lamps, engraved sections, console keys, needle gauges, verify-everything contract. 6. The bar glyph reflects live state: speaker + three arcs normally, a speaker-with-✕ when muted (Craig's called glyphs). * Design sketch Prototype: =working/sound-panel/sound-panel-prototype.html= (the reference for layout + idioms below). ** Surface (from the prototype) - *Faceplate* — status lamp, sound glyph, state word (PLAYBACK / MUTED), a MUTED badge, the SND·01 unit label, and the *master quick-mute switch* (same switch idiom as net wifi / bt power), plus the close ✕. - *OUTPUTS section* — one row per sink. Row body click = set default (gold DEF tag). A machined fader sets that sink's volume; the trailing glyph mutes just that device. Active/default row is emphasized (cream name, gold lamp/glyph). - *INPUTS section* — one row per source, same idioms. - *Mic mode* — three console keys: LIVE / MUTED / PUSH·TALK. Push-to-talk keeps the mic muted (red IN needle) and un-mutes only while Space is held. - *Twin VU needles* — output level + input level, the sound analog of net throughput and bt battery gauges. Needle goes red when its side is muted. ** Architecture — clone the net/bt panel stack - GTK4 + gtk4-layer-shell, Blueprint =.blp= → committed =.ui= (=make ui=, dev-only build dep). - Humble-object split: a GTK-free PanelModel presenter (unit-tested like the net/bt PanelModels) + thin composite-widget pages. Backing actions in a GTK-free =audio.py= that shells to the audio control layer (pactl / wpctl / pulsemixer — pick below), TDD'd with fake binaries. - One gated AT-SPI smoke (=run-panel-smoke.sh= pattern). - Shared instrument-console palette CSS asset (the one net/bt/settings all load) — do not duplicate the palette block. - Code lives in dotfiles =audio/= sibling to =net/= (src-layout, tests in =tests/audio/=). * Decisions (Craig) ** DONE Audio control backend — pactl vs wpctl vs pulsemixer CLOSED: [2026-07-03 Fri] Resolved: =pactl= (the engine module is =pactl.py=). Both ratio and velox run PipeWire with the pipewire-pulse compat layer and no PulseAudio daemon, so pactl and wpctl hit the same graph — but =pactl -f json= gives structured, name-addressable output where wpctl offers only a volatile-id tree. Reads go through =pactl -f json list sinks|sources= + =get-default-*=; writes target devices by stable name behind an argv-charset guard. ** DONE Push-to-talk mechanism under Wayland (feasibility — phase 1) CLOSED: [2026-07-03 Fri] Resolved: route (a), Hyprland dynamic binds. The phase-1 spike confirmed all three primitives on velox (Hyprland 0.55.4): =hyprctl keyword bind/unbind= adds and removes a bind live, =bindr= fires on release, and =pactl set-source-mute @DEFAULT_SOURCE@ 0|1= toggles the mic cleanly. =ptt.py= arms a press bind (un-mute) + a bindr (re-mute) on entering PTT mode and unbinds on leaving, so the talk key isn't grabbed globally otherwise. No evdev needed. Documented behavior: while PTT is armed, the talk key is the talk key. ** DONE Quick-mute keybind + scope CLOSED: [2026-07-03 Fri] Resolved: the XF86AudioMute hardware key (Super+Shift+M turned out to be taken by the monocle-layout bind, so the spec's assumption was wrong). The mute key now runs =audio quick-mute=, which mutes every output (master), not just the default sink — identical on a single-sink machine, correct on a multi-sink one. Also reachable from the faceplate master switch and the panel. Scope: master mute of all sinks, with verify-after-apply per sink. ** DONE Bar glyph click map CLOSED: [2026-07-03 Fri] Resolved with the low-regret wiring: kept the existing =pulseaudio= waybar module (left-click mute, scroll volume — no regression) and repointed its right-click from the retired pulsemixer scratchpad to =audio-panel=. So: left = mute, right = open panel, scroll = volume. A fuller =custom/audio= indicator (state-following speaker glyph + its own click map) is built and tested (=indicator.py= + =waybar-audio=) but stays unwired until the new bar glyph gets a live eyeball — the swap is a one-line waybar edit when Craig's ready. ** DONE Fate of the existing audio affordances CLOSED: [2026-07-03 Fri] Resolved: Super+A repurposed from =pypr toggle audio= (the pulsemixer scratchpad) to =audio-panel= — the panel is the primary audio UI now, so the scratchpad is retired. Its definition still sits in the machine-local =pyprland.toml= (not stowed) and can be deleted by hand. Kept: =pulseaudio= + =pulseaudio#mic= waybar modules (glance + scroll + the mic-mute glance), Super+M cycle, Super+Shift+A + XF86AudioMicMute mic-toggle. Changed: XF86AudioMute → master quick-mute (see the quick-mute decision above). * Implementation phases 1. Push-to-talk feasibility spike (decision above) — the one unknown; settle the mechanism before committing the mic-mode design. 2. =audio.py= backings (list/get/set/mute/default for sinks + sources) — pure engine, TDD with a fake audio backend. 3. PanelModel presenter (rows, default tracking, mic modes, master mute, verify-after-apply) — unit-tested, no GTK. 4. Blueprint UI + sound bar glyph (normal / muted / ptt states) + open/close wiring; shared palette css; AT-SPI smoke. 5. Bar-affordance consolidation per the decision above; retire the Super+A scratchpad; keybinds.