252 lines
11 KiB
Markdown
252 lines
11 KiB
Markdown
---
|
|
task_id: souveraine-native-audio-001
|
|
title: "Souveraine native audio and privacy surfaces — PulseAudio for sound, PipeWire for portals"
|
|
status: in_progress
|
|
priority: high
|
|
phase: delivery
|
|
created: 2026-07-18
|
|
references:
|
|
- surfaces/quickshell/services/Audio.qml
|
|
- surfaces/quickshell/modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml
|
|
- surfaces/quickshell/modules/ii/bar/UtilButtons.qml
|
|
- surfaces/quickshell/deploy.sh
|
|
- /home/casey/Projects/Pixel3Arch/rootfs-overlay/etc/pulse/default.pa.d/90-blueline-alsa.pa
|
|
- /home/casey/Projects/Pixel3Arch/pkgs/pipewire-abi-provider/PKGBUILD
|
|
- docs/tasks/souveraine-settings-ipc-profiles.md
|
|
- docs/tasks/souveraine-lock-primitives.md
|
|
---
|
|
|
|
# Souveraine native audio and privacy surfaces
|
|
|
|
## End goal
|
|
|
|
Souveraine owns the complete audio control and privacy presentation for phone
|
|
and laptop. Native PulseAudio is the only audio server. PipeWire exists only as
|
|
the video transport required by screen-cast/camera portals; `pipewire-pulse` is
|
|
never started and never owns an audio device.
|
|
|
|
The user and agents see one truthful audio model:
|
|
|
|
- the real default output and input;
|
|
- output/input volume and mute;
|
|
- which applications hold playback or recording streams;
|
|
- a visible, persistent microphone/camera/screen-capture privacy indicator;
|
|
- device switching where more than one real endpoint exists;
|
|
- structured IPC methods guarded by the settings/capability authority;
|
|
- no ii or PipeWire audio assumptions in the active dependency graph.
|
|
|
|
This is an original AGPL Souveraine implementation. The current files under
|
|
`modules/ii/` are functional scaffolding and behavioral evidence, not the
|
|
destination and not files to relabel.
|
|
|
|
## Ground truth as of 2026-07-18
|
|
|
|
The important PulseAudio foundation is already present:
|
|
|
|
- `services/Audio.qml` is PulseAudio-backed and mirrors both the default sink
|
|
and the default source through `pactl`.
|
|
- It has real source volume/mute setters, `toggleMicMute()`, `sourceReady`,
|
|
`inputDevices`, and a first `micActive` signal based on Pulse source outputs.
|
|
- `VolumeDialogContent.qml` presents a real input page and binds the slider to
|
|
`Audio.source` rather than a fake microphone object.
|
|
- `UtilButtons.qml` now calls `Audio.toggleMicMute()` and reads the real source
|
|
mute state.
|
|
- Pixel3Arch autoloads the handset capture PCM as the named Pulse source
|
|
`blueline_mic`, makes it the default source, and uses UCM to establish the
|
|
handset route before PulseAudio starts.
|
|
|
|
Do not reopen those solved questions or restore PipeWire audio. The remaining
|
|
work is ownership, completeness, event accuracy, privacy presentation, IPC,
|
|
and portal coexistence.
|
|
|
|
## The architectural boundary
|
|
|
|
| Function | Authority |
|
|
|---|---|
|
|
| Speaker/headphone playback | native PulseAudio |
|
|
| Handset/external microphone capture | native PulseAudio |
|
|
| Per-app audio streams | native PulseAudio sink-input/source-output model |
|
|
| Audio device selection | native PulseAudio defaults and profiles |
|
|
| Screen-cast frames | PipeWire portal transport |
|
|
| Camera frames | PipeWire portal transport where the portal requires it |
|
|
| Audio for screen recording | explicit PulseAudio capture chosen by the recorder; never `pipewire-pulse` |
|
|
|
|
PipeWire's presence must not imply it is an audio server. Packaging and user
|
|
units enforce the boundary: no Pulse compatibility daemon, no ALSA device
|
|
acquisition by PipeWire, and no competing session policy for sound.
|
|
|
|
## Owned service model
|
|
|
|
Replace the scaffolding with an original `SouveraineAudio.qml` service (final
|
|
name may remain `Audio` for call-site stability). It projects:
|
|
|
|
```text
|
|
serverReady
|
|
defaultSink / defaultSource
|
|
sinks[] / sources[]
|
|
playbackStreams[] / recordingStreams[]
|
|
outputActive / microphoneActive
|
|
lastError / generation
|
|
```
|
|
|
|
Each device and stream has a stable, typed shape: Pulse name/index,
|
|
description, availability, volume, mute, monitor/physical classification,
|
|
application identity, media role, corked/running state, and owning PID when
|
|
Pulse exposes it. Do not infer a microphone from `hw:0,1` naming alone;
|
|
`blueline_mic` is the board's canonical source name, while laptops may publish
|
|
different source names.
|
|
|
|
### Event model
|
|
|
|
The current 1.5-second `pactl` snapshot is acceptable diagnostic scaffolding
|
|
but not the final privacy mechanism. The owned service keeps one long-lived
|
|
`pactl subscribe` observer, debounces server events, then refreshes a complete
|
|
machine-readable snapshot. Prefer `pactl --format=json` when supported and a
|
|
small owned parser/fallback when it is not; do not parse localized display
|
|
labels.
|
|
|
|
Loss/restart of PulseAudio increments `generation`, clears stale objects, and
|
|
repopulates defaults. Sink readiness and source readiness remain independent:
|
|
a missing microphone must never disable speaker controls.
|
|
|
|
Every setter returns a structured result or emits an error with the affected
|
|
object. The UI does not optimistically lie when `pactl` fails.
|
|
|
|
## Privacy truth
|
|
|
|
Build one owned `PrivacyService` that consumes actual subsystem activity, not
|
|
UI toggles:
|
|
|
|
- `microphoneActive`: at least one Pulse source-output exists; expose the
|
|
application identities and whether streams are corked.
|
|
- `cameraActive`: portal/camera session ownership, not merely device presence.
|
|
- `screenCaptureActive`: active portal screencast or Souveraine recorder
|
|
session.
|
|
|
|
The prior borrowed Privacy model must not be reused: converting an array with
|
|
`.map(...)` to a boolean is always truthy and is not evidence of access. The
|
|
owned implementation uses explicit collection predicates and retains the
|
|
participants needed for disclosure.
|
|
|
|
Privacy indication is not configurable away. Appearance may vary, but active
|
|
capture always has a persistent visible indicator on the unlocked shell and a
|
|
safe ambient indication on the lock surface. Opening it shows which
|
|
application is accessing what and offers a stop/revoke action where the
|
|
underlying subsystem supports one. Unknown clients are displayed as unknown,
|
|
not hidden.
|
|
|
|
Muting the microphone and stopping access are distinct operations:
|
|
|
|
- mute changes the default source's audio value;
|
|
- stop/revoke terminates or denies a recording stream/session;
|
|
- the indicator remains visible while a client still holds the stream, even if
|
|
the source is muted.
|
|
|
|
This distinction prevents a muted-but-still-recording process from becoming
|
|
invisible.
|
|
|
|
## Owned surfaces
|
|
|
|
Create original Souveraine AGPL components under Souveraine paths:
|
|
|
|
- `services/SouveraineAudio.qml` — authoritative Pulse model and methods.
|
|
- `services/PrivacyService.qml` — cross-subsystem access truth.
|
|
- `modules/souveraine/audio/VolumeSurface.qml` — output/input tabs, real
|
|
endpoint state, and per-app streams.
|
|
- `modules/souveraine/audio/DeviceRow.qml` and `StreamRow.qml` — reusable owned
|
|
presentation primitives.
|
|
- `modules/souveraine/privacy/PrivacyIndicator.qml` — persistent compact state.
|
|
- `modules/souveraine/privacy/PrivacyDetails.qml` — identities and stop/revoke
|
|
actions.
|
|
- phone and laptop hosts that place the same semantic components according to
|
|
their own form-factor contract.
|
|
|
|
Rewire active bars/sidebars to these components, then remove the corresponding
|
|
`modules/ii` overrides and borrowed imports from the active graph. Do not copy
|
|
or vendor ii widgets while doing so; use the Souveraine-owned primitive layer.
|
|
|
|
## IPC and agent reach
|
|
|
|
Expose a JSON-over-string `audio` IPC surface because Quickshell drops `var`
|
|
returns:
|
|
|
|
```text
|
|
audio.state()
|
|
audio.devices(direction)
|
|
audio.streams(direction)
|
|
audio.setVolume(direction, value, reason)
|
|
audio.setMuted(direction, muted, reason)
|
|
audio.setDefault(direction, pulseName, reason)
|
|
audio.stopStream(streamId, reason)
|
|
```
|
|
|
|
Read-only state is ambient. Ordinary volume/mute changes are direct low-risk
|
|
settings/actions and audited. Changing a default endpoint is direct while
|
|
unlocked unless the settings schema says otherwise. Stopping another
|
|
application's recording stream is a physical/privacy action: an agent may
|
|
propose it, and human surfaces may act according to the settings authority's
|
|
policy. IPC never invents a second authorization model.
|
|
|
|
All methods validate direction, ranges, endpoint existence, Pulse generation,
|
|
and current lock/security state. They return `{ok:false, reason}` rather than
|
|
silently no-oping.
|
|
|
|
## Portal-only PipeWire
|
|
|
|
The current Pixel3Arch `pipewire-abi-provider` conflicts with the PipeWire
|
|
daemon and WirePlumber. That satisfies ABI consumers but makes real portal
|
|
screen sharing impossible. Split the packaging policy:
|
|
|
|
1. retain native PulseAudio and its ALSA ownership;
|
|
2. install a real PipeWire daemon plus the minimal session/portal policy needed
|
|
for video nodes;
|
|
3. explicitly mask/omit `pipewire-pulse`;
|
|
4. prevent WirePlumber/PipeWire from claiming ALSA audio devices, or use a
|
|
portal-video-only configuration if the selected session manager supports
|
|
it;
|
|
5. verify `xdg-desktop-portal` and the compositor portal can establish a
|
|
screencast without changing Pulse sinks/sources.
|
|
|
|
This packaging repair belongs in Pixel3Arch and must be tested as a system
|
|
contract. Souveraine surfaces consume its portal state; they do not launch an
|
|
ad-hoc competing PipeWire audio stack.
|
|
|
|
## Verification matrix
|
|
|
|
Verify on both phone and laptop:
|
|
|
|
1. Pulse starts with the intended default sink/source and no `pipewire-pulse`
|
|
socket or daemon.
|
|
2. Source/sink volume and mute changes round-trip through Pulse and survive a
|
|
service refresh.
|
|
3. Starting and stopping recording changes the privacy indicator promptly and
|
|
names the client.
|
|
4. Muting during recording leaves the access indicator visible.
|
|
5. A Pulse restart clears stale devices/streams and recovers without restarting
|
|
Quickshell.
|
|
6. A missing source leaves output controls fully functional.
|
|
7. Screen sharing works through the portal-only PipeWire path while Pulse
|
|
remains the sole audio server.
|
|
8. Lock/unlock transitions never expose private application metadata on the
|
|
lock surface, but active capture remains visibly indicated.
|
|
9. IPC calls return structured success/refusal and cannot bypass the settings
|
|
or step-up authority.
|
|
|
|
## Done when
|
|
|
|
- [ ] Active audio/privacy services and surfaces are original Souveraine AGPL
|
|
files with no ii widget dependency in their graph.
|
|
- [ ] Native PulseAudio is the sole audio authority on phone and laptop.
|
|
- [ ] Real sinks, sources, playback streams, and recording streams are modeled
|
|
eventfully and recover across daemon restart.
|
|
- [ ] A persistent truthful privacy indicator identifies active capture clients
|
|
and distinguishes mute from revocation.
|
|
- [ ] Owned phone and laptop presentations share semantics without sharing
|
|
inappropriate layout.
|
|
- [ ] Agent IPC covers discovery and guarded actions through the common
|
|
settings/capability policy.
|
|
- [ ] Portal video works with a real PipeWire transport while `pipewire-pulse`
|
|
remains absent.
|
|
- [ ] Pixel 3 microphone capture is verified non-zero through the named
|
|
`blueline_mic` Pulse source; no raw-ALSA-only success is accepted as the
|
|
product result.
|