android audio代码分析,Android Audio代码分析21 - 创建AudioEffect对象

今天来看看AudioEffect的构造,以及相关的一些函数。

*****************************************源码*************************************************

//Test case 1.0: test constructor from effect type and get effect ID

@LargeTest

public void test1_0ConstructorFromType() throws Exception {

boolean result = true;

String msg = "test1_0ConstructorFromType()";

AudioEffect.Descriptor[] desc = AudioEffect.queryEffects();

assertTrue(msg+": no effects found", (desc.length != 0));

try {

AudioEffect effect = new AudioEffect(desc[0].type,

AudioEffect.EFFECT_TYPE_NULL,

0,

0);

assertNotNull(msg + ": could not create AudioEffect", effect);

try {

assertTrue(msg +": invalid effect ID", (effect.getId() != 0));

} catch (IllegalStateException e) {

msg = msg.concat(": AudioEffect not initialized");

result = false;

} finally {

effect.release();

}

} catch (IllegalArgumentException e) {

msg = msg.concat(": Effect not found: "+desc[0].name);

result = false;

} catch (UnsupportedOperationException e) {

msg = msg.concat(": Effect library not loaded");

result = false;

}

assertTrue(msg, result);

}

**********************************************************************************************

源码路径:

frameworksbasemediatestsmediaframeworktestsrccomandroidmediaframeworktestfunctionalMediaAudioEffectTest.java

#######################说明################################

//Test case 1.0: test constructor from effect type and get effect ID

@LargeTest

public void test1_0ConstructorFromType() throws Exception {

boolean result = true;

String msg = "test1_0ConstructorFromType()";

AudioEffect.Descriptor[] desc = AudioEffect.queryEffects();

assertTrue(msg+": no effects found", (desc.length != 0));

try {

AudioEffect effect = new AudioEffect(desc[0].type,

AudioEffect.EFFECT_TYPE_NULL,

0,

0);

+++++++++++++++++++++++++++++++AudioEffect+++++++++++++++++++++++++++++++++

/**

* Class constructor.

*

* @param type type of effect engine created. See {@link #EFFECT_TYPE_ENV_REVERB},

*            {@link #EFFECT_TYPE_EQUALIZER} ... Types corresponding to

*            built-in effects are defined by AudioEffect class. Other types

*            can be specified provided they correspond an existing OpenSL

*            ES interface ID and the corresponsing effect is available on

*            the platform. If an unspecified effect type is requested, the

*            constructor with throw the IllegalArgumentException. This

*            parameter can be set to {@link #EFFECT_TYPE_NULL} in which

*            case only the uuid will be used to select the effect.

* @param uuid unique identifier of a particular effect implementation.

*            Must be specified if the caller wants to use a particular

*            implementation of an effect type. This parameter can be set to

*            {@link #EFFECT_TYPE_NULL} in which case only the type will

*            be used to select the effect.

* @param priority the priority level requested by the application for

*            controlling the effect engine. As the same effect engine can

*            be shared by several applications, this parameter indicates

*            how much the requesting application needs control of effect

*            parameters. The normal priority is 0, above normal is a

*            positive number, below normal a negative number.

* @param audioSession system wide unique audio session identifier. If audioSession

*            is not 0, the effect will be attached to the MediaPlayer or

*            AudioTrack in the same audio session. Otherwise, the effect

*            will apply to the output mix.

*

* @throws java.lang.IllegalArgumentException

* @throws java.lang.UnsupportedOperationException

* @throws java.lang.RuntimeException

* @hide

*/

public AudioEffect(UUID type, UUID uuid, int priority, int audioSession)

throws IllegalArgumentException, UnsupportedOperationException,

RuntimeException {

int[] id = new int[1];

Descriptor[] desc = new Descriptor[1];

// native initialization

int initResult = native_setup(new WeakReference(this),

type.toString(), uuid.toString(), priority, audioSession, id,

desc);

+++++++++++++++++++++++++android_media_AudioEffect_native_setup+++++++++++++++++++++++++++++++++++++++

static jint

android_media_AudioEffect_native_setup(JNIEnv *env, jobject thiz, jobject weak_this,

jstring type, jstring uuid, jint priority, jint sessionId, jintArray jId, jobjectArray javadesc)

{

LOGV("android_media_AudioEffect_native_setup");

AudioEffectJniStorage* lpJniStorage = NULL;

int lStatus = AUDIOEFFECT_ERROR_NO_MEMORY;

AudioEffect* lpAudioEffect = NULL;

jint* nId = NULL;

const char *typeStr = NULL;

const char *uuidStr = NULL;

effect_descriptor_t desc;

jobject jdesc;

char str[EFFECT_STRING_LEN_MAX];

jstring jdescType;

jstring jdescUuid;

jstring jdescConnect;

jstring jdescName;

jstring jdescImplementor;

if (type != NULL) {

typeStr = env->GetStringUTFChars(type, NULL);

if (typeStr == NULL) {  // Out of memory

jniThrowException(env, "java/lang/RuntimeException", "Out of memory");

goto setup_failure;

}

}

if (uuid != NULL) {

uuidStr = env->GetStringUTFChars(uuid, NULL);

if (uuidStr == NULL) {  // Out of memory

jniThrowException(env, "java/lang/RuntimeException", "Out of memory");

goto setup_failure;

}

}

// type 和uuid 必须有一个不为空

// 否则,没办法确定用哪个effect lib

if (typeStr == NULL && uuidStr == NULL) {

lStatus = AUDIOEFFECT_ERROR_BAD_VALUE;

goto setup_failure;

}

lpJniStorage = new AudioEffectJniStorage();

if (lpJniStorage == NULL) {

LOGE("setup: Error creating JNI Storage");

goto setup_failure;

}

++++++++++++++++++++++++++++AudioEffectJniStorage++++++++++++++++++++++++++++++++++++

struct effect_callback_cookie {

jclass      audioEffect_class;  // AudioEffect class

jobject     audioEffect_ref;    // AudioEffect object instance

};

class AudioEffectJniStorage {

public:

effect_callback_cookie mCallbackData;

AudioEffectJniStorage() {

}

~AudioEffectJniStorage() {

}

};

----------------------------AudioEffectJniStorage------------------------------------

lpJniStorage->mCallbackData.audioEffect_class = (jclass)env->NewGlobalRef(fields.clazzEffect);

// we use a weak reference so the AudioEffect object can be garbage collected.

lpJniStorage->mCallbackData.audioEffect_ref = env->NewGlobalRef(weak_this);

LOGV("setup: lpJniStorage: %p audioEffect_ref %p audioEffect_class %p, &mCallbackData %p",

lpJniStorage,

lpJniStorage->mCallbackData.audioEffect_ref,

lpJniStorage->mCallbackData.audioEffect_class,

&lpJniStorage->mCallbackData);

if (jId == NULL) {

LOGE("setup: NULL java array for id pointer");

lStatus = AUDIOEFFECT_ERROR_BAD_VALUE;

goto setup_failure;

}

// create the native AudioEffect object

lpAudioEffect = new AudioEffect(typeStr,

uuidStr,

priority,

effectCallback,

&lpJniStorage->mCallbackData,

sessionId,

0);

if (lpAudioEffect == NULL) {

LOGE("Error creating AudioEffect");

goto setup_failure;

}

++++++++++++++++++++++++++++native AudioEffect++++++++++++++++++++++++++++++++++++

AudioEffect::AudioEffect(const char *typeStr,

const char *uuidStr,

int32_t priority,

effect_callback_t cbf,

void* user,

int sessionId,

audio_io_handle_t output

)

: mStatus(NO_INIT)

{

effect_uuid_t type;

effect_uuid_t *pType = NULL;

effect_uuid_t uuid;

effect_uuid_t *pUuid = NULL;

LOGV("Constructor stringn - type: %sn - uuid: %s", typeStr, uuidStr);

if (typeStr != NULL) {

if (stringToGuid(typeStr, &type) == NO_ERROR) {

pType = &type;

}

}

if (uuidStr != NULL) {

if (stringToGuid(uuidStr, &uuid) == NO_ERROR) {

pUuid = &uuid;

}

}

mStatus = set(pType, pUuid, priority, cbf, user, sessionId, output);

+++++++++++++++++++++++++++++AudioEffect::set+++++++++++++++++++++++++++++++++++

status_t AudioEffect::set(const effect_uuid_t *type,

const effect_uuid_t *uuid,

int32_t priority,

effect_callback_t cbf,

void* user,

int sessionId,

audio_io_handle_t output)

{

sp iEffect;

sp cblk;

int enabled;

LOGV("set %p mUserData: %p", this, user);

// 如果effect 对象已经被创建了,返回无效操作

if (mIEffect != 0) {

LOGW("Effect already in use");

return INVALID_OPERATION;

}

const sp& audioFlinger = AudioSystem::get_audio_flinger();

if (audioFlinger == 0) {

LOGE("set(): Could not get audioflinger");

return NO_INIT;

}

// 再一次检查type和uuid

if (type == NULL && uuid == NULL) {

LOGW("Must specify at least type or uuid");

return BAD_VALUE;

}

mPriority = priority;

// cbf其实是函数effectCallback

mCbf = cbf;

+++++++++++++++++++++++++++++effectCallback+++++++++++++++++++++++++++++++++++

static void effectCallback(int event, void* user, void *info) {

effect_param_t *p;

int arg1 = 0;

int arg2 = 0;

jobject obj = NULL;

jbyteArray array = NULL;

jbyte *bytes;

bool param;

size_t size;

effect_callback_cookie *callbackInfo = (effect_callback_cookie *)user;

JNIEnv *env = AndroidRuntime::getJNIEnv();

LOGV("effectCallback: callbackInfo %p, audioEffect_ref %p audioEffect_class %p",

callbackInfo,

callbackInfo->audioEffect_ref,

callbackInfo->audioEffect_class);

if (!user || !env) {

LOGW("effectCallback error user %p, env %p", user, env);

return;

}

switch (event) {

case AudioEffect::EVENT_CONTROL_STATUS_CHANGED:

if (info == 0) {

LOGW("EVENT_CONTROL_STATUS_CHANGED info == NULL");

goto effectCallback_Exit;

}

param = *(bool *)info;

arg1 = (int)param;

LOGV("EVENT_CONTROL_STATUS_CHANGED");

break;

case AudioEffect::EVENT_ENABLE_STATUS_CHANGED:

if (info == 0) {

LOGW("EVENT_ENABLE_STATUS_CHANGED info == NULL");

goto effectCallback_Exit;

}

param = *(bool *)info;

arg1 = (int)param;

LOGV("EVENT_ENABLE_STATUS_CHANGED");

break;

case AudioEffect::EVENT_PARAMETER_CHANGED:

if (info == 0) {

LOGW("EVENT_PARAMETER_CHANGED info == NULL");

goto effectCallback_Exit;

}

p = (effect_param_t *)info;

if (p->psize == 0 || p->vsize == 0) {

goto effectCallback_Exit;

}

// arg1 contains offset of parameter value from start of byte array

arg1 = sizeof(effect_param_t) + ((p->psize - 1) / sizeof(int) + 1) * sizeof(int);

size = arg1 + p->vsize;

array = env->NewByteArray(size);

if (array == NULL) {

LOGE("effectCallback: Couldn't allocate byte array for parameter data");

goto effectCallback_Exit;

}

bytes = env->GetByteArrayElements(array, NULL);

memcpy(bytes, p, size);

env->ReleaseByteArrayElements(array, bytes, 0);

obj = array;

LOGV("EVENT_PARAMETER_CHANGED");

break;

case AudioEffect::EVENT_ERROR:

LOGW("EVENT_ERROR");

break;

}

env->CallStaticVoidMethod(

callbackInfo->audioEffect_class,

fields.midPostNativeEvent,

callbackInfo->audioEffect_ref, event, arg1, arg2, obj);

effectCallback_Exit:

if (array) {

env->DeleteLocalRef(array);

}

if (env->ExceptionCheck()) {

env->ExceptionDescribe();

env->ExceptionClear();

}

}

-----------------------------effectCallback-----------------------------------

// user其实是AudioEffectJniStorage对象中mCallbackData成员的地址

mUserData = user;

mSessionId = sessionId;

memset(&mDescriptor, 0, sizeof(effect_descriptor_t));

memcpy(&mDescriptor.type, EFFECT_UUID_NULL, sizeof(effect_uuid_t));

memcpy(&mDescriptor.uuid, EFFECT_UUID_NULL, sizeof(effect_uuid_t));

if (type != NULL) {

memcpy(&mDescriptor.type, type, sizeof(effect_uuid_t));

}

if (uuid != NULL) {

memcpy(&mDescriptor.uuid, uuid, sizeof(effect_uuid_t));

}

mIEffectClient = new EffectClient(this);

+++++++++++++++++++++++++++++EffectClient+++++++++++++++++++++++++++++++++++

// Implements the IEffectClient interface

class EffectClient : public android::BnEffectClient,  public android::IBinder::DeathRecipient

{

public:

EffectClient(AudioEffect *effect) : mEffect(effect){}

// IEffectClient

virtual void controlStatusChanged(bool controlGranted) {

mEffect->controlStatusChanged(controlGranted);

}

virtual void enableStatusChanged(bool enabled) {

mEffect->enableStatusChanged(enabled);

}

virtual void commandExecuted(uint32_t cmdCode,

uint32_t cmdSize,

void *pCmdData,

uint32_t replySize,

void *pReplyData) {

mEffect->commandExecuted(cmdCode, cmdSize, pCmdData, replySize, pReplyData);

}

// IBinder::DeathRecipient

virtual void binderDied(const wp& who) {mEffect->binderDied();}

private:

AudioEffect *mEffect;

};

-----------------------------EffectClient-----------------------------------

iEffect = audioFlinger->createEffect(getpid(), (effect_descriptor_t *)&mDescriptor,

mIEffectClient, priority, output, mSessionId, &mStatus, &mId, &enabled);

if (iEffect == 0 || (mStatus != NO_ERROR && mStatus != ALREADY_EXISTS)) {

LOGE("set(): AudioFlinger could not create effect, status: %d", mStatus);

return mStatus;

}

++++++++++++++++++++++++++AudioFlinger::createEffect++++++++++++++++++++++++++++++++++++++

sp AudioFlinger::createEffect(pid_t pid,

effect_descriptor_t *pDesc,

const sp& effectClient,

int32_t priority,

int output,

int sessionId,

status_t *status,

int *id,

int *enabled)

{

status_t lStatus = NO_ERROR;

sp handle;

effect_interface_t itfe;

effect_descriptor_t desc;

sp client;

wp wclient;

LOGV("createEffect pid %d, client %p, priority %d, sessionId %d, output %d",

pid, effectClient.get(), priority, sessionId, output);

if (pDesc == NULL) {

lStatus = BAD_VALUE;

goto Exit;

}

// check audio settings permission for global effects

if (sessionId == AudioSystem::SESSION_OUTPUT_MIX && !settingsAllowed()) {

lStatus = PERMISSION_DENIED;

goto Exit;

}

+++++++++++++++++++++++++++++++audio_sessions+++++++++++++++++++++++++++++++++

// special audio session values

enum audio_sessions {

SESSION_OUTPUT_STAGE = -1, // session for effects attached to a particular output stream

// (value must be less than 0)

SESSION_OUTPUT_MIX = 0,    // session for effects applied to output mix. These effects can

// be moved by audio policy manager to another output stream

// (value must be 0)

};

-------------------------------audio_sessions---------------------------------

// Session AudioSystem::SESSION_OUTPUT_STAGE is reserved for output stage effects

// that can only be created by audio policy manager (running in same process)

if (sessionId == AudioSystem::SESSION_OUTPUT_STAGE && getpid() != pid) {

lStatus = PERMISSION_DENIED;

goto Exit;

}

// check recording permission for visualizer

if ((memcmp(&pDesc->type, SL_IID_VISUALIZATION, sizeof(effect_uuid_t)) == 0 ||

memcmp(&pDesc->uuid, &VISUALIZATION_UUID_, sizeof(effect_uuid_t)) == 0) &&

!recordingAllowed()) {

lStatus = PERMISSION_DENIED;

goto Exit;

}

if (output == 0) {

if (sessionId == AudioSystem::SESSION_OUTPUT_STAGE) {

// output must be specified by AudioPolicyManager when using session

// AudioSystem::SESSION_OUTPUT_STAGE

// 如果sessionId为AudioSystem::SESSION_OUTPUT_STAGE,effect必须绑定到特定的output

lStatus = BAD_VALUE;

goto Exit;

} else if (sessionId == AudioSystem::SESSION_OUTPUT_MIX) {

// if the output returned by getOutputForEffect() is removed before we lock the

// mutex below, the call to checkPlaybackThread_l(output) below will detect it

// and we will exit safely

output = AudioSystem::getOutputForEffect(&desc);

++++++++++++++++++++++++++++++AudioSystem::getOutputForEffect++++++++++++++++++++++++++++++++++

audio_io_handle_t AudioSystem::getOutputForEffect(effect_descriptor_t *desc)

{

const sp& aps = AudioSystem::get_audio_policy_service();

if (aps == 0) return PERMISSION_DENIED;

return aps->getOutputForEffect(desc);

+++++++++++++++++++++++++++++AudioPolicyService::getOutputForEffect+++++++++++++++++++++++++++++++++++

audio_io_handle_t AudioPolicyService::getOutputForEffect(effect_descriptor_t *desc)

{

if (mpPolicyManager == NULL) {

return NO_INIT;

}

Mutex::Autolock _l(mLock);

return mpPolicyManager->getOutputForEffect(desc);

++++++++++++++++++++++++++++++AudioPolicyManagerBase::getOutputForEffect++++++++++++++++++++++++++++++++++

audio_io_handle_t AudioPolicyManagerBase::getOutputForEffect(effect_descriptor_t *desc)

{

LOGV("getOutputForEffect()");

// apply simple rule where global effects are attached to the same output as MUSIC streams

// 所有的effect都会被绑定到MUSIT stream上,所以,这儿只是简单的将MUSIC stream对应的output返回

return getOutput(AudioSystem::MUSIC);

}

------------------------------AudioPolicyManagerBase::getOutputForEffect----------------------------------

}

-----------------------------AudioPolicyService::getOutputForEffect-----------------------------------

}

------------------------------AudioSystem::getOutputForEffect----------------------------------

}

}

{

Mutex::Autolock _l(mLock);

if (!EffectIsNullUuid(&pDesc->uuid)) {

// 如果指定了uuid,则根据uuid寻找合适的effect descriptor

// if uuid is specified, request effect descriptor

lStatus = EffectGetDescriptor(&pDesc->uuid, &desc);

if (lStatus < 0) {

LOGW("createEffect() error %d from EffectGetDescriptor", lStatus);

goto Exit;

}

++++++++++++++++++++++++++++++EffectGetDescriptor++++++++++++++++++++++++++++++++++

路径:frameworksbasemedialibeffectsfactoryEffectsFactory.c

int EffectGetDescriptor(effect_uuid_t *uuid, effect_descriptor_t *pDescriptor)

{

lib_entry_t *l = NULL;

effect_descriptor_t *d = NULL;

// init 函数我们已经看过

// 如果init已经被调用过,再次进入init函数并不会有真正的动作

// 也就是说,初始化操作只会进行一次

int ret = init();

if (ret < 0) {

return ret;

}

if (pDescriptor == NULL || uuid == NULL) {

return -EINVAL;

}

pthread_mutex_lock(&gLibLock);

ret = findEffect(uuid, &l, &d);

if (ret == 0) {

memcpy(pDescriptor, d, sizeof(effect_descriptor_t));

}

+++++++++++++++++++++++++++++++findEffect+++++++++++++++++++++++++++++++++

int findEffect(effect_uuid_t *uuid, lib_entry_t **lib, effect_descriptor_t **desc)

{

// gLibraryList是保存所有effect lib的列表

list_elem_t *e = gLibraryList;

lib_entry_t *l = NULL;

effect_descriptor_t *d = NULL;

int found = 0;

int ret = 0;

while (e && !found) {

l = (lib_entry_t *)e->object;

list_elem_t *efx = l->effects;

while (efx) {

d = (effect_descriptor_t *)efx->object;

// 比较是否与给定的uuid一致

if (memcmp(&d->uuid, uuid, sizeof(effect_uuid_t)) == 0) {

found = 1;

break;

}

efx = efx->next;

}

e = e->next;

}

if (!found) {

LOGV("findEffect() effect not found");

ret = -ENOENT;

} else {

LOGV("findEffect() found effect: %s in lib %s", d->name, l->path);

*lib = l;

*desc = d;

}

return ret;

}

-------------------------------findEffect---------------------------------

pthread_mutex_unlock(&gLibLock);

return ret;

}

------------------------------EffectGetDescriptor----------------------------------

} else {

// if uuid is not specified, look for an available implementation

// of the required type in effect factory

// 如果没指定uuid,则寻找一个相同类型的可用的effect lib

if (EffectIsNullUuid(&pDesc->type)) {

LOGW("createEffect() no effect type");

lStatus = BAD_VALUE;

goto Exit;

}

uint32_t numEffects = 0;

effect_descriptor_t d;

bool found = false;

lStatus = EffectQueryNumberEffects(&numEffects);

if (lStatus < 0) {

LOGW("createEffect() error %d from EffectQueryNumberEffects", lStatus);

goto Exit;

}

for (uint32_t i = 0; i < numEffects; i++) {

lStatus = EffectQueryEffect(i, &desc);

if (lStatus < 0) {

LOGW("createEffect() error %d from EffectQueryEffect", lStatus);

continue;

}

if (memcmp(&desc.type, &pDesc->type, sizeof(effect_uuid_t)) == 0) {

// If matching type found save effect descriptor. If the session is

// 0 and the effect is not auxiliary, continue enumeration in case

// an auxiliary version of this effect type is available

// 如果session id为0,即AudioSystem::SESSION_OUTPUT_MIX,

// 若找到的effect为auxiliary,则退出,否则的话就继续查找

// 以防止存在可用的该类型的auxiliary的effect。

// 也就是说,对于AudioSystem::SESSION_OUTPUT_MIX的session id来说,

// 会优先使用auxiliary的effect。

found = true;

memcpy(&d, &desc, sizeof(effect_descriptor_t));

if (sessionId != AudioSystem::SESSION_OUTPUT_MIX ||

(desc.flags & EFFECT_FLAG_TYPE_MASK) == EFFECT_FLAG_TYPE_AUXILIARY) {

break;

}

}

}

if (!found) {

lStatus = BAD_VALUE;

LOGW("createEffect() effect not found");

goto Exit;

}

// For same effect type, chose auxiliary version over insert version if

// connect to output mix (Compliance to OpenSL ES)

// 如果不存在可用的该类型的auxiliary的effect,也只能勉强使用刚才找到的insert的了

if (sessionId == AudioSystem::SESSION_OUTPUT_MIX &&

(d.flags & EFFECT_FLAG_TYPE_MASK) != EFFECT_FLAG_TYPE_AUXILIARY) {

memcpy(&desc, &d, sizeof(effect_descriptor_t));

}

}

// auxiliary的effect只能作用在AudioSystem::SESSION_OUTPUT_MIX的session id上。

// Do not allow auxiliary effects on a session different from 0 (output mix)

if (sessionId != AudioSystem::SESSION_OUTPUT_MIX &&

(desc.flags & EFFECT_FLAG_TYPE_MASK) == EFFECT_FLAG_TYPE_AUXILIARY) {

lStatus = INVALID_OPERATION;

goto Exit;

}

// return effect descriptor

memcpy(pDesc, &desc, sizeof(effect_descriptor_t));

// If output is not specified try to find a matching audio session ID in one of the

// output threads.

// 如果没有指定output,则尝试在output threads中寻找匹配的Session ID。

// If output is 0 here, sessionId is neither SESSION_OUTPUT_STAGE nor SESSION_OUTPUT_MIX

// because of code checking output when entering the function.

// 此时,如果output仍然为0,则说明session ID即不是SESSION_OUTPUT_STAGE,也不是SESSION_OUTPUT_MIX。

// 因为前面我们做过一个检查,如果output为0,若session ID是SESSION_OUTPUT_STAGE,则直接退出;

// 若session ID是SESSION_OUTPUT_MIX,则会调用函数AudioSystem::getOutputForEffect来get一个output

// (从最终的实现可知,函数AudioSystem::getOutputForEffect最终返回的是music stream对应的output)。

if (output == 0) {

// look for the thread where the specified audio session is present

for (size_t i = 0; i < mPlaybackThreads.size(); i++) {

if (mPlaybackThreads.valueAt(i)->hasAudioSession(sessionId) != 0) {

output = mPlaybackThreads.keyAt(i);

break;

++++++++++++++++++++++++++++AudioFlinger::PlaybackThread::hasAudioSession++++++++++++++++++++++++++++++++++++

uint32_t AudioFlinger::PlaybackThread::hasAudioSession(int sessionId)

{

Mutex::Autolock _l(mLock);

uint32_t result = 0;

if (getEffectChain_l(sessionId) != 0) {

result = EFFECT_SESSION;

}

for (size_t i = 0; i < mTracks.size(); ++i) {

sp track = mTracks[i];

// 在创建track的时候,会传入一个session ID

if (sessionId == track->sessionId() &&

!(track->mCblk->flags & CBLK_INVALID_MSK)) {

result |= TRACK_SESSION;

break;

}

}

return result;

}

----------------------------AudioFlinger::PlaybackThread::hasAudioSession------------------------------------

}

}

// If no output thread contains the requested session ID, default to

// first output. The effect chain will be moved to the correct output

// thread when a track with the same session ID is created

// 如果找不到output,默认使用第一个。

// 如果相同session ID的trak被创建了,effect链表会被移动到新的track上。

// effect的移动工作在函数AudioFlinger::createTrack中完成见随后代码

if (output == 0 && mPlaybackThreads.size()) {

output = mPlaybackThreads.keyAt(0);

}

+++++++++++++++++++++++++++++AudioFlinger::createTrack moveEffectChain_l+++++++++++++++++++++++++++++++++++

if (sessionId != NULL && *sessionId != AudioSystem::SESSION_OUTPUT_MIX) {

for (size_t i = 0; i < mPlaybackThreads.size(); i++) {

sp t = mPlaybackThreads.valueAt(i);

if (mPlaybackThreads.keyAt(i) != output) {

// prevent same audio session on different output threads

uint32_t sessions = t->hasAudioSession(*sessionId);

if (sessions & PlaybackThread::TRACK_SESSION) {

lStatus = BAD_VALUE;

goto Exit;

}

// check if an effect with same session ID is waiting for a track to be created

if (sessions & PlaybackThread::EFFECT_SESSION) {

effectThread = t.get();

}

}

}

lSessionId = *sessionId;

} else {

// if no audio session id is provided, create one here

lSessionId = nextUniqueId();

if (sessionId != NULL) {

*sessionId = lSessionId;

}

}

LOGV("createTrack() lSessionId: %d", lSessionId);

track = thread->createTrack_l(client, streamType, sampleRate, format,

channelCount, frameCount, sharedBuffer, lSessionId, &lStatus);

// move effect chain to this output thread if an effect on same session was waiting

// for a track to be created

if (lStatus == NO_ERROR && effectThread != NULL) {

Mutex::Autolock _dl(thread->mLock);

Mutex::Autolock _sl(effectThread->mLock);

moveEffectChain_l(lSessionId, effectThread, thread, true);

+++++++++++++++++++++++++++++AudioFlinger::moveEffectChain_l+++++++++++++++++++++++++++++++++++

// moveEffectChain_l mustbe called with both srcThread and dstThread mLocks held

status_t AudioFlinger::moveEffectChain_l(int session,

AudioFlinger::PlaybackThread *srcThread,

AudioFlinger::PlaybackThread *dstThread,

bool reRegister)

{

LOGV("moveEffectChain_l() session %d from thread %p to thread %p",

session, srcThread, dstThread);

sp chain = srcThread->getEffectChain_l(session);

if (chain == 0) {

LOGW("moveEffectChain_l() effect chain for session %d not on source thread %p",

session, srcThread);

return INVALID_OPERATION;

}

// remove chain first. This is useful only if reconfiguring effect chain on same output thread,

// so that a new chain is created with correct parameters when first effect is added. This is

// otherwise unecessary as removeEffect_l() will remove the chain when last effect is

// removed.

srcThread->removeEffectChain_l(chain);

// transfer all effects one by one so that new effect chain is created on new thread with

// correct buffer sizes and audio parameters and effect engines reconfigured accordingly

int dstOutput = dstThread->id();

sp dstChain;

uint32_t strategy;

sp effect = chain->getEffectFromId_l(0);

while (effect != 0) {

srcThread->removeEffect_l(effect);

dstThread->addEffect_l(effect);

// if the move request is not received from audio policy manager, the effect must be

// re-registered with the new strategy and output

if (dstChain == 0) {

dstChain = effect->chain().promote();

if (dstChain == 0) {

LOGW("moveEffectChain_l() cannot get chain from effect %p", effect.get());

srcThread->addEffect_l(effect);

return NO_INIT;

}

strategy = dstChain->strategy();

}

if (reRegister) {

AudioSystem::unregisterEffect(effect->id());

AudioSystem::registerEffect(&effect->desc(),

dstOutput,

strategy,

session,

effect->id());

++++++++++++++++++++++++++++AudioSystem::registerEffect++++++++++++++++++++++++++++++++++++

status_t AudioSystem::registerEffect(effect_descriptor_t *desc,

audio_io_handle_t output,

uint32_t strategy,

int session,

int id)

{

const sp& aps = AudioSystem::get_audio_policy_service();

if (aps == 0) return PERMISSION_DENIED;

return aps->registerEffect(desc, output, strategy, session, id);

++++++++++++++++++++++++++++AudioPolicyService::registerEffect++++++++++++++++++++++++++++++++++++

status_t AudioPolicyService::registerEffect(effect_descriptor_t *desc,

audio_io_handle_t output,

uint32_t strategy,

int session,

int id)

{

if (mpPolicyManager == NULL) {

return NO_INIT;

}

return mpPolicyManager->registerEffect(desc, output, strategy, session, id);

+++++++++++++++++++++++++++AudioPolicyManagerBase::registerEffect+++++++++++++++++++++++++++++++++++++

status_t AudioPolicyManagerBase::registerEffect(effect_descriptor_t *desc,

audio_io_handle_t output,

uint32_t strategy,

int session,

int id)

{

ssize_t index = mOutputs.indexOfKey(output);

if (index < 0) {

LOGW("registerEffect() unknown output %d", output);

return INVALID_OPERATION;

}

if (mTotalEffectsCpuLoad + desc->cpuLoad > getMaxEffectsCpuLoad()) {

LOGW("registerEffect() CPU Load limit exceeded for Fx %s, CPU %f MIPS",

desc->name, (float)desc->cpuLoad/10);

return INVALID_OPERATION;

}

if (mTotalEffectsMemory + desc->memoryUsage > getMaxEffectsMemory()) {

LOGW("registerEffect() memory limit exceeded for Fx %s, Memory %d KB",

desc->name, desc->memoryUsage);

return INVALID_OPERATION;

}

mTotalEffectsCpuLoad += desc->cpuLoad;

mTotalEffectsMemory += desc->memoryUsage;

LOGV("registerEffect() effect %s, output %d, strategy %d session %d id %d",

desc->name, output, strategy, session, id);

LOGV("registerEffect() CPU %d, memory %d", desc->cpuLoad, desc->memoryUsage);

LOGV("  total CPU %d, total memory %d", mTotalEffectsCpuLoad, mTotalEffectsMemory);

EffectDescriptor *pDesc = new EffectDescriptor();

memcpy (&pDesc->mDesc, desc, sizeof(effect_descriptor_t));

pDesc->mOutput = output;

pDesc->mStrategy = (routing_strategy)strategy;

pDesc->mSession = session;

mEffects.add(id, pDesc);

return NO_ERROR;

}

---------------------------AudioPolicyManagerBase::registerEffect-------------------------------------

}

----------------------------AudioPolicyService::registerEffect------------------------------------

}

----------------------------AudioSystem::registerEffect------------------------------------

}

effect = chain->getEffectFromId_l(0);

}

return NO_ERROR;

}

-----------------------------AudioFlinger::moveEffectChain_l-----------------------------------

}

-----------------------------AudioFlinger::createTrack moveEffectChain_l-----------------------------------

}

LOGV("createEffect() got output %d for effect %s", output, desc.name);

PlaybackThread *thread = checkPlaybackThread_l(output);

if (thread == NULL) {

LOGE("createEffect() unknown output thread");

lStatus = BAD_VALUE;

goto Exit;

}

// TODO: allow attachment of effect to inputs

wclient = mClients.valueFor(pid);

if (wclient != NULL) {

client = wclient.promote();

} else {

client = new Client(this, pid);

mClients.add(pid, client);

}

// create effect on selected output trhead

handle = thread->createEffect_l(client, effectClient, priority, sessionId,

&desc, enabled, &lStatus);

if (handle != 0 && id != NULL) {

*id = handle->id();

}

++++++++++++++++++++++++++++++AudioFlinger::PlaybackThread::createEffect_l++++++++++++++++++++++++++++++++++

// PlaybackThread::createEffect_l() must be called with AudioFlinger::mLock held

sp<:effecthandle> AudioFlinger::PlaybackThread::createEffect_l(

const sp<:client>& client,

const sp& effectClient,

int32_t priority,

int sessionId,

effect_descriptor_t *desc,

int *enabled,

status_t *status

)

{

sp effect;

sp handle;

status_t lStatus;

sp track;

sp chain;

bool chainCreated = false;

bool effectCreated = false;

bool effectRegistered = false;

if (mOutput == 0) {

LOGW("createEffect_l() Audio driver not initialized.");

lStatus = NO_INIT;

goto Exit;

}

// Do not allow auxiliary effect on session other than 0

if ((desc->flags & EFFECT_FLAG_TYPE_MASK) == EFFECT_FLAG_TYPE_AUXILIARY &&

sessionId != AudioSystem::SESSION_OUTPUT_MIX) {

LOGW("createEffect_l() Cannot add auxiliary effect %s to session %d",

desc->name, sessionId);

lStatus = BAD_VALUE;

goto Exit;

}

// Do not allow effects with session ID 0 on direct output or duplicating threads

// TODO: add rule for hw accelerated effects on direct outputs with non PCM format

if (sessionId == AudioSystem::SESSION_OUTPUT_MIX && mType != MIXER) {

LOGW("createEffect_l() Cannot add auxiliary effect %s to session %d",

desc->name, sessionId);

lStatus = BAD_VALUE;

goto Exit;

}

LOGV("createEffect_l() thread %p effect %s on session %d", this, desc->name, sessionId);

{ // scope for mLock

Mutex::Autolock _l(mLock);

// check for existing effect chain with the requested audio session

chain = getEffectChain_l(sessionId);

if (chain == 0) {

// create a new chain for this session

LOGV("createEffect_l() new effect chain for session %d", sessionId);

chain = new EffectChain(this, sessionId);

addEffectChain_l(chain);

+++++++++++++++++++++++++++AudioFlinger::PlaybackThread::addEffectChain_l+++++++++++++++++++++++++++++++++++++

status_t AudioFlinger::PlaybackThread::addEffectChain_l(const sp& chain)

{

int session = chain->sessionId();

int16_t *buffer = mMixBuffer;

bool ownsBuffer = false;

LOGV("addEffectChain_l() %p on thread %p for session %d", chain.get(), this, session);

if (session > 0) {

// Only one effect chain can be present in direct output thread and it uses

// the mix buffer as input

if (mType != DIRECT) {

size_t numSamples = mFrameCount * mChannelCount;

buffer = new int16_t[numSamples];

memset(buffer, 0, numSamples * sizeof(int16_t));

LOGV("addEffectChain_l() creating new input buffer %p session %d", buffer, session);

ownsBuffer = true;

}

// Attach all tracks with same session ID to this chain.

for (size_t i = 0; i < mTracks.size(); ++i) {

sp track = mTracks[i];

if (session == track->sessionId()) {

LOGV("addEffectChain_l() track->setMainBuffer track %p buffer %p", track.get(), buffer);

track->setMainBuffer(buffer);

}

}

// indicate all active tracks in the chain

for (size_t i = 0 ; i < mActiveTracks.size() ; ++i) {

sp track = mActiveTracks[i].promote();

if (track == 0) continue;

if (session == track->sessionId()) {

LOGV("addEffectChain_l() activating track %p on session %d", track.get(), session);

chain->startTrack();

}

}

}

chain->setInBuffer(buffer, ownsBuffer);

chain->setOutBuffer(mMixBuffer);

// Effect chain for session AudioSystem::SESSION_OUTPUT_STAGE is inserted at end of effect

// chains list in order to be processed last as it contains output stage effects

// Effect chain for session AudioSystem::SESSION_OUTPUT_MIX is inserted before

// session AudioSystem::SESSION_OUTPUT_STAGE to be processed

// after track specific effects and before output stage

// It is therefore mandatory that AudioSystem::SESSION_OUTPUT_MIX == 0 and

// that AudioSystem::SESSION_OUTPUT_STAGE < AudioSystem::SESSION_OUTPUT_MIX

// Effect chain for other sessions are inserted at beginning of effect

// chains list to be processed before output mix effects. Relative order between other

// sessions is not important

size_t size = mEffectChains.size();

size_t i = 0;

for (i = 0; i < size; i++) {

if (mEffectChains[i]->sessionId() < session) break;

}

mEffectChains.insertAt(chain, i);

return NO_ERROR;

}

---------------------------AudioFlinger::PlaybackThread::addEffectChain_l-------------------------------------

chain->setStrategy(getStrategyForSession_l(sessionId));

chainCreated = true;

} else {

effect = chain->getEffectFromDesc_l(desc);

}

LOGV("createEffect_l() got effect %p on chain %p", effect == 0 ? 0 : effect.get(), chain.get());

if (effect == 0) {

int id = mAudioFlinger->nextUniqueId();

// Check CPU and memory usage

lStatus = AudioSystem::registerEffect(desc, mId, chain->strategy(), sessionId, id);

if (lStatus != NO_ERROR) {

goto Exit;

}

effectRegistered = true;

// create a new effect module if none present in the chain

effect = new EffectModule(this, chain, desc, id, sessionId);

lStatus = effect->status();

if (lStatus != NO_ERROR) {

goto Exit;

}

lStatus = chain->addEffect_l(effect);

if (lStatus != NO_ERROR) {

goto Exit;

}

effectCreated = true;

++++++++++++++++++++++++++++AudioFlinger::EffectModule::EffectModule++++++++++++++++++++++++++++++++++++

AudioFlinger::EffectModule::EffectModule(const wp& wThread,

const wp<:effectchain>& chain,

effect_descriptor_t *desc,

int id,

int sessionId)

: mThread(wThread), mChain(chain), mId(id), mSessionId(sessionId), mEffectInterface(NULL),

mStatus(NO_INIT), mState(IDLE)

{

LOGV("Constructor %p", this);

int lStatus;

sp thread = mThread.promote();

if (thread == 0) {

return;

}

PlaybackThread *p = (PlaybackThread *)thread.get();

memcpy(&mDescriptor, desc, sizeof(effect_descriptor_t));

// create effect engine from effect factory

// 这个函数在看函数queryEffects代码的时候已经接触过

mStatus = EffectCreate(&desc->uuid, sessionId, p->id(), &mEffectInterface);

if (mStatus != NO_ERROR) {

return;

}

lStatus = init();

if (lStatus < 0) {

mStatus = lStatus;

goto Error;

}

LOGV("Constructor success name %s, Interface %p", mDescriptor.name, mEffectInterface);

return;

Error:

EffectRelease(mEffectInterface);

mEffectInterface = NULL;

LOGV("Constructor Error %d", mStatus);

}

----------------------------AudioFlinger::EffectModule::EffectModule------------------------------------

effect->setDevice(mDevice);

+++++++++++++++++++++++++++AudioFlinger::EffectModule::setDevice+++++++++++++++++++++++++++++++++++++

status_t AudioFlinger::EffectModule::setDevice(uint32_t device)

{

Mutex::Autolock _l(mLock);

status_t status = NO_ERROR;

if ((mDescriptor.flags & EFFECT_FLAG_DEVICE_MASK) == EFFECT_FLAG_DEVICE_IND) {

// convert device bit field from AudioSystem to EffectApi format.

device = deviceAudioSystemToEffectApi(device);

if (device == 0) {

return BAD_VALUE;

}

status_t cmdStatus;

uint32_t size = sizeof(status_t);

// 在EffectModule的构造函数中,调用函数EffectCreate对mEffectInterface进行赋值:

// mStatus = EffectCreate(&desc->uuid, sessionId, p->id(), &mEffectInterface);

// 此处调用的command函数其实是调用的具体的effect lib中的command 函数

// 如EffectReverb 中的Reverb_Command函数。

status = (*mEffectInterface)->command(mEffectInterface,

EFFECT_CMD_SET_DEVICE,

sizeof(uint32_t),

&device,

&size,

&cmdStatus);

if (status == NO_ERROR) {

status = cmdStatus;

}

}

return status;

}

---------------------------AudioFlinger::EffectModule::setDevice-------------------------------------

effect->setMode(mAudioFlinger->getMode());

}

// create effect handle and connect it to effect module

handle = new EffectHandle(effect, client, effectClient, priority);

lStatus = effect->addHandle(handle);

if (enabled) {

*enabled = (int)effect->isEnabled();

}

}

Exit:

if (lStatus != NO_ERROR && lStatus != ALREADY_EXISTS) {

Mutex::Autolock _l(mLock);

if (effectCreated) {

chain->removeEffect_l(effect);

}

if (effectRegistered) {

AudioSystem::unregisterEffect(effect->id());

}

if (chainCreated) {

removeEffectChain_l(chain);

}

handle.clear();

}

if(status) {

*status = lStatus;

}

return handle;

}

------------------------------AudioFlinger::PlaybackThread::createEffect_l----------------------------------

}

Exit:

if(status) {

*status = lStatus;

}

return handle;

}

--------------------------AudioFlinger::createEffect--------------------------------------

mEnabled = (volatile int32_t)enabled;

mIEffect = iEffect;

cblk = iEffect->getCblk();

if (cblk == 0) {

mStatus = NO_INIT;

LOGE("Could not get control block");

return mStatus;

}

mIEffect = iEffect;

mCblkMemory = cblk;

mCblk = static_cast(cblk->pointer());

int bufOffset = ((sizeof(effect_param_cblk_t) - 1) / sizeof(int) + 1) * sizeof(int);

mCblk->buffer = (uint8_t *)mCblk + bufOffset;

iEffect->asBinder()->linkToDeath(mIEffectClient);

LOGV("set() %p OK effect: %s id: %d status %d enabled %d, ", this, mDescriptor.name, mId, mStatus, mEnabled);

return mStatus;

}

-----------------------------AudioEffect::set-----------------------------------

}

----------------------------native AudioEffect------------------------------------

lStatus = translateError(lpAudioEffect->initCheck());

if (lStatus != AUDIOEFFECT_SUCCESS && lStatus != AUDIOEFFECT_ERROR_ALREADY_EXISTS) {

LOGE("AudioEffect initCheck failed %d", lStatus);

goto setup_failure;

}

nId = (jint *) env->GetPrimitiveArrayCritical(jId, NULL);

if (nId == NULL) {

LOGE("setup: Error retrieving id pointer");

lStatus = AUDIOEFFECT_ERROR_BAD_VALUE;

goto setup_failure;

}

nId[0] = lpAudioEffect->id();

env->ReleasePrimitiveArrayCritical(jId, nId, 0);

nId = NULL;

if (typeStr) {

env->ReleaseStringUTFChars(type, typeStr);

typeStr = NULL;

}

if (uuidStr) {

env->ReleaseStringUTFChars(uuid, uuidStr);

uuidStr = NULL;

}

// get the effect descriptor

desc = lpAudioEffect->descriptor();

AudioEffect::guidToString(&desc.type, str, EFFECT_STRING_LEN_MAX);

jdescType = env->NewStringUTF(str);

AudioEffect::guidToString(&desc.uuid, str, EFFECT_STRING_LEN_MAX);

jdescUuid = env->NewStringUTF(str);

if ((desc.flags & EFFECT_FLAG_TYPE_MASK) == EFFECT_FLAG_TYPE_AUXILIARY) {

jdescConnect = env->NewStringUTF("Auxiliary");

} else {

jdescConnect = env->NewStringUTF("Insert");

}

jdescName = env->NewStringUTF(desc.name);

jdescImplementor = env->NewStringUTF(desc.implementor);

jdesc = env->NewObject(fields.clazzDesc,

fields.midDescCstor,

jdescType,

jdescUuid,

jdescConnect,

jdescName,

jdescImplementor);

env->DeleteLocalRef(jdescType);

env->DeleteLocalRef(jdescUuid);

env->DeleteLocalRef(jdescConnect);

env->DeleteLocalRef(jdescName);

env->DeleteLocalRef(jdescImplementor);

if (jdesc == NULL) {

LOGE("env->NewObject(fields.clazzDesc, fields.midDescCstor)");

goto setup_failure;

}

env->SetObjectArrayElement(javadesc, 0, jdesc);

env->SetIntField(thiz, fields.fidNativeAudioEffect, (int)lpAudioEffect);

env->SetIntField(thiz, fields.fidJniData, (int)lpJniStorage);

return AUDIOEFFECT_SUCCESS;

// failures:

setup_failure:

if (nId != NULL) {

env->ReleasePrimitiveArrayCritical(jId, nId, 0);

}

if (lpAudioEffect) {

delete lpAudioEffect;

}

env->SetIntField(thiz, fields.fidNativeAudioEffect, 0);

if (lpJniStorage) {

delete lpJniStorage;

}

env->SetIntField(thiz, fields.fidJniData, 0);

if (uuidStr != NULL) {

env->ReleaseStringUTFChars(uuid, uuidStr);

}

if (typeStr != NULL) {

env->ReleaseStringUTFChars(type, typeStr);

}

return lStatus;

}

-------------------------android_media_AudioEffect_native_setup---------------------------------------

if (initResult != SUCCESS && initResult != ALREADY_EXISTS) {

Log.e(TAG, "Error code " + initResult

+ " when initializing AudioEffect.");

switch (initResult) {

case ERROR_BAD_VALUE:

throw (new IllegalArgumentException("Effect type: " + type

+ " not supported."));

case ERROR_INVALID_OPERATION:

throw (new UnsupportedOperationException(

"Effect library not loaded"));

default:

throw (new RuntimeException(

"Cannot initialize effect engine for type: " + type

+ "Error: " + initResult));

}

}

mId = id[0];

mDescriptor = desc[0];

synchronized (mStateLock) {

mState = STATE_INITIALIZED;

}

}

--------------------------------AudioEffect--------------------------------

assertNotNull(msg + ": could not create AudioEffect", effect);

try {

assertTrue(msg +": invalid effect ID", (effect.getId() != 0));

} catch (IllegalStateException e) {

msg = msg.concat(": AudioEffect not initialized");

result = false;

} finally {

effect.release();

}

} catch (IllegalArgumentException e) {

msg = msg.concat(": Effect not found: "+desc[0].name);

result = false;

} catch (UnsupportedOperationException e) {

msg = msg.concat(": Effect library not loaded");

result = false;

}

assertTrue(msg, result);

}

###########################################################

&&&&&&&&&&&&&&&&&&&&&&&总结&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&

在创建AudioEffect的时候:

1、如果指定了uuid,则会根据uuid 寻找匹配的effect lib来创建effect。

2、若没有指定uuid而指定了type,则会寻找相同type中可用的effect lib。

注意,如果session id为SESSION_OUTPUT_MIX,则优先使用auxiliary的effect。

在指定的type中没有可用的auxiliary的effect的情况下,才会使用insert的effect。

effect lib都被注册到一个列表中。

EffectsFactory中的init函数会将build-in的effect lib添加到该列表。

用户可以调用函数EffectLoadLibrary/EffectUnloadLibrary来注册/删除effect lib。

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&

摘自:江风的专栏

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android 平台上的音频功能适配牵涉到多个系统组件和底层库,需要进行相应的底层代码适配。以下是一些可能涉及的底层代码: 1. Audio HAL:Android 平台上的音频硬件抽象层 (Audio HAL) 是连接操作系统和硬件音频驱动程序的桥梁,需要根据不同的硬件平台进行适配。适配 Audio HAL 可以通过实现 HAL 接口以及硬件相关的 AudioPolicyService 和 AudioFlinger 等系统服务来完成。 2. Audio Codec:Android 平台上支持多种音频编解码器,如 MP3、AAC、WAV、FLAC 等。需要根据不同的编解码器进行相应的底层代码适配,以确保音频的兼容性和播放效果。 3. Audio EffectAndroid 平台上支持多种音频效果处理器,如混响、均衡器、降噪、回声消除等。需要根据不同的效果处理器进行相应的底层代码适配,以确保音频处理的效果和性能。 4. Audio Policy:Android 平台上的音频策略管理器 (Audio Policy) 负责管理音频输入输出设备的选择和配置,需要根据不同的设备进行相应的底层代码适配,以确保音频输入输出的质量和稳定性。 5. Audio Service:Android 平台上的音频服务 (Audio Service) 负责处理音频播放、录制、通话等业务逻辑,需要根据不同的业务需求进行相应的底层代码适配,以确保音频播放的用户体验和交互效果。 总之,Android 平台上的音频功能适配需要涉及多个系统组件和底层库,需要根据不同的业务需求进行相应的底层代码适配。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值