/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.audio; import static android.media.AudioAttributes.USAGE_MEDIA; import static com.android.car.audio.CarAudioUtils.getAudioDeviceInfo; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.PRIVATE_CONSTRUCTOR; import android.car.builtin.util.Slogf; import android.media.AudioAttributes; import android.media.AudioDeviceInfo; import android.media.AudioFormat; import android.media.AudioManager; import android.media.audiopolicy.AudioMix; import android.media.audiopolicy.AudioMixingRule; import android.media.audiopolicy.AudioPolicy; import android.util.Log; import android.util.SparseArray; import com.android.car.CarLog; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import java.util.Arrays; import java.util.List; /** * Builds dynamic audio routing in a car from audio zone configuration. */ final class CarAudioDynamicRouting { // For legacy stream type based volume control. // Values in STREAM_TYPES and STREAM_TYPE_USAGES should be aligned. static final int[] STREAM_TYPES = new int[] { AudioManager.STREAM_MUSIC, AudioManager.STREAM_ALARM, AudioManager.STREAM_RING }; static final int[] STREAM_TYPE_USAGES = new int[] { USAGE_MEDIA, AudioAttributes.USAGE_ALARM, AudioAttributes.USAGE_NOTIFICATION_RINGTONE }; static void setupAudioDynamicRouting(CarAudioContext carAudioContext, AudioManagerWrapper audioManager, AudioPolicy.Builder builder, SparseArray carAudioZones) { for (int i = 0; i < carAudioZones.size(); i++) { CarAudioZone zone = carAudioZones.valueAt(i); List zoneConfigs = zone.getAllCarAudioZoneConfigs(); CarAudioZoneConfig defaultConfig = null; boolean foundSelected = false; for (int configIndex = 0; configIndex < zoneConfigs.size(); configIndex++) { CarAudioZoneConfig config = zoneConfigs.get(configIndex); // Default config will be added at the end if (config.isDefault()) { defaultConfig = config; continue; } if (!config.isSelected() || !config.isActive()) { continue; } foundSelected = true; setupAudioDynamicRoutingForZoneConfig(builder, config, carAudioContext, audioManager); } // Always setup default configuration at the end, so that zone routing has a backup for // routing in case the dynamic device disconnects. if (defaultConfig.isSelected()) { foundSelected = true; } if (!foundSelected) { throw new IllegalStateException("Selected configuration for zone " + zone.getId() + " was not available"); } // Default configuration should always be available, in case the dynamic // device disappears the default configuration will be selected setupAudioDynamicRoutingForZoneConfig(builder, defaultConfig, carAudioContext, audioManager); } } private static void setupAudioDynamicRoutingForZoneConfig(AudioPolicy.Builder builder, CarAudioZoneConfig zoneConfig, CarAudioContext carAudioContext, AudioManagerWrapper audioManager) { CarVolumeGroup[] volumeGroups = zoneConfig.getVolumeGroups(); for (int index = 0; index < volumeGroups.length; index++) { setupAudioDynamicRoutingForGroup(builder, volumeGroups[index], carAudioContext, audioManager); } } /** * Enumerates all physical buses in a given volume group and attach the mixing rules. * * @param builder {@link AudioPolicy.Builder} to attach the mixing rules * @param group {@link CarVolumeGroup} instance to enumerate the buses with * @param carAudioContext car audio context * @param audioManager audio manager to find audio configuration for the passed in info */ private static void setupAudioDynamicRoutingForGroup(AudioPolicy.Builder builder, CarVolumeGroup group, CarAudioContext carAudioContext, AudioManagerWrapper audioManager) { // Note that one can not register audio mix for same bus more than once. List addresses = group.getAddresses(); for (int index = 0; index < addresses.size(); index++) { String address = addresses.get(index); boolean hasContext = false; CarAudioDeviceInfo info = group.getCarAudioDeviceInfoForAddress(address); if (!info.canBeRoutedWithDynamicPolicyMix()) { if (Slogf.isLoggable(CarLog.TAG_AUDIO, Log.DEBUG)) { Slogf.d(CarLog.TAG_AUDIO, "Address: %s AudioContext: %s cannot be routed with " + "Dynamic Policy Mixing", address, carAudioContext); } continue; } AudioFormat mixFormat = createMixFormatFromDevice(info); AudioMixingRule.Builder mixingRuleBuilder = new AudioMixingRule.Builder(); List contextIdsForAddress = group.getContextsForAddress(address); for (int contextIndex = 0; contextIndex < contextIdsForAddress.size(); contextIndex++) { @CarAudioContext.AudioContext int contextId = contextIdsForAddress.get(contextIndex); hasContext = true; AudioAttributes[] allAudioAttributes = carAudioContext.getAudioAttributesForContext(contextId); for (int attrIndex = 0; attrIndex < allAudioAttributes.length; attrIndex++) { AudioAttributes attributes = allAudioAttributes[attrIndex]; mixingRuleBuilder.addRule(attributes, AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE); } if (Slogf.isLoggable(CarLog.TAG_AUDIO, Log.DEBUG)) { Slogf.d(CarLog.TAG_AUDIO, "Address: %s AudioContext: %s sampleRate: %d " + "channels: %d attributes: %s", address, carAudioContext, info.getSampleRate(), info.getChannelCount(), Arrays.toString(allAudioAttributes)); } } if (hasContext) { AudioDeviceInfo audioDeviceInfo = getAudioDeviceInfo(info.getAudioDevice(), audioManager); // It's a valid case that an audio output address is defined in // audio_policy_configuration and no context is assigned to it. // In such case, do not build a policy mix with zero rules. addMix(builder, audioDeviceInfo, mixFormat, mixingRuleBuilder); } } } @ExcludeFromCodeCoverageGeneratedReport(reason = PRIVATE_CONSTRUCTOR) private CarAudioDynamicRouting() { throw new UnsupportedOperationException("contains only static methods"); } public static void setupAudioDynamicRoutingForMirrorDevice( AudioPolicy.Builder mirrorPolicyBuilder, List audioDeviceInfos, AudioManagerWrapper audioManager) { for (int index = 0; index < audioDeviceInfos.size(); index++) { AudioFormat mixFormat = createMixFormatFromDevice(audioDeviceInfos.get(index)); AudioMixingRule.Builder mixingRuleBuilder = new AudioMixingRule.Builder(); mixingRuleBuilder.addRule(CarAudioContext.getAudioAttributeFromUsage(USAGE_MEDIA), AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE); AudioDeviceInfo info = getAudioDeviceInfo( audioDeviceInfos.get(index).getAudioDevice(), audioManager); addMix(mirrorPolicyBuilder, info, mixFormat, mixingRuleBuilder); } } private static AudioFormat createMixFormatFromDevice(CarAudioDeviceInfo mirrorDevice) { AudioFormat mixFormat = new AudioFormat.Builder() .setSampleRate(mirrorDevice.getSampleRate()) .setEncoding(mirrorDevice.getEncodingFormat()) .setChannelMask(mirrorDevice.getChannelCount()) .build(); return mixFormat; } private static void addMix(AudioPolicy.Builder builder, AudioDeviceInfo deviceInfo, AudioFormat mixFormat, AudioMixingRule.Builder mixingRuleBuilder) { AudioMix audioMix = new AudioMix.Builder(mixingRuleBuilder.build()) .setFormat(mixFormat) .setDevice(deviceInfo) .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER) .build(); builder.addMix(audioMix); } }