Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/AUDIO-PRIVACY.md

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
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:

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:

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.