WolOS
Reference contents

Reference · Decisions

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.

Status: accepted (recorded in v0.12 for what the sound service already does; written from the round-6 audio audit). Builds on 0016 (Arch base: PipeWire and WirePlumber come from the distribution).

On this page

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::Actions and draw Snapshots; 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.

From the developer documentation, wonderlandos/wolos docs/adr/0038-sound-service.md.