Documentation
Audio Controller
Source:
controllers/audio/README.md
The audio controller provides a common interface for controlling system audio output.
Audio applications and input components can use the audio controller without depending directly on a specific operating system audio implementation.
Components
| Component | Description |
|---|---|
AudioControllerIf |
Defines the common audio control interface. |
PipewireAudioController |
Controls PipeWire audio output using wpctl. |
PactlAudioController |
Controls a Linux development host through pactl. |
AudioControllerStub |
Provides deterministic in-memory audio state. |
UnconfiguredAudioController |
Reports unavailable audio configuration. |
Directory Layout
audio/
├── __init__.py
├── audio_controller_if.py
├── audio_controller_stub.py
├── pipewire_audio_controller.py
├── unconfigured_audio_controller.py
├── README.md
└── component_test/
├── __init__.py
└── audio_cli.py
Features
The audio controller provides:
- Volume increase
- Volume decrease
- Discrete volume levels
- Direct volume level selection
- Relative volume adjustment
- Replaceable audio controller implementations
Audio Controller Interface
AudioControllerIf defines the common audio control interface.
Implementations provide:
is_available
status_message
maximum_level
volume_up()
volume_down()
get_volume_level()
set_volume_level(level)
adjust_volume(steps)
is_muted()
toggle_mute()
Higher-level components should depend on AudioControllerIf rather than a specific audio implementation.
AudioControllerStub is intended for demos and UI development. It keeps
volume and mute state in memory without accessing host audio.
UnconfiguredAudioController reports is_available == False, exposes a
configuration reason through status_message, and raises RuntimeError for
audio operations rather than returning fabricated system state.
Example:
from controllers.audio import AudioControllerIf
def adjust_output_volume(
controller: AudioControllerIf,
steps: int,
) -> None:
controller.adjust_volume(steps)
PipeWire Audio Controller
PipewireAudioController controls the default PipeWire audio sink using wpctl.
Car UI selects the implementation at runtime. Native Raspberry Pi 4 and Pi 5
targets use PipewireAudioController; linux-dev hosts use
PactlAudioController. Set OPENROAD_RUNTIME_TARGET to linux-dev, rpi4, or
rpi5 only when an explicit override is needed for testing or deployment.
With audio.output = "auto", Pi 4 prefers its onboard analog sink and Pi 5
prefers a USB audio sink. Set audio.device_match in config/runtime.toml to
select a particular adapter by a stable portion of its PipeWire description.
The controller operates on:
@DEFAULT_AUDIO_SINK@
This allows the operating system to determine the active audio output device.
Relative volume increases pass a 1.0 limit to wpctl, preventing software
amplification above 100%.
Example:
from controllers.audio import PipewireAudioController
controller = PipewireAudioController()
controller.volume_up()
controller.volume_down()
level = controller.get_volume_level()
controller.set_volume_level(10)
Volume Levels
The PipeWire controller exposes system volume as discrete levels.
By default:
steps = 20
step_percent = 5
This produces levels from:
0 through 20
where:
0 = 0%
1 = 5%
2 = 10%
...
20 = 100%
Volume-up operations clamp at 100%; further clockwise encoder steps leave the
system at level 20 rather than enabling PipeWire amplification.
Mute is controlled with wpctl set-mute ... toggle. toggle_mute() returns the
resulting state so application widgets can update immediately.
The number of levels can be configured:
controller = PipewireAudioController(
steps=10,
step_percent=10,
)
Relative Volume Adjustment
adjust_volume() changes the current volume by a number of discrete steps.
Example:
controller.adjust_volume(1)
Increase volume by one level.
controller.adjust_volume(-1)
Decrease volume by one level.
Multiple steps may be applied at once:
controller.adjust_volume(3)
controller.adjust_volume(-4)
This is useful for input devices that report relative movement, such as rotary encoders.
Example:
def volume_rotated(turns: int) -> None:
audio_controller.adjust_volume(turns)
Dependencies
The PipeWire implementation requires wpctl.
Verify that wpctl is available:
wpctl --version
The active audio sink can be inspected using:
wpctl status
Current volume can be read using:
wpctl get-volume @DEFAULT_AUDIO_SINK@
Component Test
A CLI component test is provided.
Run from the project root:
python3 -m controllers.audio.component_test.audio_cli
Available commands:
+ Volume up
- Volume down
s <level> Set volume level
g Get volume level
q Quit
Example session:
audio> g
Volume level: 8
audio> +
Volume level: 9
audio> s 12
Volume level: 12
audio> -
Volume level: 11
Car UI Volume Encoder Integration
The Car UI reserves one configured rotary encoder for global system volume.
The input router depends on RotaryEncoderIf and invokes the audio controller’s
volume_up() or volume_down() operation for each signed encoder step:
RotaryEncoderIf
|
v
EncoderEventRouter
|
v
AudioControllerIf
|
v
PipewireAudioController
The encoder driver and volume device index are configured in
config/runtime.toml. Neither AudioControllerIf nor
PipewireAudioController depends on Seesaw addresses or GPIO pins.
Test the real configured volume knob and default PipeWire sink with:
python3 -m apps.carUi.input.component_test.volume_encoder_cli
This test starts only the configured volume device. Other configured encoders may be disconnected without preventing the volume test from running. It reports 20 levels by default, matching the controller’s 5% PipeWire step. The Car UI theme’s eight volume bars are a visual scale rather than the audio controller’s native resolution.
VolumeManager maps the controller range proportionally onto that visual
scale:
Audio level 0/20 -> 0 bars
Audio level 5/20 -> 2 bars
Audio level 10/20 -> 4 bars
Audio level 15/20 -> 6 bars
Audio level 20/20 -> 8 bars
Positive nonzero audio levels display at least one bar. Encoder events and top-bar volume buttons use the same mapping.
Pressing the configured volume encoder calls AudioControllerIf.toggle_mute().
While muted, the top bar renders all eight bars in red. The underlying volume
level is retained and displayed again when the encoder is pressed to unmute.
Design
The audio controller represents system audio control behavior.
Higher-level components interact with AudioControllerIf and do not need to know how system volume is controlled.
Concrete implementations are responsible for interacting with the underlying audio system.
For example:
Input Device
|
v
AudioControllerIf
|
v
PipewireAudioController
|
v
wpctl
|
v
PipeWire
This allows additional audio controller implementations to be added without changing components that use audio control.
Possible implementations may include:
- PipeWire
- PulseAudio
- ALSA
- Remote audio services
- Mock audio controllers
Operating system and audio-system-specific behavior remains inside the concrete audio controller implementation.