Improve AudioCapabilities with AudioManager API in Android 13
PiperOrigin-RevId: 604700601
This commit is contained in:
parent
20053dcdc6
commit
ccd603acb0
@ -122,6 +122,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CancellationException;
|
||||
@ -505,7 +506,7 @@ public final class Util {
|
||||
|
||||
/**
|
||||
* Tests two objects for {@link Object#equals(Object)} equality, handling the case where one or
|
||||
* both may be null.
|
||||
* both may be {@code null}.
|
||||
*
|
||||
* @param o1 The first object.
|
||||
* @param o2 The second object.
|
||||
@ -516,6 +517,64 @@ public final class Util {
|
||||
return o1 == null ? o2 == null : o1.equals(o2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests two {@link SparseArray} instances for content equality, handling the case where one or
|
||||
* both may be {@code null}.
|
||||
*
|
||||
* @see SparseArray#contentEquals(SparseArray)
|
||||
* @param sparseArray1 The first {@link SparseArray} instance.
|
||||
* @param sparseArray2 The second {@link SparseArray} instance.
|
||||
* @return True if the two {@link SparseArray} instances are equal in contents.
|
||||
*/
|
||||
@UnstableApi
|
||||
public static <T> boolean contentEquals(
|
||||
@Nullable SparseArray<T> sparseArray1, @Nullable SparseArray<T> sparseArray2) {
|
||||
if (sparseArray1 == null) {
|
||||
return sparseArray2 == null;
|
||||
} else if (sparseArray2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Util.SDK_INT >= 31) {
|
||||
return sparseArray1.contentEquals(sparseArray2);
|
||||
}
|
||||
|
||||
int size = sparseArray1.size();
|
||||
if (size != sparseArray2.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int index = 0; index < size; index++) {
|
||||
int key = sparseArray1.keyAt(index);
|
||||
if (!Objects.equals(sparseArray1.valueAt(index), sparseArray2.get(key))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hash code value for the contents of this {@link SparseArray}, combining the {@link
|
||||
* Objects#hashCode(Object)} result of all its keys and values.
|
||||
*
|
||||
* @see SparseArray#contentHashCode()
|
||||
* @param sparseArray The {@link SparseArray} instance.
|
||||
* @return The hash code.
|
||||
*/
|
||||
@UnstableApi
|
||||
public static <T> int contentHashCode(SparseArray<T> sparseArray) {
|
||||
if (Util.SDK_INT >= 31) {
|
||||
return sparseArray.contentHashCode();
|
||||
}
|
||||
int hash = 17;
|
||||
for (int index = 0; index < sparseArray.size(); index++) {
|
||||
hash = 31 * hash + sparseArray.keyAt(index);
|
||||
hash = 31 * hash + Objects.hashCode(sparseArray.valueAt(index));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether an {@code items} array contains an object equal to {@code item}, according to
|
||||
* {@link Object#equals(Object)}.
|
||||
|
@ -17,6 +17,8 @@ package androidx.media3.common.util;
|
||||
|
||||
import static androidx.media3.common.util.Util.binarySearchCeil;
|
||||
import static androidx.media3.common.util.Util.binarySearchFloor;
|
||||
import static androidx.media3.common.util.Util.contentEquals;
|
||||
import static androidx.media3.common.util.Util.contentHashCode;
|
||||
import static androidx.media3.common.util.Util.escapeFileName;
|
||||
import static androidx.media3.common.util.Util.getCodecsOfType;
|
||||
import static androidx.media3.common.util.Util.getStringForTime;
|
||||
@ -42,6 +44,7 @@ import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.util.SparseArray;
|
||||
import android.util.SparseLongArray;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
@ -1523,6 +1526,77 @@ public class UtilTest {
|
||||
assertThat(roleFlags).containsExactly("describes-music", "easy-read");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contentEquals_twoNullSparseArrays_returnsTrue() {
|
||||
assertThat(contentEquals(null, null)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contentEquals_oneNullSparseArrayAndOneNonNullSparseArray_returnsFalse() {
|
||||
SparseArray<Integer> sparseArray = new SparseArray<>();
|
||||
sparseArray.put(1, 2);
|
||||
|
||||
assertThat(contentEquals(sparseArray, null)).isFalse();
|
||||
assertThat(contentEquals(null, sparseArray)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(minSdk = 16) // Specifies the minimum SDK to enforce the test to run with all API levels.
|
||||
public void contentEquals_sparseArraysWithEqualContent_returnsTrue() {
|
||||
SparseArray<Integer> sparseArray1 = new SparseArray<>();
|
||||
sparseArray1.put(1, 2);
|
||||
sparseArray1.put(3, 4);
|
||||
SparseArray<Integer> sparseArray2 = new SparseArray<>();
|
||||
sparseArray2.put(3, 4);
|
||||
sparseArray2.put(1, 2);
|
||||
|
||||
assertThat(contentEquals(sparseArray1, sparseArray2)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(minSdk = 16) // Specifies the minimum SDK to enforce the test to run with all API levels.
|
||||
public void contentEquals_sparseArraysWithDifferentContents_returnsFalse() {
|
||||
SparseArray<Integer> sparseArray1 = new SparseArray<>();
|
||||
sparseArray1.put(1, 2);
|
||||
sparseArray1.put(3, 4);
|
||||
SparseArray<Integer> sparseArray2 = new SparseArray<>();
|
||||
sparseArray2.put(3, 4);
|
||||
SparseArray<Integer> sparseArray3 = new SparseArray<>();
|
||||
sparseArray3.put(1, 3);
|
||||
sparseArray3.put(3, 4);
|
||||
|
||||
assertThat(contentEquals(sparseArray1, sparseArray2)).isFalse();
|
||||
assertThat(contentEquals(sparseArray1, sparseArray3)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(minSdk = 16) // Specifies the minimum SDK to enforce the test to run with all API levels.
|
||||
public void contentHashCode_sparseArraysWithEqualContent_returnsEqualContentHashCode() {
|
||||
SparseArray<Integer> sparseArray1 = new SparseArray<>();
|
||||
sparseArray1.put(1, 2);
|
||||
sparseArray1.put(3, 4);
|
||||
SparseArray<Integer> sparseArray2 = new SparseArray<>();
|
||||
sparseArray2.put(3, 4);
|
||||
sparseArray2.put(1, 2);
|
||||
|
||||
assertThat(contentHashCode(sparseArray1)).isEqualTo(contentHashCode(sparseArray2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(minSdk = 16) // Specifies the minimum SDK to enforce the test to run with all API levels.
|
||||
public void contentHashCode_sparseArraysWithDifferentContent_returnsDifferentContentHashCode() {
|
||||
// In theory this is not guaranteed though, adding this test to ensure a sensible
|
||||
// contentHashCode implementation.
|
||||
SparseArray<Integer> sparseArray1 = new SparseArray<>();
|
||||
sparseArray1.put(1, 2);
|
||||
sparseArray1.put(3, 4);
|
||||
SparseArray<Integer> sparseArray2 = new SparseArray<>();
|
||||
sparseArray2.put(3, 2);
|
||||
sparseArray2.put(1, 4);
|
||||
|
||||
assertThat(contentHashCode(sparseArray1)).isNotEqualTo(contentHashCode(sparseArray2));
|
||||
}
|
||||
|
||||
private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) {
|
||||
assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName);
|
||||
assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName);
|
||||
|
@ -15,7 +15,9 @@
|
||||
*/
|
||||
package androidx.media3.exoplayer.audio;
|
||||
|
||||
import static android.media.AudioFormat.CHANNEL_OUT_STEREO;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
@ -28,6 +30,7 @@ import android.media.AudioTrack;
|
||||
import android.net.Uri;
|
||||
import android.provider.Settings.Global;
|
||||
import android.util.Pair;
|
||||
import android.util.SparseArray;
|
||||
import androidx.annotation.DoNotInline;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
@ -42,8 +45,11 @@ import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.primitives.Ints;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Represents the set of audio formats that a device is capable of playing. */
|
||||
@UnstableApi
|
||||
@ -51,12 +57,12 @@ public final class AudioCapabilities {
|
||||
|
||||
// TODO(internal b/283945513): Have separate default max channel counts in `AudioCapabilities`
|
||||
// for PCM and compressed audio.
|
||||
private static final int DEFAULT_MAX_CHANNEL_COUNT = 10;
|
||||
@VisibleForTesting /* package */ static final int DEFAULT_MAX_CHANNEL_COUNT = 10;
|
||||
@VisibleForTesting /* package */ static final int DEFAULT_SAMPLE_RATE_HZ = 48_000;
|
||||
|
||||
/** The minimum audio capabilities supported by all devices. */
|
||||
public static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES =
|
||||
new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, DEFAULT_MAX_CHANNEL_COUNT);
|
||||
new AudioCapabilities(ImmutableList.of(AudioProfile.DEFAULT_AUDIO_PROFILE));
|
||||
|
||||
/** Encodings supported when the device specifies external surround sound. */
|
||||
@SuppressLint("InlinedApi") // Compile-time access to integer constants defined in API 21.
|
||||
@ -68,7 +74,8 @@ public final class AudioCapabilities {
|
||||
* All surround sound encodings that a device may be capable of playing mapped to a maximum
|
||||
* channel count.
|
||||
*/
|
||||
private static final ImmutableMap<Integer, Integer> ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS =
|
||||
@VisibleForTesting /* package */
|
||||
static final ImmutableMap<Integer, Integer> ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS =
|
||||
new ImmutableMap.Builder<Integer, Integer>()
|
||||
.put(C.ENCODING_AC3, 6)
|
||||
.put(C.ENCODING_AC4, 6)
|
||||
@ -134,6 +141,14 @@ public final class AudioCapabilities {
|
||||
: Util.SDK_INT >= 33
|
||||
? Api33.getDefaultRoutedDeviceForAttributes(audioManager, audioAttributes)
|
||||
: null;
|
||||
|
||||
if (Util.SDK_INT >= 33 && (Util.isTv(context) || Util.isAutomotive(context))) {
|
||||
// TV or automotive devices generally shouldn't support audio offload for surround encodings,
|
||||
// so the encodings we get from AudioManager.getDirectProfilesForAttributes should include
|
||||
// the PCM encodings and surround encodings for passthrough mode.
|
||||
return Api33.getCapabilitiesInternalForDirectPlayback(audioManager, audioAttributes);
|
||||
}
|
||||
|
||||
// If a connection to Bluetooth device is detected, we only return the minimum capabilities that
|
||||
// is supported by all the devices.
|
||||
if (Util.SDK_INT >= 23 && Api23.isBluetoothConnected(audioManager, currentDevice)) {
|
||||
@ -153,7 +168,7 @@ public final class AudioCapabilities {
|
||||
if (Util.SDK_INT >= 29 && (Util.isTv(context) || Util.isAutomotive(context))) {
|
||||
supportedEncodings.addAll(Api29.getDirectPlaybackSupportedEncodings(audioAttributes));
|
||||
return new AudioCapabilities(
|
||||
Ints.toArray(supportedEncodings.build()), DEFAULT_MAX_CHANNEL_COUNT);
|
||||
getAudioProfiles(Ints.toArray(supportedEncodings.build()), DEFAULT_MAX_CHANNEL_COUNT));
|
||||
}
|
||||
|
||||
if (intent != null && intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 0) == 1) {
|
||||
@ -162,13 +177,17 @@ public final class AudioCapabilities {
|
||||
supportedEncodings.addAll(Ints.asList(encodingsFromExtra));
|
||||
}
|
||||
return new AudioCapabilities(
|
||||
getAudioProfiles(
|
||||
Ints.toArray(supportedEncodings.build()),
|
||||
intent.getIntExtra(
|
||||
AudioManager.EXTRA_MAX_CHANNEL_COUNT, /* defaultValue= */ DEFAULT_MAX_CHANNEL_COUNT));
|
||||
AudioManager.EXTRA_MAX_CHANNEL_COUNT,
|
||||
/* defaultValue= */ DEFAULT_MAX_CHANNEL_COUNT)));
|
||||
}
|
||||
|
||||
return new AudioCapabilities(
|
||||
Ints.toArray(supportedEncodings.build()), /* maxChannelCount= */ DEFAULT_MAX_CHANNEL_COUNT);
|
||||
getAudioProfiles(
|
||||
Ints.toArray(supportedEncodings.build()),
|
||||
/* maxChannelCount= */ DEFAULT_MAX_CHANNEL_COUNT));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -182,28 +201,26 @@ public final class AudioCapabilities {
|
||||
: null;
|
||||
}
|
||||
|
||||
private final int[] supportedEncodings;
|
||||
private final SparseArray<AudioProfile> encodingToAudioProfile;
|
||||
private final int maxChannelCount;
|
||||
|
||||
/**
|
||||
* Constructs new audio capabilities based on a set of supported encodings and a maximum channel
|
||||
* count.
|
||||
*
|
||||
* <p>Applications should generally call {@link #getCapabilities(Context, AudioAttributes,
|
||||
* AudioDeviceInfo)} to obtain an instance based on the capabilities advertised by the platform,
|
||||
* rather than calling this constructor.
|
||||
*
|
||||
* @param supportedEncodings Supported audio encodings from {@link android.media.AudioFormat}'s
|
||||
* {@code ENCODING_*} constants. Passing {@code null} indicates that no encodings are
|
||||
* supported.
|
||||
* @param maxChannelCount The maximum number of audio channels that can be played simultaneously.
|
||||
* @deprecated Use {@link #getCapabilities(Context, AudioAttributes, AudioDeviceInfo)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public AudioCapabilities(@Nullable int[] supportedEncodings, int maxChannelCount) {
|
||||
if (supportedEncodings != null) {
|
||||
this.supportedEncodings = Arrays.copyOf(supportedEncodings, supportedEncodings.length);
|
||||
Arrays.sort(this.supportedEncodings);
|
||||
} else {
|
||||
this.supportedEncodings = new int[0];
|
||||
this(getAudioProfiles(supportedEncodings, maxChannelCount));
|
||||
}
|
||||
|
||||
private AudioCapabilities(List<AudioProfile> audioProfiles) {
|
||||
encodingToAudioProfile = new SparseArray<>();
|
||||
for (int i = 0; i < audioProfiles.size(); i++) {
|
||||
AudioProfile audioProfile = audioProfiles.get(i);
|
||||
encodingToAudioProfile.put(audioProfile.encoding, audioProfile);
|
||||
}
|
||||
int maxChannelCount = 0;
|
||||
for (int i = 0; i < encodingToAudioProfile.size(); i++) {
|
||||
maxChannelCount = max(maxChannelCount, encodingToAudioProfile.valueAt(i).maxChannelCount);
|
||||
}
|
||||
this.maxChannelCount = maxChannelCount;
|
||||
}
|
||||
@ -215,7 +232,7 @@ public final class AudioCapabilities {
|
||||
* @return Whether this device supports playback the specified audio {@code encoding}.
|
||||
*/
|
||||
public boolean supportsEncoding(@C.Encoding int encoding) {
|
||||
return Arrays.binarySearch(supportedEncodings, encoding) >= 0;
|
||||
return Util.contains(encodingToAudioProfile, encoding);
|
||||
}
|
||||
|
||||
/** Returns the maximum number of channels the device can play at the same time. */
|
||||
@ -279,6 +296,8 @@ public final class AudioCapabilities {
|
||||
if (!supportsEncoding(encoding)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
AudioProfile audioProfile = checkNotNull(encodingToAudioProfile.get(encoding));
|
||||
int channelCount;
|
||||
if (format.channelCount == Format.NO_VALUE || encoding == C.ENCODING_E_AC3_JOC) {
|
||||
// In HLS chunkless preparation, the format channel count and sample rate may be unset. See
|
||||
@ -287,16 +306,16 @@ public final class AudioCapabilities {
|
||||
int sampleRate =
|
||||
format.sampleRate != Format.NO_VALUE ? format.sampleRate : DEFAULT_SAMPLE_RATE_HZ;
|
||||
channelCount =
|
||||
getMaxSupportedChannelCountForPassthrough(encoding, sampleRate, audioAttributes);
|
||||
audioProfile.getMaxSupportedChannelCountForPassthrough(sampleRate, audioAttributes);
|
||||
} else {
|
||||
channelCount = format.channelCount;
|
||||
if (format.sampleMimeType.equals(MimeTypes.AUDIO_DTS_X) && Util.SDK_INT < 33) {
|
||||
// Some DTS:X TVs reports ACTION_HDMI_AUDIO_PLUG.EXTRA_MAX_CHANNEL_COUNT as 8
|
||||
// instead of 10. See https://github.com/androidx/media/issues/396
|
||||
if (format.sampleMimeType.equals(MimeTypes.AUDIO_DTS_X)) {
|
||||
if (channelCount > 10) {
|
||||
return null;
|
||||
}
|
||||
} else if (channelCount > maxChannelCount) {
|
||||
} else if (!audioProfile.supportsChannelCount(channelCount)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -316,21 +335,21 @@ public final class AudioCapabilities {
|
||||
return false;
|
||||
}
|
||||
AudioCapabilities audioCapabilities = (AudioCapabilities) other;
|
||||
return Arrays.equals(supportedEncodings, audioCapabilities.supportedEncodings)
|
||||
return Util.contentEquals(encodingToAudioProfile, audioCapabilities.encodingToAudioProfile)
|
||||
&& maxChannelCount == audioCapabilities.maxChannelCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return maxChannelCount + 31 * Arrays.hashCode(supportedEncodings);
|
||||
return maxChannelCount + 31 * Util.contentHashCode(encodingToAudioProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AudioCapabilities[maxChannelCount="
|
||||
+ maxChannelCount
|
||||
+ ", supportedEncodings="
|
||||
+ Arrays.toString(supportedEncodings)
|
||||
+ ", audioProfiles="
|
||||
+ encodingToAudioProfile
|
||||
+ "]";
|
||||
}
|
||||
|
||||
@ -339,21 +358,6 @@ public final class AudioCapabilities {
|
||||
&& ("Amazon".equals(Util.MANUFACTURER) || "Xiaomi".equals(Util.MANUFACTURER));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum number of channels supported for passthrough playback of audio in the given
|
||||
* encoding, or {@code 0} if the format is unsupported.
|
||||
*/
|
||||
private static int getMaxSupportedChannelCountForPassthrough(
|
||||
@C.Encoding int encoding, int sampleRate, AudioAttributes audioAttributes) {
|
||||
// From API 29 we can get the channel count from the platform, but before then there is no way
|
||||
// to query the platform so we assume the channel count matches the maximum channel count per
|
||||
// audio encoding spec.
|
||||
if (Util.SDK_INT >= 29) {
|
||||
return Api29.getMaxSupportedChannelCountForPassthrough(encoding, sampleRate, audioAttributes);
|
||||
}
|
||||
return checkNotNull(ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.getOrDefault(encoding, 0));
|
||||
}
|
||||
|
||||
private static int getChannelConfigForPassthrough(int channelCount) {
|
||||
if (Util.SDK_INT <= 28) {
|
||||
// In passthrough mode the channel count used to configure the audio track doesn't affect how
|
||||
@ -376,6 +380,152 @@ public final class AudioCapabilities {
|
||||
return Util.getAudioTrackChannelConfig(channelCount);
|
||||
}
|
||||
|
||||
// Suppression needed for IntDef casting.
|
||||
@SuppressLint("WrongConstant")
|
||||
@RequiresApi(33)
|
||||
private static ImmutableList<AudioProfile> getAudioProfiles(
|
||||
List<android.media.AudioProfile> audioProfiles) {
|
||||
Map<Integer, Set<Integer>> formatToChannelMasks = new HashMap<>();
|
||||
// Enforce the support of stereo 16bit-PCM.
|
||||
formatToChannelMasks.put(C.ENCODING_PCM_16BIT, new HashSet<>(Ints.asList(CHANNEL_OUT_STEREO)));
|
||||
for (int i = 0; i < audioProfiles.size(); i++) {
|
||||
android.media.AudioProfile audioProfile = audioProfiles.get(i);
|
||||
if ((audioProfile.getEncapsulationType()
|
||||
== android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_IEC61937)) {
|
||||
// Skip the IEC61937 encapsulation because we don't support it yet.
|
||||
continue;
|
||||
}
|
||||
int encoding = audioProfile.getFormat();
|
||||
if (!Util.isEncodingLinearPcm(encoding)
|
||||
&& !ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.containsKey(encoding)) {
|
||||
continue;
|
||||
}
|
||||
if (formatToChannelMasks.containsKey(encoding)) {
|
||||
checkNotNull(formatToChannelMasks.get(encoding))
|
||||
.addAll(Ints.asList(audioProfile.getChannelMasks()));
|
||||
} else {
|
||||
formatToChannelMasks.put(
|
||||
encoding, new HashSet<>(Ints.asList(audioProfile.getChannelMasks())));
|
||||
}
|
||||
}
|
||||
|
||||
ImmutableList.Builder<AudioProfile> localAudioProfiles = ImmutableList.builder();
|
||||
for (Map.Entry<Integer, Set<Integer>> formatAndChannelMasks : formatToChannelMasks.entrySet()) {
|
||||
localAudioProfiles.add(
|
||||
new AudioProfile(formatAndChannelMasks.getKey(), formatAndChannelMasks.getValue()));
|
||||
}
|
||||
return localAudioProfiles.build();
|
||||
}
|
||||
|
||||
private static ImmutableList<AudioProfile> getAudioProfiles(
|
||||
@Nullable int[] supportedEncodings, int maxChannelCount) {
|
||||
ImmutableList.Builder<AudioProfile> audioProfiles = ImmutableList.builder();
|
||||
if (supportedEncodings == null) {
|
||||
supportedEncodings = new int[0];
|
||||
}
|
||||
for (int i = 0; i < supportedEncodings.length; i++) {
|
||||
int encoding = supportedEncodings[i];
|
||||
audioProfiles.add(new AudioProfile(encoding, maxChannelCount));
|
||||
}
|
||||
return audioProfiles.build();
|
||||
}
|
||||
|
||||
private static final class AudioProfile {
|
||||
|
||||
public static final AudioProfile DEFAULT_AUDIO_PROFILE =
|
||||
(Util.SDK_INT >= 33)
|
||||
? new AudioProfile(
|
||||
C.ENCODING_PCM_16BIT,
|
||||
getAllChannelMasksForMaxChannelCount(DEFAULT_MAX_CHANNEL_COUNT))
|
||||
: new AudioProfile(C.ENCODING_PCM_16BIT, DEFAULT_MAX_CHANNEL_COUNT);
|
||||
|
||||
public final @C.Encoding int encoding;
|
||||
public final int maxChannelCount;
|
||||
@Nullable private final ImmutableSet<Integer> channelMasks;
|
||||
|
||||
@RequiresApi(33)
|
||||
public AudioProfile(@C.Encoding int encoding, Set<Integer> channelMasks) {
|
||||
this.encoding = encoding;
|
||||
this.channelMasks = ImmutableSet.copyOf(channelMasks);
|
||||
int maxChannelCount = 0;
|
||||
for (int channelMask : this.channelMasks) {
|
||||
maxChannelCount = max(maxChannelCount, Integer.bitCount(channelMask));
|
||||
}
|
||||
this.maxChannelCount = maxChannelCount;
|
||||
}
|
||||
|
||||
public AudioProfile(@C.Encoding int encoding, int maxChannelCount) {
|
||||
this.encoding = encoding;
|
||||
this.maxChannelCount = maxChannelCount;
|
||||
this.channelMasks = null;
|
||||
}
|
||||
|
||||
public boolean supportsChannelCount(int channelCount) {
|
||||
if (channelMasks == null) {
|
||||
return channelCount <= maxChannelCount;
|
||||
}
|
||||
|
||||
int channelMask = Util.getAudioTrackChannelConfig(channelCount);
|
||||
if (channelMask == AudioFormat.CHANNEL_INVALID) {
|
||||
return false;
|
||||
}
|
||||
return channelMasks.contains(channelMask);
|
||||
}
|
||||
|
||||
public int getMaxSupportedChannelCountForPassthrough(
|
||||
int sampleRate, AudioAttributes audioAttributes) {
|
||||
if (channelMasks != null) {
|
||||
// We built the AudioProfile on API 33.
|
||||
return maxChannelCount;
|
||||
} else if (Util.SDK_INT >= 29) {
|
||||
return Api29.getMaxSupportedChannelCountForPassthrough(
|
||||
encoding, sampleRate, audioAttributes);
|
||||
}
|
||||
return checkNotNull(ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.getOrDefault(encoding, 0));
|
||||
}
|
||||
|
||||
private static ImmutableSet<Integer> getAllChannelMasksForMaxChannelCount(int maxChannelCount) {
|
||||
ImmutableSet.Builder<Integer> allChannelMasks = new ImmutableSet.Builder<>();
|
||||
for (int i = 1; i <= maxChannelCount; i++) {
|
||||
allChannelMasks.add(Util.getAudioTrackChannelConfig(i));
|
||||
}
|
||||
return allChannelMasks.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof AudioProfile)) {
|
||||
return false;
|
||||
}
|
||||
AudioProfile audioProfile = (AudioProfile) other;
|
||||
return encoding == audioProfile.encoding
|
||||
&& maxChannelCount == audioProfile.maxChannelCount
|
||||
&& Util.areEqual(channelMasks, audioProfile.channelMasks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = encoding;
|
||||
result = 31 * result + maxChannelCount;
|
||||
result = 31 * result + (channelMasks == null ? 0 : channelMasks.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AudioProfile[format="
|
||||
+ encoding
|
||||
+ ", maxChannelCount="
|
||||
+ maxChannelCount
|
||||
+ ", channelMasks="
|
||||
+ channelMasks
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(23)
|
||||
private static final class Api23 {
|
||||
private Api23() {}
|
||||
@ -438,7 +588,7 @@ public final class AudioCapabilities {
|
||||
}
|
||||
if (AudioTrack.isDirectPlaybackSupported(
|
||||
new AudioFormat.Builder()
|
||||
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
|
||||
.setChannelMask(CHANNEL_OUT_STEREO)
|
||||
.setEncoding(encoding)
|
||||
.setSampleRate(DEFAULT_SAMPLE_RATE_HZ)
|
||||
.build(),
|
||||
@ -482,6 +632,17 @@ public final class AudioCapabilities {
|
||||
@RequiresApi(33)
|
||||
private static final class Api33 {
|
||||
|
||||
private Api33() {}
|
||||
|
||||
@DoNotInline
|
||||
public static AudioCapabilities getCapabilitiesInternalForDirectPlayback(
|
||||
AudioManager audioManager, AudioAttributes audioAttributes) {
|
||||
List<android.media.AudioProfile> directAudioProfiles =
|
||||
audioManager.getDirectProfilesForAttributes(
|
||||
audioAttributes.getAudioAttributesV21().audioAttributes);
|
||||
return new AudioCapabilities(getAudioProfiles(directAudioProfiles));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@DoNotInline
|
||||
public static AudioDeviceInfoApi23 getDefaultRoutedDeviceForAttributes(
|
||||
|
Loading…
x
Reference in New Issue
Block a user