11 KiB
| task_id | title | status | priority | phase | created | references | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| souveraine-native-audio-001 | Souveraine native audio and privacy surfaces — PulseAudio for sound, PipeWire for portals | in_progress | high | delivery | 2026-07-18 |
|
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.qmlis PulseAudio-backed and mirrors both the default sink and the default source throughpactl.- It has real source volume/mute setters,
toggleMicMute(),sourceReady,inputDevices, and a firstmicActivesignal based on Pulse source outputs. VolumeDialogContent.qmlpresents a real input page and binds the slider toAudio.sourcerather than a fake microphone object.UtilButtons.qmlnow callsAudio.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:
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.qmlandStreamRow.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:
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:
- retain native PulseAudio and its ALSA ownership;
- install a real PipeWire daemon plus the minimal session/portal policy needed for video nodes;
- explicitly mask/omit
pipewire-pulse; - prevent WirePlumber/PipeWire from claiming ALSA audio devices, or use a portal-video-only configuration if the selected session manager supports it;
- verify
xdg-desktop-portaland 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:
- Pulse starts with the intended default sink/source and no
pipewire-pulsesocket or daemon. - Source/sink volume and mute changes round-trip through Pulse and survive a service refresh.
- Starting and stopping recording changes the privacy indicator promptly and names the client.
- Muting during recording leaves the access indicator visible.
- A Pulse restart clears stale devices/streams and recovers without restarting Quickshell.
- A missing source leaves output controls fully functional.
- Screen sharing works through the portal-only PipeWire path while Pulse remains the sole audio server.
- Lock/unlock transitions never expose private application metadata on the lock surface, but active capture remains visibly indicated.
- 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-pulseremains absent. - Pixel 3 microphone capture is verified non-zero through the named
blueline_micPulse source; no raw-ALSA-only success is accepted as the product result.