1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.car.audio;
17 
18 import static android.media.AudioAttributes.USAGE_MEDIA;
19 
20 import static com.android.car.audio.CarAudioUtils.getAudioDeviceInfo;
21 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.PRIVATE_CONSTRUCTOR;
22 
23 import android.car.builtin.util.Slogf;
24 import android.media.AudioAttributes;
25 import android.media.AudioDeviceInfo;
26 import android.media.AudioFormat;
27 import android.media.AudioManager;
28 import android.media.audiopolicy.AudioMix;
29 import android.media.audiopolicy.AudioMixingRule;
30 import android.media.audiopolicy.AudioPolicy;
31 import android.util.Log;
32 import android.util.SparseArray;
33 
34 import com.android.car.CarLog;
35 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
36 
37 import java.util.Arrays;
38 import java.util.List;
39 
40 /**
41  * Builds dynamic audio routing in a car from audio zone configuration.
42  */
43 final class CarAudioDynamicRouting {
44     // For legacy stream type based volume control.
45     // Values in STREAM_TYPES and STREAM_TYPE_USAGES should be aligned.
46     static final int[] STREAM_TYPES = new int[] {
47             AudioManager.STREAM_MUSIC,
48             AudioManager.STREAM_ALARM,
49             AudioManager.STREAM_RING
50     };
51     static final int[] STREAM_TYPE_USAGES = new int[] {
52             USAGE_MEDIA,
53             AudioAttributes.USAGE_ALARM,
54             AudioAttributes.USAGE_NOTIFICATION_RINGTONE
55     };
56 
setupAudioDynamicRouting(CarAudioContext carAudioContext, AudioManagerWrapper audioManager, AudioPolicy.Builder builder, SparseArray<CarAudioZone> carAudioZones)57     static void setupAudioDynamicRouting(CarAudioContext carAudioContext,
58             AudioManagerWrapper audioManager, AudioPolicy.Builder builder,
59             SparseArray<CarAudioZone> carAudioZones) {
60         for (int i = 0; i < carAudioZones.size(); i++) {
61             CarAudioZone zone = carAudioZones.valueAt(i);
62             List<CarAudioZoneConfig> zoneConfigs = zone.getAllCarAudioZoneConfigs();
63             CarAudioZoneConfig defaultConfig = null;
64             boolean foundSelected = false;
65             for (int configIndex = 0; configIndex < zoneConfigs.size(); configIndex++) {
66                 CarAudioZoneConfig config = zoneConfigs.get(configIndex);
67                 // Default config will be added at the end
68                 if (config.isDefault()) {
69                     defaultConfig = config;
70                     continue;
71                 }
72                 if (!config.isSelected() || !config.isActive()) {
73                     continue;
74                 }
75                 foundSelected = true;
76                 setupAudioDynamicRoutingForZoneConfig(builder, config, carAudioContext,
77                         audioManager);
78             }
79             // Always setup default configuration at the end, so that zone routing has a backup for
80             // routing in case the dynamic device disconnects.
81             if (defaultConfig.isSelected()) {
82                 foundSelected = true;
83             }
84 
85             if (!foundSelected) {
86                 throw new IllegalStateException("Selected configuration for zone " + zone.getId()
87                 + " was not available");
88             }
89 
90             // Default configuration should always be available, in case the dynamic
91             // device disappears the default configuration will be selected
92             setupAudioDynamicRoutingForZoneConfig(builder, defaultConfig, carAudioContext,
93                     audioManager);
94         }
95     }
96 
setupAudioDynamicRoutingForZoneConfig(AudioPolicy.Builder builder, CarAudioZoneConfig zoneConfig, CarAudioContext carAudioContext, AudioManagerWrapper audioManager)97     private static void setupAudioDynamicRoutingForZoneConfig(AudioPolicy.Builder builder,
98             CarAudioZoneConfig zoneConfig, CarAudioContext carAudioContext,
99             AudioManagerWrapper audioManager) {
100         CarVolumeGroup[] volumeGroups = zoneConfig.getVolumeGroups();
101         for (int index = 0; index < volumeGroups.length; index++) {
102             setupAudioDynamicRoutingForGroup(builder, volumeGroups[index], carAudioContext,
103                     audioManager);
104         }
105     }
106 
107     /**
108      * Enumerates all physical buses in a given volume group and attach the mixing rules.
109      *
110      * @param builder {@link AudioPolicy.Builder} to attach the mixing rules
111      * @param group {@link CarVolumeGroup} instance to enumerate the buses with
112      * @param carAudioContext car audio context
113      * @param audioManager audio manager to find audio configuration for the passed in info
114      */
setupAudioDynamicRoutingForGroup(AudioPolicy.Builder builder, CarVolumeGroup group, CarAudioContext carAudioContext, AudioManagerWrapper audioManager)115     private static void setupAudioDynamicRoutingForGroup(AudioPolicy.Builder builder,
116             CarVolumeGroup group, CarAudioContext carAudioContext,
117             AudioManagerWrapper audioManager) {
118         // Note that one can not register audio mix for same bus more than once.
119         List<String> addresses = group.getAddresses();
120         for (int index = 0; index < addresses.size(); index++) {
121             String address = addresses.get(index);
122             boolean hasContext = false;
123             CarAudioDeviceInfo info = group.getCarAudioDeviceInfoForAddress(address);
124             if (!info.canBeRoutedWithDynamicPolicyMix()) {
125                 if (Slogf.isLoggable(CarLog.TAG_AUDIO, Log.DEBUG)) {
126                     Slogf.d(CarLog.TAG_AUDIO, "Address: %s AudioContext: %s cannot be routed with "
127                             + "Dynamic Policy Mixing", address, carAudioContext);
128                 }
129                 continue;
130             }
131             AudioFormat mixFormat = createMixFormatFromDevice(info);
132             AudioMixingRule.Builder mixingRuleBuilder = new AudioMixingRule.Builder();
133             List<Integer> contextIdsForAddress = group.getContextsForAddress(address);
134             for (int contextIndex = 0; contextIndex < contextIdsForAddress.size(); contextIndex++) {
135                 @CarAudioContext.AudioContext int contextId =
136                         contextIdsForAddress.get(contextIndex);
137                 hasContext = true;
138                 AudioAttributes[] allAudioAttributes =
139                         carAudioContext.getAudioAttributesForContext(contextId);
140                 for (int attrIndex = 0; attrIndex < allAudioAttributes.length; attrIndex++) {
141                     AudioAttributes attributes = allAudioAttributes[attrIndex];
142                     mixingRuleBuilder.addRule(attributes,
143                             AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
144                 }
145                 if (Slogf.isLoggable(CarLog.TAG_AUDIO, Log.DEBUG)) {
146                     Slogf.d(CarLog.TAG_AUDIO, "Address: %s AudioContext: %s sampleRate: %d "
147                             + "channels: %d attributes: %s", address, carAudioContext,
148                             info.getSampleRate(), info.getChannelCount(),
149                             Arrays.toString(allAudioAttributes));
150                 }
151             }
152             if (hasContext) {
153                 AudioDeviceInfo audioDeviceInfo =
154                         getAudioDeviceInfo(info.getAudioDevice(), audioManager);
155                 // It's a valid case that an audio output address is defined in
156                 // audio_policy_configuration and no context is assigned to it.
157                 // In such case, do not build a policy mix with zero rules.
158                 addMix(builder, audioDeviceInfo, mixFormat, mixingRuleBuilder);
159             }
160         }
161     }
162 
163     @ExcludeFromCodeCoverageGeneratedReport(reason = PRIVATE_CONSTRUCTOR)
CarAudioDynamicRouting()164     private CarAudioDynamicRouting() {
165         throw new UnsupportedOperationException("contains only static methods");
166     }
167 
setupAudioDynamicRoutingForMirrorDevice( AudioPolicy.Builder mirrorPolicyBuilder, List<CarAudioDeviceInfo> audioDeviceInfos, AudioManagerWrapper audioManager)168     public static void setupAudioDynamicRoutingForMirrorDevice(
169             AudioPolicy.Builder mirrorPolicyBuilder, List<CarAudioDeviceInfo> audioDeviceInfos,
170             AudioManagerWrapper audioManager) {
171         for (int index = 0; index < audioDeviceInfos.size(); index++) {
172             AudioFormat mixFormat = createMixFormatFromDevice(audioDeviceInfos.get(index));
173             AudioMixingRule.Builder mixingRuleBuilder = new AudioMixingRule.Builder();
174             mixingRuleBuilder.addRule(CarAudioContext.getAudioAttributeFromUsage(USAGE_MEDIA),
175                     AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
176             AudioDeviceInfo info = getAudioDeviceInfo(
177                     audioDeviceInfos.get(index).getAudioDevice(), audioManager);
178 
179             addMix(mirrorPolicyBuilder, info, mixFormat, mixingRuleBuilder);
180         }
181     }
182 
createMixFormatFromDevice(CarAudioDeviceInfo mirrorDevice)183     private static AudioFormat createMixFormatFromDevice(CarAudioDeviceInfo mirrorDevice) {
184         AudioFormat mixFormat = new AudioFormat.Builder()
185                 .setSampleRate(mirrorDevice.getSampleRate())
186                 .setEncoding(mirrorDevice.getEncodingFormat())
187                 .setChannelMask(mirrorDevice.getChannelCount())
188                 .build();
189         return mixFormat;
190     }
191 
addMix(AudioPolicy.Builder builder, AudioDeviceInfo deviceInfo, AudioFormat mixFormat, AudioMixingRule.Builder mixingRuleBuilder)192     private static void addMix(AudioPolicy.Builder builder, AudioDeviceInfo deviceInfo,
193             AudioFormat mixFormat, AudioMixingRule.Builder mixingRuleBuilder) {
194         AudioMix audioMix = new AudioMix.Builder(mixingRuleBuilder.build())
195                 .setFormat(mixFormat)
196                 .setDevice(deviceInfo)
197                 .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER)
198                 .build();
199         builder.addMix(audioMix);
200     }
201 }
202