PAF becomes saf/device (history kept), STATE.md dissolves into saf/state.md with the dated era archived, the substrate SAF moves up from souveraine, and every agreement points at saf/INDEX.md and nowhere else. one map, nothing to remember
191 lines
9.8 KiB
Markdown
191 lines
9.8 KiB
Markdown
# TASK 39 — Audio levels as tracked state, and sound roles
|
|
|
|
**Status:** open-policy, reconciled 2026-08-07. Audio transport works and
|
|
WirePlumber persistence is verified. What remains is roles, defaults,
|
|
lock-tier behaviour, and state-driven profile intent — not microphone
|
|
bring-up and not another persistence layer. **Repo:** `souveraine` (sessiond +
|
|
surfaces), `Pixel3Arch`.
|
|
|
|
## What Casey asked
|
|
|
|
> "we now are monitoring / tracking the brightness before shutdown/reboots but
|
|
> we should probably add sound levels to that; or at least consider the
|
|
> approaches later on how we might separate some notification level sounds vs
|
|
> others. Frankly most times I think the phone should be expected to have audio
|
|
> up to around 70%; but I may be errant."
|
|
|
|
## Measured 2026-07-28 — WirePlumber persistence is NOT the bug
|
|
|
|
Step 1 below is done, and it came back clean. Do not build a second persistence
|
|
layer.
|
|
|
|
`~/.local/state/wireplumber/default-routes` carries the speaker route's volume:
|
|
|
|
alsa_card.platform-sound:output:...Speaker={"mute":false, ...
|
|
"channelVolumes":[0.118778, 0.118778]}
|
|
|
|
`0.118778 = 0.49³`, and `wpctl get-volume @DEFAULT_AUDIO_SINK@` reports exactly
|
|
`0.49` — wpctl shows the cubic-mapped value, the state file stores linear. They
|
|
agree.
|
|
|
|
Persistence was tested directly rather than inferred: set the sink to `0.55`,
|
|
and within 6 s `default-routes` was rewritten with `channelVolumes":[0.166378`
|
|
(`= 0.55³`). Restored to `0.49` cleanly. WirePlumber 0.5.15, `restore-device`
|
|
working through `default-routes` (there is no separate `restore-device` file in
|
|
this version — that naming is older).
|
|
|
|
**So "sound seems pinned on boot" is not a failure to save.** It is WirePlumber
|
|
faithfully restoring `0.49` every boot, which is simply lower than the ~70% Casey
|
|
expects. That makes this entirely a *defaults and roles* question — which is what
|
|
the rest of this file already says the real question is — and removes the one
|
|
scenario that would have justified sessiond owning the value.
|
|
|
|
## Persistence guardrail
|
|
|
|
The WirePlumber check is complete. If restoration regresses, diagnose its
|
|
state file and endpoint identity before adding ownership elsewhere. A second
|
|
persistence layer would create two writers for one value; sessiond does not
|
|
need to own volume on current evidence.
|
|
|
|
## The real question is not persistence, it is the stream model
|
|
|
|
"Sound level" is not one number, and choosing to persist it as one number bakes
|
|
in a model we would then have to undo. Every phone platform splits audio by
|
|
**role**, because a single slider cannot express the thing people actually
|
|
want — ringer loud while media is quiet, or media loud while notifications stay
|
|
out of the way. Android's stream split (music / notification / ring / alarm /
|
|
voice call) is the reference, the same way it was for TASK-37.
|
|
|
|
PipeWire already carries the mechanism: per-stream volumes plus
|
|
`media.role` / `media.category` on the stream. What is missing is *our policy* —
|
|
which roles exist on this device, what each defaults to, and which ones a
|
|
volume gesture actually moves.
|
|
|
|
Casey's "notification level sounds vs others" is exactly this split, and it is
|
|
the part worth designing. The persistence falls out of it: you restore per
|
|
role, not one master.
|
|
|
|
## This touches AUDIO-PRIVACY, not just ergonomics
|
|
|
|
Which sounds are audible **while the screen is locked** is not a volume
|
|
question, it is the same tier question as visual redaction: a notification that
|
|
speaks its contents aloud on a locked phone leaks exactly what the lockscreen
|
|
redaction rules exist to prevent. Any role model here has to state what each
|
|
role does at each lock tier, or we will have solved audibility and reopened the
|
|
leak we already closed on the display side.
|
|
|
|
## On the 70%
|
|
|
|
Worth measuring rather than adopting. Two reasons to be careful:
|
|
|
|
- Volume scales are perceptual, not linear — "70%" on a slider is not 70% of
|
|
loudness, and the mapping differs between the PipeWire volume, the UI slider,
|
|
and the codec's own gain. A number picked in one of those three does not mean
|
|
the same thing in the others.
|
|
- The relevant default is probably per role, not global. A 70% that is right
|
|
for media is very likely wrong for a ringtone that has to be heard from
|
|
another room.
|
|
|
|
Casey flagged he may be errant here, and the honest answer is that the number
|
|
should come out of the role model, not go into it.
|
|
|
|
## Remaining shape
|
|
|
|
1. Define the roles this device has and each one's default and lock-tier
|
|
behaviour.
|
|
2. Keep WirePlumber as value owner unless a concrete device-state reason
|
|
proves otherwise; volume is preference, not the security-relevant state the
|
|
state machine was built for.
|
|
3. Define state-driven profile intent for media, calls, speakerphone, and a
|
|
future headset so a graph restart cannot choose by priority alone.
|
|
4. Make volume gestures and the settings UI move roles, not one master number.
|
|
|
|
## Connects to
|
|
|
|
`AUDIO-PRIVACY.md` (the lock-tier half), `SETTINGS-AUTHORITY.md`,
|
|
TASK-08 (device state manager — and the question of whether audio belongs in
|
|
it at all), TASK-19 (settings/control centre — where roles would surface).
|
|
|
|
## Profile selection belongs in the state machine (raised 2026-08-01, Casey)
|
|
|
|
The card sat in **`Voice Call (Earpiece, Mic)`** instead of `HiFi`, so every
|
|
sound came out of the earpiece rather than the `Both CS35L36 amps, speakerphone`
|
|
port, and it read as "the speaker got weaker". A `pactl set-card-profile ... HiFi`
|
|
restores it and wireplumber persists the choice — but that is a manual poke, and
|
|
the profile got there because a wireplumber restart re-ran selection with no
|
|
notion of what the device was *for* at that moment.
|
|
|
|
Casey's framing: the device has to know **when** a profile is needed and carry a
|
|
proper classification for them, the way Android and iOS do — in-call vs media vs
|
|
speakerphone vs headset, with the routing following the classification rather
|
|
than whatever priority number happened to win. That is state-machine work and
|
|
belongs with audio-as-state, not a udev rule or a login hook.
|
|
|
|
The stack is settled: PipeWire + WirePlumber own runtime audio and
|
|
`pipewire-pulse` provides the compatibility API. The uninstalled
|
|
`pkgs/pipewire-abi-provider` package records a retired native-Pulse experiment;
|
|
it must not drive current policy. See `AUDIO-PRIVACY.md` for the authority map.
|
|
|
|
## False diagnosis retired — 2026-08-02
|
|
|
|
HiFi capture is not structurally silent and does not require a Q6 voice
|
|
session. With the stale dual channel map cleared, the normal
|
|
DMIC4 → DEC5/TX5 → SLIMBUS_0_TX → `hw:0,1` path produced **235520 frames,
|
|
204136 nonzero, peak 32726**. Speaker and microphone both work in HiFi.
|
|
|
|
The failed measurements had left TX5 and TX7 mapped into the mono capture
|
|
front-end at once. The UCM transition must leave exactly one capture leg and
|
|
must dismantle the outgoing route. `saf/device/audio.md` retains the lifecycle
|
|
evidence; Git retains the discarded branch survey. Neither belongs in this
|
|
policy task.
|
|
|
|
## FIXED — the in-call speaker button (2026-08-02)
|
|
|
|
Casey, observed: an inbound call **defaulted to speakerphone**, and tapping the
|
|
speaker button (to *turn on* speakerphone) **switched to headset mode**.
|
|
|
|
**Root cause.** `gnome-calls`' speaker button calls `org.mobian_project.CallAudio`,
|
|
a name owned by upstream **`callaudiod`** (`extra` 0.1.99-3, D-Bus activated).
|
|
callaudiod moves audio by switching a sink *port* (`cad-pulse.c`,
|
|
`set_output_port`), which needs one sink carrying both an earpiece and a speaker
|
|
port. blueline has both CS35L36 amps behind a single PCM, so UCM models them as
|
|
separate devices and ACP splits them into `Voice Call (Earpiece, Mic)` and
|
|
`Voice Call (Mic, Speaker)` — no such sink exists. Disabling the speaker asks
|
|
for "the highest priority port that is **not** the speaker", which resolves to
|
|
the headset. The call was already on speakerphone, so the tap sent
|
|
`EnableSpeaker(FALSE)` and the phone went to headset. Both halves of the report
|
|
are one bug.
|
|
|
|
**Fix: `pkgs/souveraine-callaudiod`.** Upstream 0.1.99 plus one patch routing
|
|
`CAD_OPERATION_ENABLE_SPEAKER` through `blueline-callaudio-hook speaker on|off`,
|
|
which writes the amplifier controls directly — no profile switch, so the PCM a
|
|
live call runs on is never disturbed. Completion mirrors `operation_complete_cb`
|
|
so the `speaker-state` property stays truthful. Gated on the hook being
|
|
executable, so it is upstream behaviour on any other device. Cross-built on
|
|
ArchDev; `provides/conflicts/replaces callaudiod`, which `gnome-calls` and
|
|
`phosh` both depend on by name.
|
|
|
|
**Verified on the device over the same D-Bus interface the button uses** — no
|
|
call needed, `callaudiocli` drives it:
|
|
|
|
earpiece baseline Right AMP Enable Switch = off
|
|
callaudiocli --enable-speaker 1 -> on (loudspeaker, both amps)
|
|
callaudiocli --enable-speaker 0 -> off (earpiece, NOT headset)
|
|
callaudiocli --status -> CALL_AUDIO_SPEAKER_OFF
|
|
|
|
Casey confirms the phone has carried real calls since this fix. The exact
|
|
speaker-button acceptance was not separately recorded during a live bearer, so
|
|
keep that narrow UI check in the next convenient call rather than treating the
|
|
call path itself as unverified.
|
|
|
|
**Three notes worth keeping.** `pacman -U --noconfirm` answers *No* to the
|
|
"remove the conflicting package?" prompt, and `replaces=` only applies to repo
|
|
syncs — so installing a replacement locally means answering the prompt, not
|
|
reaching for `-Rdd`. meson resolves glib's codegen tools from pkg-config
|
|
variables, which come back **sysroot-prefixed** under `PKG_CONFIG_SYSROOT_DIR`;
|
|
the shared cross file pins `glib-compile-resources`/`gdbus-codegen` but not
|
|
`glib-mkenums`, so the package ships a supplementary cross file rather than
|
|
editing that shared file, which lives unversioned on the builder and would lose
|
|
the change. And neither this package nor the kernel is published to
|
|
`souveraine-aarch64`, so both are hand-installed even though the repo is
|
|
configured on the phone.
|