Merge "Add hotword stream configuration to AudioRecord"
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 5f4c19b..dbd4019e 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -6763,12 +6763,16 @@
public class AudioRecord implements android.media.AudioRecordingMonitor android.media.AudioRouting android.media.MicrophoneDirection {
ctor @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public AudioRecord(android.media.AudioAttributes, android.media.AudioFormat, int, int) throws java.lang.IllegalArgumentException;
method public static long getMaxSharedAudioHistoryMillis();
+ method public boolean isHotwordLookbackStream();
+ method public boolean isHotwordStream();
method @NonNull @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public android.media.MediaSyncEvent shareAudioHistory(@NonNull String, @IntRange(from=0) long);
}
public static class AudioRecord.Builder {
method public android.media.AudioRecord.Builder setAudioAttributes(@NonNull android.media.AudioAttributes) throws java.lang.IllegalArgumentException;
method @NonNull @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public android.media.AudioRecord.Builder setMaxSharedAudioHistoryMillis(long) throws java.lang.IllegalArgumentException;
+ method @NonNull @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public android.media.AudioRecord.Builder setRequestHotwordLookbackStream(boolean);
+ method @NonNull @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public android.media.AudioRecord.Builder setRequestHotwordStream(boolean);
method public android.media.AudioRecord.Builder setSessionId(int) throws java.lang.IllegalArgumentException;
method @NonNull public android.media.AudioRecord.Builder setSharedAudioEvent(@NonNull android.media.MediaSyncEvent) throws java.lang.IllegalArgumentException;
}
diff --git a/core/jni/android_media_AudioRecord.cpp b/core/jni/android_media_AudioRecord.cpp
index 30333d2..1910331 100644
--- a/core/jni/android_media_AudioRecord.cpp
+++ b/core/jni/android_media_AudioRecord.cpp
@@ -149,7 +149,8 @@
jint channelIndexMask, jint audioFormat,
jint buffSizeInBytes, jintArray jSession,
jobject jAttributionSource, jlong nativeRecordInJavaObj,
- jint sharedAudioHistoryMs) {
+ jint sharedAudioHistoryMs,
+ jint halFlags) {
//ALOGV(">> Entering android_media_AudioRecord_setup");
//ALOGV("sampleRate=%d, audioFormat=%d, channel mask=%x, buffSizeInBytes=%d "
// "nativeRecordInJavaObj=0x%llX",
@@ -239,10 +240,7 @@
}
ALOGV("AudioRecord_setup for source=%d tags=%s flags=%08x", paa->source, paa->tags, paa->flags);
- audio_input_flags_t flags = AUDIO_INPUT_FLAG_NONE;
- if (paa->flags & AUDIO_FLAG_HW_HOTWORD) {
- flags = AUDIO_INPUT_FLAG_HW_HOTWORD;
- }
+ const auto flags = static_cast<audio_input_flags_t>(halFlags);
// create the callback information:
// this data will be passed with every AudioRecord callback
// we use a weak reference so the AudioRecord object can be garbage collected.
@@ -831,7 +829,7 @@
{"native_start", "(II)I", (void *)android_media_AudioRecord_start},
{"native_stop", "()V", (void *)android_media_AudioRecord_stop},
{"native_setup",
- "(Ljava/lang/Object;Ljava/lang/Object;[IIIII[ILandroid/os/Parcel;JI)I",
+ "(Ljava/lang/Object;Ljava/lang/Object;[IIIII[ILandroid/os/Parcel;JII)I",
(void *)android_media_AudioRecord_setup},
{"native_finalize", "()V", (void *)android_media_AudioRecord_finalize},
{"native_release", "()V", (void *)android_media_AudioRecord_release},
diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java
index 6e3829a..b6ab262 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -37,6 +37,7 @@
import android.content.AttributionSource.ScopedParcelState;
import android.content.Context;
import android.media.MediaRecorder.Source;
+import android.media.audio.common.AudioInputFlags;
import android.media.audiopolicy.AudioMix;
import android.media.audiopolicy.AudioMixingRule;
import android.media.audiopolicy.AudioPolicy;
@@ -276,6 +277,11 @@
*/
private int mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE;
/**
+ * Audio HAL Input Flags as bitfield.
+ */
+ private int mHalInputFlags = 0;
+
+ /**
* AudioAttributes
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@@ -360,7 +366,8 @@
public AudioRecord(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
int sessionId) throws IllegalArgumentException {
this(attributes, format, bufferSizeInBytes, sessionId,
- ActivityThread.currentApplication(), 0 /*maxSharedAudioHistoryMs*/);
+ ActivityThread.currentApplication(),
+ 0 /*maxSharedAudioHistoryMs*/, 0 /* halInputFlags */);
}
/**
@@ -382,14 +389,15 @@
* time. See also {@link AudioManager#generateAudioSessionId()} to obtain a session ID before
* construction.
* @param context An optional context on whose behalf the recoding is performed.
- *
+ * @param maxSharedAudioHistoryMs
+ * @param halInputFlags Bitfield indexed by {@link AudioInputFlags} which is passed to the HAL.
* @throws IllegalArgumentException
*/
private AudioRecord(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
int sessionId, @Nullable Context context,
- int maxSharedAudioHistoryMs) throws IllegalArgumentException {
+ int maxSharedAudioHistoryMs, int halInputFlags) throws IllegalArgumentException {
mRecordingState = RECORDSTATE_STOPPED;
-
+ mHalInputFlags = halInputFlags;
if (attributes == null) {
throw new IllegalArgumentException("Illegal null AudioAttributes");
}
@@ -469,7 +477,7 @@
int initResult = native_setup(new WeakReference<AudioRecord>(this), mAudioAttributes,
sampleRate, mChannelMask, mChannelIndexMask, mAudioFormat,
mNativeBufferSizeInBytes, session, attributionSourceState.getParcel(),
- 0 /*nativeRecordInJavaObj*/, maxSharedAudioHistoryMs);
+ 0 /*nativeRecordInJavaObj*/, maxSharedAudioHistoryMs, mHalInputFlags);
if (initResult != SUCCESS) {
loge("Error code " + initResult + " when initializing native AudioRecord object.");
return; // with mState == STATE_UNINITIALIZED
@@ -535,7 +543,8 @@
session,
attributionSourceState.getParcel(),
nativeRecordInJavaObj,
- 0);
+ 0 /*maxSharedAudioHistoryMs*/,
+ 0 /*halInputFlags*/);
}
if (initResult != SUCCESS) {
loge("Error code "+initResult+" when initializing native AudioRecord object.");
@@ -597,6 +606,8 @@
private int mPrivacySensitive = PRIVACY_SENSITIVE_DEFAULT;
private int mMaxSharedAudioHistoryMs = 0;
private int mCallRedirectionMode = AudioManager.CALL_REDIRECT_NONE;
+ private boolean mIsHotwordStream = false;
+ private boolean mIsHotwordLookback = false;
private static final int PRIVACY_SENSITIVE_DEFAULT = -1;
private static final int PRIVACY_SENSITIVE_DISABLED = 0;
@@ -906,17 +917,73 @@
}
/**
+ * @hide
+ * Set to indicate that the requested AudioRecord object should produce the same type
+ * of audio content that the hotword recognition model consumes. SoundTrigger hotword
+ * recognition will not be disrupted. The source in the set AudioAttributes and the set
+ * audio source will be overridden if this API is used.
+ * <br> Use {@link AudioManager#isHotwordStreamSupported(boolean)} to query support.
+ * @param hotwordContent true if AudioRecord should produce content captured from the
+ * hotword pipeline. false if AudioRecord should produce content captured outside
+ * the hotword pipeline.
+ * @return the same Builder instance.
+ **/
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD)
+ public @NonNull Builder setRequestHotwordStream(boolean hotwordContent) {
+ mIsHotwordStream = hotwordContent;
+ return this;
+ }
+
+ /**
+ * @hide
+ * Set to indicate that the requested AudioRecord object should produce the same type
+ * of audio content that the hotword recognition model consumes and that the stream will
+ * be able to provide buffered audio content from an unspecified duration prior to stream
+ * open. The source in the set AudioAttributes and the set audio source will be overridden
+ * if this API is used.
+ * <br> Use {@link AudioManager#isHotwordStreamSupported(boolean)} to query support.
+ * <br> If this is set, {@link AudioRecord.Builder#setRequestHotwordStream(boolean)}
+ * must not be set, or {@link AudioRecord.Builder#build()} will throw.
+ * @param hotwordLookbackContent true if AudioRecord should produce content captured from
+ * the hotword pipeline with capture content from prior to open. false if AudioRecord
+ * should not capture such content.
+ * to stream open is requested.
+ * @return the same Builder instance.
+ **/
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD)
+ public @NonNull Builder setRequestHotwordLookbackStream(boolean hotwordLookbackContent) {
+ mIsHotwordLookback = hotwordLookbackContent;
+ return this;
+ }
+
+
+ /**
* @return a new {@link AudioRecord} instance successfully initialized with all
* the parameters set on this <code>Builder</code>.
* @throws UnsupportedOperationException if the parameters set on the <code>Builder</code>
- * were incompatible, or if they are not supported by the device,
- * or if the device was not available.
+ * were incompatible, if the parameters are not supported by the device, if the caller
+ * does not hold the appropriate permissions, or if the device was not available.
*/
@RequiresPermission(android.Manifest.permission.RECORD_AUDIO)
public AudioRecord build() throws UnsupportedOperationException {
if (mAudioPlaybackCaptureConfiguration != null) {
return buildAudioPlaybackCaptureRecord();
}
+ int halInputFlags = 0;
+ if (mIsHotwordStream) {
+ if (mIsHotwordLookback) {
+ throw new UnsupportedOperationException(
+ "setRequestHotwordLookbackStream and " +
+ "setRequestHotwordStream used concurrently");
+ } else {
+ halInputFlags = (1 << AudioInputFlags.HOTWORD_TAP);
+ }
+ } else if (mIsHotwordLookback) {
+ halInputFlags = (1 << AudioInputFlags.HOTWORD_TAP) |
+ (1 << AudioInputFlags.HW_LOOKBACK);
+ }
if (mFormat == null) {
mFormat = new AudioFormat.Builder()
@@ -942,6 +1009,12 @@
.build();
}
+ if (mIsHotwordStream || mIsHotwordLookback) {
+ mAttributes = new AudioAttributes.Builder(mAttributes)
+ .setInternalCapturePreset(MediaRecorder.AudioSource.VOICE_RECOGNITION)
+ .build();
+ }
+
// If mPrivacySensitive is default, the privacy flag is already set
// according to audio source in audio attributes.
if (mPrivacySensitive != PRIVACY_SENSITIVE_DEFAULT) {
@@ -980,7 +1053,7 @@
}
final AudioRecord record = new AudioRecord(
mAttributes, mFormat, mBufferSizeInBytes, mSessionId, mContext,
- mMaxSharedAudioHistoryMs);
+ mMaxSharedAudioHistoryMs, halInputFlags);
if (record.getState() == STATE_UNINITIALIZED) {
// release is not necessary
throw new UnsupportedOperationException("Cannot create AudioRecord");
@@ -1041,8 +1114,8 @@
case AudioFormat.CHANNEL_IN_DEFAULT: // AudioFormat.CHANNEL_CONFIGURATION_DEFAULT
case AudioFormat.CHANNEL_IN_MONO:
case AudioFormat.CHANNEL_CONFIGURATION_MONO:
- mask = AudioFormat.CHANNEL_IN_MONO;
- break;
+ mask = AudioFormat.CHANNEL_IN_MONO;
+ break;
case AudioFormat.CHANNEL_IN_STEREO:
case AudioFormat.CHANNEL_CONFIGURATION_STEREO:
mask = AudioFormat.CHANNEL_IN_STEREO;
@@ -1384,6 +1457,35 @@
return (mAudioAttributes.getAllFlags() & AudioAttributes.FLAG_CAPTURE_PRIVATE) != 0;
}
+ /**
+ * @hide
+ * Returns whether the AudioRecord object produces the same type of audio content that
+ * the hotword recognition model consumes.
+ * <br> If {@link isHotwordLookbackStream(boolean)} is true, this will return false
+ * <br> See {@link Builder#setRequestHotwordStream(boolean)}
+ * @return true if AudioRecord produces hotword content, false otherwise
+ **/
+ @SystemApi
+ public boolean isHotwordStream() {
+ return ((mHalInputFlags & (1 << AudioInputFlags.HOTWORD_TAP)) != 0 &&
+ (mHalInputFlags & (1 << AudioInputFlags.HW_LOOKBACK)) == 0);
+ }
+
+ /**
+ * @hide
+ * Returns whether the AudioRecord object produces the same type of audio content that
+ * the hotword recognition model consumes, and includes capture content from prior to
+ * stream open.
+ * <br> See {@link Builder#setRequestHotwordLookbackStream(boolean)}
+ * @return true if AudioRecord produces hotword capture content from
+ * prior to stream open, false otherwise
+ **/
+ @SystemApi
+ public boolean isHotwordLookbackStream() {
+ return ((mHalInputFlags & (1 << AudioInputFlags.HW_LOOKBACK)) != 0);
+ }
+
+
//---------------------------------------------------------
// Transport control methods
//--------------------
@@ -2346,13 +2448,13 @@
Object /*AudioAttributes*/ attributes,
int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
int buffSizeInBytes, int[] sessionId, String opPackageName,
- long nativeRecordInJavaObj) {
+ long nativeRecordInJavaObj, int halInputFlags) {
AttributionSource attributionSource = AttributionSource.myAttributionSource()
.withPackageName(opPackageName);
try (ScopedParcelState attributionSourceState = attributionSource.asScopedParcelState()) {
return native_setup(audiorecordThis, attributes, sampleRate, channelMask,
channelIndexMask, audioFormat, buffSizeInBytes, sessionId,
- attributionSourceState.getParcel(), nativeRecordInJavaObj, 0);
+ attributionSourceState.getParcel(), nativeRecordInJavaObj, 0, halInputFlags);
}
}
@@ -2360,7 +2462,7 @@
Object /*AudioAttributes*/ attributes,
int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource,
- long nativeRecordInJavaObj, int maxSharedAudioHistoryMs);
+ long nativeRecordInJavaObj, int maxSharedAudioHistoryMs, int halInputFlags);
// TODO remove: implementation calls directly into implementation of native_release()
private native void native_finalize();