/* * 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 devices) { Objects.requireNonNull(devices, "Audio devices can not be null"); // Audio device type bus do not allow for devices to be swapped at run time. synchronized (mLock) { if (getTypeLocked() == TYPE_BUS || isActiveLocked()) { return false; } for (int c = 0; c < devices.size(); c++) { if (getTypeLocked() != devices.get(c).getType()) { continue; } setAudioDeviceInfoLocked(devices.get(c)); return true; } } return false; } /** * Updates the volume group with removed 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 removed, {@code false} otherwise. */ public boolean audioDevicesRemoved(List devices) { Objects.requireNonNull(devices, "Audio devices can not be null"); // Audio device type bus do not allow for devices to be swapped at run time. synchronized (mLock) { if (getTypeLocked() == TYPE_BUS) { return false; } for (int c = 0; c < devices.size(); c++) { if (getTypeLocked() != devices.get(c).getType() || !getAddressLocked().equals(devices.get(c).getAddress())) { continue; } setAudioDeviceInfoLocked(null); return true; } } return false; } /** * Sets the audio device info * *

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); } }