Phone audio has one output and a volume key. Car audio has a dozen speakers, an external amplifier, chimes that must never be ducked, a rear-seat zone with its own volume, and a navigation prompt that has to be audible over music without stopping it.
The model that handles this is zones, contexts and buses.
The three concepts#
Audio context — what kind of sound is this?#
A semantic category derived from the stream's AudioAttributes: MUSIC,
NAVIGATION, VOICE_COMMAND, CALL, ALARM, NOTIFICATION,
SYSTEM_SOUND, EMERGENCY, SAFETY, VEHICLE_STATUS.
Context is not a channel. It is a statement of intent, and it is what the routing and ducking rules act on.
Output device (bus) — where does it physically go?#
A bus is an output the audio HAL exposes — bus0_media_out, bus1_nav_out and
so on. Behind each is a path through the amplifier to real speakers.
Multiple buses matter because an external amplifier can then mix them independently: navigation can be attenuated relative to music by the amp, in hardware, with no software ducking at all.
Zone — where in the vehicle?#
A set of buses plus its own volume groups and its own focus state. Zone 0 is the main cabin. A rear-seat entertainment system is another zone, where a passenger can play something at their own volume while the driver hears navigation.
Zones have independent focus
Two zones can each have an active focus owner at the same time. That is the whole point: a rear passenger's video should not pause because the driver got a navigation prompt in the front.
The configuration file decides everything#
car_audio_configuration.xml is where the whole topology is declared. Get it
wrong and audio either does not play or plays out of the wrong speakers.
<carAudioConfiguration version="3">
<zones>
<zone name="primary" isPrimary="true" occupantZoneId="0">
<volumeGroups>
<group>
<device address="bus0_media_out">
<context context="music"/>
<context context="announcement"/>
</device>
</group>
<group>
<device address="bus1_navigation_out">
<context context="navigation"/>
</device>
<device address="bus2_voice_command_out">
<context context="voice_command"/>
</device>
</group>
<group>
<device address="bus3_call_out">
<context context="call"/>
<context context="call_ring"/>
</device>
</group>
<!-- Safety chimes get their own group so they can never be ducked
along with media. -->
<group>
<device address="bus4_alarm_out">
<context context="alarm"/>
<context context="emergency"/>
<context context="safety"/>
</device>
</group>
</volumeGroups>
</zone>
<zone name="rear_seat" occupantZoneId="1">
<volumeGroups>
<group>
<device address="bus100_rear_media_out">
<context context="music"/>
<context context="navigation"/>
<context context="voice_command"/>
<context context="call"/>
<context context="alarm"/>
<context context="notification"/>
<context context="system_sound"/>
<context context="emergency"/>
<context context="safety"/>
<context context="vehicle_status"/>
<context context="announcement"/>
</device>
</group>
</volumeGroups>
</zone>
</zones>
</carAudioConfiguration>Every context must be routed in every zone
A zone that omits a context has undefined behaviour for that sound — typically silence, discovered when someone finally makes a phone call from the rear seat. The rear zone above lists all eleven contexts on one device precisely because of this. Validate exhaustively, not by ear.
Volume groups#
A volume group is a set of devices that move together under one control. The driver's volume knob adjusts the group for whatever is currently dominant, not one global level — which is why turning the knob during a navigation prompt changes navigation volume, not music.
val audio = car.getCarManager(Car.AUDIO_SERVICE) as CarAudioManager
val zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE
val groupCount = audio.getVolumeGroupCount(zoneId)
val max = audio.getGroupMaxVolume(zoneId, groupId)
val current = audio.getGroupVolume(zoneId, groupId)
audio.setGroupVolume(zoneId, groupId, current + 1, /* flags = */ 0)setGroupVolume needs android.car.permission.CAR_CONTROL_AUDIO_VOLUME, which
is signature-level. Ordinary apps change their own stream volume; only system
apps drive the vehicle's volume model.
Getting your audio into the right context#
Your app controls its context through AudioAttributes. This is the single most
important audio decision an automotive app makes.
// Music player
val musicAttrs = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
// Navigation prompt — routed to the nav bus, ducks media rather than pausing it
val navAttrs = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()Declare what the sound is. An app that marks navigation prompts as
USAGE_MEDIA will have them routed to the media bus, ducked like music, and
possibly silenced by a rear passenger's volume control.
Debugging#
adb shell dumpsys car_service --services CarAudioService
adb shell dumpsys audio_flinger
adb shell dumpsys audio_policy
# What devices does the HAL actually expose?
adb shell lshal | grep -i audioThe CarAudioService dump shows zones, volume groups, device addresses and the
current focus holders. When audio comes out of the wrong speaker, compare that
dump against your configuration XML — the mismatch is almost always there.
Common failures#
| Symptom | Cause |
|---|---|
| No audio at all from one app | Its context is not routed in that zone |
| Navigation ducks everything, including chimes | Safety contexts share a group with media |
| Rear zone silent on calls | call context missing from the rear zone |
| Volume knob affects the wrong thing | Contexts grouped into the wrong volume group |
| Works on emulator, silent on target | Bus addresses in XML do not match the HAL's |
That last one is the classic bring-up bug: the configuration references
bus0_media_out and the audio HAL calls it bus0_output. Nothing errors; the
routing simply never matches.
Next#
Focus and ducking — the policy layer that decides who is audible when.

