# 0038: One sound service, and 100% is 0 dB

> One sound service sets every volume on one scale, where 100% is the device's own 0 dB, so a new microphone no longer clips.

Source: https://wolos.org/reference/adr-0038

## Context

PipeWire reports where a device's 0 dB sits (`volumeBase`) and WirePlumber starts a new source at the hardware maximum, which on most codecs is +20 to +30 dB of microphone boost, so every new microphone clipped, and a slider labelled 100% meant "as loud as the hardware can go". Three tools also moved volumes by their own scale: `wpctl set-volume N%` and `pactl N%` count from the hardware maximum, the Settings and Control Center sliders drew percent of 0 dB. Filters (echo cancel with automatic gain, RNNoise) and the App Mix for OBS are PipeWire-pulse modules that must exist only while someone asks. The sound pages in Settings and the Control Center each need the same devices, levels and rules.

## Decision

- **100% on a slider is the device's 0 dB** (`wol_core::audio::percent_of`, `linear_for`, cubic like PulseAudio), never the hardware maximum. Above 100 only with the user's explicit "Allow louder" (one switch for the output and the input slider, `Service::set_boost`), up to `max_percent`.
- **One service owns the rules:** `crates/audio` (`wol_audio`) runs one PipeWire main loop on its own thread and a service thread that applies the rules (`service::Service`) to events and emits commands; the pipewire backend only moves bytes, so every rule is tested against `fake::FakeBackend`. Settings and the Control Center send `wol_core::audio::Action`s and draw `Snapshot`s; neither asks PipeWire itself. Every volume the sliders set (`Action::MicVolume`, `Action::OutputVolume`) goes through the service, through the device's `Route` when it has one (the hardware mixer moves, as `wpctl` does), so the scale conversion lives in one place. The volume keys, the OSD and `wol volume` keep the `wpctl` scale in `wol_core::controls`: the same number on a device whose `volumeBase` is 1.0.
- **Snap once:** a microphone seen above 0 dB for the first time is put at 0 dB and remembered (`~/.local/state/wolos/audio-snapped`); WirePlumber's default for new sources is lowered to 0.064 (`71-wolos-input.conf`) so the snap is rarely needed. The service exists only while a Sound page is open; it is not a resident client.
- **On demand, never static:** filters and App Mix are pipewire-pulse modules loaded when switched on and unloaded when off; no fragment under `/usr/share` loads one. When PipeWire restarts the modules go with it, so the service loads them again once the microphone is back (`Service::refilter`).
- **The level meter is not a recorder:** its capture stream carries a `node.link-group`, which WirePlumber's Bluetooth autoswitch skips, so opening Sound does not move headphones to the call profile (`model::METER_PROPS`).

## Consequences

the sliders are right on every device, including outputs with a gain range, at the cost of one more `Action` per slider and a service that must be running for them (a fixed preview or a machine with no PipeWire falls back to `System`'s `wpctl`, capped at 100). The service runs only with a Sound page open, so a microphone is not snapped until then; the 0.064 default and `wol audio doctor` cover that gap. The volume keys can still disagree with the slider on a device whose `volumeBase` is below 1.0 (they count from the hardware maximum); moving them onto the service is future work and would need the service to run for the whole session. A change to the 0 dB rule or to what the service owns needs a new ADR that names this one.
