七、通过libfdk_aac编解码器实现aac音频和pcm的编解码

前言


测试环境:

  • ffmpeg的4.3.2自行编译版本
  • windows环境
  • qt5.12

AAC编码是MP3格式的后继产品,通常在相同的比特率下可以获得比MP3更高的声音质量,是iPhone、iPod、iPad、iTunes的标准音频格式。

AAC相较于MP3的改进包含:

  • 更多的采样率选择:8kHz ~ 96kHz,MP3为16kHz ~ 48kHz
  • 更高的声道数上限:48个,MP3在MPEG-1模式下为最多双声道,MPEG-2模式下5.1声道
  • 改进的压缩功能:以较小的文件大小提供更高的质量
  • 改进的解码效率:需要较少的处理能力进行解码

AAC编码为了使用不同场景的需求,设计了很多规格

  • MPEG-2 AAC LC:低复杂度规格(Low Complexity)
  • MPEG-2 AAC Main:主规格
  • MPEG-2 AAC SSR:可变采样率规格(Scaleable Sample Rate)
  • MPEG-4 AAC LC:低复杂度规格(Low Complexity)
    • 现在的手机比较常见的MP4文件中的音频部分使用了该规格
  • MPEG-4 AAC Main:主规格
  • MPEG-4 AAC SSR:可变采样率规格(Scaleable Sample Rate)
  • MPEG-4 AAC LTP:长时期预测规格(Long Term Predicition)
  • MPEG-4 AAC LD:低延迟规格(Low Delay)
  • MPEG-4 AAC HE:高效率规格(High Efficiency)

众多规格中只需关注LC和HE


pcm与aac的转换需要AAC编解码器(如下列举几种常用的AAC编解码器)

  • Nero AAC
    • 支持LC/HE规格
    • 目前已经停止开发维护
  • FFmpeg AAC
    • 支持LC规格
    • FFmpeg官方内置的AAC编解码器,在libavcodec库中
      • 编解码器名字叫做aac
      • 在开发过程中通过这个名字找到编解码器
  • FAAC(Freeware Advanced Audio Coder)
    • 支持LC规格
    • 可以集成到FFmpeg的libavcodec中
      • 编解码器名字叫做libfaac
      • 在开发过程中通过这个名字找到编解码器,最后调用FAAC库的功能
    • 从2016年开始,FFmpeg已经移除了对FAAC的支持
  • Fraunhofer FDK AAC
    • 支持LC/HE规格
    • 目前质量最高的AAC编解码器
    • 可以集成到FFmpeg的libavcodec中
      • 编解码器名字叫做libfdk_aac
      • 在开发过程中通过这个名字找到编解码器,最后调用FDK AAC库的功能

编码质量排名:Fraunhofer FDK AAC > FFmpeg AAC > FAAC。

由于libfdk_aac最好,但是网上下载好的ffmpeg编译好的版本不带libfdk_aac编解码器。所以我们只能自行编译ffmpeg。

如下命令可以查看FFmpeg目前集成的AAC编解码器

ffmpeg -codecs | findstr aac

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


自己手动编译FFmpeg源码,将libfdk_aac集成到FFmpeg中,这种方式最好,但在windows环境下较为麻烦。

因为编译源码需要在类Unix系统上的(Linux、Mac等),默认无法直接用在Windows上。所以必须先用MSYS2软件在Windows上模拟出Linux环境,然后再在其中用MinGW软件对FFmpeg进行编译。

链接:windows下msys2编译64位的ffmpeg源码


编译好源码后,需要把.pro文件配置成新编译的源码。

fdk-aac对需要编解码的pcm音频有一定的格式要求

  • 采样格式必须为16位整数PCM
  • 采样率只支持:8000、11025、12000、16000、22050、24000、32000、44100、48000、64000、88200、96000

命令行将pcm和wav文件编码成aac音频

# pcm -> aac
ffmpeg -ar 44100 -ac 2 -f s16le -i in.pcm -c:a libfdk_aac out.aac
-ar 44100 -ac 2 -f s16le   --PCM输入数据的参数
-c:a	 设置音频编码器,c表示codec(编解码器),a表示audio(音频)。 等价写法 -codec:a或-acodec
    
# wav -> aac
ffmpeg -i in.wav -c:a libfdk_aac out.aac   

默认生成的aac文件是LC规格的。aac文件比之前的pcm文件小了很多很多。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

aac的缩写还可以是m4a和mp4。虽然现在都只认为mp4是视频文件


首先是pcm编码为aac

完整代码

AacEncodeThread.h

#ifndef AACENCODETHREAD_H
#define AACENCODETHREAD_H

#include <QFile>
#include <QObject>
#include <QThread>

extern "C" {
#include <libavformat/avformat.h>
}

typedef struct {
    const char *filename;
    int sampleRate;
    AVSampleFormat sampleFmt;
    int chLayout;
} AudioEncodeSpec;

class AacEncodeThread : public QThread
{
    Q_OBJECT
public:
    explicit AacEncodeThread(QObject *parent = nullptr);
    ~AacEncodeThread();

    static int check_sample_fmt(const AVCodec *codec,enum AVSampleFormat sample_fmt);
    static int encode(AVCodecContext *ctx,AVFrame *frame,AVPacket *pkt,QFile &outFile);
    static void aacEncode(AudioEncodeSpec &in,const char *outFilename);

signals:


    // QThread interface
protected:
    virtual void run() override;
};

#endif // AACENCODETHREAD_H

AacEncodeThread.cpp

#include "aacencodethread.h"

#include <QDebug>
#include <QFile>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
}

#define ERROR_BUF(ret) \
    char errbuf[1024]; \
    av_strerror(ret, errbuf, sizeof (errbuf));

AacEncodeThread::AacEncodeThread(QObject *parent) : QThread(parent)
{
    // 当监听到线程结束时(finished),就调用deleteLater回收内存
    connect(this, &AacEncodeThread::finished,
            this, &AacEncodeThread::deleteLater);
}

AacEncodeThread::~AacEncodeThread()
{
    // 断开所有的连接
    disconnect();
    // 内存回收之前,正常结束线程
    requestInterruption();
    // 安全退出
    quit();
    wait();
    qDebug() << this << "析构(内存被回收)";
}

// 检查采样格式
int AacEncodeThread::check_sample_fmt(const AVCodec *codec,enum AVSampleFormat sample_fmt) {
    const enum AVSampleFormat *p = codec->sample_fmts;

    while (*p != AV_SAMPLE_FMT_NONE) {
//        qDebug() << av_get_sample_fmt_name(*p);
        if (*p == sample_fmt) return 1;
        p++;
    }
    return 0;
}

// 音频编码
// 返回负数:中途出现了错误
// 返回0:编码操作正常完成
int AacEncodeThread::AacEncodeThread::encode(AVCodecContext *ctx,
                  AVFrame *frame,
                  AVPacket *pkt,
                  QFile &outFile) {
    // 发送数据到编码器
    int ret = avcodec_send_frame(ctx, frame);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_send_frame error" << errbuf;
        return ret;
    }

    // 不断从编码器中取出编码后的数据
    // while (ret >= 0)
    while (true) {
        ret = avcodec_receive_packet(ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            // 继续读取数据到frame,然后送到编码器
            return 0;
        } else if (ret < 0) { // 其他错误
            return ret;
        }

        // 成功从编码器拿到编码后的数据
        // 将编码后的数据写入文件
        outFile.write((char *) pkt->data, pkt->size);

        // 释放pkt内部的资源
        av_packet_unref(pkt);
    }
}

void AacEncodeThread::aacEncode(AudioEncodeSpec &in, const char *outFilename)
{
    // 文件
    QFile inFile(in.filename);
    QFile outFile(outFilename);

    // 返回结果
    int ret = 0;

    // 编码器
    AVCodec *codec = nullptr;

    // 编码上下文
    AVCodecContext *ctx = nullptr;

    // 存放编码前的数据(pcm)
    AVFrame *frame = nullptr;

    // 存放编码后的数据(aac)
    AVPacket *pkt = nullptr;

    // 获取编码器
//    codec = avcodec_find_encoder(AV_CODEC_ID_AAC);
    codec = avcodec_find_encoder_by_name("libfdk_aac");
    if (!codec) {
        qDebug() << "encoder not found";
        return;
    }

    // libfdk_aac对输入数据的要求:采样格式必须是16位整数
    // 检查输入数据的采样格式
    if (!check_sample_fmt(codec, in.sampleFmt)) {
        qDebug() << "unsupported sample format"
                 << av_get_sample_fmt_name(in.sampleFmt);
        return;
    }

    // 创建编码上下文
    ctx = avcodec_alloc_context3(codec);
    if (!ctx) {
        qDebug() << "avcodec_alloc_context3 error";
        return;
    }

    // 设置PCM参数
    ctx->sample_rate = in.sampleRate;
    ctx->sample_fmt = in.sampleFmt;
    ctx->channel_layout = in.chLayout;
    // 比特率
    ctx->bit_rate = 32000;
    // 规格
    ctx->profile = FF_PROFILE_AAC_HE_V2;

    // 打开编码器
//    AVDictionary *options = nullptr;
//    av_dict_set(&options, "vbr", "5", 0);
//    ret = avcodec_open2(ctx, codec, &options);
    ret = avcodec_open2(ctx, codec, nullptr);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_open2 error" << errbuf;
        goto end;
    }

    // 创建AVFrame
    frame = av_frame_alloc();
    if (!frame) {
        qDebug() << "av_frame_alloc error";
        goto end;
    }

    // frame缓冲区中的样本帧数量(由ctx->frame_size决定)
    frame->nb_samples = ctx->frame_size;
    frame->format = ctx->sample_fmt;
    frame->channel_layout = ctx->channel_layout;

    // 利用nb_samples、format、channel_layout创建缓冲区
    ret = av_frame_get_buffer(frame, 0);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "av_frame_get_buffer error" << errbuf;
        goto end;
    }

    // 创建AVPacket
    pkt = av_packet_alloc();
    if (!pkt) {
        qDebug() << "av_packet_alloc error";
        goto end;
    }

    // 打开文件
    if (!inFile.open(QFile::ReadOnly)) {
        qDebug() << "file open error" << in.filename;
        goto end;
    }
    if (!outFile.open(QFile::WriteOnly)) {
        qDebug() << "file open error" << outFilename;
        goto end;
    }

    // 读取数据到frame中
    while ((ret = inFile.read((char *) frame->data[0],
                              frame->linesize[0])) > 0) {
        // 从文件中读取的数据,不足以填满frame缓冲区
        if (ret < frame->linesize[0]) {
            int bytes = av_get_bytes_per_sample((AVSampleFormat) frame->format);
            int ch = av_get_channel_layout_nb_channels(frame->channel_layout);
            // 设置真正有效的样本帧数量
            // 防止编码器编码了一些冗余数据
            frame->nb_samples = ret / (bytes * ch);
        }

        // 进行编码
        if (encode(ctx, frame, pkt, outFile) < 0) {
            goto end;
        }
    }

    // 刷新缓冲区
    encode(ctx, nullptr, pkt, outFile);

end:
    // 关闭文件
    inFile.close();
    outFile.close();

    // 释放资源
    av_frame_free(&frame);
    av_packet_free(&pkt);
    avcodec_free_context(&ctx);

    qDebug() << "线程正常结束";
}

void AacEncodeThread::run()
{
    AudioEncodeSpec in;
    in.filename = "E:/media/test.pcm";
    in.sampleRate = 44100;
    in.sampleFmt = AV_SAMPLE_FMT_S16;
    in.chLayout = AV_CH_LAYOUT_STEREO;

    aacEncode(in, "E:/media/test.aac");
}

线程调用:

void MainWindow::on_pushButton_aac_encode_clicked()
{
    m_pAacEncodeThread=new AacEncodeThread(this);
    m_pAacEncodeThread->start();
}

注意:.h文件中提前声明了以下全局变量

AacEncodeThread *m_pAacEncodeThread=nullptr;


下面是aac解码成pcm

完整代码

AacDecodeThread.h

#ifndef AACDECODETHREAD_H
#define AACDECODETHREAD_H

#include <QFile>
#include <QObject>
#include <QThread>

extern "C" {
#include <libavformat/avformat.h>
}

typedef struct {
    const char *filename;
    int sampleRate;
    AVSampleFormat sampleFmt;
    int chLayout;
} AudioDecodeSpec;

class AacDecodeThread : public QThread
{
    Q_OBJECT
public:
    explicit AacDecodeThread(QObject *parent = nullptr);
    ~AacDecodeThread();

    static int decode(AVCodecContext *ctx,
                      AVPacket *pkt,
                      AVFrame *frame,
                      QFile &outFile);
    static void aacDecode(const char *inFilename,AudioDecodeSpec &out);

signals:


    // QThread interface
protected:
    virtual void run() override;
};

#endif // AACDECODETHREAD_H

AacDecodeThread.cpp

#include "aacdecodethread.h"

#include <QDebug>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
}

#define ERROR_BUF(ret) \
    char errbuf[1024]; \
    av_strerror(ret, errbuf, sizeof (errbuf));

// 输入缓冲区的大小
#define IN_DATA_SIZE 20480
// 需要再次读取输入文件数据的阈值
#define REFILL_THRESH 4096


AacDecodeThread::AacDecodeThread(QObject *parent) : QThread(parent)
{
    // 当监听到线程结束时(finished),就调用deleteLater回收内存
    connect(this, &AacDecodeThread::finished,
            this, &AacDecodeThread::deleteLater);
}

AacDecodeThread::~AacDecodeThread()
{
    // 断开所有的连接
    disconnect();
    // 内存回收之前,正常结束线程
    requestInterruption();
    // 安全退出
    quit();
    wait();
    qDebug() << this << "析构(内存被回收)";
}

int AacDecodeThread::decode(AVCodecContext *ctx,
                  AVPacket *pkt,
                  AVFrame *frame,
                  QFile &outFile) {
    // 发送压缩数据到解码器
    int ret = avcodec_send_packet(ctx, pkt);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_send_packet error" << errbuf;
        return ret;
    }

    while (true) {
        // 获取解码后的数据
        ret = avcodec_receive_frame(ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            ERROR_BUF(ret);
            qDebug() << "avcodec_receive_frame error" << errbuf;
            return ret;
        }

//        for (int i = 0; i < frame->channels; i++) {
//            frame->data[i];
//        }

        // 将解码后的数据写入文件
        outFile.write((char *) frame->data[0], frame->linesize[0]);
    }
}

void AacDecodeThread::aacDecode(const char *inFilename, AudioDecodeSpec &out)
{
    // 返回结果
    int ret = 0;

    // 用来存放读取的输入文件数据(aac)
    // 加上AV_INPUT_BUFFER_PADDING_SIZE是为了防止某些优化过的reader一次性读取过多导致越界
    char inDataArray[IN_DATA_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
    char *inData = inDataArray;

    // 每次从输入文件中读取的长度(aac)
    int inLen;
    // 是否已经读取到了输入文件的尾部
    int inEnd = 0;

    // 文件
    QFile inFile(inFilename);
    QFile outFile(out.filename);

    // 解码器
    AVCodec *codec = nullptr;
    // 上下文
    AVCodecContext *ctx = nullptr;
    // 解析器上下文
    AVCodecParserContext *parserCtx = nullptr;

    // 存放解码前的数据(aac)
    AVPacket *pkt = nullptr;
    // 存放解码后的数据(pcm)
    AVFrame *frame = nullptr;

    // 获取解码器
    codec = avcodec_find_decoder_by_name("libfdk_aac");
    if (!codec) {
        qDebug() << "decoder not found";
        return;
    }

    // 初始化解析器上下文
    parserCtx = av_parser_init(codec->id);
    if (!parserCtx) {
        qDebug() << "av_parser_init error";
        return;
    }

    // 创建上下文
    ctx = avcodec_alloc_context3(codec);
    if (!ctx) {
        qDebug() << "avcodec_alloc_context3 error";
        goto end;
    }

    // 创建AVPacket
    pkt = av_packet_alloc();
    if (!pkt) {
        qDebug() << "av_packet_alloc error";
        goto end;
    }

    // 创建AVFrame
    frame = av_frame_alloc();
    if (!frame) {
        qDebug() << "av_frame_alloc error";
        goto end;
    }

    // 打开解码器
    ret = avcodec_open2(ctx, codec, nullptr);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_open2 error" << errbuf;
        goto end;
    }

    // 打开文件
    if (!inFile.open(QFile::ReadOnly)) {
        qDebug() << "file open error:" << inFilename;
        goto end;
    }
    if (!outFile.open(QFile::WriteOnly)) {
        qDebug() << "file open error:" << out.filename;
        goto end;
    }

    while ((inLen = inFile.read(inDataArray, IN_DATA_SIZE)) > 0) {
        inData = inDataArray;

        while (inLen > 0) {
            // 经过解析器解析
            // 内部调用的核心逻辑是:ff_aac_ac3_parse
            ret = av_parser_parse2(parserCtx, ctx,
                                   &pkt->data, &pkt->size,
                                   (uint8_t *) inData, inLen,
                                   AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);

            if (ret < 0) {
                ERROR_BUF(ret);
                qDebug() << "av_parser_parse2 error" << errbuf;
                goto end;
            }

            // 跳过已经解析过的数据
            inData += ret;
            // 减去已经解析过的数据大小
            inLen -= ret;

            // 解码
            if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
                goto end;
            }
        }
    }
    decode(ctx, nullptr, frame, outFile);

    // 赋值输出参数
    out.sampleRate = ctx->sample_rate;
    out.sampleFmt = ctx->sample_fmt;
    out.chLayout = ctx->channel_layout;

end:
    inFile.close();
    outFile.close();
    av_packet_free(&pkt);
    av_frame_free(&frame);
    av_parser_close(parserCtx);
    avcodec_free_context(&ctx);
}

void AacDecodeThread::run()
{
    AudioDecodeSpec out;
    out.filename = "E:/media/test.pcm";

    aacDecode("E:/media/test.aac", out);

    qDebug() << "采样率:" << out.sampleRate;
    qDebug() << "采样格式:" << av_get_sample_fmt_name(out.sampleFmt);
    qDebug() << "声道数:" << av_get_channel_layout_nb_channels(out.chLayout);
}

注意:本文为个人记录,新手照搬可能会出现各种问题,请谨慎使用


码字不易,如果这篇博客对你有帮助,麻烦点赞收藏,非常感谢!有不对的地方

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在 MFC 框架下读取 AAC 音频文件并进行解码,可以使用开源的解码库如 FFMPEG 或者 OpenAL,这些库都提供了相应的 API 接口,可以在 MFC 中调用。 以下是一个简单的示例代码,使用 FFMPEG 库实现读取 AAC 音频文件并解码: ```c++ #include <iostream> #include <string> #include <vector> #include <fstream> #include <sstream> // FFmpeg 头文件 extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswresample/swresample.h> } int main() { const char* input_filename = "test.aac"; // AAC 音频文件路径 const char* output_filename = "output.pcm"; // 输出解码后的 PCM 数据路径 // 1. 初始化 FFmpeg 库 av_register_all(); avcodec_register_all(); // 2. 打开输入音频文件 AVFormatContext* format_ctx = nullptr; if (avformat_open_input(&format_ctx, input_filename, nullptr, nullptr) != 0) { std::cerr << "Error: could not open input file " << input_filename << std::endl; return -1; } // 3. 获取音频流信息 if (avformat_find_stream_info(format_ctx, nullptr) < 0) { std::cerr << "Error: could not find stream information" << std::endl; avformat_close_input(&format_ctx); return -1; } // 4. 查找音频流 int audio_stream_index = -1; for (unsigned int i = 0; i < format_ctx->nb_streams; i++) { if (format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { audio_stream_index = i; break; } } if (audio_stream_index == -1) { std::cerr << "Error: could not find audio stream" << std::endl; avformat_close_input(&format_ctx); return -1; } // 5. 获取音频解码器 AVCodecParameters* codecpar = format_ctx->streams[audio_stream_index]->codecpar; AVCodec* codec = avcodec_find_decoder(codecpar->codec_id); if (codec == nullptr) { std::cerr << "Error: could not find decoder for codec ID " << codecpar->codec_id << std::endl; avformat_close_input(&format_ctx); return -1; } // 6. 打开音频解码器 AVCodecContext* codec_ctx = avcodec_alloc_context3(codec); if (avcodec_parameters_to_context(codec_ctx, codecpar) < 0) { std::cerr << "Error: could not copy codec parameters to decoder context" << std::endl; avcodec_free_context(&codec_ctx); avformat_close_input(&format_ctx); return -1; } if (avcodec_open2(codec_ctx, codec, nullptr) < 0) { std::cerr << "Error: could not open decoder" << std::endl; avcodec_free_context(&codec_ctx); avformat_close_input(&format_ctx); return -1; } // 7. 初始化音频重采样器 SwrContext* swr_ctx = swr_alloc_set_opts(nullptr, codec_ctx->channel_layout, AV_SAMPLE_FMT_S16, codec_ctx->sample_rate, codec_ctx->channel_layout, codec_ctx->sample_fmt, codec_ctx->sample_rate, 0, nullptr); if (swr_ctx == nullptr) { std::cerr << "Error: could not allocate resampler context" << std::endl; avcodec_close(codec_ctx); avcodec_free_context(&codec_ctx); avformat_close_input(&format_ctx); return -1; } if (swr_init(swr_ctx) < 0) { std::cerr << "Error: could not initialize resampler context" << std::endl; swr_free(&swr_ctx); avcodec_close(codec_ctx); avcodec_free_context(&codec_ctx); avformat_close_input(&format_ctx); return -1; } // 8. 打开输出文件 std::ofstream output_file(output_filename, std::ios::binary); if (!output_file.is_open()) { std::cerr << "Error: could not open output file " << output_filename << std::endl; swr_free(&swr_ctx); avcodec_close(codec_ctx); avcodec_free_context(&codec_ctx); avformat_close_input(&format_ctx); return -1; } // 9. 解码音频数据 AVPacket packet; av_init_packet(&packet); packet.data = nullptr; packet.size = 0; AVFrame* frame = av_frame_alloc(); while (av_read_frame(format_ctx, &packet) >= 0) { if (packet.stream_index == audio_stream_index) { if (avcodec_send_packet(codec_ctx, &packet) == 0) { while (avcodec_receive_frame(codec_ctx, frame) == 0) { // 重采样音频数据 std::vector<uint8_t> buffer(codec_ctx->channels * frame->nb_samples * av_get_bytes_per_sample(AV_SAMPLE_FMT_S16)); uint8_t* output_data[1] = { buffer.data() }; int output_samples = swr_convert(swr_ctx, output_data, frame->nb_samples, (const uint8_t**)frame->extended_data, frame->nb_samples); if (output_samples > 0) { output_file.write((char*)buffer.data(), output_samples * codec_ctx->channels * av_get_bytes_per_sample(AV_SAMPLE_FMT_S16)); } } } } av_packet_unref(&packet); } // 10. 释放资源 av_frame_free(&frame); swr_free(&swr_ctx); avcodec_close(codec_ctx); avcodec_free_context(&codec_ctx); avformat_close_input(&format_ctx); return 0; } ``` 该示例程序会把读取的 AAC 音频文件解码并重采样成 16 位 PCM 数据,然后保存到输出文件中。你可以根据需要修改代码,以满足你的具体需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值