/* * 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.AudioDeviceInfo.TYPE_BUS; import static android.media.AudioFormat.ENCODING_DEFAULT; import static android.media.AudioFormat.ENCODING_PCM_16BIT; import static android.media.AudioFormat.ENCODING_PCM_24BIT_PACKED; import static android.media.AudioFormat.ENCODING_PCM_32BIT; import static android.media.AudioFormat.ENCODING_PCM_8BIT; import static android.media.AudioFormat.ENCODING_PCM_FLOAT; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; import android.annotation.Nullable; import android.car.builtin.media.AudioManagerHelper; import android.car.builtin.media.AudioManagerHelper.AudioGainInfo; import android.car.builtin.util.Slogf; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.util.proto.ProtoOutputStream; import com.android.car.CarLog; import com.android.car.audio.CarAudioDumpProto.CarAudioDeviceInfoProto; import com.android.car.audio.hal.HalAudioDeviceInfo; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.car.internal.util.IndentingPrintWriter; import com.android.internal.annotations.GuardedBy; import java.util.List; import java.util.Objects; /** * A helper class wraps {@link AudioDeviceAttributes}, and helps manage the details of the audio * device: gains, format, sample rate, channel count * * Note to the reader. For whatever reason, it seems that AudioGain contains only configuration * information (min/max/step, etc) while the AudioGainConfig class contains the * actual currently active gain value(s). */ /* package */ final class CarAudioDeviceInfo { public static final int DEFAULT_SAMPLE_RATE = 48000; private static final int DEFAULT_NUM_CHANNELS = 1; private static final int UNINITIALIZED_GAIN = -1; /* * PCM 16 bit is supposed to be guaranteed for all devices * per {@link ENCODING_PCM_16BIT}'s documentation. */ private static final int DEFAULT_ENCODING_FORMAT = ENCODING_PCM_16BIT; private final AudioManagerWrapper mAudioManager; private final Object mLock = new Object(); @GuardedBy("mLock") private AudioDeviceAttributes mAudioDeviceAttributes; @GuardedBy("mLock") private int mDefaultGain; @GuardedBy("mLock") private int mMaxGain; @GuardedBy("mLock") private int mMinGain; @GuardedBy("mLock") private int mStepValue; @GuardedBy("mLock") private boolean mCanBeRoutedWithDynamicPolicyMixRule = true; @GuardedBy("mLock") private int mSampleRate; @GuardedBy("mLock") private int mEncodingFormat; @GuardedBy("mLock") private int mChannelCount; /** * We need to store the current gain because it is not accessible from the current * audio engine implementation. It would be nice if AudioPort#activeConfig() would return it, * but in the current implementation, that function actually works only for mixer ports. */ @GuardedBy("mLock") private int mCurrentGain; @GuardedBy("mLock") private boolean mIsActive; CarAudioDeviceInfo(AudioManagerWrapper audioManager, AudioDeviceAttributes audioDeviceAttributes) { mAudioManager = audioManager; mAudioDeviceAttributes = audioDeviceAttributes; // Device specific information will be initialized once an actual audio device info is set mSampleRate = DEFAULT_SAMPLE_RATE; mEncodingFormat = DEFAULT_ENCODING_FORMAT; mChannelCount = DEFAULT_NUM_CHANNELS; mDefaultGain = UNINITIALIZED_GAIN; mMaxGain = UNINITIALIZED_GAIN; mMinGain = UNINITIALIZED_GAIN; mStepValue = UNINITIALIZED_GAIN; mCurrentGain = UNINITIALIZED_GAIN; // Not initialized till explicitly set } boolean isActive() { synchronized (mLock) { return isActiveLocked(); } } /** * Updates the volume group with new audio device information if the device info matches * *
Note only updates car audio devices that are dynamic (i.e. non bus devices)
*
* @param devices List of audio devices that will be use for update
* @return {@code true} if the device is updated, {@code false} otherwise.
*/
boolean audioDevicesAdded(List Note only updates car audio devices that are dynamic (i.e. non bus devices)
*
* @param devices List of audio devices that will be use for update
* @return {@code true} if the device is removed, {@code false} otherwise.
*/
public boolean audioDevicesRemoved(List Given that the audio device information may not be available at the time of construction,
* the method must call to set the audio device info, so that the actual details of the device
* are known.
*
* Setting the audio device info to {@code null} means the device is not active, such is
* the case for dynamic audio devices that should disappear on disconnection.
*
* @param info that will be use to obtain the device specific information
*/
void setAudioDeviceInfo(@Nullable AudioDeviceInfo info) {
synchronized (mLock) {
setAudioDeviceInfoLocked(info);
}
}
AudioDeviceAttributes getAudioDevice() {
synchronized (mLock) {
return mAudioDeviceAttributes;
}
}
String getAddress() {
synchronized (mLock) {
return getAddressLocked();
}
}
int getType() {
synchronized (mLock) {
return getTypeLocked();
}
}
/**
* By default, considers all AudioDevice can be used to establish dynamic policy mixing rules.
* until validation state is performed.
* Once called, the device is marked definitively as "connot be routed with dynamic mixes".
*/
void resetCanBeRoutedWithDynamicPolicyMix() {
synchronized (mLock) {
mCanBeRoutedWithDynamicPolicyMixRule = false;
}
}
boolean canBeRoutedWithDynamicPolicyMix() {
synchronized (mLock) {
return mCanBeRoutedWithDynamicPolicyMixRule;
}
}
int getDefaultGain() {
synchronized (mLock) {
return mDefaultGain;
}
}
int getMaxGain() {
synchronized (mLock) {
return mMaxGain;
}
}
int getMinGain() {
synchronized (mLock) {
return mMinGain;
}
}
int getSampleRate() {
synchronized (mLock) {
return mSampleRate;
}
}
int getEncodingFormat() {
synchronized (mLock) {
return mEncodingFormat;
}
}
int getChannelCount() {
synchronized (mLock) {
return mChannelCount;
}
}
int getStepValue() {
synchronized (mLock) {
return mStepValue;
}
}
void setCurrentGain(int gainInMillibels) {
int gain = gainInMillibels;
// Clamp the incoming value to our valid range. Out of range values ARE legal input
synchronized (mLock) {
if (gain < mMinGain) {
gain = mMinGain;
} else if (gain > mMaxGain) {
gain = mMaxGain;
}
}
if (mAudioManager.setAudioDeviceGain(getAddress(), gain, true)) {
// Since we can't query for the gain on a device port later,
// we have to remember what we asked for
synchronized (mLock) {
mCurrentGain = gain;
}
} else {
Slogf.e(CarLog.TAG_AUDIO, "Failed to setAudioPortGain " + gain
+ " for output device " + getAddress());
}
}
// Updates audio device info for dynamic gain stage configurations
void updateAudioDeviceInfo(HalAudioDeviceInfo halDeviceInfo) {
synchronized (mLock) {
mMinGain = halDeviceInfo.getGainMinValue();
mMaxGain = halDeviceInfo.getGainMaxValue();
mStepValue = halDeviceInfo.getGainStepValue();
mDefaultGain = halDeviceInfo.getGainDefaultValue();
}
}
@GuardedBy("mLock")
private int getTypeLocked() {
return mAudioDeviceAttributes.getType();
}
@GuardedBy("mLock")
private void setAudioDeviceInfoLocked(AudioDeviceInfo info) {
if (info != null && info.getType() != getTypeLocked()) {
return;
}
// BUS device type can not be unset
if ((getTypeLocked() == TYPE_BUS)
&& (info == null || !getAddressLocked().equals(info.getAddress()))) {
return;
}
resetAudioDeviceInfoToDefaultLocked();
if (info == null) {
return;
}
setAudioDeviceInfoWithNewInfoLocked(info);
}
@GuardedBy("mLock")
private void setAudioDeviceInfoWithNewInfoLocked(AudioDeviceInfo info) {
setAudioGainInfoIfNeededLocked(info);
mIsActive = true;
}
@GuardedBy("mLock")
private void setAudioGainInfoIfNeededLocked(AudioDeviceInfo info) {
mAudioDeviceAttributes = new AudioDeviceAttributes(info);
// Only audio device bus supports audio gain management by car audio service
// Dynamic devices only support core audio volume management
if (info.getType() != TYPE_BUS) {
return;
}
AudioGainInfo audioGainInfo = AudioManagerHelper.getAudioGainInfo(info);
mDefaultGain = audioGainInfo.getDefaultGain();
mMaxGain = audioGainInfo.getMaxGain();
mMinGain = audioGainInfo.getMinGain();
mStepValue = audioGainInfo.getStepValue();
mChannelCount = getMaxChannels(info);
mSampleRate = getMaxSampleRate(info);
mEncodingFormat = getEncodingFormat(info);
}
@GuardedBy("mLock")
private void resetAudioDeviceInfoToDefaultLocked() {
int type = mAudioDeviceAttributes.getType();
mAudioDeviceAttributes = new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
type, /* address= */ "");
mSampleRate = DEFAULT_SAMPLE_RATE;
mEncodingFormat = DEFAULT_ENCODING_FORMAT;
mChannelCount = DEFAULT_NUM_CHANNELS;
mDefaultGain = UNINITIALIZED_GAIN;
mMaxGain = UNINITIALIZED_GAIN;
mMinGain = UNINITIALIZED_GAIN;
mStepValue = UNINITIALIZED_GAIN;
mCurrentGain = UNINITIALIZED_GAIN;
mIsActive = false;
}
@GuardedBy("mLock")
private String getAddressLocked() {
return mAudioDeviceAttributes.getAddress();
}
@GuardedBy("mLock")
private boolean isActiveLocked() {
return mIsActive;
}
private static int getMaxSampleRate(AudioDeviceInfo info) {
int[] sampleRates = info.getSampleRates();
if (sampleRates == null || sampleRates.length == 0) {
return DEFAULT_SAMPLE_RATE;
}
int sampleRate = sampleRates[0];
for (int i = 1; i < sampleRates.length; i++) {
if (sampleRates[i] > sampleRate) {
sampleRate = sampleRates[i];
}
}
return sampleRate;
}
private static int getEncodingFormat(AudioDeviceInfo info) {
int[] formats = info.getEncodings();
// If the formats are not specified, then arbitrary encoding are supported
if (formats == null) {
return DEFAULT_ENCODING_FORMAT;
}
for (int c = 0; c < formats.length; c++) {
// Audio policy mix limits linear PCMs
if (isEncodingLinearPcm(formats[c])) {
return formats[c];
}
}
return DEFAULT_ENCODING_FORMAT;
}
private static boolean isEncodingLinearPcm(int audioFormat) {
switch (audioFormat) {
case ENCODING_PCM_16BIT:
case ENCODING_PCM_8BIT:
case ENCODING_PCM_FLOAT:
case ENCODING_PCM_24BIT_PACKED:
case ENCODING_PCM_32BIT:
case ENCODING_DEFAULT:
return true;
default:
return false;
}
}
/*
* If there are no profiles in the device this will return the {@link #DEFAULT_NUM_CHANNELS}
*/
private static int getMaxChannels(AudioDeviceInfo info) {
int numChannels = 1;
int[] channelMasks = info.getChannelMasks();
if (channelMasks == null) {
return numChannels;
}
for (int channelMask : channelMasks) {
int currentNumChannels = Integer.bitCount(channelMask);
if (currentNumChannels > numChannels) {
numChannels = currentNumChannels;
}
}
return numChannels;
}
@Override
@ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
public String toString() {
int currentGain;
synchronized (mLock) {
currentGain = mCurrentGain;
}
return "address: " + getAddress()
+ " sampleRate: " + getSampleRate()
+ " encodingFormat: " + getEncodingFormat()
+ " channelCount: " + getChannelCount()
+ " currentGain: " + currentGain
+ " maxGain: " + getMaxGain()
+ " minGain: " + getMinGain();
}
@ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
void dump(IndentingPrintWriter writer) {
synchronized (mLock) {
writer.printf("CarAudioDeviceInfo Device(%s)\n", mAudioDeviceAttributes.getAddress());
writer.printf("CarAudioDeviceInfo Type(%s)\n", mAudioDeviceAttributes.getType());
writer.increaseIndent();
writer.printf("Is active (%b)\n", mIsActive);
writer.printf("Routing with Dynamic Mix enabled (%b)\n",
mCanBeRoutedWithDynamicPolicyMixRule);
writer.printf("sample rate / encoding format / channel count: %d %d %d\n",
getSampleRate(), getEncodingFormat(), getChannelCount());
writer.printf("Gain values (min / max / default/ current): %d %d %d %d\n",
mMinGain, mMaxGain, mDefaultGain, mCurrentGain);
writer.decreaseIndent();
}
}
@ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
void dumpProto(long fieldId, ProtoOutputStream proto) {
long token = proto.start(fieldId);
synchronized (mLock) {
proto.write(CarAudioDeviceInfoProto.ADDRESS, mAudioDeviceAttributes.getAddress());
proto.write(CarAudioDeviceInfoProto.CAN_BE_ROUTED_WITH_DYNAMIC_POLICY_MIX_RULE,
mCanBeRoutedWithDynamicPolicyMixRule);
proto.write(CarAudioDeviceInfoProto.SAMPLE_RATE, getSampleRate());
proto.write(CarAudioDeviceInfoProto.ENCODING_FORMAT, getEncodingFormat());
proto.write(CarAudioDeviceInfoProto.CHANNEL_COUNT, getChannelCount());
long volumeGainToken = proto.start(CarAudioDeviceInfoProto.VOLUME_GAIN);
proto.write(CarAudioDumpProto.CarVolumeGain.MIN_GAIN_INDEX, mMinGain);
proto.write(CarAudioDumpProto.CarVolumeGain.MAX_GAIN_INDEX, mMaxGain);
proto.write(CarAudioDumpProto.CarVolumeGain.DEFAULT_GAIN_INDEX, mDefaultGain);
proto.write(CarAudioDumpProto.CarVolumeGain.CURRENT_GAIN_INDEX, mCurrentGain);
proto.write(CarAudioDeviceInfoProto.IS_ACTIVE , mIsActive);
proto.end(volumeGainToken);
}
proto.end(token);
}
}