6.3 KiB
| task_id | title | status | priority | phase | created | updated | references | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| souveraine-native-audio-001 | Souveraine audio and privacy surfaces | in_progress | high | delivery | 2026-07-18 | 2026-08-07 |
|
Souveraine audio and privacy surfaces
Current contract — 2026-08-07
The phone's audio stack works. PipeWire 1.6.8 owns the graph,
WirePlumber 0.5.15 owns session policy, pipewire-pulse provides the Pulse
compatibility API, and ALSA UCM describes the board routes. Native PulseAudio
and the old ABI-provider shims are historical experiments, not the desired
architecture.
- HiFi exposes both CS35L36 speakers and clean mono WCD9340 capture through DMIC4 → DEC5/TX5. Casey uses the microphone for STT.
- The Voice Call profile carries real two-way calls. The call packages manage the UCM transition, modem PCM, and earpiece/speaker selection.
- WirePlumber restores device volume and selected profile correctly. TASK-39 owns the remaining role/default/lock-tier policy; it must not invent a second persistence writer.
- TASK-07 owns the remaining hardware debt: the kernel holds codec sysclk and MICB2 permanently. That is a power and suspend defect, not an audio functionality defect.
The detailed hardware and lifecycle evidence lives in PAF/audio.md. Do not
reopen microphone bring-up from this surface document.
Authority
| Concern | Authority |
|---|---|
| Board verbs, devices, routes, and gains | souveraine-ucm-blueline |
| Runtime graph, profiles, streams, and volume restoration | PipeWire + WirePlumber |
| Pulse client compatibility | pipewire-pulse |
| Call start/end UCM transition | souveraine-callaudio |
| Modem voice PCM bridge | souveraine-q6voiced |
| In-call earpiece/speaker action | souveraine-callaudiod + callaudio hook |
| User-facing state, privacy truth, and guarded actions | Souveraine surfaces |
pactl remains a valid control and observation API because pipewire-pulse
implements it. Calling that interface does not make native PulseAudio the
authority.
Surface model
The owned audio service projects one truthful model:
serverReady
defaultSink / defaultSource
sinks[] / sources[]
playbackStreams[] / recordingStreams[]
activeProfile
outputActive / microphoneActive
lastError / generation
Each endpoint and stream needs a stable identifier, description, availability, volume, mute, application identity, media role, and running or corked state. Sink and source readiness are independent: loss of a microphone must not disable speaker controls.
The current pactl snapshots are acceptable scaffolding. The durable service
should keep a long-lived pactl subscribe observer, debounce graph changes,
and refresh from machine-readable JSON. A PipeWire restart increments
generation, clears stale objects, and repopulates the model. Setters report
failure; the UI does not optimistically lie.
Privacy truth
Privacy state comes from active clients and sessions, never from the appearance of a toggle.
microphoneActive: one or more recording streams exist; expose client identity and whether each stream is running or corked.cameraActive: an active camera or portal session, not device presence.screenCaptureActive: an active portal screencast or Souveraine recorder.
Muting and revoking access are different actions. A muted source can still be held by a recording client, so the privacy indicator remains visible until the stream ends. Unknown clients are shown as unknown, not omitted.
Active capture always has a persistent indicator on the unlocked shell and a safe ambient indication while locked. Details may name clients when unlocked; the lock surface must not leak private application metadata.
Owned surfaces and IPC
The present modules/ii files are working scaffolding. The destination is an
original Souveraine service and presentation layer, with the same semantics on
phone and laptop and form-factor-specific layout.
Expose JSON-over-string methods 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, endpointId, reason)
audio.stopStream(streamId, reason)
Read-only state is ambient. Volume and mute are ordinary audited actions. Stopping another application's recording is a privacy action and follows the common settings/capability authority. Every method validates direction, ranges, endpoint existence, graph generation, and lock state, then returns a structured success or refusal.
Verification
- Speaker, microphone, STT, and two-way calls remain functional through the PipeWire/WirePlumber stack.
- Volume, mute, and profile changes round-trip and survive a service refresh; WirePlumber remains the sole persistence writer.
- Starting and stopping a recording updates the privacy indicator promptly and identifies the client when safe to do so.
- Muting during recording leaves the access indicator visible.
- A graph restart clears stale devices and streams without restarting the shell.
- A missing source leaves output controls usable.
- Call entry, earpiece/speaker switching, hangup, and HiFi restoration all preserve subsequent microphone capture.
- Lock transitions retain an ambient capture warning without exposing client metadata.
- IPC cannot bypass settings, step-up, or lock authority.
Done when
- Active audio and privacy services are original Souveraine AGPL code with no borrowed widget dependency in their active graph.
- Real endpoints and streams are modeled eventfully and recover after a graph restart.
- Capture indicators distinguish access, mute, and revocation.
- Phone and laptop share semantics without sharing inappropriate layout.
- Role defaults and lock-tier behaviour are settled in TASK-39.
- Agent IPC uses the common settings/capability policy.