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 */
16package com.android.car;
17
18import android.media.AudioDeviceInfo;
19import android.media.AudioDevicePort;
20import android.media.AudioFormat;
21import android.media.AudioGain;
22import android.media.AudioGainConfig;
23import android.media.AudioManager;
24import android.media.AudioPort;
25import android.util.Log;
26
27import com.android.internal.util.Preconditions;
28
29import java.io.PrintWriter;
30
31/**
32 * A helper class wraps {@link AudioDeviceInfo}, and helps get/set the gain on a specific port
33 * in terms of millibels.
34 * Note to the reader. For whatever reason, it seems that AudioGain contains only configuration
35 * information (min/max/step, etc) while the AudioGainConfig class contains the
36 * actual currently active gain value(s).
37 */
38/* package */ class CarAudioDeviceInfo {
39
40    private final AudioDeviceInfo mAudioDeviceInfo;
41    private final int mBusNumber;
42    private final int mSampleRate;
43    private final int mEncodingFormat;
44    private final int mChannelCount;
45    private final int mDefaultGain;
46    private final int mMaxGain;
47    private final int mMinGain;
48
49    /**
50     * We need to store the current gain because it is not accessible from the current
51     * audio engine implementation. It would be nice if AudioPort#activeConfig() would return it,
52     * but in the current implementation, that function actually works only for mixer ports.
53     */
54    private int mCurrentGain;
55
56    CarAudioDeviceInfo(AudioDeviceInfo audioDeviceInfo) {
57        mAudioDeviceInfo = audioDeviceInfo;
58        mBusNumber = parseDeviceAddress(audioDeviceInfo.getAddress());
59        mSampleRate = getMaxSampleRate(audioDeviceInfo);
60        mEncodingFormat = getEncodingFormat(audioDeviceInfo);
61        mChannelCount = getMaxChannels(audioDeviceInfo);
62        final AudioGain audioGain = Preconditions.checkNotNull(
63                getAudioGain(), "No audio gain on device port " + audioDeviceInfo);
64        mDefaultGain = audioGain.defaultValue();
65        mMaxGain = audioGain.maxValue();
66        mMinGain = audioGain.minValue();
67
68        mCurrentGain = -1; // Not initialized till explicitly set
69    }
70
71    AudioDeviceInfo getAudioDeviceInfo() {
72        return mAudioDeviceInfo;
73    }
74
75    AudioDevicePort getAudioDevicePort() {
76        return mAudioDeviceInfo.getPort();
77    }
78
79    int getBusNumber() {
80        return mBusNumber;
81    }
82
83    int getDefaultGain() {
84        return mDefaultGain;
85    }
86
87    int getMaxGain() {
88        return mMaxGain;
89    }
90
91    int getMinGain() {
92        return mMinGain;
93    }
94
95    int getSampleRate() {
96        return mSampleRate;
97    }
98
99    int getEncodingFormat() {
100        return mEncodingFormat;
101    }
102
103    int getChannelCount() {
104        return mChannelCount;
105    }
106
107    // Input is in millibels
108    void setCurrentGain(int gainInMillibels) {
109        // Clamp the incoming value to our valid range.  Out of range values ARE legal input
110        if (gainInMillibels < mMinGain) {
111            gainInMillibels = mMinGain;
112        } else if (gainInMillibels > mMaxGain) {
113            gainInMillibels = mMaxGain;
114        }
115
116        // Push the new gain value down to our underlying port which will cause it to show up
117        // at the HAL.
118        AudioGain audioGain = getAudioGain();
119        if (audioGain == null) {
120            Log.e(CarLog.TAG_AUDIO, "getAudioGain() returned null.");
121            return;
122        }
123
124        // size of gain values is 1 in MODE_JOINT
125        AudioGainConfig audioGainConfig = audioGain.buildConfig(
126                AudioGain.MODE_JOINT,
127                audioGain.channelMask(),
128                new int[] { gainInMillibels },
129                0);
130        if (audioGainConfig == null) {
131            Log.e(CarLog.TAG_AUDIO, "Failed to construct AudioGainConfig");
132            return;
133        }
134
135        int r = AudioManager.setAudioPortGain(getAudioDevicePort(), audioGainConfig);
136        if (r == AudioManager.SUCCESS) {
137            // Since we can't query for the gain on a device port later,
138            // we have to remember what we asked for
139            mCurrentGain = gainInMillibels;
140        } else {
141            Log.e(CarLog.TAG_AUDIO, "Failed to setAudioPortGain: " + r);
142        }
143    }
144
145    /**
146     * Parse device address. Expected format is BUS%d_%s, address, usage hint
147     * @return valid address (from 0 to positive) or -1 for invalid address.
148     */
149    private int parseDeviceAddress(String address) {
150        String[] words = address.split("_");
151        int addressParsed = -1;
152        if (words[0].toLowerCase().startsWith("bus")) {
153            try {
154                addressParsed = Integer.parseInt(words[0].substring(3));
155            } catch (NumberFormatException e) {
156                //ignore
157            }
158        }
159        if (addressParsed < 0) {
160            return -1;
161        }
162        return addressParsed;
163    }
164
165    private int getMaxSampleRate(AudioDeviceInfo info) {
166        int[] sampleRates = info.getSampleRates();
167        if (sampleRates == null || sampleRates.length == 0) {
168            return 48000;
169        }
170        int sampleRate = sampleRates[0];
171        for (int i = 1; i < sampleRates.length; i++) {
172            if (sampleRates[i] > sampleRate) {
173                sampleRate = sampleRates[i];
174            }
175        }
176        return sampleRate;
177    }
178
179    /** Always returns {@link AudioFormat#ENCODING_PCM_16BIT} as for now */
180    private int getEncodingFormat(AudioDeviceInfo info) {
181        return AudioFormat.ENCODING_PCM_16BIT;
182    }
183
184    /**
185     * Gets the maximum channel count for a given {@link AudioDeviceInfo}
186     *
187     * @param info {@link AudioDeviceInfo} instance to get maximum channel count for
188     * @return Maximum channel count for a given {@link AudioDeviceInfo},
189     * 1 (mono) if there is no channel masks configured
190     */
191    private int getMaxChannels(AudioDeviceInfo info) {
192        int numChannels = 1;
193        int[] channelMasks = info.getChannelMasks();
194        if (channelMasks == null) {
195            return numChannels;
196        }
197        for (int channelMask : channelMasks) {
198            int currentNumChannels = Integer.bitCount(channelMask);
199            if (currentNumChannels > numChannels) {
200                numChannels = currentNumChannels;
201            }
202        }
203        return numChannels;
204    }
205
206    /**
207     * @return {@link AudioGain} with {@link AudioGain#MODE_JOINT} on a given {@link AudioPort}.
208     * This is useful for inspecting the configuration data associated with this gain controller
209     * (min/max/step/default).
210     */
211    AudioGain getAudioGain() {
212        final AudioDevicePort audioPort = getAudioDevicePort();
213        if (audioPort != null && audioPort.gains().length > 0) {
214            for (AudioGain audioGain : audioPort.gains()) {
215                if ((audioGain.mode() & AudioGain.MODE_JOINT) != 0) {
216                    return checkAudioGainConfiguration(audioGain);
217                }
218            }
219        }
220        return null;
221    }
222
223    /**
224     * Constraints applied to gain configuration, see also audio_policy_configuration.xml
225     */
226    private AudioGain checkAudioGainConfiguration(AudioGain audioGain) {
227        Preconditions.checkArgument(audioGain.maxValue() >= audioGain.minValue());
228        Preconditions.checkArgument((audioGain.defaultValue() >= audioGain.minValue())
229                && (audioGain.defaultValue() <= audioGain.maxValue()));
230        Preconditions.checkArgument(
231                ((audioGain.maxValue() - audioGain.minValue()) % audioGain.stepValue()) == 0);
232        Preconditions.checkArgument(
233                ((audioGain.defaultValue() - audioGain.minValue()) % audioGain.stepValue()) == 0);
234        return audioGain;
235    }
236
237    @Override
238    public String toString() {
239        return "bus number: " + mBusNumber
240                + " address: " + mAudioDeviceInfo.getAddress()
241                + " sampleRate: " + getSampleRate()
242                + " encodingFormat: " + getEncodingFormat()
243                + " channelCount: " + getChannelCount()
244                + " currentGain: " + mCurrentGain
245                + " maxGain: " + mMaxGain
246                + " minGain: " + mMinGain;
247    }
248
249    void dump(PrintWriter writer) {
250        writer.printf("Bus Number (%d) / address (%s)\n ",
251                mBusNumber, mAudioDeviceInfo.getAddress());
252        writer.printf("\tsample rate / encoding format / channel count: %d %d %d\n",
253                getSampleRate(), getEncodingFormat(), getChannelCount());
254        writer.printf("\tGain in millibel (min / max / default/ current): %d %d %d %d\n",
255                mMinGain, mMaxGain, mDefaultGain, mCurrentGain);
256    }
257}
258