VolumeShaper: Initial implementation

The VolumeShaper is used to apply a volume
envelope to an AudioTrack or a MediaPlayer.

Test: CTS
Bug: 30920125
Bug: 31015569
Change-Id: If8b4bed29760aa3bd15a4b54cae60e40b4f518ee
This commit is contained in:
Andy Hung
2017-01-24 13:45:02 -08:00
parent 30110eaeec
commit 035d4ec772
11 changed files with 1738 additions and 0 deletions

View File

@@ -37,6 +37,7 @@
#include "android_media_AudioErrors.h"
#include "android_media_PlaybackParams.h"
#include "android_media_DeviceCallback.h"
#include "android_media_VolumeShaper.h"
#include <cinttypes>
@@ -64,6 +65,7 @@ struct audio_attributes_fields_t {
static audio_track_fields_t javaAudioTrackFields;
static audio_attributes_fields_t javaAudioAttrFields;
static PlaybackParams::fields_t gPlaybackParamsFields;
static VolumeShaperHelper::fields_t gVolumeShaperFields;
struct audiotrack_callback_cookie {
jclass audioTrack_class;
@@ -1178,6 +1180,50 @@ static jint android_media_AudioTrack_get_FCC_8(JNIEnv *env, jobject thiz) {
return FCC_8;
}
// Pass through the arguments to the AudioFlinger track implementation.
static jint android_media_AudioTrack_apply_volume_shaper(JNIEnv *env, jobject thiz,
jobject jconfig, jobject joperation) {
// NOTE: hard code here to prevent platform issues. Must match VolumeShaper.java
const int VOLUME_SHAPER_INVALID_OPERATION = -38;
sp<AudioTrack> lpTrack = getAudioTrack(env, thiz);
if (lpTrack == nullptr) {
return (jint)VOLUME_SHAPER_INVALID_OPERATION;
}
sp<VolumeShaper::Configuration> configuration;
sp<VolumeShaper::Operation> operation;
if (jconfig != nullptr) {
configuration = VolumeShaperHelper::convertJobjectToConfiguration(
env, gVolumeShaperFields, jconfig);
ALOGV("applyVolumeShaper configuration: %s", configuration->toString().c_str());
}
if (joperation != nullptr) {
operation = VolumeShaperHelper::convertJobjectToOperation(
env, gVolumeShaperFields, joperation);
ALOGV("applyVolumeShaper operation: %s", operation->toString().c_str());
}
VolumeShaper::Status status = lpTrack->applyVolumeShaper(configuration, operation);
if (status == INVALID_OPERATION) {
status = VOLUME_SHAPER_INVALID_OPERATION;
}
return (jint)status; // if status < 0 an error, else a VolumeShaper id
}
// Pass through the arguments to the AudioFlinger track implementation.
static jobject android_media_AudioTrack_get_volume_shaper_state(JNIEnv *env, jobject thiz,
jint id) {
sp<AudioTrack> lpTrack = getAudioTrack(env, thiz);
if (lpTrack == nullptr) {
return (jobject)nullptr;
}
sp<VolumeShaper::State> state = lpTrack->getVolumeShaperState((int)id);
if (state.get() == nullptr) {
return (jobject)nullptr;
}
return VolumeShaperHelper::convertStateToJobject(env, gVolumeShaperFields, state);
}
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
@@ -1242,6 +1288,12 @@ static const JNINativeMethod gMethods[] = {
{"native_enableDeviceCallback", "()V", (void *)android_media_AudioTrack_enableDeviceCallback},
{"native_disableDeviceCallback", "()V", (void *)android_media_AudioTrack_disableDeviceCallback},
{"native_get_FCC_8", "()I", (void *)android_media_AudioTrack_get_FCC_8},
{"native_applyVolumeShaper",
"(Landroid/media/VolumeShaper$Configuration;Landroid/media/VolumeShaper$Operation;)I",
(void *)android_media_AudioTrack_apply_volume_shaper},
{"native_getVolumeShaperState",
"(I)Landroid/media/VolumeShaper$State;",
(void *)android_media_AudioTrack_get_volume_shaper_state},
};
@@ -1312,6 +1364,7 @@ int register_android_media_AudioTrack(JNIEnv *env)
// initialize PlaybackParams field info
gPlaybackParamsFields.init(env);
gVolumeShaperFields.init(env);
return res;
}

View File

@@ -1741,6 +1741,17 @@ public class AudioTrack extends PlayerBase
return setStereoVolume(gain, gain);
}
@Override
/* package */ int playerApplyVolumeShaper(
@NonNull VolumeShaper.Configuration configuration,
@NonNull VolumeShaper.Operation operation) {
return native_applyVolumeShaper(configuration, operation);
}
@Override
/* package */ @Nullable VolumeShaper.State playerGetVolumeShaperState(int id) {
return native_getVolumeShaperState(id);
}
/**
* Sets the playback sample rate for this track. This sets the sampling rate at which
@@ -3093,6 +3104,12 @@ public class AudioTrack extends PlayerBase
private native final void native_disableDeviceCallback();
static private native int native_get_FCC_8();
private native int native_applyVolumeShaper(
@NonNull VolumeShaper.Configuration configuration,
@NonNull VolumeShaper.Operation operation);
private native @Nullable VolumeShaper.State native_getVolumeShaperState(int id);
//---------------------------------------------------------
// Utility methods
//------------------

View File

@@ -16,6 +16,7 @@
package android.media;
import android.media.VolumeShaper;
/**
* @hide
@@ -27,4 +28,6 @@ interface IPlayer {
oneway void setVolume(float vol);
oneway void setPan(float pan);
oneway void setStartDelayMs(int delayMs);
oneway void applyVolumeShaper(in VolumeShaper.Configuration configuration,
in VolumeShaper.Operation operation);
}

View File

@@ -1331,6 +1331,24 @@ public class MediaPlayer extends PlayerBase
stop();
}
@Override
/* package */ int playerApplyVolumeShaper(
@NonNull VolumeShaper.Configuration configuration,
@NonNull VolumeShaper.Operation operation) {
return native_applyVolumeShaper(configuration, operation);
}
@Override
/* package */ @Nullable VolumeShaper.State playerGetVolumeShaperState(int id) {
return native_getVolumeShaperState(id);
}
private native int native_applyVolumeShaper(
@NonNull VolumeShaper.Configuration configuration,
@NonNull VolumeShaper.Operation operation);
private native @Nullable VolumeShaper.State native_getVolumeShaperState(int id);
/**
* Set the low-level power management behavior for this MediaPlayer. This
* can be used when the MediaPlayer is not playing through a SurfaceHolder

View File

@@ -17,9 +17,11 @@
package android.media;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.app.AppOpsManager;
import android.content.Context;
import android.media.VolumeShaper;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
@@ -357,6 +359,42 @@ public abstract class PlayerBase {
* @param rightVolume the right volume to use if muting is false
*/
abstract void playerSetVolume(boolean muting, float leftVolume, float rightVolume);
/**
* Abstract method to apply a {@link VolumeShaper.Configuration}
* and a {@link VolumeShaper.Operation} to the Player.
* This should be overridden by the Player to call into the native
* VolumeShaper implementation. Multiple {@code VolumeShapers} may be
* concurrently active for a given Player, each accessible by the
* {@code VolumeShaper} id.
*
* The {@code VolumeShaper} implementation caches the id returned
* when applying a fully specified configuration
* from {VolumeShaper.Configuration.Builder} to track later
* operation changes requested on it.
*
* @param configuration a {@code VolumeShaper.Configuration} object
* created by {@link VolumeShaper.Configuration.Builder} or
* an created from a {@code VolumeShaper} id
* by the {@link VolumeShaper.Configuration} constructor.
* @param operation a {@code VolumeShaper.Operation}.
* @return a negative error status or a
* non-negative {@code VolumeShaper} id on success.
*/
/* package */ abstract int playerApplyVolumeShaper(
@NonNull VolumeShaper.Configuration configuration,
@NonNull VolumeShaper.Operation operation);
/**
* Abstract method to get the current VolumeShaper state.
* @param id the {@code VolumeShaper} id returned from
* sending a fully specified {@code VolumeShaper.Configuration}
* through {@link #playerApplyVolumeShaper}
* @return a {@code VolumeShaper.State} object or null if
* there is no {@code VolumeShaper} for the id.
*/
/* package */ abstract @Nullable VolumeShaper.State playerGetVolumeShaperState(int id);
abstract int playerSetAuxEffectSendLevel(boolean muting, float level);
abstract void playerStart();
abstract void playerPause();
@@ -396,6 +434,13 @@ public abstract class PlayerBase {
public void setStartDelayMs(int delayMs) {
baseSetStartDelayMs(delayMs);
}
@Override
public void applyVolumeShaper(
@NonNull VolumeShaper.Configuration configuration,
@NonNull VolumeShaper.Operation operation) {
/* void */ playerApplyVolumeShaper(configuration, operation);
}
};
//=====================================================================

View File

@@ -18,6 +18,7 @@ package android.media;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.media.VolumeShaper;
import android.os.RemoteException;
import android.util.Log;
@@ -132,4 +133,21 @@ public class PlayerProxy {
}
}
/**
* @hide
* @param configuration
* @param operation
* @return volume shaper id or error
*/
public void applyVolumeShaper(
@NonNull VolumeShaper.Configuration configuration,
@NonNull VolumeShaper.Operation operation) {
try {
mConf.getIPlayer().applyVolumeShaper(configuration, operation);
} catch (NullPointerException|RemoteException e) {
throw new IllegalStateException(
"No player to proxy for applyVolumeShaper operation,"
+ " player already released?", e);
}
}
}

View File

@@ -20,6 +20,8 @@ import java.io.File;
import java.io.FileDescriptor;
import java.lang.ref.WeakReference;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.app.AppOpsManager;
import android.content.Context;
@@ -388,6 +390,17 @@ public class SoundPool extends PlayerBase {
_setVolume(streamID, leftVolume, rightVolume);
}
@Override
/* package */ int playerApplyVolumeShaper(
@NonNull VolumeShaper.Configuration configuration,
@Nullable VolumeShaper.Operation operation) {
return -1;
}
@Override
/* package */ @Nullable VolumeShaper.State playerGetVolumeShaperState(int id) {
return null;
}
@Override
void playerSetVolume(boolean muting, float leftVolume, float rightVolume) {

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2017 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;
parcelable VolumeShaper.Configuration;
parcelable VolumeShaper.Operation;
parcelable VolumeShaper.State;

File diff suppressed because it is too large Load Diff

View File

@@ -44,6 +44,7 @@
#include "android_media_MediaMetricsJNI.h"
#include "android_media_PlaybackParams.h"
#include "android_media_SyncParams.h"
#include "android_media_VolumeShaper.h"
#include "android_media_Utils.h"
#include "android_os_Parcel.h"
@@ -160,6 +161,7 @@ static fields_t fields;
static BufferingParams::fields_t gBufferingParamsFields;
static PlaybackParams::fields_t gPlaybackParamsFields;
static SyncParams::fields_t gSyncParamsFields;
static VolumeShaperHelper::fields_t gVolumeShaperFields;
static Mutex sLock;
@@ -1088,6 +1090,7 @@ android_media_MediaPlayer_native_init(JNIEnv *env)
gPlaybackParamsFields.init(env);
gSyncParamsFields.init(env);
gVolumeShaperFields.init(env);
}
static void
@@ -1259,6 +1262,51 @@ android_media_MediaPlayer_setNextMediaPlayer(JNIEnv *env, jobject thiz, jobject
;
}
// Pass through the arguments to the MediaServer player implementation.
static jint android_media_MediaPlayer_applyVolumeShaper(JNIEnv *env, jobject thiz,
jobject jconfig, jobject joperation) {
// NOTE: hard code here to prevent platform issues. Must match VolumeShaper.java
const int VOLUME_SHAPER_INVALID_OPERATION = -38;
sp<MediaPlayer> mp = getMediaPlayer(env, thiz);
if (mp == nullptr) {
return (jint)VOLUME_SHAPER_INVALID_OPERATION;
}
sp<VolumeShaper::Configuration> configuration;
sp<VolumeShaper::Operation> operation;
if (jconfig != nullptr) {
configuration = VolumeShaperHelper::convertJobjectToConfiguration(
env, gVolumeShaperFields, jconfig);
ALOGV("applyVolumeShaper configuration: %s", configuration->toString().c_str());
}
if (joperation != nullptr) {
operation = VolumeShaperHelper::convertJobjectToOperation(
env, gVolumeShaperFields, joperation);
ALOGV("applyVolumeShaper operation: %s", operation->toString().c_str());
}
VolumeShaper::Status status = mp->applyVolumeShaper(configuration, operation);
if (status == INVALID_OPERATION) {
status = VOLUME_SHAPER_INVALID_OPERATION;
}
return (jint)status; // if status < 0 an error, else a VolumeShaper id
}
// Pass through the arguments to the MediaServer player implementation.
static jobject android_media_MediaPlayer_getVolumeShaperState(JNIEnv *env, jobject thiz,
jint id) {
sp<MediaPlayer> mp = getMediaPlayer(env, thiz);
if (mp == nullptr) {
return (jobject)nullptr;
}
sp<VolumeShaper::State> state = mp->getVolumeShaperState((int)id);
if (state.get() == nullptr) {
return (jobject)nullptr;
}
return VolumeShaperHelper::convertStateToJobject(env, gVolumeShaperFields, state);
}
/////////////////////////////////////////////////////////////////////////////////////
// Modular DRM begin
@@ -1747,6 +1795,12 @@ static const JNINativeMethod gMethods[] = {
{"native_pullBatteryData", "(Landroid/os/Parcel;)I", (void *)android_media_MediaPlayer_pullBatteryData},
{"native_setRetransmitEndpoint", "(Ljava/lang/String;I)I", (void *)android_media_MediaPlayer_setRetransmitEndpoint},
{"setNextMediaPlayer", "(Landroid/media/MediaPlayer;)V", (void *)android_media_MediaPlayer_setNextMediaPlayer},
{"native_applyVolumeShaper",
"(Landroid/media/VolumeShaper$Configuration;Landroid/media/VolumeShaper$Operation;)I",
(void *)android_media_MediaPlayer_applyVolumeShaper},
{"native_getVolumeShaperState",
"(I)Landroid/media/VolumeShaper$State;",
(void *)android_media_MediaPlayer_getVolumeShaperState},
// Modular DRM
{ "_prepareDrm", "([BI)V", (void *)android_media_MediaPlayer_prepareDrm },
{ "_releaseDrm", "()V", (void *)android_media_MediaPlayer_releaseDrm },

View File

@@ -0,0 +1,221 @@
/*
* Copyright 2017, 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.
*/
#ifndef _ANDROID_MEDIA_VOLUME_SHAPER_H_
#define _ANDROID_MEDIA_VOLUME_SHAPER_H_
#include <media/VolumeShaper.h>
namespace android {
// This entire class is inline as it is used from both core and media
struct VolumeShaperHelper {
struct fields_t {
// VolumeShaper.Configuration
jclass coClazz;
jmethodID coConstructId;
jfieldID coTypeId;
jfieldID coIdId;
jfieldID coInterpolatorTypeId;
jfieldID coOptionFlagsId;
jfieldID coDurationMsId;
jfieldID coTimesId;
jfieldID coVolumesId;
// VolumeShaper.Operation
jclass opClazz;
jmethodID opConstructId;
jfieldID opFlagsId;
jfieldID opReplaceIdId;
// VolumeShaper.State
jclass stClazz;
jmethodID stConstructId;
jfieldID stVolumeId;
jfieldID stXOffsetId;
void init(JNIEnv *env) {
jclass lclazz = env->FindClass("android/media/VolumeShaper$Configuration");
if (lclazz == nullptr) {
return;
}
coClazz = (jclass)env->NewGlobalRef(lclazz);
if (coClazz == nullptr) {
return;
}
coConstructId = env->GetMethodID(coClazz, "<init>", "(IIIID[F[F)V");
coTypeId = env->GetFieldID(coClazz, "mType", "I");
coIdId = env->GetFieldID(coClazz, "mId", "I");
coInterpolatorTypeId = env->GetFieldID(coClazz, "mInterpolatorType", "I");
coOptionFlagsId = env->GetFieldID(coClazz, "mOptionFlags", "I");
coDurationMsId = env->GetFieldID(coClazz, "mDurationMs", "D");
coTimesId = env->GetFieldID(coClazz, "mTimes", "[F");
coVolumesId = env->GetFieldID(coClazz, "mVolumes", "[F");
env->DeleteLocalRef(lclazz);
lclazz = env->FindClass("android/media/VolumeShaper$Operation");
if (lclazz == nullptr) {
return;
}
opClazz = (jclass)env->NewGlobalRef(lclazz);
if (opClazz == nullptr) {
return;
}
opConstructId = env->GetMethodID(opClazz, "<init>", "(II)V");
opFlagsId = env->GetFieldID(opClazz, "mFlags", "I");
opReplaceIdId = env->GetFieldID(opClazz, "mReplaceId", "I");
env->DeleteLocalRef(lclazz);
lclazz = env->FindClass("android/media/VolumeShaper$State");
if (lclazz == nullptr) {
return;
}
stClazz = (jclass)env->NewGlobalRef(lclazz);
if (stClazz == nullptr) {
return;
}
stConstructId = env->GetMethodID(stClazz, "<init>", "(FF)V");
stVolumeId = env->GetFieldID(stClazz, "mVolume", "F");
stXOffsetId = env->GetFieldID(stClazz, "mXOffset", "F");
env->DeleteLocalRef(lclazz);
}
void exit(JNIEnv *env) {
env->DeleteGlobalRef(coClazz);
coClazz = nullptr;
}
};
static sp<VolumeShaper::Configuration> convertJobjectToConfiguration(
JNIEnv *env, const fields_t &fields, jobject jshaper) {
sp<VolumeShaper::Configuration> configuration = new VolumeShaper::Configuration();
configuration->setType(
(VolumeShaper::Configuration::Type)env->GetIntField(jshaper, fields.coTypeId));
configuration->setId(
(int)env->GetIntField(jshaper, fields.coIdId));
if (configuration->getType() == VolumeShaper::Configuration::TYPE_SCALE) {
configuration->setInterpolatorType(
(VolumeShaper::Configuration::InterpolatorType)
env->GetIntField(jshaper, fields.coInterpolatorTypeId));
configuration->setOptionFlags(
(VolumeShaper::Configuration::OptionFlag)
env->GetIntField(jshaper, fields.coOptionFlagsId));
configuration->setDurationMs(
(double)env->GetDoubleField(jshaper, fields.coDurationMsId));
// convert point arrays
jobject xobj = env->GetObjectField(jshaper, fields.coTimesId);
jfloatArray *xarray = reinterpret_cast<jfloatArray*>(&xobj);
jsize xlen = env->GetArrayLength(*xarray);
/* const */ float * const x =
env->GetFloatArrayElements(*xarray, nullptr /* isCopy */);
jobject yobj = env->GetObjectField(jshaper, fields.coVolumesId);
jfloatArray *yarray = reinterpret_cast<jfloatArray*>(&yobj);
jsize ylen = env->GetArrayLength(*yarray);
/* const */ float * const y =
env->GetFloatArrayElements(*yarray, nullptr /* isCopy */);
if (xlen != ylen) {
ALOGE("array size must match");
return nullptr;
}
for (jsize i = 0; i < xlen; ++i) {
configuration->emplace(x[i], y[i]);
}
env->ReleaseFloatArrayElements(*xarray, x, JNI_ABORT); // no need to copy back
env->ReleaseFloatArrayElements(*yarray, y, JNI_ABORT);
}
return configuration;
}
static jobject convertVolumeShaperToJobject(
JNIEnv *env, const fields_t &fields,
const sp<VolumeShaper::Configuration> &configuration) {
jfloatArray xarray = nullptr;
jfloatArray yarray = nullptr;
if (configuration->getType() == VolumeShaper::Configuration::TYPE_SCALE) {
// convert curve arrays
jfloatArray xarray = env->NewFloatArray(configuration->size());
jfloatArray yarray = env->NewFloatArray(configuration->size());
float * const x = env->GetFloatArrayElements(xarray, nullptr /* isCopy */);
float * const y = env->GetFloatArrayElements(yarray, nullptr /* isCopy */);
float *xptr = x, *yptr = y;
for (const auto &pt : *configuration.get()) {
*xptr++ = pt.first;
*yptr++ = pt.second;
}
env->ReleaseFloatArrayElements(xarray, x, 0 /* mode */);
env->ReleaseFloatArrayElements(yarray, y, 0 /* mode */);
}
// prepare constructor args
jvalue args[7];
args[0].i = (jint)configuration->getType();
args[1].i = (jint)configuration->getId();
args[2].i = (jint)configuration->getInterpolatorType();
args[3].i = (jint)configuration->getOptionFlags();
args[4].d = (jdouble)configuration->getDurationMs();
args[5].l = xarray;
args[6].l = yarray;
jobject jshaper = env->NewObjectA(fields.coClazz, fields.coConstructId, args);
return jshaper;
}
static sp<VolumeShaper::Operation> convertJobjectToOperation(
JNIEnv *env, const fields_t &fields, jobject joperation) {
VolumeShaper::Operation::Flag flags =
(VolumeShaper::Operation::Flag)env->GetIntField(joperation, fields.opFlagsId);
int replaceId = env->GetIntField(joperation, fields.opReplaceIdId);
sp<VolumeShaper::Operation> operation = new VolumeShaper::Operation(flags, replaceId);
return operation;
}
static jobject convertOperationToJobject(
JNIEnv *env, const fields_t &fields, const sp<VolumeShaper::Operation> &operation) {
// prepare constructor args
jvalue args[2];
args[0].i = (jint)operation->getFlags();
args[1].i = (jint)operation->getReplaceId();
jobject joperation = env->NewObjectA(fields.opClazz, fields.opConstructId, args);
return joperation;
}
static sp<VolumeShaper::State> convertJobjectToState(
JNIEnv *env, const fields_t &fields, jobject jstate) {
float volume = env->GetFloatField(jstate, fields.stVolumeId);
float xOffset = env->GetFloatField(jstate, fields.stXOffsetId);
sp<VolumeShaper::State> state = new VolumeShaper::State(volume, xOffset);
return state;
}
static jobject convertStateToJobject(
JNIEnv *env, const fields_t &fields, const sp<VolumeShaper::State> &state) {
// prepare constructor args
jvalue args[2];
args[0].f = (jfloat)state->getVolume();
args[1].f = (jfloat)state->getXOffset();
jobject jstate = env->NewObjectA(fields.stClazz, fields.stConstructId, args);
return jstate;
}
};
} // namespace android
#endif // _ANDROID_MEDIA_VOLUME_SHAPER_H_