Android Audio代码分析20 - queryEffects函数

今天开始看看AudioEffect相关的接口。
这个类,之前有看过。不过当时只是从类的定义出发,了解了一下基本的内容。
这次从测试代码使用的接口出发,逐步撕开AudioEffect的面纱。


*****************************************源码*************************************************
    //Test case 0.0: test queryEffects() and available effects
    @LargeTest
    public void test0_0QueryEffects() throws Exception {


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


        assertTrue("test0_0QueryEffects: number of effects < 4: "+desc.length, (desc.length >= 4));


        boolean hasEQ = false;
        boolean hasBassBoost = false;
        boolean hasVirtualizer = false;
        boolean hasEnvReverb = false;


        for (int i = 0; i < desc.length; i++) {
            if (desc[i].type.equals(AudioEffect.EFFECT_TYPE_EQUALIZER)) {
                hasEQ = true;
            } if (desc[i].type.equals(AudioEffect.EFFECT_TYPE_BASS_BOOST)) {
                hasBassBoost = true;
            } else if (desc[i].type.equals(AudioEffect.EFFECT_TYPE_VIRTUALIZER)) {
                hasVirtualizer = true;
            }
            else if (desc[i].type.equals(AudioEffect.EFFECT_TYPE_ENV_REVERB)) {
                hasEnvReverb = true;
            }
        }
        assertTrue("test0_0QueryEffects: equalizer not found", hasEQ);
        assertTrue("test0_0QueryEffects: bass boost not found", hasBassBoost);
        assertTrue("test0_0QueryEffects: virtualizer not found", hasVirtualizer);
        assertTrue("test0_0QueryEffects: environmental reverb not found", hasEnvReverb);
    }

**********************************************************************************************
源码路径:
frameworks\base\media\tests\mediaframeworktest\src\com\android\mediaframeworktest\functional\MediaAudioEffectTest.java


#######################说明################################
    //Test case 0.0: test queryEffects() and available effects
    @LargeTest
    public void test0_0QueryEffects() throws Exception {


        AudioEffect.Descriptor[] desc = AudioEffect.queryEffects();
// ++++++++++++++++++++++++++++Descriptor++++++++++++++++++++++++++++++++++++
    /**
     * The effect descriptor contains information on a particular effect implemented in the
     * audio framework:<br>
     * <ul>
     *  <li>type: UUID corresponding to the OpenSL ES interface implemented by this effect</li>
     *  <li>uuid: UUID for this particular implementation</li>
     *  <li>connectMode: {@link #EFFECT_INSERT} or {@link #EFFECT_AUXILIARY}</li>
     *  <li>name: human readable effect name</li>
     *  <li>implementor: human readable effect implementor name</li>
     * </ul>
     * The method {@link #queryEffects()} returns an array of Descriptors to facilitate effects
     * enumeration.
     */
    public static class Descriptor {


        public Descriptor() {
        }


        public Descriptor(String type, String uuid, String connectMode,
                String name, String implementor) {
            this.type = UUID.fromString(type);
            this.uuid = UUID.fromString(uuid);
            this.connectMode = connectMode;
            this.name = name;
            this.implementor = implementor;
        }


        /**
         *  Indicates the generic type of the effect (Equalizer, Bass boost ...). The UUID
         *  corresponds to the OpenSL ES Interface ID for this type of effect.
         */
        public UUID type;
        /**
         *  Indicates the particular implementation of the effect in that type. Several effects
         *  can have the same type but this uuid is unique to a given implementation.
         */
// 上次看AudioEffect类的时候,对type和uuid的区别还不是很清楚
// 看了这儿的注释,就比较清楚了
        public UUID uuid;
        /**
         *  Indicates if the effect is of insert category {@link #EFFECT_INSERT} or auxiliary
         *  category {@link #EFFECT_AUXILIARY}. Insert effects (Typically an Equalizer) are applied
         *  to the entire audio source and usually not shared by several sources. Auxiliary effects
         *  (typically a reverberator) are applied to part of the signal (wet) and the effect output
         *  is added to the original signal (dry).
         */
        public String connectMode;
        /**
         * Human readable effect name
         */
        public String name;
        /**
         * Human readable effect implementor name
         */
        public String implementor;
    };
// ----------------------------Descriptor------------------------------------
// ++++++++++++++++++++++++++++queryEffects++++++++++++++++++++++++++++++++++++
    /**
     * Query all effects available on the platform. Returns an array of
     * {@link android.media.audiofx.AudioEffect.Descriptor} objects
     *
     * @throws IllegalStateException
     */


    static public Descriptor[] queryEffects() {
        return (Descriptor[]) native_query_effects();
// +++++++++++++++++++++++++++android_media_AudioEffect_native_queryEffects+++++++++++++++++++++++++++++++++++++
static jobjectArray
android_media_AudioEffect_native_queryEffects(JNIEnv *env, jclass clazz)
{
    effect_descriptor_t desc;
    char str[EFFECT_STRING_LEN_MAX];
    uint32_t numEffects;
    uint32_t i = 0;
    jstring jdescType;
    jstring jdescUuid;
    jstring jdescConnect;
    jstring jdescName;
    jstring jdescImplementor;
    jobject jdesc;


    AudioEffect::queryNumberEffects(&numEffects);
// ++++++++++++++++++++++++++++AudioEffect::queryNumberEffects++++++++++++++++++++++++++++++++++++
status_t AudioEffect::queryNumberEffects(uint32_t *numEffects)
{
    const sp<IAudioFlinger>& af = AudioSystem::get_audio_flinger();
    if (af == 0) return PERMISSION_DENIED;
    return af->queryNumberEffects(numEffects);
// ++++++++++++++++++++++++++++AudioFlinger::queryNumberEffects++++++++++++++++++++++++++++++++++++
status_t AudioFlinger::queryNumberEffects(uint32_t *numEffects)
{
    Mutex::Autolock _l(mLock);
    return EffectQueryNumberEffects(numEffects);
// ++++++++++++++++++++++++++EffectQueryNumberEffects++++++++++++++++++++++++++++++++++++++
// 函数EffectQueryNumberEffects定义的地方比较多。
// 我们使用的应该是下面这个文件中的:
// frameworks\base\media\libeffects\factory\


int EffectQueryNumberEffects(uint32_t *pNumEffects)
{
    int ret = init();
    if (ret < 0) {
        return ret;
    }
    if (pNumEffects == NULL) {
        return -EINVAL;
    }


    pthread_mutex_lock(&gLibLock);
// gNumEffects在函数updateNumEffects中有对其赋值
    *pNumEffects = gNumEffects;
// ++++++++++++++++++++++++++++++updateNumEffects++++++++++++++++++++++++++++++++++
uint32_t updateNumEffects() {
    list_elem_t *e;
    uint32_t cnt = 0;


    resetEffectEnumeration();
// ++++++++++++++++++++++++++++++resetEffectEnumeration++++++++++++++++++++++++++++++++++
void resetEffectEnumeration()
{
    gCurLib = gLibraryList;
    gCurEffect = NULL;
    if (gCurLib) {
        gCurEffect = ((lib_entry_t *)gCurLib->object)->effects;
    }
    gCurEffectIdx = 0;
}
// ------------------------------resetEffectEnumeration----------------------------------


// 函数EffectCreate和函数loadLibrary会向gLibraryList中添加成员
// 函数EffectRelease和函数unloadLibrary会删除gLibraryList中的成员
    e = gLibraryList;
// +++++++++++++++++++++++++++++++EffectCreate+++++++++++++++++++++++++++++++++
int EffectCreate(effect_uuid_t *uuid, int32_t sessionId, int32_t ioId, effect_interface_t *pInterface)
{
    list_elem_t *e = gLibraryList;
    lib_entry_t *l = NULL;
    effect_descriptor_t *d = NULL;
    effect_interface_t itfe;
    effect_entry_t *fx;
    int found = 0;
    int ret;


    if (uuid == NULL || pInterface == NULL) {
        return -EINVAL;
    }


    LOGV("EffectCreate() UUID: %08X-%04X-%04X-%04X-%02X%02X%02X%02X%02X%02X\n",
            uuid->timeLow, uuid->timeMid, uuid->timeHiAndVersion,
            uuid->clockSeq, uuid->node[0], uuid->node[1],uuid->node[2],
            uuid->node[3],uuid->node[4],uuid->node[5]);


    ret = init();
// ++++++++++++++++++++++++++++init++++++++++++++++++++++++++++++++++++
int init() {
    struct dirent *ent;
    DIR *dir = NULL;
    char libpath[PATH_MAX];
    int hdl;


// 避免重复初始化 
    if (gInitDone) {
        return 0;
    }


    pthread_mutex_init(&gLibLock, NULL);


    // load built-in libraries
// const char * const gEffectLibPath = "/system/lib/soundfx"; // path to built-in effect libraries
    dir = opendir(gEffectLibPath);
    if (dir == NULL) {
        return -ENODEV;
    }
    while ((ent = readdir(dir)) != NULL) {
        LOGV("init() reading file %s", ent->d_name);
        if ((strlen(ent->d_name) < 3) ||
            strncmp(ent->d_name, "lib", 3) != 0 ||
            strncmp(ent->d_name + strlen(ent->d_name) - 3, ".so", 3) != 0) {
            continue;
        }
        strcpy(libpath, gEffectLibPath);
        strcat(libpath, "/");
        strcat(libpath, ent->d_name);
// loadlibrary会向gLibraryList中添加成员
// 也就是说,在初始化阶段,会将built-in的effect libraries打开并添加到gLibraryList中
        if (loadLibrary(libpath, &hdl) < 0) {
            LOGW("init() failed to load library %s",libpath);
        }
    }
    closedir(dir);
// 更新effects的个数
    updateNumEffects();
    gInitDone = 1;
    LOGV("init() done");
    return 0;
}
// ----------------------------init------------------------------------


    if (ret < 0) {
        LOGW("EffectCreate() init error: %d", ret);
        return ret;
    }


    pthread_mutex_lock(&gLibLock);


// 根据uuid寻找对应的effect lib
// 若没有找到,则说明对应的effect lib不存在,也就没办法创建effect
// 如找到了,则使用对应的effect lib创建effect
    ret = findEffect(uuid, &l, &d);
    if (ret < 0){
        goto exit;
    }
// ++++++++++++++++++++++++++++++findEffect++++++++++++++++++++++++++++++++++
int findEffect(effect_uuid_t *uuid, lib_entry_t **lib, effect_descriptor_t **desc)
{
    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;
            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----------------------------------


    // create effect in library
// createFx的赋值是中loadLibrary函数中完成:createFx = (effect_CreateEffect_t)dlsym(hdl, "EffectCreate");
    ret = l->createFx(uuid, sessionId, ioId, &itfe);
    if (ret != 0) {
        LOGW("EffectCreate() library %s: could not create fx %s, error %d", l->path, d->name, ret);
        goto exit;
    }
// +++++++++++++++++++++++++++EffectReverb EffectCreate+++++++++++++++++++++++++++++++++++++
我们看看EffectReverb中的EffectCreate函数。


int EffectCreate(effect_uuid_t *uuid,
        int32_t sessionId,
        int32_t ioId,
        effect_interface_t *pInterface) {
    int ret;
    int i;
    reverb_module_t *module;
    const effect_descriptor_t *desc;
    int aux = 0;
    int preset = 0;


    LOGV("EffectLibCreateEffect start");


    if (pInterface == NULL || uuid == NULL) {
        return -EINVAL;
    }


    for (i = 0; gDescriptors[i] != NULL; i++) {
        desc = gDescriptors[i];
        if (memcmp(uuid, &desc->uuid, sizeof(effect_uuid_t))
                == 0) {
            break;
        }
    }


    if (gDescriptors[i] == NULL) {
        return -ENOENT;
    }


    module = malloc(sizeof(reverb_module_t));


    module->itfe = &gReverbInterface;
// ++++++++++++++++++++++++++++++++gReverbInterface++++++++++++++++++++++++++++++++
// effect_interface_t interface implementation for reverb effect
const struct effect_interface_s gReverbInterface = {
        Reverb_Process,
        Reverb_Command
};
// --------------------------------gReverbInterface--------------------------------


    module->context.mState = REVERB_STATE_UNINITIALIZED;


    if (memcmp(&desc->type, SL_IID_PRESETREVERB, sizeof(effect_uuid_t)) == 0) {
        preset = 1;
    }
// ++++++++++++++++++++++++++++++++SL_IID_PRESETREVERB++++++++++++++++++++++++++++++++
#ifndef OPENSL_ES_H_
static const effect_uuid_t SL_IID_PRESETREVERB_ = { 0x47382d60, 0xddd8, 0x11db, 0xbf3a, { 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b } };
const effect_uuid_t * const SL_IID_PRESETREVERB = &SL_IID_PRESETREVERB_;
#endif //OPENSL_ES_H_
// --------------------------------SL_IID_PRESETREVERB--------------------------------
    if ((desc->flags & EFFECT_FLAG_TYPE_MASK) == EFFECT_FLAG_TYPE_AUXILIARY) {
        aux = 1;
    }
    ret = Reverb_Init(module, aux, preset);
    if (ret < 0) {
        LOGW("EffectLibCreateEffect() init failed");
        free(module);
        return ret;
    }
// +++++++++++++++++++++++++++++Reverb_Init+++++++++++++++++++++++++++++++++++
/*----------------------------------------------------------------------------
 * Reverb_Init()
 *----------------------------------------------------------------------------
 * Purpose:
 * Initialize reverb context and apply default parameters
 *
 * Inputs:
 *  pRvbModule    - pointer to reverb effect module
 *  aux           - indicates if the reverb is used as auxiliary (1) or insert (0)
 *  preset        - indicates if the reverb is used in preset (1) or environmental (0) mode
 *
 * Outputs:
 *
 * Side Effects:
 *
 *----------------------------------------------------------------------------
 */


int Reverb_Init(reverb_module_t *pRvbModule, int aux, int preset) {
    int ret;


    LOGV("Reverb_Init module %p, aux: %d, preset: %d", pRvbModule,aux, preset);


    memset(&pRvbModule->context, 0, sizeof(reverb_object_t));


    pRvbModule->context.m_Aux = (uint16_t)aux;
    pRvbModule->context.m_Preset = (uint16_t)preset;


    pRvbModule->config.inputCfg.samplingRate = 44100;
    if (aux) {
        pRvbModule->config.inputCfg.channels = CHANNEL_MONO;
    } else {
        pRvbModule->config.inputCfg.channels = CHANNEL_STEREO;
    }
    pRvbModule->config.inputCfg.format = SAMPLE_FORMAT_PCM_S15;
    pRvbModule->config.inputCfg.bufferProvider.getBuffer = NULL;
    pRvbModule->config.inputCfg.bufferProvider.releaseBuffer = NULL;
    pRvbModule->config.inputCfg.bufferProvider.cookie = NULL;
    pRvbModule->config.inputCfg.accessMode = EFFECT_BUFFER_ACCESS_READ;
    pRvbModule->config.inputCfg.mask = EFFECT_CONFIG_ALL;
    pRvbModule->config.outputCfg.samplingRate = 44100;
    pRvbModule->config.outputCfg.channels = CHANNEL_STEREO;
    pRvbModule->config.outputCfg.format = SAMPLE_FORMAT_PCM_S15;
    pRvbModule->config.outputCfg.bufferProvider.getBuffer = NULL;
    pRvbModule->config.outputCfg.bufferProvider.releaseBuffer = NULL;
    pRvbModule->config.outputCfg.bufferProvider.cookie = NULL;
    pRvbModule->config.outputCfg.accessMode = EFFECT_BUFFER_ACCESS_ACCUMULATE;
    pRvbModule->config.outputCfg.mask = EFFECT_CONFIG_ALL;


    ret = Reverb_Configure(pRvbModule, &pRvbModule->config, true);
    if (ret < 0) {
        LOGV("Reverb_Init error %d on module %p", ret, pRvbModule);
    }
// +++++++++++++++++++++++++Reverb_Configure+++++++++++++++++++++++++++++++++++++++
/*----------------------------------------------------------------------------
 * Reverb_Init()
 *----------------------------------------------------------------------------
 * Purpose:
 *  Set input and output audio configuration.
 *
 * Inputs:
 *  pRvbModule    - pointer to reverb effect module
 *  pConfig       - pointer to effect_config_t structure containing input
 *              and output audio parameters configuration
 *  init          - true if called from init function
 * Outputs:
 *
 * Side Effects:
 *
 *----------------------------------------------------------------------------
 */


int Reverb_Configure(reverb_module_t *pRvbModule, effect_config_t *pConfig,
        bool init) {
    reverb_object_t *pReverb = &pRvbModule->context;
    int bufferSizeInSamples;
    int updatePeriodInSamples;
    int xfadePeriodInSamples;


    // Check configuration compatibility with build options
    if (pConfig->inputCfg.samplingRate
        != pConfig->outputCfg.samplingRate
        || pConfig->outputCfg.channels != OUTPUT_CHANNELS
        || pConfig->inputCfg.format != SAMPLE_FORMAT_PCM_S15
        || pConfig->outputCfg.format != SAMPLE_FORMAT_PCM_S15) {
        LOGV("Reverb_Configure invalid config");
        return -EINVAL;
    }
    if ((pReverb->m_Aux && (pConfig->inputCfg.channels != CHANNEL_MONO)) ||
        (!pReverb->m_Aux && (pConfig->inputCfg.channels != CHANNEL_STEREO))) {
        LOGV("Reverb_Configure invalid config");
        return -EINVAL;
    }


    memcpy(&pRvbModule->config, pConfig, sizeof(effect_config_t));


    pReverb->m_nSamplingRate = pRvbModule->config.outputCfg.samplingRate;


    switch (pReverb->m_nSamplingRate) {
    case 8000:
        pReverb->m_nUpdatePeriodInBits = 5;
        bufferSizeInSamples = 4096;
        pReverb->m_nCosWT_5KHz = -23170;
        break;
    case 16000:
        pReverb->m_nUpdatePeriodInBits = 6;
        bufferSizeInSamples = 8192;
        pReverb->m_nCosWT_5KHz = -12540;
        break;
    case 22050:
        pReverb->m_nUpdatePeriodInBits = 7;
        bufferSizeInSamples = 8192;
        pReverb->m_nCosWT_5KHz = 4768;
        break;
    case 32000:
        pReverb->m_nUpdatePeriodInBits = 7;
        bufferSizeInSamples = 16384;
        pReverb->m_nCosWT_5KHz = 18205;
        break;
    case 44100:
        pReverb->m_nUpdatePeriodInBits = 8;
        bufferSizeInSamples = 16384;
        pReverb->m_nCosWT_5KHz = 24799;
        break;
    case 48000:
        pReverb->m_nUpdatePeriodInBits = 8;
        bufferSizeInSamples = 16384;
        pReverb->m_nCosWT_5KHz = 25997;
        break;
    default:
        LOGV("Reverb_Configure invalid sampling rate %d", pReverb->m_nSamplingRate);
        return -EINVAL;
    }


    // Define a mask for circular addressing, so that array index
    // can wraparound and stay in array boundary of 0, 1, ..., (buffer size -1)
    // The buffer size MUST be a power of two
    pReverb->m_nBufferMask = (int32_t) (bufferSizeInSamples - 1);
    /* reverb parameters are updated every 2^(pReverb->m_nUpdatePeriodInBits) samples */
    updatePeriodInSamples = (int32_t) (0x1L << pReverb->m_nUpdatePeriodInBits);
    /*
     calculate the update counter by bitwise ANDING with this value to
     generate a 2^n modulo value
     */
    pReverb->m_nUpdatePeriodInSamples = (int32_t) updatePeriodInSamples;


    xfadePeriodInSamples = (int32_t) (REVERB_XFADE_PERIOD_IN_SECONDS
            * (double) pReverb->m_nSamplingRate);


    // set xfade parameters
    pReverb->m_nPhaseIncrement
            = (int16_t) (65536 / ((int16_t) xfadePeriodInSamples
                    / (int16_t) updatePeriodInSamples));


    if (init) {
        ReverbReadInPresets(pReverb);
// +++++++++++++++++++++++++++++ReverbReadInPresets+++++++++++++++++++++++++++++++++++
// 函数内容没有太多要介绍的,只列出其注释。


/*----------------------------------------------------------------------------
 * ReverbReadInPresets()
 *----------------------------------------------------------------------------
 * Purpose: sets global reverb preset bank to defaults
 *
 * Inputs:
 *
 * Outputs:
 *
 *----------------------------------------------------------------------------
 */
// -----------------------------ReverbReadInPresets-----------------------------------


        // for debugging purposes, allow noise generator
        pReverb->m_bUseNoise = true;


        // for debugging purposes, allow bypass
        pReverb->m_bBypass = 0;


        pReverb->m_nNextRoom = 1;


        pReverb->m_nNoise = (int16_t) 0xABCD;
    }


    Reverb_Reset(pReverb, init);
// +++++++++++++++++++++++++++++Reverb_Reset+++++++++++++++++++++++++++++++++++
/*----------------------------------------------------------------------------
 * Reverb_Reset()
 *----------------------------------------------------------------------------
 * Purpose:
 *  Reset internal states and clear delay lines.
 *
 * Inputs:
 *  pReverb    - pointer to reverb context
 *  init       - true if called from init function
 *
 * Outputs:
 *
 * Side Effects:
 *
 *----------------------------------------------------------------------------
 */


void Reverb_Reset(reverb_object_t *pReverb, bool init) {
    int bufferSizeInSamples = (int32_t) (pReverb->m_nBufferMask + 1);
    int maxApSamples;
    int maxDelaySamples;
    int maxEarlySamples;
    int ap1In;
    int delay0In;
    int delay1In;
    int32_t i;
    uint16_t nOffset;


    maxApSamples = ((int32_t) (MAX_AP_TIME * pReverb->m_nSamplingRate) >> 16);
    maxDelaySamples = ((int32_t) (MAX_DELAY_TIME * pReverb->m_nSamplingRate)
            >> 16);
    maxEarlySamples = ((int32_t) (MAX_EARLY_TIME * pReverb->m_nSamplingRate)
            >> 16);


    ap1In = (AP0_IN + maxApSamples + GUARD);
    delay0In = (ap1In + maxApSamples + GUARD);
    delay1In = (delay0In + maxDelaySamples + GUARD);
    // Define the max offsets for the end points of each section
    // i.e., we don't expect a given section's taps to go beyond
    // the following limits


    pReverb->m_nEarly0in = (delay1In + maxDelaySamples + GUARD);
    pReverb->m_nEarly1in = (pReverb->m_nEarly0in + maxEarlySamples + GUARD);


    pReverb->m_sAp0.m_zApIn = AP0_IN;


    pReverb->m_zD0In = delay0In;


    pReverb->m_sAp1.m_zApIn = ap1In;


    pReverb->m_zD1In = delay1In;


    pReverb->m_zOutLpfL = 0;
    pReverb->m_zOutLpfR = 0;


    pReverb->m_nRevFbkR = 0;
    pReverb->m_nRevFbkL = 0;


    // set base index into circular buffer
    pReverb->m_nBaseIndex = 0;


    // clear the reverb delay line
    for (i = 0; i < bufferSizeInSamples; i++) {
        pReverb->m_nDelayLine[i] = 0;
    }


    ReverbUpdateRoom(pReverb, init);
// +++++++++++++++++++++++++++++++ReverbUpdateRoom+++++++++++++++++++++++++++++++++
/*----------------------------------------------------------------------------
 * ReverbUpdateRoom
 *----------------------------------------------------------------------------
 * Purpose:
 * Update the room's preset parameters as required
 *
 * Inputs:
 *
 * Outputs:
 *
 *
 * Side Effects:
 * - reverb paramters (fbk, fwd, etc) will be changed
 * - m_nCurrentRoom := m_nNextRoom
 *----------------------------------------------------------------------------
 */
static int ReverbUpdateRoom(reverb_object_t *pReverb, bool fullUpdate) {
    int temp;
    int i;
    int maxSamples;
    int earlyDelay;
    int earlyGain;


    reverb_preset_t *pPreset =
            &pReverb->m_sPreset.m_sPreset[pReverb->m_nNextRoom];


    if (fullUpdate) {
        pReverb->m_nRvbLpfFwd = pPreset->m_nRvbLpfFwd;
        pReverb->m_nRvbLpfFbk = pPreset->m_nRvbLpfFbk;


        pReverb->m_nEarlyGain = pPreset->m_nEarlyGain;
        //stored as time based, convert to sample based
        pReverb->m_nLateGain = pPreset->m_nLateGain;
        pReverb->m_nRoomLpfFbk = pPreset->m_nRoomLpfFbk;
        pReverb->m_nRoomLpfFwd = pPreset->m_nRoomLpfFwd;


        // set the early reflections gains
        earlyGain = pPreset->m_nEarlyGain;
        for (i = 0; i < REVERB_MAX_NUM_REFLECTIONS; i++) {
            pReverb->m_sEarlyL.m_nGain[i]
                    = MULT_EG1_EG1(pPreset->m_sEarlyL.m_nGain[i],earlyGain);
            pReverb->m_sEarlyR.m_nGain[i]
                    = MULT_EG1_EG1(pPreset->m_sEarlyR.m_nGain[i],earlyGain);
        }


        pReverb->m_nMaxExcursion = pPreset->m_nMaxExcursion;


        pReverb->m_sAp0.m_nApGain = pPreset->m_nAp0_ApGain;
        pReverb->m_sAp1.m_nApGain = pPreset->m_nAp1_ApGain;


        // set the early reflections delay
        earlyDelay = ((int) pPreset->m_nEarlyDelay * pReverb->m_nSamplingRate)
                >> 16;
        pReverb->m_nEarlyDelay = earlyDelay;
        maxSamples = (int32_t) (MAX_EARLY_TIME * pReverb->m_nSamplingRate)
                >> 16;
        for (i = 0; i < REVERB_MAX_NUM_REFLECTIONS; i++) {
            //stored as time based, convert to sample based
            temp = earlyDelay + (((int) pPreset->m_sEarlyL.m_zDelay[i]
                    * pReverb->m_nSamplingRate) >> 16);
            if (temp > maxSamples)
                temp = maxSamples;
            pReverb->m_sEarlyL.m_zDelay[i] = pReverb->m_nEarly0in + temp;
            //stored as time based, convert to sample based
            temp = earlyDelay + (((int) pPreset->m_sEarlyR.m_zDelay[i]
                    * pReverb->m_nSamplingRate) >> 16);
            if (temp > maxSamples)
                temp = maxSamples;
            pReverb->m_sEarlyR.m_zDelay[i] = pReverb->m_nEarly1in + temp;
        }


        maxSamples = (int32_t) (MAX_DELAY_TIME * pReverb->m_nSamplingRate)
                >> 16;
        //stored as time based, convert to sample based
        /*lint -e{702} shift for performance */
        temp = (pPreset->m_nLateDelay * pReverb->m_nSamplingRate) >> 16;
        if ((temp + pReverb->m_nMaxExcursion) > maxSamples) {
            temp = maxSamples - pReverb->m_nMaxExcursion;
        }
        temp -= pReverb->m_nLateDelay;
        pReverb->m_nDelay0Out += temp;
        pReverb->m_nDelay1Out += temp;
        pReverb->m_nLateDelay += temp;


        maxSamples = (int32_t) (MAX_AP_TIME * pReverb->m_nSamplingRate) >> 16;
        //stored as time based, convert to absolute sample value
        temp = pPreset->m_nAp0_ApOut;
        /*lint -e{702} shift for performance */
        temp = (temp * pReverb->m_nSamplingRate) >> 16;
        if (temp > maxSamples)
            temp = maxSamples;
        pReverb->m_sAp0.m_zApOut = (uint16_t) (pReverb->m_sAp0.m_zApIn + temp);


        //stored as time based, convert to absolute sample value
        temp = pPreset->m_nAp1_ApOut;
        /*lint -e{702} shift for performance */
        temp = (temp * pReverb->m_nSamplingRate) >> 16;
        if (temp > maxSamples)
            temp = maxSamples;
        pReverb->m_sAp1.m_zApOut = (uint16_t) (pReverb->m_sAp1.m_zApIn + temp);
        //gpsReverbObject->m_sAp1.m_zApOut = pPreset->m_nAp1_ApOut;
    }


    //stored as time based, convert to sample based
    temp = pPreset->m_nXfadeInterval;
    /*lint -e{702} shift for performance */
    temp = (temp * pReverb->m_nSamplingRate) >> 16;
    pReverb->m_nXfadeInterval = (uint16_t) temp;
    //gsReverbObject.m_nXfadeInterval = pPreset->m_nXfadeInterval;
    pReverb->m_nXfadeCounter = pReverb->m_nXfadeInterval + 1; // force update on first iteration


    pReverb->m_nCurrentRoom = pReverb->m_nNextRoom;


    return 0;


} /* end ReverbUpdateRoom */
// -------------------------------ReverbUpdateRoom---------------------------------


    pReverb->m_nUpdateCounter = 0;


    pReverb->m_nPhase = -32768;


    pReverb->m_nSin = 0;
    pReverb->m_nCos = 0;
    pReverb->m_nSinIncrement = 0;
    pReverb->m_nCosIncrement = 0;


    // set delay tap lengths
    nOffset = ReverbCalculateNoise(pReverb);
// ++++++++++++++++++++++++++++ReverbCalculateNoise++++++++++++++++++++++++++++++++++++
/*----------------------------------------------------------------------------
 * ReverbCalculateNoise
 *----------------------------------------------------------------------------
 * Purpose:
 * Calculate a noise sample and limit its value
 *
 * Inputs:
 * nMaxExcursion - noise value is limited to this value
 * pnNoise - return new noise sample in this (not limited)
 *
 * Outputs:
 * new limited noise value
 *
 * Side Effects:
 * - *pnNoise noise value is updated
 *
 *----------------------------------------------------------------------------
 */
static uint16_t ReverbCalculateNoise(reverb_object_t *pReverb) {
    int16_t nNoise = pReverb->m_nNoise;


    // calculate new noise value
    if (pReverb->m_bUseNoise) {
        nNoise = (int16_t) (nNoise * 5 + 1);
    } else {
        nNoise = 0;
    }


    pReverb->m_nNoise = nNoise;
    // return the limited noise value
    return (pReverb->m_nMaxExcursion & nNoise);


} /* end ReverbCalculateNoise */
// ----------------------------ReverbCalculateNoise------------------------------------


    pReverb->m_zD1Cross = pReverb->m_nDelay1Out - pReverb->m_nMaxExcursion
            + nOffset;


    nOffset = ReverbCalculateNoise(pReverb);


    pReverb->m_zD0Cross = pReverb->m_nDelay0Out - pReverb->m_nMaxExcursion
            - nOffset;


    nOffset = ReverbCalculateNoise(pReverb);


    pReverb->m_zD0Self = pReverb->m_nDelay0Out - pReverb->m_nMaxExcursion
            - nOffset;


    nOffset = ReverbCalculateNoise(pReverb);


    pReverb->m_zD1Self = pReverb->m_nDelay1Out - pReverb->m_nMaxExcursion
            + nOffset;
}
// -----------------------------Reverb_Reset-----------------------------------


    return 0;
}
// -------------------------Reverb_Configure---------------------------------------


    return ret;
}
// -----------------------------Reverb_Init-----------------------------------


    *pInterface = (effect_interface_t) module;


    module->context.mState = REVERB_STATE_INITIALIZED;


    LOGV("EffectLibCreateEffect %p ,size %d", module, sizeof(reverb_module_t));


    return 0;
}
// ---------------------------EffectReverb EffectCreate-------------------------------------


    // add entry to effect list
    fx = (effect_entry_t *)malloc(sizeof(effect_entry_t));
    fx->subItfe = itfe;
    fx->itfe = (struct effect_interface_s *)&gInterface;
    fx->lib = l;


    e = (list_elem_t *)malloc(sizeof(list_elem_t));
    e->object = fx;
    e->next = gEffectList;
    gEffectList = e;


    *pInterface = (effect_interface_t)fx;


    LOGV("EffectCreate() created entry %p with sub itfe %p in library %s", *pInterface, itfe, l->path);


exit:
    pthread_mutex_unlock(&gLibLock);
    return ret;
}
// -------------------------------EffectCreate---------------------------------
// ++++++++++++++++++++++++++++++loadLibrary++++++++++++++++++++++++++++++++++
int loadLibrary(const char *libPath, int *handle)
{
    void *hdl;
    effect_QueryNumberEffects_t queryNumFx;
    effect_QueryEffect_t queryFx;
    effect_CreateEffect_t createFx;
    effect_ReleaseEffect_t releaseFx;
    uint32_t numFx;
    uint32_t fx;
    int ret;
    list_elem_t *e, *descHead = NULL;
    lib_entry_t *l;


    if (handle == NULL) {
        return -EINVAL;
    }


    *handle = 0;


    hdl = dlopen(libPath, RTLD_NOW);
    if (hdl == 0) {
        LOGW("could open lib %s", libPath);
        return -ENODEV;
    }


    // Check functions availability
    queryNumFx = (effect_QueryNumberEffects_t)dlsym(hdl, "EffectQueryNumberEffects");
    if (queryNumFx == NULL) {
        LOGW("could not get EffectQueryNumberEffects from lib %s", libPath);
        ret = -ENODEV;
        goto error;
    }
    queryFx = (effect_QueryEffect_t)dlsym(hdl, "EffectQueryEffect");
    if (queryFx == NULL) {
        LOGW("could not get EffectQueryEffect from lib %s", libPath);
        ret = -ENODEV;
        goto error;
    }
    createFx = (effect_CreateEffect_t)dlsym(hdl, "EffectCreate");
    if (createFx == NULL) {
        LOGW("could not get EffectCreate from lib %s", libPath);
        ret = -ENODEV;
        goto error;
    }
    releaseFx = (effect_ReleaseEffect_t)dlsym(hdl, "EffectRelease");
    if (releaseFx == NULL) {
        LOGW("could not get EffectRelease from lib %s", libPath);
        ret = -ENODEV;
        goto error;
    }


    // load effect descriptors
    ret = queryNumFx(&numFx);
    if (ret) {
        goto error;
    }


    for (fx = 0; fx < numFx; fx++) {
        effect_descriptor_t *d = malloc(sizeof(effect_descriptor_t));
        if (d == NULL) {
            ret = -ENOMEM;
            goto error;
        }
        ret = queryFx(fx, d);
        if (ret == 0) {
#if (LOG_NDEBUG==0)
            char s[256];
            dumpEffectDescriptor(d, s, 256);
            LOGV("loadLibrary() read descriptor %p:%s",d, s);
#endif
            if (d->apiVersion != EFFECT_API_VERSION) {
                LOGW("Bad API version %04x on lib %s", d->apiVersion, libPath);
                free(d);
                continue;
            }
            e = malloc(sizeof(list_elem_t));
            if (e == NULL) {
                free(d);
                ret = -ENOMEM;
                goto error;
            }
            e->object = d;
            e->next = descHead;
            descHead = e;
        } else {
            LOGW("Error querying effect # %d on lib %s", fx, libPath);
        }
    }


    pthread_mutex_lock(&gLibLock);


    // add entry for library in gLibraryList
    l = malloc(sizeof(lib_entry_t));
    l->id = ++gNextLibId;
    l->handle = hdl;
    strncpy(l->path, libPath, PATH_MAX);
    l->createFx = createFx;
    l->releaseFx = releaseFx;
    l->effects = descHead;
    pthread_mutex_init(&l->lock, NULL);


    e = malloc(sizeof(list_elem_t));
    e->next = gLibraryList;
    e->object = l;
    gLibraryList = e;
    pthread_mutex_unlock(&gLibLock);
    LOGV("loadLibrary() linked library %p", l);


    *handle = l->id;


    return 0;


error:
    LOGW("loadLibrary() error: %d on lib: %s", ret, libPath);
    while (descHead) {
        free(descHead->object);
        e = descHead->next;
        free(descHead);
        descHead = e;;
    }
    dlclose(hdl);
    return ret;
}


// 我们已经知道,init函数中有调用loadLibrary,另外,EffectLoadLibrary中也有调用loadLibrary函数。
// ++++++++++++++++++++++++++++++EffectLoadLibrary++++++++++++++++++++++++++++++++++
int EffectLoadLibrary(const char *libPath, int *handle)
{
    int ret = init();
    if (ret < 0) {
        return ret;
    }
    if (libPath == NULL) {
        return -EINVAL;
    }


    ret = loadLibrary(libPath, handle);
    updateNumEffects();
    return ret;
}


// 函数AudioFlinger::loadEffectLibrary中调用了函数EffectLoadLibrary
// +++++++++++++++++++++++++++++AudioFlinger::loadEffectLibrary+++++++++++++++++++++++++++++++++++
status_t AudioFlinger::loadEffectLibrary(const char *libPath, int *handle)
{
    // check calling permissions
    if (!settingsAllowed()) {
        return PERMISSION_DENIED;
    }
    // only allow libraries loaded from /system/lib/soundfx for now
    if (strncmp(gEffectLibPath, libPath, strlen(gEffectLibPath)) != 0) {
        return PERMISSION_DENIED;
    }


    Mutex::Autolock _l(mLock);
    return EffectLoadLibrary(libPath, handle);
}
// -----------------------------AudioFlinger::loadEffectLibrary-----------------------------------
// ------------------------------EffectLoadLibrary----------------------------------
// ------------------------------loadLibrary----------------------------------
// +++++++++++++++++++++++++++++EffectRelease+++++++++++++++++++++++++++++++++++
int EffectRelease(effect_interface_t interface)
{
    effect_entry_t *fx;
    list_elem_t *e1;
    list_elem_t *e2;


    int ret = init();
    if (ret < 0) {
        return ret;
    }


    // remove effect from effect list
    pthread_mutex_lock(&gLibLock);
    e1 = gEffectList;
    e2 = NULL;
    while (e1) {
        if (e1->object == interface) {
            if (e2) {
                e2->next = e1->next;
            } else {
                gEffectList = e1->next;
            }
            fx = (effect_entry_t *)e1->object;
            free(e1);
            break;
        }
        e2 = e1;
        e1 = e1->next;
    }
    if (e1 == NULL) {
        ret = -ENOENT;
        goto exit;
    }


    // release effect in library
    if (fx->lib == NULL) {
        LOGW("EffectRelease() fx %p library already unloaded", interface);
    } else {
        pthread_mutex_lock(&fx->lib->lock);
        fx->lib->releaseFx(fx->subItfe);
        pthread_mutex_unlock(&fx->lib->lock);
    }
    free(fx);


exit:
    pthread_mutex_unlock(&gLibLock);
    return ret;
}
// -----------------------------EffectRelease-----------------------------------
// +++++++++++++++++++++++++++++unloadLibrary+++++++++++++++++++++++++++++++++++
int unloadLibrary(int handle)
{
    void *hdl;
    int ret;
    list_elem_t *el1, *el2;
    lib_entry_t *l;
    effect_entry_t *fx;


    pthread_mutex_lock(&gLibLock);
    el1 = gLibraryList;
    el2 = NULL;
    while (el1) {
        l = (lib_entry_t *)el1->object;
        if (handle == l->id) {
            if (el2) {
                el2->next = el1->next;
            } else {
                gLibraryList = el1->next;
            }
            free(el1);
            break;
        }
        el2 = el1;
        el1 = el1->next;
    }
    pthread_mutex_unlock(&gLibLock);
    if (el1 == NULL) {
        return -ENOENT;
    }


    // clear effect descriptor list
    el1 = l->effects;
    while (el1) {
        free(el1->object);
        el2 = el1->next;
        free(el1);
        el1 = el2;
    }


    // disable all effects from this library
    pthread_mutex_lock(&l->lock);


    el1 = gEffectList;
    while (el1) {
        fx = (effect_entry_t *)el1->object;
        if (fx->lib == l) {
            fx->lib = NULL;
        }
        el1 = el1->next;
    }
    pthread_mutex_unlock(&l->lock);


    dlclose(l->handle);
    free(l);
    return 0;
}


// 函数EffectUnloadLibrary调用了unloadLibrary函数。
// +++++++++++++++++++++++++++++EffectUnloadLibrary+++++++++++++++++++++++++++++++++++
int EffectUnloadLibrary(int handle)
{
    int ret = init();
    if (ret < 0) {
        return ret;
    }


    ret = unloadLibrary(handle);
    updateNumEffects();
    return ret;
}


// 函数AudioFlinger::unloadEffectLibrary中有调用了EffectUnloadLibrary函数。
// +++++++++++++++++++++++++++++AudioFlinger::unloadEffectLibrary+++++++++++++++++++++++++++++++++++
status_t AudioFlinger::unloadEffectLibrary(int handle)
{
    // check calling permissions
    if (!settingsAllowed()) {
        return PERMISSION_DENIED;
    }


    Mutex::Autolock _l(mLock);
    return EffectUnloadLibrary(handle);
}
// -----------------------------AudioFlinger::unloadEffectLibrary-----------------------------------
// -----------------------------EffectUnloadLibrary-----------------------------------
// -----------------------------unloadLibrary-----------------------------------
    while (e) {
        lib_entry_t *l = (lib_entry_t *)e->object;
        list_elem_t *efx = l->effects;
        while (efx) {
            cnt++;
            efx = efx->next;
        }
        e = e->next;
    }
    gNumEffects = cnt;
    gCanQueryEffect = 0;
    return cnt;
}
// ------------------------------updateNumEffects----------------------------------
    gCanQueryEffect = 1;
    pthread_mutex_unlock(&gLibLock);
    LOGV("EffectQueryNumberEffects(): %d", *pNumEffects);
    return ret;
}
// --------------------------EffectQueryNumberEffects--------------------------------------
}
// ----------------------------AudioFlinger::queryNumberEffects------------------------------------
}
// ----------------------------AudioEffect::queryNumberEffects------------------------------------
    jobjectArray ret = env->NewObjectArray(numEffects, fields.clazzDesc, NULL);
    if (ret == NULL) {
        return ret;
    }


    LOGV("queryEffects() numEffects: %d", numEffects);


    for (i = 0; i < numEffects; i++) {
        if (AudioEffect::queryEffect(i, &desc) != NO_ERROR) {
            goto queryEffects_failure;
        }
// +++++++++++++++++++++++++++++++AudioEffect::queryEffect+++++++++++++++++++++++++++++++++
status_t AudioEffect::queryEffect(uint32_t index, effect_descriptor_t *descriptor)
{
    const sp<IAudioFlinger>& af = AudioSystem::get_audio_flinger();
    if (af == 0) return PERMISSION_DENIED;
    return af->queryEffect(index, descriptor);
// +++++++++++++++++++++++++++++++AudioFlinger::queryEffect+++++++++++++++++++++++++++++++++
status_t AudioFlinger::queryEffect(uint32_t index, effect_descriptor_t *descriptor)
{
    Mutex::Autolock _l(mLock);
    return EffectQueryEffect(index, descriptor);
// +++++++++++++++++++++++++++++EffectQueryEffect+++++++++++++++++++++++++++++++++++
int EffectQueryEffect(uint32_t index, effect_descriptor_t *pDescriptor)
{
    int ret = init();
    if (ret < 0) {
        return ret;
    }
    if (pDescriptor == NULL ||
        index >= gNumEffects) {
        return -EINVAL;
    }
    if (gCanQueryEffect == 0) {
        return -ENOSYS;
    }


    pthread_mutex_lock(&gLibLock);
    ret = -ENOENT;
    if (index < gCurEffectIdx) {
        resetEffectEnumeration();
// +++++++++++++++++++++++++++++++resetEffectEnumeration+++++++++++++++++++++++++++++++++
void resetEffectEnumeration()
{
    gCurLib = gLibraryList;
    gCurEffect = NULL;
    if (gCurLib) {
        gCurEffect = ((lib_entry_t *)gCurLib->object)->effects;
    }
    gCurEffectIdx = 0;
}
// -------------------------------resetEffectEnumeration---------------------------------
    }
    while (gCurLib) {
        if (gCurEffect) {
            if (index == gCurEffectIdx) {
                memcpy(pDescriptor, gCurEffect->object, sizeof(effect_descriptor_t));
                ret = 0;
                break;
            } else {
                gCurEffect = gCurEffect->next;
                gCurEffectIdx++;
            }
        } else {
            gCurLib = gCurLib->next;
            gCurEffect = ((lib_entry_t *)gCurLib->object)->effects;
        }
    }


#if (LOG_NDEBUG == 0)
    char str[256];
    dumpEffectDescriptor(pDescriptor, str, 256);
    LOGV("EffectQueryEffect() desc:%s", str);
#endif
    pthread_mutex_unlock(&gLibLock);
    return ret;
}
// ------------------------------EffectQueryEffect----------------------------------
}
// -------------------------------AudioFlinger::queryEffect---------------------------------
}
// -------------------------------AudioEffect::queryEffect---------------------------------


        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 queryEffects_failure;
        }


        env->SetObjectArrayElement(ret, i, jdesc);
   }


    return ret;


queryEffects_failure:


    if (ret != NULL) {
        env->DeleteLocalRef(ret);
    }
    return NULL;


}
// ---------------------------android_media_AudioEffect_native_queryEffects-------------------------------------
    }
// ----------------------------queryEffects------------------------------------


        assertTrue("test0_0QueryEffects: number of effects < 4: "+desc.length, (desc.length >= 4));


        boolean hasEQ = false;
        boolean hasBassBoost = false;
        boolean hasVirtualizer = false;
        boolean hasEnvReverb = false;


        for (int i = 0; i < desc.length; i++) {
            if (desc[i].type.equals(AudioEffect.EFFECT_TYPE_EQUALIZER)) {
                hasEQ = true;
            } if (desc[i].type.equals(AudioEffect.EFFECT_TYPE_BASS_BOOST)) {
                hasBassBoost = true;
            } else if (desc[i].type.equals(AudioEffect.EFFECT_TYPE_VIRTUALIZER)) {
                hasVirtualizer = true;
            }
            else if (desc[i].type.equals(AudioEffect.EFFECT_TYPE_ENV_REVERB)) {
                hasEnvReverb = true;
            }
        }
        assertTrue("test0_0QueryEffects: equalizer not found", hasEQ);
        assertTrue("test0_0QueryEffects: bass boost not found", hasBassBoost);
        assertTrue("test0_0QueryEffects: virtualizer not found", hasVirtualizer);
        assertTrue("test0_0QueryEffects: environmental reverb not found", hasEnvReverb);
    }

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


&&&&&&&&&&&&&&&&&&&&&&&总结&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
queryEffects返回effect 列表中所有effect的描述。
通过函数EffectQueryNumberEffects可以取到effect列表中包含的effect的个数。
中EffectFactory的init函数中,会将build-in的effect lib添加到effect列表。
可以通过EffectLoadLibrary加载新的effect lib。
也可以通过EffectUnloadLibrary卸载已加载的effect lib。
可以通过函数EffectCreate创建effect。
也可以通过函数EffectRelease删除effect。
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值