ffmpeg开发之旅(4):MP3编码格式分析与lame库编译封装

ffmpeg开发之旅(4):MP3编码格式分析与lame库编译封装

原文链接:http://blog.csdn.net/andrexpert/article/77683776

一、Mp3编码格式分析

      MP3,全称MPEG Audio Layer3,是一种高效的计算机音频编码方案,它以较大的压缩比(1:10至1:12)将音频文件转换成较小的扩展名为.mp3的文件,且能基本保持原文件的音质。假如有一个4分钟的CD音质的WAV音频,其音频参数为44.1kHz抽样、立体声、采样精度为16位(2字节),那么该音频所占空间为441000*2(声道)*2(字节)*60(秒)*4(分钟)=40.4MB,而对于MP3格式来说,MP3音频只占4MB左右,有利于存储和网络传输。
1. MP3文件结构
MP3文件有由帧(frame)构成的,帧是MP3文件最小的组成单位。MP3音频文件本身没有头部,当希望读取有关MP3音频文件的信息时,可以读取第一帧的头部信息,因此可以切割MP3音频文件的任何部分进行正确播放。整个MP3文件结构大体包括三部分,即TAG_V2(ID3V2)、Frame、TAG_V1(ID3V1),具体描述如下:

2. MP3帧格式
     每个帧都是独立的,它由帧头、附加信息和声音数据组成,其长度随位率的不同而不等,通常每个帧的播放时间为0.026秒。MP3帧结构如下:

     每帧的帧头占4字节(32位),帧头后面可能有两个字节的CRC校验,这两个字节的是否存在取决于帧头部的第16bit,如果为0,则帧头后面无校验,为1则有校验。帧头结构如下:
[cpp]  view plain  copy
  1. typedefstruct-tagHeader{   
  2.     unsigned int sync:        占11位   //同步信息  
  3.     unsigned int version:    2;    //版本  
  4.     unsigned int layer:          2;  //层   
  5.     unsigned int error2protection:     1;   //CRC校正  
  6.     unsigned int bit2rate2index:        4;   //位率索引  
  7.     unsigned int sample2rate2index: 2;   //采样率索引  
  8.     unsigned int padding:                  1;   //空白字  
  9.     unsigned int extension:               1;    //私有标志  
  10.     unsigned int channel2mode:       2;   //立体声模式  
  11.     unsigned int modeextension:      2   ;//保留  
  12.     unsigned int copyright:                1;  //版权标志  
  13.     unsigned int original:                   1;  //原始媒体  
  14.     unsigned int emphasis:               2   ;//强调方式  
  15.   } HEADER;  
其中,sync为同步信息,占11位,全部被设置为1;channel2mode为立体声通道模式,占2为,11表示Single立体声(Mono);其他参数请看 这篇文章
二、lame编译与封装
1. Lame库简介

     Lame是Mike Cheng于1998年发起的一个开源项目,是目前最好的MP3编码引擎。Lame编码出来的MP3音色纯厚、空间宽广、低音清晰、细节表现良好,它独创的心理音响模型技术保证了CD音频还原的真实性,配合VBR和ABR参数,音质几乎可以媲美CD音频,但文件体积却非常小。

最新版下载: https://sourceforge.net/projects/lame/files/lame/3.99/
2. Lame库编译与封装

(1) 移植Lame库到Android工程
      a. 解压lame-3.99.5,将源码中的libmp3lame目录拷贝到Android工程的cpp目录下;
      b. 将libmp3lame重命名为lame,并删除i386目录、vector目录、depcomp、lame.rc、logoe.ico、Makefile.am、Makefile.in文件;
      c. 拷贝源码中inlude目录下lame.h文件到Android工程cpp目录下lame目录中,lame.h头文件包含了所有调用函数的声明;
      d. 配置CMakeLists.txt文件
          set(SRC_DIR src/main/cpp/lame)
          include_directories(src/main/cpp/lame)
         aux_source_directory(src/main/cpp/lame SRC_LIST)
         add_library(...... ${SRC_LIST})
(2) LameMp3.java,创建调用lame库函数的native方法
[java]  view plain  copy
  1. /** JNI调用lame库实现mp3文件封装 
  2.  * Created by Jiangdg on 2017/6/9. 
  3.  */  
  4. public class LameMp3 {  
  5.    // 静态加载共享库LameMp3  
  6.     static {  
  7.         System.loadLibrary("LameMp3");  
  8.     }  
  9.     /** 初始化lame库,配置相关信息 
  10.      * 
  11.      * @param inSampleRate pcm格式音频采样率 
  12.      * @param outChannel pcm格式音频通道数量 
  13.      * @param outSampleRate mp3格式音频采样率 
  14.      * @param outBitRate mp3格式音频比特率 
  15.      * @param quality mp3格式音频质量,0~9,最慢最差~最快最好 
  16.      */  
  17.     public native static void lameInit(int inSampleRate, int outChannel,int outSampleRate, int outBitRate, int quality);  
  18.   
  19.   
  20.     /** 编码pcm成mp3格式 
  21.      * 
  22.      * @param letftBuf  左pcm数据 
  23.      * @param rightBuf 右pcm数据,如果是单声道,则一致 
  24.      * @param sampleRate 读入的pcm字节大小 
  25.      * @param mp3Buf 存放mp3数据缓存 
  26.      * @return 编码数据字节长度 
  27.      */  
  28.     public native static int lameEncode(short[] letftBuf, short[] rightBuf,int sampleRate, byte[] mp3Buf);  
  29.   
  30.   
  31.     /** 保存mp3音频流到文件 
  32.      * 
  33.      * @param mp3buf mp3数据流 
  34.      * @return 数据流长度rty 
  35.      */  
  36.     public native static int lameFlush(byte[] mp3buf);  
  37.   
  38.   
  39.     /** 
  40.      * 释放lame库资源 
  41.      */  
  42.     public native static void lameClose();  
  43. }  
讲解一下 :通过查看Lame库的API文档(lame-3.99.5\API)可知,使用Lame封装Mp3需要经历四个步骤,即初始化lame引擎、编码pcm为mp3数据帧、写入文件、释放lame引擎资源。因此,在LameMp3 .java中,我们定义与之对应的native方法以便java层调用,最终生成所需的mp3格式文件。
(3) LameMp3.c
[java]  view plain  copy
  1. // 本地实现  
  2. // Created by jianddongguo on 2017/6/14.  
  3. #include <jni.h>  
  4. #include "LameMp3.h"  
  5. #include "lame/lame.h"  
  6. // 声明一个lame_global_struct指针变量  
  7. // 可认为是一个全局上下文  
  8. static lame_global_flags *gfp = NULL;  
  9.   
  10.   
  11. JNIEXPORT void JNICALL  
  12. Java_com_teligen_lametomp3_LameMp3_lameInit(JNIEnv *env, jclass type, jint inSampleRate,  
  13. jint outChannelNum, jint outSampleRate, jint outBitRate,  
  14.         jint quality) {  
  15.     if(gfp != NULL){  
  16.         lame_close(gfp);  
  17.         gfp = NULL;  
  18.     }  
  19.     //  初始化编码器引擎,返回一个lame_global_flags结构体类型指针  
  20.     //  说明编码所需内存分配完成,否则,返回NULL   
  21.     gfp = lame_init();  
  22.     LOGI("初始化lame库完成");  
  23.   
  24.   
  25.     // 设置输入数据流的采样率,默认为44100Hz  
  26.     lame_set_in_samplerate(gfp,inSampleRate);  
  27.     // 设置输入数据流的通道数量,默认为2  
  28.     lame_set_num_channels(gfp,outChannelNum);  
  29.     // 设置输出数据流的采样率,默认为0,单位KHz  
  30.     lame_set_out_samplerate(gfp,outSampleRate);  
  31.     lame_set_mode(gfp,MPEG_mode);  
  32.      // 设置比特压缩率,默认为11  
  33.     lame_set_brate(gfp,outBitRate);  
  34.     // 编码质量,推荐2、5、7  
  35.     lame_set_quality(gfp,quality);  
  36.     // 配置参数  
  37.     lame_init_params(gfp);  
  38.     LOGI("配置lame参数完成");  
  39. }  
  40.   
  41.   
  42. JNIEXPORT jint JNICALL  
  43.         Java_com_teligen_lametomp3_LameMp3_lameFlush(JNIEnv *env, jclass type, jbyteArray mp3buf_) {  
  44.     jbyte *mp3buf = (*env)->GetByteArrayElements(env, mp3buf_, NULL);  
  45.     jsize len = (*env)->GetArrayLength(env,mp3buf_);  
  46.     // 刷新pcm缓存,以"0"填充保证最后几帧的完整  
  47.     // 刷新mp3缓存,返回最后的几帧  
  48.     int resut = lame_encode_flush(gfp,        // 全局上下文  
  49.     mp3buf, // 指向mp3缓存的指针  
  50.     len);  // 有效mp3数据长度  
  51.     (*env)->ReleaseByteArrayElements(env, mp3buf_, mp3buf, 0);  
  52.     LOG_I("写入mp3数据到文件,返回帧数=%d",resut);  
  53.     return  resut;  
  54. }  
  55.   
  56.   
  57. JNIEXPORT void JNICALL  
  58. Java_com_teligen_lametomp3_LameMp3_lameClose(JNIEnv *env, jclass type) {  
  59.     // 释放所占内存资源  
  60.     lame_close(gfp);  
  61.     gfp = NULL;  
  62.     LOGI("释放lame资源");  
  63. }  
  64.   
  65.   
  66. JNIEXPORT jint JNICALL  
  67. Java_com_teligen_lametomp3_LameMp3_lameEncode(JNIEnv *env, jclass type, jshortArray letftBuf_,  
  68.                                               jshortArray rightBuf_, jint sampleRate,  
  69.                                               jbyteArray mp3Buf_) {  
  70.     if(letftBuf_ == NULL || mp3Buf_ == NULL){  
  71.         LOGI("letftBuf和rightBuf 或mp3Buf_不能为空");  
  72.         return -1;  
  73.     }  
  74.     jshort *letftBuf = NULL;  
  75.     jshort *rightBuf = NULL;  
  76.     if(letftBuf_ != NULL){  
  77.         letftBuf = (*env)->GetShortArrayElements(env, letftBuf_, NULL);  
  78.     }  
  79.     if(rightBuf_ != NULL){  
  80.         rightBuf = (*env)->GetShortArrayElements(env, rightBuf_, NULL);  
  81.     }  
  82.     jbyte *mp3Buf = (*env)->GetByteArrayElements(env, mp3Buf_, NULL);  
  83.     jsize readSizes = (*env)->GetArrayLength(env,mp3Buf_);  
  84.     // 将PCM数据编码为mp3  
  85.     int result = lame_encode_buffer(gfp, // 全局上下文  
  86.                                   letftBuf,    // 左通道pcm数据  
  87.                                   rightBuf,   // 右通道pcm数据  
  88.                                   sampleRate, // 通道数据流采样率  
  89.                                   mp3Buf, // mp3数据缓存起始地址  
  90.                                    readSizes);      // 缓存地址中有效mp3数据长度  
  91.     // 释放资源  
  92.     if(letftBuf_ != NULL){  
  93.         (*env)->ReleaseShortArrayElements(env, letftBuf_, letftBuf, 0);  
  94.     }  
  95.     if(rightBuf_ != NULL){  
  96.         (*env)->ReleaseShortArrayElements(env, rightBuf_, rightBuf, 0);  
  97.     }  
  98.     (*env)->ReleaseByteArrayElements(env, mp3Buf_, mp3Buf, 0);  
  99.     LOG_I("编码pcm为mp3,数据长度=%d",result);  
  100.     return  result;  
  101. }  
讲解一下 :通过查看lame.h源码,gfp 为结构体lame_global_struct的一个指针变量,该变量用于指向该结构体。lame_global_struct结构体声明了编码所需的各种参数,具体代码如下:
lame_global_flags *gfp = NULL;
typedef struct lame_global_struct lame_global_flags;
struct lame_global_struct {
    unsigned int class_id;
    unsigned long num_samples; 
    int     num_channels;    
    int     samplerate_in;  
    int     samplerate_out;    brate;          
    float   compression_ratio; 
    .....
}
另外,在配置lame编码引擎时,有一个lame_set_quality函数用来设定编码的质量。也许你会问,音频编码质量一般不是由比特率决定的,为什么还需要这个设置?嗯,比特率决定编码质量是没错的,这里的参数主要是用来选择编码处理的算法,不同的算法处理的效果和速度是不一样的。比如,当quality为0时,选择的算法是最好的,但处理的速度是最慢的;当quality为9时,选择的算法是最差的,但是速度是最快的。通常,官方推荐以下三种设置,即:
         quality= 2     质量接近最好,速度不是很慢;
         quality=5     质量很好,速度还行;
         quality=7     质量良好, 速度很快;
(4) CMakeList.txt
[html]  view plain  copy
  1. #指定所需的Cmake最低版本  
  2. cmake_minimum_required(VERSION 3.4.1)  
  3.   
  4.   
  5. #指定源码路径,即将src/main/cpp/lame路径赋值给SRC_DIR   
  6. set(SRC_DIR src/main/cpp/lame)  
  7. # 指定头文件路径  
  8. include_directories(src/main/cpp/lame)  
  9. # 将src/main/cpp/lame目录下的所有文件名赋值给SRC_LIST  
  10. aux_source_directory(src/main/cpp/lame SRC_LIST)  
  11.   
  12.   
  13. # add_library:指定生成库文件,包括三个参数:  
  14. # LameMp3为库文件的名称;SHARED表示动态链接库;  
  15. # src/main/cpp/LameMp3.c和${SRC_LIST}指定生成库文件所需的源文件  
  16. #其中,${}的作用是引入src/main/cpp/lame目录下的所有源文件  
  17. add_library(  
  18.              LameMp3  
  19.              SHARED  
  20.              src/main/cpp/LameMp3.c ${SRC_LIST})  
  21. #在指定的目录中搜索库log,并将其路径保存到变量log-lib中  
  22. find_library( # Sets the name of the path variable.  
  23.               log-lib  
  24.               # Specifies the name of the NDK library that  
  25.               # you want CMake to locate.  
  26.               log )  
  27. # 将库${log-lib} 链接到LameMp3动态库中,包括两个参数  
  28. #LameMp3为目标库  
  29. # ${log-lib}为要链接的库  
  30. target_link_libraries( # Specifies the target library.  
  31.                        LameMp3  
  32.   
  33.   
  34.                        # Links the target library to the log library  
  35.                        # included in the NDK.  
  36.                        ${log-lib} )  
讲解一下 :Cmake是一个跨平台的编译工具,它允许使用简单的语句来描述所有平台的编译过程,并输出各种类型的Makefile或Project文件。Cmake所有的语句命令都写在CMakeLists.txt文件中,主要规则如下:
    a. 在Cmake中,注释由#字符开始到此行的结束;
    b. 命令不区分大小写,参数需区分大小写;
    c. 命令由命令名、参数列表组成,参数间使用空格进行分隔;
(5) build.gradle(Module app),选择编译平台
[html]  view plain  copy
  1. android {  
  2.      
  3.     defaultConfig {  
  4.         // ...代码省略  
  5.         externalNativeBuild {  
  6.             cmake {  
  7.                 cppFlags ""  
  8.             }  
  9.         }  
  10. // 选择编译平台  
  11.         ndk{  
  12.             abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a','arm64-v8a'  
  13.         }  
  14.     }  
  15.     // ...代码省略  
  16.     externalNativeBuild {  
  17.         cmake {  
  18.             path "CMakeLists.txt"  
  19.         }  
  20.     }  
  21. }  
三、开源项目:Lame4Mp3
       Lame4Mp3是基于Lame库实现的开源项目,本项目结合Android官方提供的MediaCodec API,可以满足将PCM数据流编码为AAC或MP3格式数据,并且支持AAC和Mp3同时编码,适用于本地录制mp3/aac文件和在Android直播中进行边播边录(mp3)等场合。使用方法和源码分析如下:
1. 添加依赖
(1) 在工程build.gradle中添加
[html]  view plain  copy
  1. allprojects {  
  2.    repositories {  
  3.     ...  
  4.    maven { url 'https://jitpack.io' }  
  5.   }  
  6. }  
(2) 在module的gradle中添加
[html]  view plain  copy
  1. dependencies {  
  2.    compile 'com.github.jiangdongguo:Lame4Mp3:v1.0.0'  
  3. }  
2. Lame4Mp3使用方法
(1) 配置参数

[java]  view plain  copy
  1. Mp3Recorder mMp3Recorder = Mp3Recorder.getInstance();  
  2.   // 配置AudioRecord参数  
  3.   mMp3Recorder.setAudioSource(Mp3Recorder.AUDIO_SOURCE_MIC);  
  4.   mMp3Recorder.setAudioSampleRare(Mp3Recorder.SMAPLE_RATE_8000HZ);  
  5.   mMp3Recorder.setAudioChannelConfig(Mp3Recorder.AUDIO_CHANNEL_MONO);  
  6.   mMp3Recorder.setAduioFormat(Mp3Recorder.AUDIO_FORMAT_16Bit);  
  7.   // 配置Lame参数  
  8.   mMp3Recorder.setLameBitRate(Mp3Recorder.LAME_BITRATE_32);  
  9.   mMp3Recorder.setLameOutChannel(Mp3Recorder.LAME_OUTCHANNEL_1);  
  10.   // 配置MediaCodec参数  
  11.   mMp3Recorder.setMediaCodecBitRate(Mp3Recorder.ENCODEC_BITRATE_1600HZ);  
  12.   mMp3Recorder.setMediaCodecSampleRate(Mp3Recorder.SMAPLE_RATE_8000HZ);  
  13.   // 设置模式  
  14.   //  Mp3Recorder.MODE_AAC 仅编码得到AAC数据流  
  15.   //  Mp3Recorder.MODE_MP3 仅编码得到Mp3文件  
  16.   //  Mp3Recorder.MODE_BOTH 同时编码  
  17.   mMp3Recorder.setMode(Mp3Recorder.MODE_BOTH);  
(2) 开始编码
[java]  view plain  copy
  1. mMp3Recorder.start(filePath, fileName, new Mp3Recorder.OnAACStreamResultListener() {  
  2.     @Override  
  3.     public void onEncodeResult(byte[] data, int offset, int length, long timestamp) {  
  4.            Log.i("MainActivity","acc数据流长度:"+data.length);  
  5.        }  
  6.     });  
(3) 停止编码
[java]  view plain  copy
  1. mMp3Recorder.stop();  
3. Lame4Mp3源码解析
     Mp3Recorder.java中主要包括三个功能块:PCM数据采集、AAC编码、Mp3编码,其中,PCM数据采集和AAC编码在以前的博文中有详细剖析,所以这里只着重解析Mp3编码,核心代码如下:
[java]  view plain  copy
  1. public void start(final String filePath, final String fileName,final OnAACStreamResultListener listener){  
  2.         this.listener = listener;  
  3.         new Thread(new Runnable() {  
  4.             @Override  
  5.             public void run() {  
  6.                 try {  
  7.                     if(!isRecording){  
  8.                         // 第一步:初始化lame引擎  
  9.                         initLameMp3();  
  10.                         initAudioRecord();  
  11.                         initMediaCodec();  
  12.                     }  
  13.                     int readBytes = 0;  
  14.                     byte[] audioBuffer = new byte[2048];  
  15.                     byte[] mp3Buffer = new byte[1024];  
  16.                     // 如果文件路径不存在,则创建  
  17.                     if(TextUtils.isEmpty(filePath) || TextUtils.isEmpty(fileName)){  
  18.                         Log.i(TAG,"文件路径或文件名为空");  
  19.                         return;  
  20.                     }  
  21.                     File file = new File(filePath);  
  22.                     if(! file.exists()){  
  23.                         file.mkdirs();  
  24.                     }  
  25.                     String mp3Path = file.getAbsoluteFile().toString()+File.separator+fileName+".mp3";  
  26.                     FileOutputStream fops = null;  
  27.                     try {  
  28.                         while(isRecording){  
  29.                             readBytes = mAudioRecord.read(audioBuffer,0,bufferSizeInBytes);  
  30.                             Log.i(TAG,"读取pcm数据流,大小为:"+readBytes);  
  31.                             if(readBytes >0 ){  
  32.                                 if(mode == MODE_AAC || mode == MODE_BOTH){  
  33.                                     // 将PCM编码为AAC  
  34.                                     encodeBytes(audioBuffer,readBytes);  
  35.                                 }  
  36.                                 if(mode == MODE_MP3 || mode == MODE_BOTH){  
  37.                                     // 打开mp3文件输出流  
  38.                                     if(fops == null){  
  39.                                         try {  
  40.                                             fops = new FileOutputStream(mp3Path);  
  41.                                         } catch (FileNotFoundException e) {  
  42.                                             e.printStackTrace();  
  43.                                         }  
  44.                                     }  
  45.                                     // 将byte[] 转换为 short[]  
  46.                                     // 将PCM编码为Mp3,并写入文件  
  47.                                     short[] data = transferByte2Short(audioBuffer,readBytes);  
  48.                                     int encResult = LameMp3.lameEncode(data,null,data.length,mp3Buffer);  
  49.                                     Log.i(TAG,"lame编码,大小为:"+encResult);  
  50.                                     if(encResult != 0){  
  51.                                         try {  
  52.                                             fops.write(mp3Buffer,0,encResult);  
  53.                                         } catch (IOException e) {  
  54.                                             e.printStackTrace();  
  55.                                         }  
  56.                                     }  
  57.                                 }  
  58.                             }  
  59.                         }  
  60.                         // 录音完毕  
  61.                         if(fops != null){  
  62.                             int flushResult =  LameMp3.lameFlush(mp3Buffer);  
  63.                             Log.i(TAG,"录制完毕,大小为:"+flushResult);  
  64.                             if(flushResult > 0){  
  65.                                 try {  
  66.                                     fops.write(mp3Buffer,0,flushResult);  
  67.                                 } catch (IOException e) {  
  68.                                     e.printStackTrace();  
  69.                                 }  
  70.                             }  
  71.                             try {  
  72.                                 fops.close();  
  73.                             } catch (IOException e) {  
  74.                                 e.printStackTrace();  
  75.                             }  
  76.                         }  
  77.                     }finally {  
  78.                         Log.i(TAG,"释放AudioRecorder资源");  
  79.                         stopAudioRecorder();  
  80.                         stopMediaCodec();  
  81.   
  82.   
  83.                     }  
  84.                 }finally {  
  85.                     Log.i(TAG,"释放Lame库资源");  
  86.                     stopLameMp3();  
  87.                 }  
  88.             }  
  89.         }).start();  
  90.     }  
    从代码可以看出,使用lame引擎编码pcm得到mp3数据,将经历四个步骤:初始化引擎、编码、写入文件、释放内存资源,这个过程与之前我们详细分析的流程一致。但是,有一点需要注意的是,当同时编码AAC和Mp3时,向MediaCodec和Lame引擎输入PCM数据流的方式是不一样的,前者只接受byte[]存储的数据,后者接收short[]存储的数据。也就是说,如果将采集的pcm数据以byte[]来存储,我们需要将其转换为short[],并且需要注意大小端的问题。具体代码如下:
 
[java]  view plain  copy
  1. private short[] transferByte2Short(byte[] data,int readBytes){  
  2.      // byte[] 转 short[],数组长度缩减一半  
  3.      int shortLen = readBytes / 2;  
  4.      // 将byte[]数组装如ByteBuffer缓冲区  
  5.      ByteBuffer byteBuffer = ByteBuffer.wrap(data, 0, readBytes);  
  6.      // 将ByteBuffer转成小端并获取shortBuffer  
  7.      // 小端:数据的高字节保存到内存的高地址中,数据的低字节保存到内存的低地址中  
  8.      ShortBuffer shortBuffer = byteBuffer.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();  
  9.      short[] shortData = new short[shortLen];  
  10.      shortBuffer.get(shortData, 0, shortLen);  
  11.      return shortData;  
  12.  }  


GitHub地址:https://github.com/jiangdongguo/Lame4Mp3   欢迎大家star~( 附上LameToMp3 NDK工程)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值