ANDROID音频系统散记之三:resample-2

本文探讨了Android系统中音频采样率固定为44.1kHz的原因,并介绍了Samsung为解决不同采样率需求而设计的DownSampler组件的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这篇是承接上一篇提到的底层resample处理,以Samsung的mini alsa-lib为例说明。


mini alsa-lib

这个mini alsa-lib位于android2.3.1-gingerbread/device/samsung/crespo/libaudio中。如之前所说alsa-lib实现了太多plugin的功能,显得复杂臃肿。因此我建议如果想了解alsa在上层调用过程,最好从这个mini alsa-lib入手,就两个源文件:alsa_pcm.c和alsa_mixer.c,前者是pcm回放录音接口,后者是mixer controls的控制接口。

alsa-lib其实也是通过操作/dev目录的设备节点来调用内核空间的音频驱动接口,这点跟平常的字符设备的调用方法一样的。如open:

  1. structpcm*pcm_open(unsignedflags)
  2. {
  3. constchar*dname;
  4. structpcm*pcm;
  5. structsnd_pcm_infoinfo;
  6. structsnd_pcm_hw_paramsparams;
  7. structsnd_pcm_sw_paramssparams;
  8. unsignedperiod_sz;
  9. unsignedperiod_cnt;
  10. LOGV("pcm_open(0x%08x)",flags);
  11. pcm=calloc(1,sizeof(structpcm));
  12. if(!pcm)
  13. return&bad_pcm;
  14. if(flags&PCM_IN){
  15. dname="/dev/snd/pcmC0D0c";//capture设备节点
  16. }else{
  17. dname="/dev/snd/pcmC0D0p";//playback设备节点
  18. }
  19. ...
  20. pcm->flags=flags;
  21. pcm->fd=open(dname,O_RDWR);
  22. if(pcm->fd<0){
  23. oops(pcm,errno,"cannotopendevice'%s'");
  24. returnpcm;
  25. }
  26. if(ioctl(pcm->fd,SNDRV_PCM_IOCTL_INFO,&info)){
  27. oops(pcm,errno,"cannotgetinfo-%s");
  28. gotofail;
  29. }
  30. ...
  31. }

这里不多考究这些接口实现。alsa_pcm.c中有个函数挺有趣的:
  1. staticvoidparam_set_mask(structsnd_pcm_hw_params*p,intn,unsignedbit)
  2. {
  3. if(bit>=SNDRV_MASK_MAX)
  4. return;
  5. if(param_is_mask(n)){
  6. structsnd_mask*m=param_to_mask(p,n);
  7. m->bits[0]=0;
  8. m->bits[1]=0;
  9. m->bits[bit>>5]|=(1<<(bit&31));
  10. }
  11. }

其中SNDRV_MASK_MAX和snd_mask的定义分别如下:

  1. #defineSNDRV_MASK_MAX256
  2. structsnd_mask{
  3. __u32bits[(SNDRV_MASK_MAX+31)/32];
  4. };
结合SNDRV_MASK_MAX和snd_mask来理解:可以mask的位数高达256,但是我们计算机字长是32位,因此用8个32位的数组来构成一个256位的掩码,param_set_mask函数就是这个掩码进行设置。

其中m->bits[bit >> 5] |= (1 << (bit & 31));为核心语句,bit>>5其实就是bit除以32(即数组元素长度)取得数组下标,1 << (bit & 31)是掩码位在数组元素中的偏移量。如bit=255时,则数组下标是7,即数组bits最后一个元素,偏移量是1<<31,这时整个bits数据就是这样:bits[7:0] = 0x80000000:0x00000000:0x00000000:0x00000000:0x00000000:0x00000000:0x00000000:0x00000000,这个256位的掩码的最高位就置1了。当然在实际应用中并不会用到那么高位的掩码,这里应该是为了方便以后扩展使用的,因此也只需要m->bits[0] = 0;m->bits[1] = 0,看来仅仅最多用到64位掩码。


ADCLRC约束条件

在pcm_open中,有

  1. param_set_int(¶ms,SNDRV_PCM_HW_PARAM_RATE,44100);
  2. if(ioctl(pcm->fd,SNDRV_PCM_IOCTL_HW_PARAMS,¶ms)){
  3. oops(pcm,errno,"cannotsethwparams");
  4. gotofail;
  5. }

可见,无论放音还是录音,都是设置44.1khz的采样率的。在我们的底层I2S驱动中,放音录音也是固定一个采样率44.1khz。为什么这样做?放音就罢了,Android由于需要混合各个track的数据,故把放音采样率固定在44.1khz,而录音为什么也固定用44.1khz?注:这里的采样率直接对应硬件信号ADCLRC/DACLRC频率。

首先需要了解一下I2S协议方面的知识。放音采样率DACLRC,录音采样率ADCLRC都是通过同一个主时钟MCLK分频出来的。在底层音频驱动中,一般有如下的结构体:

  1. struct_coeff_div{
  2. u32mclk;
  3. u32rate;
  4. u16fs;
  5. u8sr;
  6. u8bclk_div;
  7. };
  8. /*codechifimclkclockdividercoefficients*/
  9. staticconststruct_coeff_divcoeff_div[]={
  10. /*8k*/
  11. {12288000,8000,1536,0x4,0x0},
  12. /*11.025k*/
  13. {11289600,11025,1024,0x8,0x0},
  14. /*16k*/
  15. {12288000,16000,768,0x5,0x0},
  16. /*22.05k*/
  17. {11289600,22050,512,0x9,0x0},
  18. /*32k*/
  19. {12288000,32000,384,0x7,0x0},
  20. /*44.1k*/
  21. {11289600,44100,256,0x6,0x07},
  22. /*48k*/
  23. {12288000,48000,256,0x0,0x07},
  24. /*96k*/
  25. {12288000,96000,128,0x1,0x04},
  26. };

其中MCLK有两个可配频率,分别是12288000和11289600,前者用于8k、16k、32k、48k、96khz的分频,后者用于11.025k、22.05k、44.1khz的分频。具体算式是rate=mclk/fs,如44100=11289600/256。

看出问题了没有?如果录音采样率设置为8khz,则MCLK必须转变为12288000,此时DACLRC就会被改变(放音声音会变得尖锐),不利于同时放音录音。因此录音采样率是受其约束的,其实也不是一定是44.1khz,是11.025khz的倍数即可,能保证是可以从同一个MCLK分频。


DownSampler

在android2.3.1-gingerbread/device/samsung/crespo/libaudio中,除了mini alsa-lib外,就是Samsung为Android写的AudioHAL了,如AudioHardware.cpp,这相当于alsa_sound中的文件。这个HAL有很大的通用性,移植到无通话功能的MID上都可以正常工作的,当然也保留Samsung的一些专用性,主要是通话语音通道处理。这里不详述这个音频HAL文件,如果对AudioFlinger和alsa_sound比较熟悉的话,会很快上手掌握。

如上个章节所说,底层录音采样率ADCLRC固定是44.1khz,那么上层如果想要其他的采样率如8khz,怎么办?resample无疑。由于这里支持的录音采样率有:8000, 11025, 16000, 22050, 44100,都低于或等于44.1khz,则只需要downsample(同理从低采样率转换到高采样率叫upsample)。如下是简单的分析:

  1. status_tAudioHardware::AudioStreamInALSA::set(
  2. AudioHardware*hw,uint32_tdevices,int*pFormat,
  3. uint32_t*pChannels,uint32_t*pRate,AudioSystem::audio_in_acousticsacoustics)
  4. {
  5. if(pFormat==0||*pFormat!=AUDIO_HW_IN_FORMAT){
  6. *pFormat=AUDIO_HW_IN_FORMAT;//AudioSystem::PCM_16_BIT
  7. returnBAD_VALUE;
  8. }
  9. if(pRate==0){
  10. returnBAD_VALUE;
  11. }
  12. //getInputSampleRate:取得与参数sampleRate最接近的且被支持的采样率
  13. //支持的采样率有:8000,11025,16000,22050,44100
  14. //事实上,这里传入来的sampleRate必须是被支持的,否则返回BAD_VALUE
  15. uint32_trate=AudioHardware::getInputSampleRate(*pRate);
  16. if(rate!=*pRate){
  17. *pRate=rate;
  18. returnBAD_VALUE;
  19. }
  20. if(pChannels==0||(*pChannels!=AudioSystem::CHANNEL_IN_MONO&&
  21. *pChannels!=AudioSystem::CHANNEL_IN_STEREO)){
  22. *pChannels=AUDIO_HW_IN_CHANNELS;//AudioSystem::CHANNEL_IN_MONO
  23. returnBAD_VALUE;
  24. }
  25. mHardware=hw;
  26. LOGV("AudioStreamInALSA::set(%d,%d,%u)",*pFormat,*pChannels,*pRate);
  27. //getBufferSize:根据采样率和声道数确定buffer的大小
  28. //popCount:计算参数u有多少个非0位,其实现很有趣,大家可以研究下它的算法
  29. mBufferSize=getBufferSize(*pRate,AudioSystem::popCount(*pChannels));
  30. mDevices=devices;
  31. mChannels=*pChannels;
  32. mChannelCount=AudioSystem::popCount(mChannels);
  33. mSampleRate=rate;
  34. //检查mSampleRate是否与AUDIO_HW_OUT_SAMPLERATE(44.1khz)一致,否则需要downresample
  35. if(mSampleRate!=AUDIO_HW_OUT_SAMPLERATE){
  36. mDownSampler=newAudioHardware::DownSampler(mSampleRate,
  37. mChannelCount,
  38. AUDIO_HW_IN_PERIOD_SZ,
  39. this);
  40. status_tstatus=mDownSampler->initCheck();
  41. if(status!=NO_ERROR){
  42. deletemDownSampler;
  43. LOGW("AudioStreamInALSA::set()downsamplerinitfailed:%d",status);
  44. returnstatus;
  45. }
  46. mPcmIn=newint16_t[AUDIO_HW_IN_PERIOD_SZ*mChannelCount];
  47. }
  48. returnNO_ERROR;
  49. }

以上是set方法,检查参数format、samplerate和channelcount的合法性,检查samplerate是否与ADCLRC一致,如果不一致,则创建一个DownSampler。

我们再看看read方法代码片段:

  1. ssize_tAudioHardware::AudioStreamInALSA::read(void*buffer,ssize_tbytes)
  2. {
  3. ......
  4. //检查是否创建了DownSampler
  5. if(mDownSampler!=NULL){
  6. size_tframes=bytes/frameSize();
  7. size_tframesIn=0;
  8. mReadStatus=0;
  9. do{
  10. size_toutframes=frames-framesIn;
  11. //调用DownSampler的resample方法,该方法从音频接口读取pcm数据,然后对这些数据resample
  12. mDownSampler->resample(
  13. (int16_t*)buffer+(framesIn*mChannelCount),
  14. &outframes);
  15. framesIn+=outframes;
  16. }while((framesIn<frames)&&mReadStatus==0);
  17. ret=mReadStatus;
  18. bytes=framesIn*frameSize();
  19. }else{
  20. TRACE_DRIVER_IN(DRV_PCM_READ)
  21. //并未创建DownSampler,直接读取pcm数据送到缓冲区
  22. ret=pcm_read(mPcm,buffer,bytes);
  23. TRACE_DRIVER_OUT
  24. }
  25. ......
  26. }
可知,当上层需要的samplerate与44.1khz不符时,会转入DownSampler::resample处理:

1、调用AudioHardware::AudioStreamInALSA::getNextBuffer方法,获取音频pcm数据,存放到buffer,并计算下一次buffer的地址;

2、将buffer中的数据分解成各个声道的数据并保存到mInLeft和mInRight;

3、由于原始的音频pcm数据采样率是44.1khz的,调用resample_2_1将数据转为22.05khz采样率;

4、1) 如果上层需要的samplerate=11.025khz,调用resample_2_1将数据采样率从22.05khz转换到11.025khz;

2) 如果上层需要的samplerate=8khz,调用resample_441_320将数据采样率从11.025khz转换到8khz;

5、如果上层需要的samplerate=16khz,调用resample_441_320将数据采样率从22.05khz转换到16khz。

可见真正的resample处理是在resample_2_1()和resample_441_320()这两个函数中。前者是对倍数2的采样率进行resample的,如44100->22050, 22050->11025, 16000->8000等;后者是对比率为441/320的采样率进行resample的,如44100->32000, 22050->16000, 11025->8000等。


这篇是承接上一篇提到的底层resample处理,以Samsung的mini alsa-lib为例说明。


mini alsa-lib

这个mini alsa-lib位于android2.3.1-gingerbread/device/samsung/crespo/libaudio中。如之前所说alsa-lib实现了太多plugin的功能,显得复杂臃肿。因此我建议如果想了解alsa在上层调用过程,最好从这个mini alsa-lib入手,就两个源文件:alsa_pcm.c和alsa_mixer.c,前者是pcm回放录音接口,后者是mixer controls的控制接口。

alsa-lib其实也是通过操作/dev目录的设备节点来调用内核空间的音频驱动接口,这点跟平常的字符设备的调用方法一样的。如open:

  1. structpcm*pcm_open(unsignedflags)
  2. {
  3. constchar*dname;
  4. structpcm*pcm;
  5. structsnd_pcm_infoinfo;
  6. structsnd_pcm_hw_paramsparams;
  7. structsnd_pcm_sw_paramssparams;
  8. unsignedperiod_sz;
  9. unsignedperiod_cnt;
  10. LOGV("pcm_open(0x%08x)",flags);
  11. pcm=calloc(1,sizeof(structpcm));
  12. if(!pcm)
  13. return&bad_pcm;
  14. if(flags&PCM_IN){
  15. dname="/dev/snd/pcmC0D0c";//capture设备节点
  16. }else{
  17. dname="/dev/snd/pcmC0D0p";//playback设备节点
  18. }
  19. ...
  20. pcm->flags=flags;
  21. pcm->fd=open(dname,O_RDWR);
  22. if(pcm->fd<0){
  23. oops(pcm,errno,"cannotopendevice'%s'");
  24. returnpcm;
  25. }
  26. if(ioctl(pcm->fd,SNDRV_PCM_IOCTL_INFO,&info)){
  27. oops(pcm,errno,"cannotgetinfo-%s");
  28. gotofail;
  29. }
  30. ...
  31. }

这里不多考究这些接口实现。alsa_pcm.c中有个函数挺有趣的:
  1. staticvoidparam_set_mask(structsnd_pcm_hw_params*p,intn,unsignedbit)
  2. {
  3. if(bit>=SNDRV_MASK_MAX)
  4. return;
  5. if(param_is_mask(n)){
  6. structsnd_mask*m=param_to_mask(p,n);
  7. m->bits[0]=0;
  8. m->bits[1]=0;
  9. m->bits[bit>>5]|=(1<<(bit&31));
  10. }
  11. }

其中SNDRV_MASK_MAX和snd_mask的定义分别如下:

  1. #defineSNDRV_MASK_MAX256
  2. structsnd_mask{
  3. __u32bits[(SNDRV_MASK_MAX+31)/32];
  4. };
结合SNDRV_MASK_MAX和snd_mask来理解:可以mask的位数高达256,但是我们计算机字长是32位,因此用8个32位的数组来构成一个256位的掩码,param_set_mask函数就是这个掩码进行设置。

其中m->bits[bit >> 5] |= (1 << (bit & 31));为核心语句,bit>>5其实就是bit除以32(即数组元素长度)取得数组下标,1 << (bit & 31)是掩码位在数组元素中的偏移量。如bit=255时,则数组下标是7,即数组bits最后一个元素,偏移量是1<<31,这时整个bits数据就是这样:bits[7:0] = 0x80000000:0x00000000:0x00000000:0x00000000:0x00000000:0x00000000:0x00000000:0x00000000,这个256位的掩码的最高位就置1了。当然在实际应用中并不会用到那么高位的掩码,这里应该是为了方便以后扩展使用的,因此也只需要m->bits[0] = 0;m->bits[1] = 0,看来仅仅最多用到64位掩码。


ADCLRC约束条件

在pcm_open中,有

  1. param_set_int(¶ms,SNDRV_PCM_HW_PARAM_RATE,44100);
  2. if(ioctl(pcm->fd,SNDRV_PCM_IOCTL_HW_PARAMS,¶ms)){
  3. oops(pcm,errno,"cannotsethwparams");
  4. gotofail;
  5. }

可见,无论放音还是录音,都是设置44.1khz的采样率的。在我们的底层I2S驱动中,放音录音也是固定一个采样率44.1khz。为什么这样做?放音就罢了,Android由于需要混合各个track的数据,故把放音采样率固定在44.1khz,而录音为什么也固定用44.1khz?注:这里的采样率直接对应硬件信号ADCLRC/DACLRC频率。

首先需要了解一下I2S协议方面的知识。放音采样率DACLRC,录音采样率ADCLRC都是通过同一个主时钟MCLK分频出来的。在底层音频驱动中,一般有如下的结构体:

  1. struct_coeff_div{
  2. u32mclk;
  3. u32rate;
  4. u16fs;
  5. u8sr;
  6. u8bclk_div;
  7. };
  8. /*codechifimclkclockdividercoefficients*/
  9. staticconststruct_coeff_divcoeff_div[]={
  10. /*8k*/
  11. {12288000,8000,1536,0x4,0x0},
  12. /*11.025k*/
  13. {11289600,11025,1024,0x8,0x0},
  14. /*16k*/
  15. {12288000,16000,768,0x5,0x0},
  16. /*22.05k*/
  17. {11289600,22050,512,0x9,0x0},
  18. /*32k*/
  19. {12288000,32000,384,0x7,0x0},
  20. /*44.1k*/
  21. {11289600,44100,256,0x6,0x07},
  22. /*48k*/
  23. {12288000,48000,256,0x0,0x07},
  24. /*96k*/
  25. {12288000,96000,128,0x1,0x04},
  26. };

其中MCLK有两个可配频率,分别是12288000和11289600,前者用于8k、16k、32k、48k、96khz的分频,后者用于11.025k、22.05k、44.1khz的分频。具体算式是rate=mclk/fs,如44100=11289600/256。

看出问题了没有?如果录音采样率设置为8khz,则MCLK必须转变为12288000,此时DACLRC就会被改变(放音声音会变得尖锐),不利于同时放音录音。因此录音采样率是受其约束的,其实也不是一定是44.1khz,是11.025khz的倍数即可,能保证是可以从同一个MCLK分频。


DownSampler

在android2.3.1-gingerbread/device/samsung/crespo/libaudio中,除了mini alsa-lib外,就是Samsung为Android写的AudioHAL了,如AudioHardware.cpp,这相当于alsa_sound中的文件。这个HAL有很大的通用性,移植到无通话功能的MID上都可以正常工作的,当然也保留Samsung的一些专用性,主要是通话语音通道处理。这里不详述这个音频HAL文件,如果对AudioFlinger和alsa_sound比较熟悉的话,会很快上手掌握。

如上个章节所说,底层录音采样率ADCLRC固定是44.1khz,那么上层如果想要其他的采样率如8khz,怎么办?resample无疑。由于这里支持的录音采样率有:8000, 11025, 16000, 22050, 44100,都低于或等于44.1khz,则只需要downsample(同理从低采样率转换到高采样率叫upsample)。如下是简单的分析:

  1. status_tAudioHardware::AudioStreamInALSA::set(
  2. AudioHardware*hw,uint32_tdevices,int*pFormat,
  3. uint32_t*pChannels,uint32_t*pRate,AudioSystem::audio_in_acousticsacoustics)
  4. {
  5. if(pFormat==0||*pFormat!=AUDIO_HW_IN_FORMAT){
  6. *pFormat=AUDIO_HW_IN_FORMAT;//AudioSystem::PCM_16_BIT
  7. returnBAD_VALUE;
  8. }
  9. if(pRate==0){
  10. returnBAD_VALUE;
  11. }
  12. //getInputSampleRate:取得与参数sampleRate最接近的且被支持的采样率
  13. //支持的采样率有:8000,11025,16000,22050,44100
  14. //事实上,这里传入来的sampleRate必须是被支持的,否则返回BAD_VALUE
  15. uint32_trate=AudioHardware::getInputSampleRate(*pRate);
  16. if(rate!=*pRate){
  17. *pRate=rate;
  18. returnBAD_VALUE;
  19. }
  20. if(pChannels==0||(*pChannels!=AudioSystem::CHANNEL_IN_MONO&&
  21. *pChannels!=AudioSystem::CHANNEL_IN_STEREO)){
  22. *pChannels=AUDIO_HW_IN_CHANNELS;//AudioSystem::CHANNEL_IN_MONO
  23. returnBAD_VALUE;
  24. }
  25. mHardware=hw;
  26. LOGV("AudioStreamInALSA::set(%d,%d,%u)",*pFormat,*pChannels,*pRate);
  27. //getBufferSize:根据采样率和声道数确定buffer的大小
  28. //popCount:计算参数u有多少个非0位,其实现很有趣,大家可以研究下它的算法
  29. mBufferSize=getBufferSize(*pRate,AudioSystem::popCount(*pChannels));
  30. mDevices=devices;
  31. mChannels=*pChannels;
  32. mChannelCount=AudioSystem::popCount(mChannels);
  33. mSampleRate=rate;
  34. //检查mSampleRate是否与AUDIO_HW_OUT_SAMPLERATE(44.1khz)一致,否则需要downresample
  35. if(mSampleRate!=AUDIO_HW_OUT_SAMPLERATE){
  36. mDownSampler=newAudioHardware::DownSampler(mSampleRate,
  37. mChannelCount,
  38. AUDIO_HW_IN_PERIOD_SZ,
  39. this);
  40. status_tstatus=mDownSampler->initCheck();
  41. if(status!=NO_ERROR){
  42. deletemDownSampler;
  43. LOGW("AudioStreamInALSA::set()downsamplerinitfailed:%d",status);
  44. returnstatus;
  45. }
  46. mPcmIn=newint16_t[AUDIO_HW_IN_PERIOD_SZ*mChannelCount];
  47. }
  48. returnNO_ERROR;
  49. }

以上是set方法,检查参数format、samplerate和channelcount的合法性,检查samplerate是否与ADCLRC一致,如果不一致,则创建一个DownSampler。

我们再看看read方法代码片段:

  1. ssize_tAudioHardware::AudioStreamInALSA::read(void*buffer,ssize_tbytes)
  2. {
  3. ......
  4. //检查是否创建了DownSampler
  5. if(mDownSampler!=NULL){
  6. size_tframes=bytes/frameSize();
  7. size_tframesIn=0;
  8. mReadStatus=0;
  9. do{
  10. size_toutframes=frames-framesIn;
  11. //调用DownSampler的resample方法,该方法从音频接口读取pcm数据,然后对这些数据resample
  12. mDownSampler->resample(
  13. (int16_t*)buffer+(framesIn*mChannelCount),
  14. &outframes);
  15. framesIn+=outframes;
  16. }while((framesIn<frames)&&mReadStatus==0);
  17. ret=mReadStatus;
  18. bytes=framesIn*frameSize();
  19. }else{
  20. TRACE_DRIVER_IN(DRV_PCM_READ)
  21. //并未创建DownSampler,直接读取pcm数据送到缓冲区
  22. ret=pcm_read(mPcm,buffer,bytes);
  23. TRACE_DRIVER_OUT
  24. }
  25. ......
  26. }
可知,当上层需要的samplerate与44.1khz不符时,会转入DownSampler::resample处理:

1、调用AudioHardware::AudioStreamInALSA::getNextBuffer方法,获取音频pcm数据,存放到buffer,并计算下一次buffer的地址;

2、将buffer中的数据分解成各个声道的数据并保存到mInLeft和mInRight;

3、由于原始的音频pcm数据采样率是44.1khz的,调用resample_2_1将数据转为22.05khz采样率;

4、1) 如果上层需要的samplerate=11.025khz,调用resample_2_1将数据采样率从22.05khz转换到11.025khz;

2) 如果上层需要的samplerate=8khz,调用resample_441_320将数据采样率从11.025khz转换到8khz;

5、如果上层需要的samplerate=16khz,调用resample_441_320将数据采样率从22.05khz转换到16khz。

可见真正的resample处理是在resample_2_1()和resample_441_320()这两个函数中。前者是对倍数2的采样率进行resample的,如44100->22050, 22050->11025, 16000->8000等;后者是对比率为441/320的采样率进行resample的,如44100->32000, 22050->16000, 11025->8000等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值