625 lines
24 KiB
Java
625 lines
24 KiB
Java
![]() |
/*
|
||
|
* Copyright (C) 2014 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 android.media.audiopolicy;
|
||
|
|
||
|
import static android.media.AudioSystem.getDeviceName;
|
||
|
import static android.media.AudioSystem.isRemoteSubmixDevice;
|
||
|
|
||
|
import android.annotation.IntDef;
|
||
|
import android.annotation.NonNull;
|
||
|
import android.annotation.Nullable;
|
||
|
import android.annotation.SystemApi;
|
||
|
import android.compat.annotation.UnsupportedAppUsage;
|
||
|
import android.content.Context;
|
||
|
import android.media.AudioDeviceInfo;
|
||
|
import android.media.AudioFormat;
|
||
|
import android.media.AudioSystem;
|
||
|
import android.os.Binder;
|
||
|
import android.os.Build;
|
||
|
import android.os.IBinder;
|
||
|
import android.os.Parcel;
|
||
|
import android.os.Parcelable;
|
||
|
|
||
|
import com.android.internal.annotations.VisibleForTesting;
|
||
|
|
||
|
import java.lang.annotation.Retention;
|
||
|
import java.lang.annotation.RetentionPolicy;
|
||
|
import java.util.Objects;
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
@SystemApi
|
||
|
public class AudioMix implements Parcelable {
|
||
|
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
private @NonNull AudioMixingRule mRule;
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
private @NonNull AudioFormat mFormat;
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
private int mRouteFlags;
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
private int mMixType = MIX_TYPE_INVALID;
|
||
|
|
||
|
private final IBinder mToken;
|
||
|
|
||
|
// written by AudioPolicy
|
||
|
int mMixState = MIX_STATE_DISABLED;
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
int mCallbackFlags;
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
@NonNull String mDeviceAddress;
|
||
|
|
||
|
// initialized in constructor, read by AudioPolicyConfig
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
||
|
final int mDeviceSystemType; // an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_*
|
||
|
|
||
|
// The (virtual) device ID that this AudioMix was registered for. This value is overwritten
|
||
|
// when registering this AudioMix with an AudioPolicy or attaching this AudioMix to an
|
||
|
// AudioPolicy to match the AudioPolicy attribution. Does not imply that it only modifies
|
||
|
// audio routing for this device ID.
|
||
|
private int mVirtualDeviceId;
|
||
|
|
||
|
/**
|
||
|
* All parameters are guaranteed valid through the Builder.
|
||
|
*/
|
||
|
private AudioMix(@NonNull AudioMixingRule rule, @NonNull AudioFormat format,
|
||
|
int routeFlags, int callbackFlags,
|
||
|
int deviceType, @Nullable String deviceAddress, IBinder token,
|
||
|
int virtualDeviceId) {
|
||
|
mRule = Objects.requireNonNull(rule);
|
||
|
mFormat = Objects.requireNonNull(format);
|
||
|
mRouteFlags = routeFlags;
|
||
|
mMixType = rule.getTargetMixType();
|
||
|
mCallbackFlags = callbackFlags;
|
||
|
mDeviceSystemType = deviceType;
|
||
|
mDeviceAddress = (deviceAddress == null) ? new String("") : deviceAddress;
|
||
|
mToken = token;
|
||
|
mVirtualDeviceId = virtualDeviceId;
|
||
|
}
|
||
|
|
||
|
// CALLBACK_FLAG_* values: keep in sync with AudioMix::kCbFlag* values defined
|
||
|
// in frameworks/av/include/media/AudioPolicy.h
|
||
|
/** @hide */
|
||
|
public final static int CALLBACK_FLAG_NOTIFY_ACTIVITY = 0x1;
|
||
|
// when adding new MIX_FLAG_* flags, add them to this mask of authorized masks:
|
||
|
private final static int CALLBACK_FLAGS_ALL = CALLBACK_FLAG_NOTIFY_ACTIVITY;
|
||
|
|
||
|
// ROUTE_FLAG_* values: keep in sync with MIX_ROUTE_FLAG_* values defined
|
||
|
// in frameworks/av/include/media/AudioPolicy.h
|
||
|
/**
|
||
|
* An audio mix behavior where the output of the mix is sent to the original destination of
|
||
|
* the audio signal, i.e. an output device for an output mix, or a recording for an input mix.
|
||
|
*/
|
||
|
public static final int ROUTE_FLAG_RENDER = 0x1;
|
||
|
/**
|
||
|
* An audio mix behavior where the output of the mix is rerouted back to the framework and
|
||
|
* is accessible for injection or capture through the {@link AudioTrack} and {@link AudioRecord}
|
||
|
* APIs.
|
||
|
*/
|
||
|
public static final int ROUTE_FLAG_LOOP_BACK = 0x1 << 1;
|
||
|
|
||
|
/**
|
||
|
* An audio mix behavior where the targeted audio is played unaffected but a copy is
|
||
|
* accessible for capture through {@link AudioRecord}.
|
||
|
*
|
||
|
* Only capture of playback is supported, not capture of capture.
|
||
|
* Use concurrent capture instead to capture what is captured by other apps.
|
||
|
*
|
||
|
* The captured audio is an approximation of the played audio.
|
||
|
* Effects and volume are not applied, and track are mixed with different delay then in the HAL.
|
||
|
* As a result, this API is not suitable for echo cancelling.
|
||
|
* @hide
|
||
|
*/
|
||
|
public static final int ROUTE_FLAG_LOOP_BACK_RENDER = ROUTE_FLAG_LOOP_BACK | ROUTE_FLAG_RENDER;
|
||
|
|
||
|
private static final int ROUTE_FLAG_SUPPORTED = ROUTE_FLAG_RENDER | ROUTE_FLAG_LOOP_BACK;
|
||
|
|
||
|
// MIX_TYPE_* values to keep in sync with frameworks/av/include/media/AudioPolicy.h
|
||
|
/**
|
||
|
* @hide
|
||
|
* Invalid mix type, default value.
|
||
|
*/
|
||
|
public static final int MIX_TYPE_INVALID = -1;
|
||
|
/**
|
||
|
* @hide
|
||
|
* Mix type indicating playback streams are mixed.
|
||
|
*/
|
||
|
public static final int MIX_TYPE_PLAYERS = 0;
|
||
|
/**
|
||
|
* @hide
|
||
|
* Mix type indicating recording streams are mixed.
|
||
|
*/
|
||
|
public static final int MIX_TYPE_RECORDERS = 1;
|
||
|
|
||
|
|
||
|
// MIX_STATE_* values to keep in sync with frameworks/av/include/media/AudioPolicy.h
|
||
|
/**
|
||
|
* State of a mix before its policy is enabled.
|
||
|
*/
|
||
|
public static final int MIX_STATE_DISABLED = -1;
|
||
|
/**
|
||
|
* State of a mix when there is no audio to mix.
|
||
|
*/
|
||
|
public static final int MIX_STATE_IDLE = 0;
|
||
|
/**
|
||
|
* State of a mix that is actively mixing audio.
|
||
|
*/
|
||
|
public static final int MIX_STATE_MIXING = 1;
|
||
|
|
||
|
/** Maximum sampling rate for privileged playback capture*/
|
||
|
private static final int PRIVILEDGED_CAPTURE_MAX_SAMPLE_RATE = 16000;
|
||
|
|
||
|
/** Maximum channel number for privileged playback capture*/
|
||
|
private static final int PRIVILEDGED_CAPTURE_MAX_CHANNEL_NUMBER = 1;
|
||
|
|
||
|
/** Maximum channel number for privileged playback capture*/
|
||
|
private static final int PRIVILEDGED_CAPTURE_MAX_BYTES_PER_SAMPLE = 2;
|
||
|
|
||
|
/**
|
||
|
* The current mixing state.
|
||
|
* @return one of {@link #MIX_STATE_DISABLED}, {@link #MIX_STATE_IDLE},
|
||
|
* {@link #MIX_STATE_MIXING}.
|
||
|
*/
|
||
|
public int getMixState() {
|
||
|
return mMixState;
|
||
|
}
|
||
|
|
||
|
|
||
|
/** @hide */
|
||
|
public int getRouteFlags() {
|
||
|
return mRouteFlags;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public AudioFormat getFormat() {
|
||
|
return mFormat;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public AudioMixingRule getRule() {
|
||
|
return mRule;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public int getMixType() {
|
||
|
return mMixType;
|
||
|
}
|
||
|
|
||
|
void setRegistration(String regId) {
|
||
|
mDeviceAddress = regId;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public void setAudioMixingRule(@NonNull AudioMixingRule rule) {
|
||
|
if (mRule.getTargetMixType() != rule.getTargetMixType()) {
|
||
|
throw new UnsupportedOperationException(
|
||
|
"Target mix role of updated rule doesn't match the mix role of the AudioMix");
|
||
|
}
|
||
|
mRule = Objects.requireNonNull(rule);
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public String getRegistration() {
|
||
|
return mDeviceAddress;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public boolean isAffectingUsage(int usage) {
|
||
|
return mRule.isAffectingUsage(usage);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns {@code true} if the rule associated with this mix contains a
|
||
|
* RULE_MATCH_ATTRIBUTE_USAGE criterion for the given usage
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public boolean containsMatchAttributeRuleForUsage(int usage) {
|
||
|
return mRule.containsMatchAttributeRuleForUsage(usage);
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public boolean isRoutedToDevice(int deviceType, @NonNull String deviceAddress) {
|
||
|
if ((mRouteFlags & ROUTE_FLAG_RENDER) != ROUTE_FLAG_RENDER) {
|
||
|
return false;
|
||
|
}
|
||
|
if (deviceType != mDeviceSystemType) {
|
||
|
return false;
|
||
|
}
|
||
|
if (!deviceAddress.equals(mDeviceAddress)) {
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/** @return an error string if the format would not allow Privileged playbackCapture
|
||
|
* null otherwise
|
||
|
* @hide */
|
||
|
public static String canBeUsedForPrivilegedMediaCapture(AudioFormat format) {
|
||
|
int sampleRate = format.getSampleRate();
|
||
|
if (sampleRate > PRIVILEDGED_CAPTURE_MAX_SAMPLE_RATE || sampleRate <= 0) {
|
||
|
return "Privileged audio capture sample rate " + sampleRate
|
||
|
+ " can not be over " + PRIVILEDGED_CAPTURE_MAX_SAMPLE_RATE + "kHz";
|
||
|
}
|
||
|
int channelCount = format.getChannelCount();
|
||
|
if (channelCount > PRIVILEDGED_CAPTURE_MAX_CHANNEL_NUMBER || channelCount <= 0) {
|
||
|
return "Privileged audio capture channel count " + channelCount + " can not be over "
|
||
|
+ PRIVILEDGED_CAPTURE_MAX_CHANNEL_NUMBER;
|
||
|
}
|
||
|
int encoding = format.getEncoding();
|
||
|
if (!format.isPublicEncoding(encoding) || !format.isEncodingLinearPcm(encoding)) {
|
||
|
return "Privileged audio capture encoding " + encoding + "is not linear";
|
||
|
}
|
||
|
if (format.getBytesPerSample(encoding) > PRIVILEDGED_CAPTURE_MAX_BYTES_PER_SAMPLE) {
|
||
|
return "Privileged audio capture encoding " + encoding + " can not be over "
|
||
|
+ PRIVILEDGED_CAPTURE_MAX_BYTES_PER_SAMPLE + " bytes per sample";
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public boolean isForCallRedirection() {
|
||
|
return mRule.isForCallRedirection();
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
public boolean matchesVirtualDeviceId(int deviceId) {
|
||
|
return mVirtualDeviceId == deviceId;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public boolean equals(Object o) {
|
||
|
if (this == o) return true;
|
||
|
if (o == null || getClass() != o.getClass()) return false;
|
||
|
|
||
|
final AudioMix that = (AudioMix) o;
|
||
|
boolean tokenMatch = android.media.audiopolicy.Flags.audioMixOwnership()
|
||
|
? Objects.equals(this.mToken, that.mToken)
|
||
|
: true;
|
||
|
return Objects.equals(this.mRouteFlags, that.mRouteFlags)
|
||
|
&& Objects.equals(this.mRule, that.mRule)
|
||
|
&& Objects.equals(this.mMixType, that.mMixType)
|
||
|
&& Objects.equals(this.mFormat, that.mFormat)
|
||
|
&& tokenMatch;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
public int hashCode() {
|
||
|
if (android.media.audiopolicy.Flags.audioMixOwnership()) {
|
||
|
return Objects.hash(mRouteFlags, mRule, mMixType, mFormat, mToken);
|
||
|
}
|
||
|
return Objects.hash(mRouteFlags, mRule, mMixType, mFormat);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int describeContents() {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||
|
// write mix route flags
|
||
|
dest.writeInt(mRouteFlags);
|
||
|
// write callback flags
|
||
|
dest.writeInt(mCallbackFlags);
|
||
|
// write device information
|
||
|
dest.writeInt(mDeviceSystemType);
|
||
|
dest.writeString8(mDeviceAddress);
|
||
|
mFormat.writeToParcel(dest, flags);
|
||
|
mRule.writeToParcel(dest, flags);
|
||
|
dest.writeStrongBinder(mToken);
|
||
|
dest.writeInt(mVirtualDeviceId);
|
||
|
}
|
||
|
|
||
|
public static final @NonNull Parcelable.Creator<AudioMix> CREATOR = new Parcelable.Creator<>() {
|
||
|
/**
|
||
|
* Rebuilds an AudioMix previously stored with writeToParcel().
|
||
|
*
|
||
|
* @param p Parcel object to read the AudioMix from
|
||
|
* @return a new AudioMix created from the data in the parcel
|
||
|
*/
|
||
|
public AudioMix createFromParcel(Parcel p) {
|
||
|
final AudioMix.Builder mixBuilder = new AudioMix.Builder();
|
||
|
// read mix route flags
|
||
|
mixBuilder.setRouteFlags(p.readInt());
|
||
|
// read callback flags
|
||
|
mixBuilder.setCallbackFlags(p.readInt());
|
||
|
// read device information
|
||
|
mixBuilder.setDevice(p.readInt(), p.readString8());
|
||
|
mixBuilder.setFormat(AudioFormat.CREATOR.createFromParcel(p));
|
||
|
mixBuilder.setMixingRule(AudioMixingRule.CREATOR.createFromParcel(p));
|
||
|
mixBuilder.setToken(p.readStrongBinder());
|
||
|
mixBuilder.setVirtualDeviceId(p.readInt());
|
||
|
return mixBuilder.build();
|
||
|
}
|
||
|
|
||
|
public AudioMix[] newArray(int size) {
|
||
|
return new AudioMix[size];
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Updates the deviceId of the AudioMix to match with the AudioPolicy the mix is registered
|
||
|
* through.
|
||
|
* @hide
|
||
|
*/
|
||
|
public void setVirtualDeviceId(int virtualDeviceId) {
|
||
|
mVirtualDeviceId = virtualDeviceId;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@IntDef(flag = true,
|
||
|
value = { ROUTE_FLAG_RENDER, ROUTE_FLAG_LOOP_BACK } )
|
||
|
@Retention(RetentionPolicy.SOURCE)
|
||
|
public @interface RouteFlags {}
|
||
|
|
||
|
/**
|
||
|
* Builder class for {@link AudioMix} objects
|
||
|
*/
|
||
|
public static class Builder {
|
||
|
private AudioMixingRule mRule = null;
|
||
|
private AudioFormat mFormat = null;
|
||
|
private int mRouteFlags = 0;
|
||
|
private int mCallbackFlags = 0;
|
||
|
private IBinder mToken = null;
|
||
|
private int mVirtualDeviceId = Context.DEVICE_ID_DEFAULT;
|
||
|
// an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_*
|
||
|
private int mDeviceSystemType = AudioSystem.DEVICE_NONE;
|
||
|
private String mDeviceAddress = null;
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
* Only used by AudioPolicyConfig, not a public API.
|
||
|
*/
|
||
|
Builder() { }
|
||
|
|
||
|
/**
|
||
|
* Construct an instance for the given {@link AudioMixingRule}.
|
||
|
* @param rule a non-null {@link AudioMixingRule} instance.
|
||
|
* @throws IllegalArgumentException
|
||
|
*/
|
||
|
public Builder(@NonNull AudioMixingRule rule)
|
||
|
throws IllegalArgumentException {
|
||
|
if (rule == null) {
|
||
|
throw new IllegalArgumentException("Illegal null AudioMixingRule argument");
|
||
|
}
|
||
|
mRule = rule;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
* Only used by AudioPolicyConfig, not a public API.
|
||
|
* @param rule
|
||
|
* @return the same Builder instance.
|
||
|
* @throws IllegalArgumentException
|
||
|
*/
|
||
|
Builder setMixingRule(@NonNull AudioMixingRule rule)
|
||
|
throws IllegalArgumentException {
|
||
|
if (rule == null) {
|
||
|
throw new IllegalArgumentException("Illegal null AudioMixingRule argument");
|
||
|
}
|
||
|
mRule = rule;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
* Only used by AudioMix internally.
|
||
|
*/
|
||
|
Builder setToken(IBinder token) {
|
||
|
mToken = token;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
* Only used by AudioMix internally.
|
||
|
*/
|
||
|
Builder setVirtualDeviceId(int virtualDeviceId) {
|
||
|
mVirtualDeviceId = virtualDeviceId;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
* Only used by AudioPolicyConfig, not a public API.
|
||
|
* @param callbackFlags which callbacks are called from native
|
||
|
* @return the same Builder instance.
|
||
|
* @throws IllegalArgumentException
|
||
|
*/
|
||
|
Builder setCallbackFlags(int flags) throws IllegalArgumentException {
|
||
|
if ((flags != 0) && ((flags & CALLBACK_FLAGS_ALL) == 0)) {
|
||
|
throw new IllegalArgumentException("Illegal callback flags 0x"
|
||
|
+ Integer.toHexString(flags).toUpperCase());
|
||
|
}
|
||
|
mCallbackFlags = flags;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
* Only used by AudioPolicyConfig, not a public API.
|
||
|
* @param deviceType an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_*
|
||
|
* @param address
|
||
|
* @return the same Builder instance.
|
||
|
*/
|
||
|
@VisibleForTesting
|
||
|
public Builder setDevice(int deviceType, String address) {
|
||
|
mDeviceSystemType = deviceType;
|
||
|
mDeviceAddress = address;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the {@link AudioFormat} for the mix.
|
||
|
* @param format a non-null {@link AudioFormat} instance.
|
||
|
* @return the same Builder instance.
|
||
|
* @throws IllegalArgumentException
|
||
|
*/
|
||
|
public Builder setFormat(@NonNull AudioFormat format)
|
||
|
throws IllegalArgumentException {
|
||
|
if (format == null) {
|
||
|
throw new IllegalArgumentException("Illegal null AudioFormat argument");
|
||
|
}
|
||
|
mFormat = format;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the routing behavior for the mix. If not set, routing behavior will default to
|
||
|
* {@link AudioMix#ROUTE_FLAG_LOOP_BACK}.
|
||
|
* @param routeFlags one of {@link AudioMix#ROUTE_FLAG_LOOP_BACK},
|
||
|
* {@link AudioMix#ROUTE_FLAG_RENDER}
|
||
|
* @return the same Builder instance.
|
||
|
* @throws IllegalArgumentException
|
||
|
*/
|
||
|
public Builder setRouteFlags(@RouteFlags int routeFlags)
|
||
|
throws IllegalArgumentException {
|
||
|
if (routeFlags == 0) {
|
||
|
throw new IllegalArgumentException("Illegal empty route flags");
|
||
|
}
|
||
|
if ((routeFlags & ROUTE_FLAG_SUPPORTED) == 0) {
|
||
|
throw new IllegalArgumentException("Invalid route flags 0x"
|
||
|
+ Integer.toHexString(routeFlags) + "when configuring an AudioMix");
|
||
|
}
|
||
|
if ((routeFlags & ~ROUTE_FLAG_SUPPORTED) != 0) {
|
||
|
throw new IllegalArgumentException("Unknown route flags 0x"
|
||
|
+ Integer.toHexString(routeFlags) + "when configuring an AudioMix");
|
||
|
}
|
||
|
mRouteFlags = routeFlags;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the audio device used for playback. Cannot be used in the context of an audio
|
||
|
* policy used to inject audio to be recorded, or in a mix whose route flags doesn't
|
||
|
* specify {@link AudioMix#ROUTE_FLAG_RENDER}.
|
||
|
* @param device a non-null AudioDeviceInfo describing the audio device to play the output
|
||
|
* of this mix.
|
||
|
* @return the same Builder instance
|
||
|
* @throws IllegalArgumentException
|
||
|
*/
|
||
|
public Builder setDevice(@NonNull AudioDeviceInfo device) throws IllegalArgumentException {
|
||
|
if (device == null) {
|
||
|
throw new IllegalArgumentException("Illegal null AudioDeviceInfo argument");
|
||
|
}
|
||
|
if (!device.isSink()) {
|
||
|
throw new IllegalArgumentException("Unsupported device type on mix, not a sink");
|
||
|
}
|
||
|
mDeviceSystemType = AudioDeviceInfo.convertDeviceTypeToInternalDevice(device.getType());
|
||
|
mDeviceAddress = device.getAddress();
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Combines all of the settings and return a new {@link AudioMix} object.
|
||
|
* @return a new {@link AudioMix} object
|
||
|
* @throws IllegalArgumentException if no {@link AudioMixingRule} has been set.
|
||
|
*/
|
||
|
public AudioMix build() throws IllegalArgumentException {
|
||
|
if (mRule == null) {
|
||
|
throw new IllegalArgumentException("Illegal null AudioMixingRule");
|
||
|
}
|
||
|
if (mRouteFlags == 0) {
|
||
|
// no route flags set, use default as described in Builder.setRouteFlags(int)
|
||
|
mRouteFlags = ROUTE_FLAG_LOOP_BACK;
|
||
|
}
|
||
|
if (mFormat == null) {
|
||
|
// FIXME Can we eliminate this? Will AudioMix work with an unspecified sample rate?
|
||
|
int rate = AudioSystem.getPrimaryOutputSamplingRate();
|
||
|
if (rate <= 0) {
|
||
|
rate = 44100;
|
||
|
}
|
||
|
mFormat = new AudioFormat.Builder().setSampleRate(rate).build();
|
||
|
} else {
|
||
|
// Ensure that 'mFormat' uses an output channel mask. Using an input channel
|
||
|
// mask was not made 'illegal' initially, however the framework code
|
||
|
// assumes usage in AudioMixes of output channel masks only (b/194910301).
|
||
|
if ((mFormat.getPropertySetMask()
|
||
|
& AudioFormat.AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_MASK) != 0) {
|
||
|
if (mFormat.getChannelCount() == 1
|
||
|
&& mFormat.getChannelMask() == AudioFormat.CHANNEL_IN_MONO) {
|
||
|
mFormat = new AudioFormat.Builder(mFormat).setChannelMask(
|
||
|
AudioFormat.CHANNEL_OUT_MONO).build();
|
||
|
}
|
||
|
// CHANNEL_IN_STEREO == CHANNEL_OUT_STEREO so no need to correct.
|
||
|
// CHANNEL_IN_FRONT_BACK is hidden, should not appear.
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ((mRouteFlags & ROUTE_FLAG_LOOP_BACK) == ROUTE_FLAG_LOOP_BACK) {
|
||
|
if (mDeviceSystemType == AudioSystem.DEVICE_NONE) {
|
||
|
// If there was no device type explicitly set, configure it based on mix type.
|
||
|
mDeviceSystemType = getLoopbackDeviceSystemTypeForAudioMixingRule(mRule);
|
||
|
} else if (!isRemoteSubmixDevice(mDeviceSystemType)) {
|
||
|
// Loopback mode only supports remote submix devices.
|
||
|
throw new IllegalArgumentException("Device " + getDeviceName(mDeviceSystemType)
|
||
|
+ "is not supported for loopback mix.");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ((mRouteFlags & ROUTE_FLAG_RENDER) == ROUTE_FLAG_RENDER) {
|
||
|
if (mDeviceSystemType == AudioSystem.DEVICE_NONE) {
|
||
|
throw new IllegalArgumentException(
|
||
|
"Can't have flag ROUTE_FLAG_RENDER without an audio device");
|
||
|
}
|
||
|
|
||
|
if (AudioSystem.DEVICE_IN_ALL_SET.contains(mDeviceSystemType)) {
|
||
|
throw new IllegalArgumentException(
|
||
|
"Input device is not supported with ROUTE_FLAG_RENDER");
|
||
|
}
|
||
|
|
||
|
if (mRule.getTargetMixType() == MIX_TYPE_RECORDERS) {
|
||
|
throw new IllegalArgumentException(
|
||
|
"ROUTE_FLAG_RENDER/ROUTE_FLAG_LOOP_BACK_RENDER is not supported for "
|
||
|
+ "non-playback mix rule");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (mRule.allowPrivilegedMediaPlaybackCapture()) {
|
||
|
String error = AudioMix.canBeUsedForPrivilegedMediaCapture(mFormat);
|
||
|
if (error != null) {
|
||
|
throw new IllegalArgumentException(error);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (mToken == null) {
|
||
|
mToken = new Binder();
|
||
|
}
|
||
|
|
||
|
return new AudioMix(mRule, mFormat, mRouteFlags, mCallbackFlags, mDeviceSystemType,
|
||
|
mDeviceAddress, mToken, mVirtualDeviceId);
|
||
|
}
|
||
|
|
||
|
private int getLoopbackDeviceSystemTypeForAudioMixingRule(AudioMixingRule rule) {
|
||
|
switch (mRule.getTargetMixType()) {
|
||
|
case MIX_TYPE_PLAYERS:
|
||
|
return AudioSystem.DEVICE_OUT_REMOTE_SUBMIX;
|
||
|
case MIX_TYPE_RECORDERS:
|
||
|
return AudioSystem.DEVICE_IN_REMOTE_SUBMIX;
|
||
|
default:
|
||
|
throw new IllegalArgumentException(
|
||
|
"Unknown mixing rule type - 0x" + Integer.toHexString(
|
||
|
rule.getTargetMixType()));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|