aosp15音频AudioPolicy部分Product Strategies和Volume Groups剖析

Android 11音频服务创建以及播放的流程 这篇文章描述了音频服务的启动流程;启动之后应用播放音频的时候数据流程,从应用层到HAL层的相关内容 阅读详情

背景:

在讲解音频的AudioTrack如何设置了StreamType,就可以顺利选择到Output Thread。这里其实是需要经过很多转换的:
StreamType —> Auttributes —>Strategy —>Devcie —>Ouput Thread.
那么这里就需要剖析Auttributes —>Strategy这块的转换,所以需要深入Auttributes和Strategy的剖析。

寻找一个好的切入点分析这块内容,可以让整个剖析变得更加简单高效,而不是一上来分析源码,可能大家容易晕。在使用dumpsys media.audio_policy时候经常会看到关于Engine的2个重要输出:Product Strategies和Volume Groups相关。
涉及这块的全面视频讲解:
https://mp.weixin.qq.com/s/pQr-HrW0EUoi5QHTMOGEBQ

1、Product Strategies相关

在这里插入图片描述

2、Volume Groups相关

在这里插入图片描述
那么大家是否有考虑这个两个输出具体是怎么来的,还有Product Strategies和Volume Groups二者有什么联系么?二者又是桌面联系到一起的,带着这些疑问下面来进行详细剖析

输出内容代码剖析

一般寻找dump相关源码时候,一般可以采用grep关键字,具体如下:

test@test:~/aosp15/frameworks/av$ grep "Policy Engine dump" ./ -rn
./services/audiopolicy/managerdefault/AudioPolicyManager.cpp:4517:    dst->appendFormat("\nPolicy Engine dump:\n");

找到代码时间上在AudioPolicyManager.cpp:4517。

在这里插入图片描述可以看出这里调用到了EngineBase::dump方法中:

void EngineBase::dump(String8 *dst) const
{
    mProductStrategies.dump(dst, 2);
    dumpProductStrategyDevicesRoleMap(mProductStrategyDeviceRoleMap, dst, 2);
    dumpCapturePresetDevicesRoleMap(dst, 2);
    mVolumeGroups.dump(dst, 2);
}

对于ProductStrategies部分

void ProductStrategyMap::dump(String8 *dst, int spaces) const
{
    dst->appendFormat("%*sProduct Strategies dump:", spaces, "");
    for (const auto &iter : *this) {
        iter.second->dump(dst, spaces + 2);
    }
}

这里的mProductStrategies是ProductStrategyMap

ProductStrategyMap mProductStrategies;

而实际ProductStrategyMap是一个map类型

class ProductStrategyMap : public std::map<product_strategy_t, sp<ProductStrategy> >

key也叫first其实是product_strategy_t,value也叫second其实是ProductStrategy,所以是调用到了ProductStrategy的dump方法

void ProductStrategy::dump(String8 *dst, int spaces) const
{
    dst->appendFormat("\n%*s-%s (id: %d)\n", spaces, "", mName.c_str(), mId);
    std::string deviceLiteral = deviceTypesToString(mApplicableDevices);
    dst->appendFormat("%*sSelected Device: {%s, @:%s}\n", spaces + 2, "",
                       deviceLiteral.c_str(), mDeviceAddress.c_str());

    for (const auto &attr : mAttributesVector) {
        dst->appendFormat("%*sGroup: %d stream: %s\n", spaces + 3, "", attr.getGroupId(),
                          android::toString(attr.getStreamType()).c_str());
        dst->appendFormat("%*s Attributes: ", spaces + 3, "");
        std::string attStr = attr.getAttributes() == defaultAttr ?
                "{ Any }" : android::toString(attr.getAttributes());
        dst->appendFormat("%s\n", attStr.c_str());
    }
}

这可以看到ProductStrategy的dump部分其实主要有3部分:

1、ProductStrategy的 mName

2、ProductStrategy的mApplicableDevices

3、mAttributesVector集合,这个也是最核心部分,代表这个ProductStrategy支持哪些VolumeGroupAttributes。

下面来详细看看ProductStrategy相关结构和dump输出的信息进行对应
在这里插入图片描述那么请问这个ProductStrategy对象是哪里来的呢?
其实是在模拟器上ProductStrategy数据来源其实代码写死的硬编码
代码路径如下:

frameworks/av/services/audiopolicy/engine/common/src/EngineDefaultConfig.h
/**
 * @brief AudioProductStrategies hard coded array of strategies to fill new engine API contract.
 */
const engineConfig::ProductStrategies gOrderedStrategies = {
    {"STRATEGY_PHONE",
     {
         {AUDIO_STREAM_VOICE_CALL, "AUDIO_STREAM_VOICE_CALL",
          {{AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_VOICE_COMMUNICATION, AUDIO_SOURCE_DEFAULT,
            AUDIO_FLAG_NONE, ""}},
         },
         {AUDIO_STREAM_BLUETOOTH_SCO, "AUDIO_STREAM_BLUETOOTH_SCO",
          {{AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_UNKNOWN, AUDIO_SOURCE_DEFAULT, AUDIO_FLAG_SCO,
            ""}},
         }
     },
    },
    {"STRATEGY_SONIFICATION",
     {
         {AUDIO_STREAM_RING, "AUDIO_STREAM_RING",
          {{AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_NOTIFICATION_TELEPHONY_RINGTONE,
            AUDIO_SOURCE_DEFAULT, AUDIO_FLAG_NONE, ""}}
         },
         {AUDIO_STREAM_ALARM, "AUDIO_STREAM_ALARM",
          {{AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_ALARM, AUDIO_SOURCE_DEFAULT,
            AUDIO_FLAG_NONE, ""}},
         }
     },
    },
    {"STRATEGY_ENFORCED_AUDIBLE",
     {
         {AUDIO_STREAM_ENFORCED_AUDIBLE, "AUDIO_STREAM_ENFORCED_AUDIBLE",
          {{AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_UNKNOWN, AUDIO_SOURCE_DEFAULT,
            AUDIO_FLAG_AUDIBILITY_ENFORCED, ""}}
         }
     },
    },
    {"STRATEGY_ACCESSIBILITY",
     {
         {AUDIO_STREAM_ACCESSIBILITY, "AUDIO_STREAM_ACCESSIBILITY",
          {{AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_ASSISTANCE_ACCESSIBILITY,
            AUDIO_SOURCE_DEFAULT, AUDIO_FLAG_NONE, ""}}
         }
     },
    },
    {"STRATEGY_SONIFICATION_RESPECTFUL",
     {
         {AUDIO_STREAM_NOTIFICATION, "AUDIO_STREAM_NOTIFICATION",
          {
              {AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_NOTIFICATION, AUDIO_SOURCE_DEFAULT,
               AUDIO_FLAG_NONE, ""},
              {AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_NOTIFICATION_EVENT,
               AUDIO_SOURCE_DEFAULT, AUDIO_FLAG_NONE, ""}
          }
         }
     },
    },
    {"STRATEGY_MEDIA",
     {
         {AUDIO_STREAM_ASSISTANT, "AUDIO_STREAM_ASSISTANT",
          {{AUDIO_CONTENT_TYPE_SPEECH, AUDIO_USAGE_ASSISTANT,
            AUDIO_SOURCE_DEFAULT, AUDIO_FLAG_NONE, ""}}
         },
         {AUDIO_STREAM_MUSIC, "AUDIO_STREAM_MUSIC",
          {
              {AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_MEDIA, AUDIO_SOURCE_DEFAULT,
               AUDIO_FLAG_NONE, ""},
              {AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_GAME, AUDIO_SOURCE_DEFAULT,
               AUDIO_FLAG_NONE, ""},
              {AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_ASSISTANT, AUDIO_SOURCE_DEFAULT,
               AUDIO_FLAG_NONE, ""},
              {AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,
               AUDIO_SOURCE_DEFAULT, AUDIO_FLAG_NONE, ""},
              {AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_UNKNOWN, AUDIO_SOURCE_DEFAULT,
               AUDIO_FLAG_NONE, ""}
          },
         },
         {AUDIO_STREAM_SYSTEM, "AUDIO_STREAM_SYSTEM",
          {{AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_ASSISTANCE_SONIFICATION,
            AUDIO_SOURCE_DEFAULT, AUDIO_FLAG_NONE, ""}}
         }
     },
    },
    {"STRATEGY_DTMF",
     {
         {AUDIO_STREAM_DTMF, "AUDIO_STREAM_DTMF",
          {
              {AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_VOICE_COMMUNICATION_SIGNALLING,
               AUDIO_SOURCE_DEFAULT, AUDIO_FLAG_NONE, ""}
          }
         }
     },
    },
    {"STRATEGY_CALL_ASSISTANT",
     {
         {AUDIO_STREAM_CALL_ASSISTANT, "AUDIO_STREAM_CALL_ASSISTANT",
          {{AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_CALL_ASSISTANT, AUDIO_SOURCE_DEFAULT,
            AUDIO_FLAG_NONE, ""}}
         }
     },
    },
    {"STRATEGY_TRANSMITTED_THROUGH_SPEAKER",
     {
         {AUDIO_STREAM_TTS, "AUDIO_STREAM_TTS",
          {
              {AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_UNKNOWN, AUDIO_SOURCE_DEFAULT,
                AUDIO_FLAG_BEACON, ""},
              {AUDIO_CONTENT_TYPE_ULTRASOUND, AUDIO_USAGE_UNKNOWN, AUDIO_SOURCE_DEFAULT,
                AUDIO_FLAG_NONE, ""}
          }
         }
     },
    }
};


看到上面这个代码后,大家是不是就感觉和dump输出的基本上可以对上了,但是这个其实和dump输出ProductStrategy还是有差异的。
这里其实是struct ProductStrategy类型,即是一个结构体

struct ProductStrategy {
    std::string name;
    AttributesGroups attributesGroups;
};
using AttributesGroups = std::vector<AttributesGroup>;

struct AttributesGroup {
    audio_stream_type_t stream;
    std::string volumeGroup;
    AttributesVector attributesVect;
};
using AttributesVector = std::vector<audio_attributes_t>;

拿出一小部分进行举例子:

{"STRATEGY_ACCESSIBILITY", //---代表name
     { //---整体代表AttributesGroups
         {AUDIO_STREAM_ACCESSIBILITY, //代表stream类型audio_stream_type_t
         "AUDIO_STREAM_ACCESSIBILITY",//代表volumeGroup类型string
          {
          {AUDIO_CONTENT_TYPE_UNKNOWN, AUDIO_USAGE_ASSISTANCE_ACCESSIBILITY,
            AUDIO_SOURCE_DEFAULT, AUDIO_FLAG_NONE, ""}//代表attributesVect的一个,类型audio_attributes_t
            }
         }
     },
    },

那么这个总结构体gOrderedStrategies到底是如何变成class ProductStrategyMap这样的类集合和class ProductStrategy类对象呢?

这里就需要看如下的源码剖析出他们转换关系:
frameworks/av/services/audiopolicy/engine/common/src/EngineBase.cpp

engineConfig::ParsingResult EngineBase::loadAudioPolicyEngineConfig(const std::string& xmlFilePath)
{
//如果传递xmlFilePath则使用默认路径/vendor/etc/audio_policy_engine_configuration.xml赋值给filePath
    const std::string filePath = xmlFilePath.empty() ? engineConfig::DEFAULT_PATH : xmlFilePath;
    //文件存在则调用engineConfig::parse进行解析,如果不存在则直接赋值空的result
    engineConfig::ParsingResult result =
            fileExists(filePath.c_str()) ?
            engineConfig::parse(filePath.c_str()) : engineConfig::ParsingResult{};
    //如何赋值result为null
    if (result.parsedConfig == nullptr) {
    		 //使用gDefaultEngineConfig赋值给config
        engineConfig::Config config = gDefaultEngineConfig;
        //调用parseLegacyVolumes方法给config.volumeGroups进行赋值
        android::status_t ret = engineConfig::parseLegacyVolumes(config.volumeGroups);
        //利用新创建config构造成新的result
        result = {std::make_unique<engineConfig::Config>(config),
                  static_cast<size_t>(ret == NO_ERROR ? 0 : 1)};
    } else {
  
    }
    //调用processParsingResult来进行真正转换
    return processParsingResult(std::move(result));
}

struct ParsingResult {
    /** Parsed config, nullptr if the xml lib could not load the file */
    std::unique_ptr<Config> parsedConfig;
    size_t nbSkippedElement; //< Number of skipped invalid product strategies
};

上面可以看到最核心的是gDefaultEngineConfig赋值,那么gDefaultEngineConfig和gOrderedStrategies又有啥关系呢?
看下面源码
frameworks/av/services/audiopolicy/engine/config/include/EngineConfig.h

struct Config {
    float version;
    ProductStrategies productStrategies;
    Criteria criteria;
    CriterionTypes criterionTypes;
    VolumeGroups volumeGroups;
};

//明显gOrderedStrategies成为了gDefaultEngineConfig的成员
const engineConfig::Config gDefaultEngineConfig = {
    1.0,
    gOrderedStrategies,
    {},
    {},
    {}
};

那现在回到processParsingResult方法,看看是如何进行转换的


engineConfig::ParsingResult EngineBase::processParsingResult(
        engineConfig::ParsingResult&& rawResult)
{
//下面省略部分非本次分析核心代码
   
    auto addSupportedAttributesToGroup = [](auto &group, auto &volumeGroup, auto &strategy) {
        for (const auto &attr : group.attributesVect) {
           //strategy会关联volumeGroup
            strategy->addAttributes({volumeGroup->getId(), group.stream, attr});
           //volumeGroup会关联到对应的attr
            volumeGroup->addSupportedAttributes(attr);
        }
    };
 
    auto result = std::move(rawResult);
    
   //开始转换上面默认声明的productStrategies结构体,转换成ProductStrategyMap
    for (auto& strategyConfig : result.parsedConfig->productStrategies) {
    //strategyConfig就是struct ProductStrategy对象,利用name构造出ProductStrategy类对象,注意一个是config的struct一个是class,注意区分
        sp<ProductStrategy> strategy = new ProductStrategy(strategyConfig.name);
        //strategyConfig获取他的attributesGroups中的一个个AttributesGroup
        for (const auto &group : strategyConfig.attributesGroups) {
        //这里会用strategy的AttributesGroup与mVolumeGroups的中的进行名字比较,其实就是对stream type名字进行比较
            const auto &iter = std::find_if(begin(mVolumeGroups), end(mVolumeGroups),
                                         [&group](const auto &volumeGroup) {
                    return group.volumeGroup == volumeGroup.second->getName(); });
           
            if (iter == end(mVolumeGroups)) {
     				//没找到
            } else {
            //找打直接赋值
                volumeGroup = iter->second;
            }
            if (group.stream != AUDIO_STREAM_DEFAULT) {
                // A legacy stream can be assigned once to a volume group
        				//把stream type赋值给volumeGroup
                volumeGroup->addSupportedStream(group.stream);
            }
            //这里会把strategy和volumeGroup相互进行关联一些属性
            addSupportedAttributesToGroup(group, volumeGroup, strategy);
        }
        product_strategy_t strategyId = strategy->getId();
        //这里对上面构造strategy对象放到mProductStrategies这个map中
        mProductStrategies[strategyId] = strategy;
    }
    mProductStrategies.initialize();
    return result;
}

上面方法就完成了以下几个任务

1、把原来的struct类型的strategy变成class类型的strategy,并且对象放入到mProductStrategies集合中

2、把strategy相关的attribute数据通过StreamType与volumeGroup进行关联,这样二者就可以用共同的数据attribute数据

那么接下来看看Volume Groups相关数据。

Volume Groups数据部分

同样也是在EngineBase::loadAudioPolicyEngineConfig进行的数据加载

engineConfig::ParsingResult EngineBase::loadAudioPolicyEngineConfig(const std::string& xmlFilePath)
{
   //省略部分和本次分析无关代码
    if (result.parsedConfig == nullptr) {
        engineConfig::Config config = gDefaultEngineConfig;
        //这里调用parseLegacyVolumes方法,参数是config.volumeGroups,这里config.volumeGroups其实空的
        android::status_t ret = engineConfig::parseLegacyVolumes(config.volumeGroups);
        result = {std::make_unique<engineConfig::Config>(config),
                  static_cast<size_t>(ret == NO_ERROR ? 0 : 1)};
    } else {
   
    }
    //和上面分析strategy一样都会执行processParsingResult
    return processParsingResult(std::move(result));
}

这里重点是parseLegacyVolumes方法:

android::status_t parseLegacyVolumes(VolumeGroups &volumeGroups) {
    if (std::string audioPolicyXmlConfigFile = audio_get_audio_policy_config_file();
            !audioPolicyXmlConfigFile.empty()) {
        return parseLegacyVolumeFile(audioPolicyXmlConfigFile.c_str(), volumeGroups);
    } else {
        
        return BAD_VALUE;
    }
}

这里一般audio_get_audio_policy_config_file获取的是/vendor/etc/audio_policy_configuration.xml,在调用parseLegacyVolumeFile方法,主要对这个xml文件进行解析,赋值给VolumeGroups这个参数

android::status_t parseLegacyVolumeFile(const char* path, VolumeGroups &volumeGroups) {
    XmlErrorHandler errorHandler;
    auto doc = make_xmlUnique(xmlParseFile(path));
//最后调用到deserializeLegacyVolumeCollection方法
    return deserializeLegacyVolumeCollection(doc.get(), cur, volumeGroups, nbSkippedElements);
}
static status_t deserializeLegacyVolumeCollection(_xmlDoc *doc, const _xmlNode *cur,
                                                  VolumeGroups &volumeGroups,
                                                  size_t &nbSkippedElement)
{
    //第一部分主要是遍历xml解析成struct类型的legacyVolumeMap集合
    //legacyVolumeMap其实就是一个StreamType对应VolumeCurves,VolumeCurves就是对应多个VolumeCurve,VolumeCurve就是具体device和音量曲线的几个点
    std::map<std::string, VolumeCurves> legacyVolumeMap;
    for (cur = cur->xmlChildrenNode; cur != NULL; cur = cur->next) {
        if (xmlStrcmp(cur->name, (const xmlChar *)legacyVolumecollectionTag)) {
            continue;
        }
        const xmlNode *child = cur->xmlChildrenNode;
        for (; child != NULL; child = child->next) {
            if (!xmlStrcmp(child->name, (const xmlChar *)legacyVolumeTag)) {

                status_t status = deserializeLegacyVolume(doc, child, legacyVolumeMap);
                if (status != NO_ERROR) {
                    nbSkippedElement += 1;
                }
            }
        }
    }
    //接下来就是把上面的legacyVolumeMap转换成VolumeGroups对象,相比原区别就是多了两个最大最小参数
    VolumeGroups tempVolumeGroups = volumeGroups;
    for (const auto &volumeMapIter : legacyVolumeMap) {
        // In order to let AudioService setting the min and max (compatibility), set Min and Max
        // to -1 except for private streams
        audio_stream_type_t streamType;
        if (!StreamTypeConverter::fromString(volumeMapIter.first, streamType)) {
            ALOGE("%s: Invalid stream %s", __func__, volumeMapIter.first.c_str());
            return BAD_VALUE;
        }
        int indexMin = streamType >= AUDIO_STREAM_PUBLIC_CNT ? 0 : -1;
        int indexMax = streamType >= AUDIO_STREAM_PUBLIC_CNT ? 100 : -1;
        tempVolumeGroups.push_back(
                { volumeMapIter.first, indexMin, indexMax, volumeMapIter.second });
    }
    //把tempVolumeGroups赋给volumeGroups这个入参,因为它是引用类型
    std::swap(tempVolumeGroups, volumeGroups);
    return NO_ERROR;
}

这个方法本质就是来进行解析xml的,具体xml是audio_policy_configuration.xml
来看看它的内容:

<audioPolicyConfiguration version="7.0" xmlns:xi="http://www.w3.org/2001/XInclude">
    <!-- version section contains a “version” tag in the form “major.minor” e.g version=”1.0” -->

    <!-- Global configuration Decalaration -->
    <globalConfiguration speaker_drc_enabled="false"/>

    <modules>
        <!-- Primary Audio HAL -->
        <xi:include href="primary_audio_policy_configuration.xml"/>

        <!-- Remote Submix Audio HAL -->
        <xi:include href="r_submix_audio_policy_configuration.xml"/>

        <!-- Bluetooth Audio HAL -->
        <xi:include href="bluetooth_audio_policy_configuration_7_0.xml"/>
    </modules>
    <!-- End of Modules section -->

    <!-- Volume section:
        IMPORTANT NOTE: Volume tables have been moved to engine configuration.
                        Keep it here for legacy.
                        Engine will fallback on these files if none are provided by engine.
     -->

    <xi:include href="audio_policy_volumes.xml"/>
    <xi:include href="default_volume_tables.xml"/>

    <!-- End of Volume section -->

</audioPolicyConfiguration>

这里可以看出实际volume相关是靠include了audio_policy_volumes.xml和default_volume_tables.xml了个文件
audio_policy_volumes.xml内容如下,它主要是顶一个每个StreamType对每个设备使用哪个音量曲线:

<volumes>
    <volume stream="AUDIO_STREAM_VOICE_CALL" deviceCategory="DEVICE_CATEGORY_HEADSET">
        <point>0,-4200</point>
        <point>33,-2800</point>
        <point>66,-1400</point>
        <point>100,0</point>
    </volume>
    <volume stream="AUDIO_STREAM_VOICE_CALL" deviceCategory="DEVICE_CATEGORY_SPEAKER">
        <point>0,-2400</point>
        <point>33,-1600</point>
        <point>66,-800</point>
        <point>100,0</point>
    </volume>
    <volume stream="AUDIO_STREAM_VOICE_CALL" deviceCategory="DEVICE_CATEGORY_EARPIECE">
        <point>0,-2400</point>
        <point>33,-1600</point>
        <point>66,-800</point>
        <point>100,0</point>
    </volume>
    <volume stream="AUDIO_STREAM_VOICE_CALL" deviceCategory="DEVICE_CATEGORY_EXT_MEDIA"
                                             ref="DEFAULT_MEDIA_VOLUME_CURVE"/>
    <volume stream="AUDIO_STREAM_VOICE_CALL" deviceCategory="DEVICE_CATEGORY_HEARING_AID"
                                             ref="DEFAULT_NON_MUTABLE_HEARING_AID_VOLUME_CURVE"/>
    <volume stream="AUDIO_STREAM_SYSTEM" deviceCategory="DEVICE_CATEGORY_HEADSET">
        <point>1,-3000</point>
        <point>33,-2600</point>
        <point>66,-2200</point>
        <point>100,-1800</point>
    </volume>
    <volume stream="AUDIO_STREAM_SYSTEM" deviceCategory="DEVICE_CATEGORY_SPEAKER"
                                         ref="DEFAULT_SYSTEM_VOLUME_CURVE"/>
    <volume stream="AUDIO_STREAM_SYSTEM" deviceCategory="DEVICE_CATEGORY_EARPIECE"
                                         ref="DEFAULT_SYSTEM_VOLUME_CURVE"/>
    <volume stream="AUDIO_STREAM_SYSTEM" deviceCategory="DEVICE_CATEGORY_EXT_MEDIA"
                                         ref="DEFAULT_DEVICE_CATEGORY_EXT_MEDIA_VOLUME_CURVE"/>
    <volume stream="AUDIO_STREAM_SYSTEM" deviceCategory="DEVICE_CATEGORY_HEARING_AID"
                                         ref="DEFAULT_HEARING_AID_VOLUME_CURVE"/>
    <volume stream="AUDIO_STREAM_RING" deviceCategory="DEVICE_CATEGORY_HEADSET"
                                       ref="DEFAULT_DEVICE_CATEGORY_HEADSET_VOLUME_CURVE"/>
    <volume stream="AUDIO_STREAM_RING" deviceCategory="DEVICE_CATEGORY_SPEAKER">
        <point>1,-2970</point>
        <point>33,-2010</point>
        <point>66,-1020</point>
        <point>100,0</point>
    </volume>

在这个xml中,stream就是代表对应的StreamType的字符,后面靠这个字符会与strategy中的attribute数据进行匹配,
deviceCategory就代表具体哪个设备使用该音量曲线,最后的就是具体音量曲线,这里有2中形式,一种是直接写出曲线的几个坐标点point标签:

       <point>1,-2970</point>
        <point>33,-2010</point>
        <point>66,-1020</point>
        <point>100,0</point>

另一种是通过的 ref="DEFAULT_NON_MUTABLE_HEARING_AID_VOLUME_CURVE"这种引用一个曲线名字的方式,那么这个引用的这个曲线名字到底在哪呢?其实就是在default_volume_tables.xml中,这里面会有一个个的曲线名字声明。

default_volume_tables.xml内容如下,它主要是定义一个个的音量曲线

<volumes>
    <reference name="FULL_SCALE_VOLUME_CURVE">
    <!-- Full Scale reference Volume Curve -->
        <point>0,0</point>
        <point>100,0</point>
    </reference>
    <reference name="SILENT_VOLUME_CURVE">
        <point>0,-9600</point>
        <point>100,-9600</point>
    </reference>
    <reference name="DEFAULT_SYSTEM_VOLUME_CURVE">
    <!-- Default System reference Volume Curve -->
        <point>1,-2400</point>
        <point>33,-1800</point>
        <point>66,-1200</point>
        <point>100,-600</point>
    </reference>
    <reference name="DEFAULT_MEDIA_VOLUME_CURVE">
    <!-- Default Media reference Volume Curve -->
        <point>1,-5800</point>
        <point>20,-4000</point>
        <point>60,-1700</point>
        <point>100,0</point>
    </reference>
    <reference name="DEFAULT_DEVICE_CATEGORY_HEADSET_VOLUME_CURVE">
    <!--Default Volume Curve -->
        <point>1,-4950</point>
        <point>33,-3350</point>
        <point>66,-1700</point>
        <point>100,0</point>
    </reference>
    <reference name="DEFAULT_DEVICE_CATEGORY_SPEAKER_VOLUME_CURVE">
    <!-- Default is Speaker Media Volume Curve -->
        <point>1,-5800</point>
        <point>20,-4000</point>
        <point>60,-1700</point>
        <point>100,0</point>
    </reference>
    <reference name="DEFAULT_DEVICE_CATEGORY_EARPIECE_VOLUME_CURVE">
    <!--Default Volume Curve -->
        <point>1,-4950</point>
        <point>33,-3350</point>
        <point>66,-1700</point>
        <point>100,0</point>
    </reference>

上面这些xml解析完成后就会构成对应的VolumeGroups的一个一个个VolumeGroup。

xml变成struct VolumeGroup部分总结图如下:
在这里插入图片描述

其实和Strategy一样,VolumeGroup也需要从struct结构变成class对象,具体是在loadAudioPolicyEngineConfig的processParsingResult方法中,又回到和Strategy分析一样了

engineConfig::ParsingResult EngineBase::loadAudioPolicyEngineConfig(const std::string& xmlFilePath)
{
   //省略
    return processParsingResult(std::move(result));
}

engineConfig::ParsingResult EngineBase::processParsingResult(
        engineConfig::ParsingResult&& rawResult)
{
//真正struct的VolumeGroup变成class的VolumeGroup
    auto loadVolumeConfig = [](auto &volumeGroups, auto &volumeConfig) {
        // Ensure volume group name uniqueness.
      //基于struct的VolumeGroup属性作为来源构造class的VolumeGroup
        sp<VolumeGroup> volumeGroup = new VolumeGroup(volumeConfig.name, volumeConfig.indexMin,
                                                      volumeConfig.indexMax);
        volumeGroups[volumeGroup->getId()] = volumeGroup;

        for (auto &configCurve : volumeConfig.volumeCurves) {
            device_category deviceCat = DEVICE_CATEGORY_SPEAKER;
            if (!DeviceCategoryConverter::fromString(configCurve.deviceCategory, deviceCat)) {
                ALOGE("%s: Invalid %s", __FUNCTION__, configCurve.deviceCategory.c_str());
                continue;
            }
            if (!com::android::media::audio::alarm_min_volume_zero()) {
                /*
                 * This special handling is done because the audio_policy_volumes.xml
                 * is updated but if the flag is disabled, the min alarm volume can
                 * still be zero because the audio_policy_volumes.xml is the source of
                 * truth. So the index value is modified here to match the flag setting
                 */
                if (volumeConfig.name.compare(audio_stream_type_to_string(AUDIO_STREAM_ALARM)) == 0
                      && deviceCat == DEVICE_CATEGORY_SPEAKER) {
                    for (auto &point : configCurve.curvePoints) {
                        if (point.index == 1) {
                            ALOGD("Mute alarm disabled: Found point with index 1. setting it to 0");
                            point.index = 0;
                            break;
                        }
                    }
                }
            }
            sp<VolumeCurve> curve = new VolumeCurve(deviceCat);
            for (auto &point : configCurve.curvePoints) {
                curve->add({point.index, point.attenuationInMb});
            }
            volumeGroup->add(curve);
        }
        return volumeGroup;
    };
    
//把VolumeGroup和Strategy对象进行相互关联
    auto addSupportedAttributesToGroup = [](auto &group, auto &volumeGroup, auto &strategy) {
        for (const auto &attr : group.attributesVect) {
            strategy->addAttributes({volumeGroup->getId(), group.stream, attr});
            volumeGroup->addSupportedAttributes(attr);
        }
    };
    auto checkStreamForGroups = [](auto streamType, const auto &volumeGroups) {
        const auto &iter = std::find_if(std::begin(volumeGroups), std::end(volumeGroups),
                                     [&streamType](const auto &volumeGroup) {
            const auto& streams = volumeGroup.second->getStreamTypes();
            return std::find(std::begin(streams), std::end(streams), streamType) !=
                    std::end(streams);
        });
        return iter != end(volumeGroups);
    };

    for (auto &volumeConfig : result.parsedConfig->volumeGroups) {
        // save default volume config for streams not defined in configuration
        if (volumeConfig.name.compare(audio_stream_type_to_string(AUDIO_STREAM_MUSIC)) == 0) {
            defaultVolumeConfig = volumeConfig;
        }
        if (volumeConfig.name.compare(audio_stream_type_to_string(AUDIO_STREAM_PATCH)) == 0) {
            defaultSystemVolumeConfig = volumeConfig;
        }
        //调用上面的loadVolumeConfig实现转换,VolumeGroup都放到mVolumeGroups集合中
        loadVolumeConfig(mVolumeGroups, volumeConfig);
    }
    for (auto& strategyConfig : result.parsedConfig->productStrategies) {
        sp<ProductStrategy> strategy = new ProductStrategy(strategyConfig.name);
        //遍历么个Strategy的attributeGroup
        for (const auto &group : strategyConfig.attributesGroups) {
          //mVolumeGroups寻找到StreamType和attributesGroup一样的
            const auto &iter = std::find_if(begin(mVolumeGroups), end(mVolumeGroups),
                                         [&group](const auto &volumeGroup) {
                                 //主要靠StreamType这个字符名字比较
                    return group.volumeGroup == volumeGroup.second->getName(); });
            sp<VolumeGroup> volumeGroup = nullptr;
            if (iter == end(mVolumeGroups)) {
     
            } else {
            //获取volumeGroups集合中StreamType和当前attributeGroup一样的VolumeGroup
                volumeGroup = iter->second;
            }
            if (group.stream != AUDIO_STREAM_DEFAULT) {
                // A legacy stream can be assigned once to a volume group
                LOG_ALWAYS_FATAL_IF(checkStreamForGroups(group.stream, mVolumeGroups),
                                    "stream %s already assigned to a volume group, "
                                    "review the configuration", toString(group.stream).c_str());
                volumeGroup->addSupportedStream(group.stream);
            }
            //这里就实现了VolumeGroup和Strategy的attributeGroup相互连接持有
            addSupportedAttributesToGroup(group, volumeGroup, strategy);
        }
        product_strategy_t strategyId = strategy->getId();
        mProductStrategies[strategyId] = strategy;
        ALOGE("processParsingResult %d",(int)strategyId);
    }
    mProductStrategies.initialize();
    return result;
}

对应的class的VolumeGroup构造方法

frameworks/av/services/audiopolicy/engine/common/src/VolumeGroup.cpp

//注意这里getNextHandle还会生成对应的id
VolumeGroup::VolumeGroup(const std::string &name, int indexMin, int indexMax) :
    mName(name), mId(static_cast<volume_group_t>(HandleGenerator<uint32_t>::getNextHandle())),
    mGroupVolumeCurves(VolumeCurves(indexMin, indexMax))
{
}

Strategy和VolumeGroup之间相关连接关键,前面也有讲解就是如下核心方法

auto addSupportedAttributesToGroup = [](auto &group, auto &volumeGroup, auto &strategy) {
    for (const auto &attr : group.attributesVect) {
       //strategy会关联volumeGroup
        strategy->addAttributes({volumeGroup->getId(), group.stream, attr});
       //volumeGroup会关联到对应的attr
        volumeGroup->addSupportedAttributes(attr);
    }
};

更多framework实战干货内容,请关注下面“千里马学框架”

开源 AHB不对称半桥反激电路设计(六)死区时间,谐振腔电流,谐振电感函数表达式 电流跌落可以使副边电流峰值和有效值降低,减小损耗,但如果开关管 S2 导通期间,谐振电流谐等于励磁电流,即副边电流A 点降至 0,会在原边变压器内形成环流,产生损耗,同时也不利于副边同步整流管驱动的正常工作,为了保证开关管 S2 导通期。在开关管 S1 和开关管 S2 均关断的死区时间内,谐振电流给原边开关管结电容和副边整流管结电容充放电,因此会引起开关 S1 关断瞬间电流跌落与开关 S2 关断瞬间电流震荡现象,受寄生参数影响原边谐振电流波形和副边电流波形如图。降到最低,变压器以最高效率向副边传递能量。 阅读详情

相关推荐

Imatest-Dot Pattern

本文介绍了DotPattern模块测试方法,用于评估相机拍摄点阵图的表现。测试主要关注两点:色彩准确性(是否存在彩色噪点和杂边)和摩尔纹现象。测试步骤包括导入图片、选择分析区域、微调ROI、选择点状图类型和分析保存结果。测试结果包含横向色差、光学畸变和SMIATV畸变等数据,其中0.115%的色差值和0.004度的网格旋转角度表明相机表现良好,但-10.4%的SMIATV畸变显示存在明显桶形畸变。文章还提供了各项指标的具体解读标准。

weixin_64706696的博客 1351

Audio解析strategy配置文件

Audio解析strategy配置文件 简介 audio_policy_configuration配置文件配置了音频Audio的设备、数据流信息,而strategy相关配置文件则配置某种streamType的音频使用哪种音量曲线,如voice_call和music他们的音量曲线是不一样,后者这种关系叫做strategy策略. 默认解析路径在/vendor/etc/audio_policy_engine_configuration.xml下,如果找不到可以去: char* POLICY_USAGE_LIBRA

jackzhouyu的专栏 5608

Vector CANoe VT2516A实战:从硬件连接到CAPL脚本的完整配置指南

本文详细介绍了Vector CANoe VT2516A板卡从硬件连接到CAPL脚本编写的完整配置指南。涵盖硬件接线原则、VT System配置避坑技巧、系统变量创建规范、可视化Panel设计、CAPL脚本调试及典型故障排查案例,帮助工程师快速掌握VT2516A板卡的应用实践,提升汽车电子测试效率。

weixin_30608503的博客 255

Android Audio代码分析(6): AudioTrack::getMinBufferSize

http://thinks.me/2016/03/17/audiotrack_create/

u011279649的专栏 1698

CTFshow-pwn入门-前置基础pwn32-pwn34

CTFshow-pwn入门-前置基础pwn32-pwn34

你们de4月天的博客 1132

Cadence PSpice 仿真11:电感型低通滤波器最坏情况分析仿真实战图文教程

在进行蒙特卡罗分析时会将元器件的容差考虑进去,然后通过运行多次的随机仿真,得到电路输出的一个范围。但是多数情况下设计师关注的只是电路的最坏情况是否满足设计要求。PSpice的最坏情况分析是取在容差的边界点元件参数进行仿真以计算出电路输出的最坏情况。所以如果如果电路中有10个元件的话,需要先取10个元件的标准值仿真1次,然后再分别在10个元件的容差边界处各仿真1次,然后再对仿真结果进行统计和计算,再最坏情况下再仿真1次。

fydar的博客 1944

【Android Audio】5、EngineBase加载音量曲线和策略

5、音量曲线 【基于Android Q 】5.1、音量曲线加载code5.2、音量转换 5.1、音量曲线加载code 路径:frameworks/av/services/audiopolicy/managerdefault/engine/common/src/EngineBase.cpp engineConfig::ParsingResult EngineBase::loadAudioPolicyEngineConfig(){ ... auto loadVolumeGroups = [](auto

... 3376

11、机器学习中的聚类与隐马尔可夫模型

本文介绍了机器学习中的两种重要方法:聚类算法和隐马尔可夫模型(HMM)。聚类作为无监督学习的代表,适用于发现无标签数据中的结构,常见算法包括K-均值和自组织映射(SOM),并在传感器数据分析等场景中展现应用价值。隐马尔可夫模型则是一种可解释的序列模型,用于从观测数据中推断隐藏状态,广泛应用于语音识别、自然语言处理和生物信息学等领域。文章详细讲解了HMM的核心概念如马尔可夫性质、转移与发射概率,并通过TensorFlow实现了前向算法,展示了其计算观测序列概率的动态规划思想。最后对比了两类方法的应用场景,展望

garlic的博客 179

Audio播放音频 --- 建立播放通道

Audio播放音频 — 建立播放通道 简介 虽然文章标题是《建立播放通道》,其实播放通道早在AudioPolicyManager解析configuration配置文件时,openoutput业务逻辑就已经把输出通道打开并建立好了,而播放音频流程就是根据音频属性Attribute来决定使用哪个输出通道output而已,但是这个流程业务相对openoutput更加复杂,也涉及更多的音频专业知识;并且播放音频不只是选择输出通道,还涉及往这个输出通道灌音频数据,传送到设备去播放;本篇文章只涉及输出通道的选择,音频

jackzhouyu的专栏 1万+

43、FMCW雷达高度计仿真

本文介绍了FMCW雷达高度计的波形级建模与仿真过程,重点模拟了着陆和倾斜两种飞行场景以评估其性能。通过结合高程数据和飞行轨迹构建雷达场景,使用自定义信号处理器分析IQ样本并估计高度。结果表明,该高度计在多数情况下满足性能要求,但也存在误差较大的点,提出了地面检测算法优化的潜在方向。

neovim7hacker的博客 135

解决Office 365 SMTP问题:C#邮件发送详解

本文介绍了如何在C#中使用Office 365 SMTP服务发送邮件。重点讲解了正确配置SMTP客户端的关键参数:必须使用587端口并启用TLS加密(EnableSsl=true),同时通过NetworkCredential进行身份验证。虽然SmtpClient已不推荐用于新项目,但本文提供的代码示例仍适用于临时项目或系统维护,并建议未来可考虑使用MailKit等现代邮件库。文章还包含了错误处理机制,确保程序健壮性。

2501_90323865的博客 141

设计模式胡咧咧之策略工厂实现导入导出

定义了一组算法,将每个算法都封装起来,并且使它们之间可以互换。

fanzhe的博客 636

【S32K3 MCAL配置】-3.4-CANFD配置-接收特定的“经典CAN/CANFD标准帧”和“经典CAN/CANFD扩展帧”(基于MCAL+FreeRTOS)

【S32K3 MCAL配置】-3.4-CANFD配置-接收特定的“经典CAN/CANFD标准帧”和“经典CAN/CANFD扩展帧”(基于MCAL+FreeRTOS)

「汽车电子助手」的博客 696

Android Visualizer 实战:把声音画出来

是系统提供的音频效果(AudioEffect)之一。它挂在音频链路上,,把数据回吐给你——画波形、画频谱、做响度表。

Android的专栏 250

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

Android 开发入门实践(十三):用 Intent、UiState 和 Reducer 理解 MVI 的状态更新

本文记录第141—145课对MVI架构的入门实践,通过“返回列表”功能逐步构建意图(NewsIntent)、状态(NewsUiState)与状态转换(reduce)机制。基于单向数据流,用户操作生成意图,ViewModel通过Reducer根据旧状态计算新状态,并通过StateFlow更新Compose界面。代码实现清晰分离关注点,强化了对状态管理、事件驱动与数据流的理解。

Easyhood的博客 327

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

Android Message源码阅读

本文梳理了Android Handler机制中Message的核心设计:链表结构、消息复用池(sPool)及回收机制,重点解析了异步消息与同步屏障(SyncBarrier)的协同机制。当主线程需优先处理UI绘制等高优先级任务时,系统通过postSyncBarrier插入屏障,使Looper跳过普通同步消息,优先执行标记为异步的UI渲染任务,确保帧率流畅。该机制是保障Android界面响应性的关键技术之一。

u012116089的博客 201

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

BUUCTF-菜刀流量-MISC-学习笔记

从 1970-01-01 00:00:00 UTC 起算的秒数,——Wireshark 直接告诉你它回答的是谁;由表可知道,96帧返回了flag.rar。, 代表删除键,先删了4次,又删了1次。所以密码是:SuperMaster。解析128帧的响应,即133帧。所以这是一个标准抓包文件,用。解压压缩包拿到html代码。:选中 128 帧,展开。:128 和 131 的。能把一问一答放在一起看;是八进制(Oct)的数字。(或 TCP 层)看到。对比一下ASCii 码表。

晓梦林的博客 38

Android Laya 与 Native Bridge:页面识别、双向通信、协议设计和崩溃定位

Laya 与 Native 通信的难点不在于某个或的写法,而在于边界:页面来源是否可信、协议是否明确、线程是否正确、请求是否幂等、页面是否仍然存在、日志是否能关联一次完整请求。建立这些边界后,才能把“页面白屏”“按钮无响应”“重复消息”和“Native 崩溃”拆成可验证的层次,并沿着真实调用链找到根因。

路在脚下,幸福在手中,莫贪!!!!! 133

安卓部署Linux性能实测:一颗移动处理器,能否胜任日常 Linux 开发?

本文通过在一加Ace5上部署NimoteCode的ARM64 Linux环境,测试了其在CoreMark、SQLite编译及并行构建中的表现。结果显示,尽管单核性能约2.34GHz,多核扩展比达6.15×,但优化编译耗时仍高达35秒,反映移动处理器虽具备一定开发能力,却受限于文件系统开销与运行环境(如PRoot)。实际开发中,非优化构建效率较高,而并行编译可显著提速。结论:安卓手机可作为轻量级开发辅助工具,但无法替代桌面环境,真正价值在于便携性与即时响应,而非全功能替代。

一个不知名码农的专栏 324

Android 开发入门实践(九):把新闻数据与页面状态交给 ViewModel

本文记录第110—117课学习成果:将页面标题、新闻列表及选中编号逐步迁移至NewsViewModel,通过ViewModelProvider获取实例并读取数据。页面调用ViewModel方法处理选择与返回,实现状态管理解耦。过程中纠正了对Activity重建机制的误解,并厘清mutableStateOf的可观察性与ViewModel对象生命周期的区别,深化了对Compose状态更新机制的理解。

Easyhood的博客 221

CTF 命令执行题学习笔记:终端特殊字符 $IFS 与 %09 的用法(HelloCTF RCE-labs)

空格被过滤时:用%09(TAB)或$IFS代替空格关键词被过滤时:用通配符?或字符类[f]拼出被禁的词,让 WAF 在原始字符串里看不到它,shell 在执行时再展开这类技巧在后续的命令执行题里会反复用到,建议亲手在靶机上敲一遍感受一下。

YY888888_的博客 542

Android 17 全栈性能分析关键字与源码手册

FrameTimeline 找异常帧,ATrace 找阶段,thread_state 找等待类型,ftrace 找内核原因,Simpleperf 找 CPU 热点,驱动源码找最终触发条件;任何根因结论都必须回到同一对象、同一时间窗口和同一条依赖链。

weixin_43228946的博客 374

云手机私有化部署完整方案:基于ARM服务器搭建私有安卓云集群

ARM 服务器是私有化安卓云集群最优硬件底座,原生架构匹配 Android,高密度、低损耗,非常适合企业内网场景。整套私有化方案的核心,从来不是 “跑起来安卓”,而是隔离可控、可审计、可运维、可扩容。两条路线各有取舍:开源自建灵活、门槛高;商用私有化交付开箱即用、安全能力完备,是当前企业生产环境主流方案。六边云手机私有化方案,补齐了自建方案在驱动适配、管控平台、审计合规、信创适配上的短板,适合对数据主权、内网隔离有强需求的政企、自研 App、垂直行业客户。

a598652的博客 339

谷歌相机9.2下载(GCam 安卓通用版 安装与使用说明)

谷歌相机9.2版移植自Pixel原生相机,支持Camera2 API的Android 10+设备安装,凭借多帧合成技术在夜景、HDR+与人像模式表现优异。可提升暗光画质、优化动态范围与背景虚化效果,支持视频防抖及配置文件自定义。安装需确认机型兼容性,建议备份原相机,避免冲突。资源可通过kuake链接获取,仅限学习测试使用。

2611_96675819的博客 166

Android 系统启动与调试中的日志抓取方式全解析

│ 用户操作/系统事件 ││ │▼ ▼│ (驱动/硬件/内存管理) │ │ (App/SystemServer) ││ │ │ ││ │▼ ▼│ dmesg 命令 │ │ logd daemon ││ cat /proc/kmsg │ │ (分类存储到分区) ││ │▼ ▼│ 串口 UART │ │ logcat 命令 ││ (硬件直连,最早输出) │ │ adb logcat ││ │▼│ (汇总所有日志+系统状态) │工具最佳用途关键优势主要局限dmesg。

zhaoshuzhaoshu的专栏 269

【Android】听写播放器改版:中英文书写速度拆项与「正在读」高亮

现象:把播放页主体抽成后,里面给LazyColumn加想让它占满剩余高度,编译报错(weight 不可用)。原因weight不是通用修饰符,是RowScope的扩展函数。是个普通函数,函数体里没有接收者,自然调不到。解决private fun ColumnScope.PlaybackBody(...) { /* 这里就能用 Modifier.weight(1f) */ }调用处仍在原来的Column { }里,无需改动。预防:Compose 的布局修饰符都是作用域绑定。

木头大左的博客 242
上一篇: 性能优化:手把手教你perfetto抓取直接用chrome浏览器踩坑笔记
下一篇: perfetto抓取方式汇总官方英文文档:Quickstart: Record traces on Android
千里马学框架
博客等级 码龄6年 1万+粉丝 650原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千里马学框架

帮助你了,就请我喝杯咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值