28

FFmpeg 接口使用 - 音频编码和音频解码

 3 years ago
source link: http://blog.danthought.com/programming/2020/07/14/ffmpeg-api-audio-encode-decode/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

FFmpeg 接口使用 - 音频编码和音频解码

本文讲解如何通过 FFmpeg 将 AAC 解码为 PCM,把 PCM 编码为 AAC。

Editing Video

通过 iOS 利用 AudioToolbox 将 PCM 编码为 AAC 来了解数字音频、PCM 音频格式和 AAC 音频格式这些理论知识,再通过 FFmpeg 接口使用 - 基础和转封装 来了解如何在 macOS 上交叉编译 FFmpeg,再将其集成到 Xcode 中,以及使用 FFmpeg 接口时会用到的常用结构。

第一步,API 注册

首先在使用 FFmpeg 接口之前,需要进行 FFmpeg 使用接口的注册操作:

avcodec_register_all(); // 注册所有可用的 encoder 和 decoder
av_register_all(); // 注册所有可用的 muxer, demuxer 和 protocol

第二步,构建输入 AVFormatContext

注册之后,打开输入文件并与 AVFormatContext 建立关联:

int result = avformat_open_input(&avFormatContext, audioFile, NULL, NULL);
if (result != 0) {
    printf("can't open file %s result is %d\n", audioFile, result);
    return -1;
} else {
    printf("open file %s success and result is %d\n", audioFile, result);
}

第三步,查找流信息

建立关联之后,与解封装操作类似,可以通过接口 avformat_find_stream_info 获得流信息:

result = avformat_find_stream_info(avFormatContext, NULL); // 检查在文件中的流的信息
if (result < 0) {
    printf("fail avformat_find_stream_info result is %d\n", result);
    return -1;
} else {
    printf("sucess avformat_find_stream_info result is %d\n", result);
}

第四步,得到输入音频流

从输入的 AVFormatContext 中得到 AVStream:

stream_index = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0); // 寻找音频流下标
AVStream *audioStream = avFormatContext->streams[stream_index]; // 得到音频流

第五步,查找解码器

从 AVStream 获得解码器上下文,再根据 codec_id 找到解码器:

avCodecContext = audioStream->codec; // 获得音频流的解码器上下文
AVCodec *avCodec = avcodec_find_decoder(avCodecContext->codec_id); // 根据解码器上下文找到解码器

第六步,打开解码器

result = avcodec_open2(avCodecContext, avCodec, NULL);
if (result < 0) {
    printf("fail avcodec_open2 result is %d\n", result);
    return -1;
} else {
    printf("sucess avcodec_open2 result is %d\n", result);
}

第七步,根据情况初始化音频转换上下文

这里判断输入音频的量化格式的表示格式是否 AV_SAMPLE_FMT_S16 来决定是否初始化音频转换上下文,这里情况是将 AV_SAMPLE_FMT_FLTP 转换为 AV_SAMPLE_FMT_S16:

bool AudioDecoder::audioCodecIsSupported() {
    if (avCodecContext->sample_fmt == AV_SAMPLE_FMT_S16) {
        return true;
    }
    return false;
}

if (!audioCodecIsSupported()) {
    printf("because of audio Codec Is Not Supported so we will init swresampler...\n");
    swrContext = swr_alloc_set_opts(NULL, av_get_default_channel_layout(OUT_PUT_CHANNELS), AV_SAMPLE_FMT_S16, avCodecContext->sample_rate, av_get_default_channel_layout(avCodecContext->channels), avCodecContext->sample_fmt, avCodecContext->sample_rate, 0, NULL);
    if (!swrContext || swr_init(swrContext)) {
        if (swrContext) {
            swr_free(&swrContext);
        }
        avcodec_close(avCodecContext); // 调用对应第三方库的 API 来关闭掉对应的编码库
        printf("init resampler failed...\n");
        return -1;
    }
}

第八步,解码音频数据

使用 av_read_frame 读取 AVPacket,通过 avcodec_decode_audio4 将 AVPacket 解码为 AVFrame,如果需要音频转换,通过 swr_convert 进行转换,解码后的音频数据放在 audioBuffer 中,其大小是 audioBufferSize:

int AudioDecoder::readFrame() {
    int ret = 1;
    av_init_packet(&packet);
    int gotframe = 0;
    int readFrameCode = -1;
    while (true) {
        /*
         * 内部处理了数据不能被解码器完全处理完的情况,
         * 该函数的实现首先会委托到 Demuxer 的 read_packet 方法中去,
         * 对于音频流,一个 AVPacket 可能包含多个 AVFrame,
         * 但是对于视频流,一个 AVPacket 只包含一个 AVFrame,
         * 该函数最终只会返回一个 AVPacket 结构体。
         */
        readFrameCode = av_read_frame(avFormatContext, &packet);
        if (readFrameCode >= 0) {
            if (packet.stream_index == stream_index) {
                int len = avcodec_decode_audio4(avCodecContext, pAudioFrame, &gotframe, &packet); // 音频解码
                if (len < 0) {
                    printf("decode audio error, skip packet\n");
                }
                if (gotframe) {
                    int numChannels = OUT_PUT_CHANNELS;
                    int numFrames = 0;
                    void *audioData;
                    if (swrContext) {
                        const int bufSize = av_samples_get_buffer_size(NULL, numChannels, pAudioFrame->nb_samples, AV_SAMPLE_FMT_S16, 1);
                        if (!swrBuffer || swrBufferSize < bufSize) {
                            swrBufferSize = bufSize;
                            swrBuffer = realloc(swrBuffer, swrBufferSize);
                        }
                        uint8_t *outbuf[2] = { (uint8_t*)swrBuffer, NULL };
                        numFrames = swr_convert(swrContext, outbuf, pAudioFrame->nb_samples, (const uint8_t **)pAudioFrame->data, pAudioFrame->nb_samples);
                        if (numFrames < 0) {
                            printf("fail resample audio\n");
                            ret = -1;
                            break;
                        }
                        audioData = swrBuffer;
                    } else {
                        if (avCodecContext->sample_fmt != AV_SAMPLE_FMT_S16) {
                            printf("bucheck, audio format is invalid\n");
                            ret = -1;
                            break;
                        }
                        audioData = pAudioFrame->data[0];
                        numFrames = pAudioFrame->nb_samples;
                    }
                    if (isNeedFirstFrameCorrectFlag && position >= 0) {
                        float expectedPosition = position + duration;
                        float actualPosition = av_frame_get_best_effort_timestamp(pAudioFrame) * timeBase;
                        firstFrameCorrectionInSecs = actualPosition - expectedPosition;
                        isNeedFirstFrameCorrectFlag = false;
                    }
                    duration = av_frame_get_pkt_duration(pAudioFrame) * timeBase;
                    position = av_frame_get_best_effort_timestamp(pAudioFrame) * timeBase - firstFrameCorrectionInSecs;
                    audioBufferSize = numFrames * numChannels;
                    audioBuffer = (short *)audioData;
                    audioBufferCursor = 0;
                    break;
                }
            }
        } else {
            ret = -1;
            break;
        }
    }
    av_free_packet(&packet);
    return ret;
}

第九步,把解码后的音频数据写到文件

先把上面解码后的音频数据装到一个大缓冲里:

int AudioDecoder::readSamples(short *samples, int size) {
    int sampleSize = size;
    while (size > 0) {
        if (audioBufferCursor < audioBufferSize) {
            int audioBufferDataSize = audioBufferSize - audioBufferCursor;
            int copySize = MIN(size, audioBufferDataSize);
            memcpy(samples + (sampleSize - size), audioBuffer + audioBufferCursor, copySize * 2); // the last param is number of bytes to copy
            size -= copySize;
            audioBufferCursor += copySize;
        } else {
            if (readFrame() < 0) {
                break;
            }
        }
    }
    int fillSize = sampleSize - size;
    if (fillSize == 0) {
        return -1;
    }
    return fillSize;
}

再把这个大缓冲写到文件:

while (true) {
    AudioPacket *packet = decoder->decodePacket();
    if (packet->size == -1) {
        break;
    }
    fwrite(packet->buffer, sizeof(short), packet->size, outputFile);
}

第十步,收尾

释放各种资源:

void AudioDecoder::destroy() {
    printf("AudioDecoder start destroy!!!\n");
    if (NULL != swrBuffer) {
        free(swrBuffer);
        swrBuffer = NULL;
        swrBufferSize = 0;
    }
    if (NULL != swrContext) {
        swr_free(&swrContext);
        swrContext = NULL;
    }
    if (NULL != pAudioFrame) {
        av_free(pAudioFrame);
        pAudioFrame = NULL;
    }
    if (NULL != avCodecContext) {
        avcodec_close(avCodecContext);
        avCodecContext = NULL;
    }
    if (NULL != avFormatContext) {
        /*
         * 该函数负责释放对应的资源,
         * 首先会调用对应的 Demuxer 中的生命周期 read_close 方法,
         * 然后释放掉 AVFormatContext,
         * 最后关闭文件或者远程网络连接。
         */
        avformat_close_input(&avFormatContext);
        avFormatContext = NULL;
    }
    printf("AudioDecoder end destroy!!!\n");
}

第一步,API 注册

首先在使用 FFmpeg 接口之前,需要进行 FFmpeg 使用接口的注册操作:

avcodec_register_all(); // 注册所有可用的 encoder 和 decoder
av_register_all(); // 注册所有可用的 muxer, demuxer 和 protocol

第二步,构建输出 AVFormatContext

可以打开输出文件并与 AVFormatContext 建立关联:

/*
 * 最关键的还是根据上一步注册的 Muxer 和 Demuxer 部分(也就是封装格式部分)去找到对应的格式,
 * 有可能是 flv 格式、MP4 格式、mov 格式,甚至是 MP3 格式等,
 * 如果找不到对应的格式(即在 configure 选项中没有打开这个格式的开关),那么这里会返回找不到对应的格式的错误提示。
 */
if ((ret = avformat_alloc_output_context2(&avFormatContext, NULL, NULL, aacFilePath)) != 0 ) {
    printf("avFormatContext   alloc   failed : %s\n", av_err2str(ret));
    return -1;
}

第三步,打开输出 AVFormatContext 的缓冲区

/*
 * 首先调用函数 ffurl_open,构造出 URLContext 结构体,这个结构体中包含了 URLProtocol(需要去第一步 register_protocol 中已经注册的协议链表中寻找),
 * 接着会调用 avio_alloc_context 方法,分配出 AVIOContext 结构体,并将上一步构造出来的 URLProtocol 传递进来,
 * 然后把上一步分配出来 AVIOContext 结构体赋值给 AVFormatContext 的属性。
 */
if (ret = avio_open2(&avFormatContext->pb, aacFilePath, AVIO_FLAG_WRITE, NULL, NULL)) {
    printf("Could not avio open fail %s\n", av_err2str(ret));
    return -1;
}

第四步,申请 AVStream

audioStream = avformat_new_stream(avFormatContext, NULL);
audioStream->id = 1;

第五步,获取 AVCodecContext

从 AVStream 获得编码器上下文,设置编码参数:

avCodecContext = audioStream->codec;
avCodecContext->codec_type = AVMEDIA_TYPE_AUDIO; // 基本属性 - 音频类型
avCodecContext->sample_rate = audioSampleRate; // 基本属性 - 采样率
if (publishBitRate > 0) { // 基本属性 - 码率
    avCodecContext->bit_rate = publishBitRate;
} else {
    avCodecContext->bit_rate = PUBLISH_BITE_RATE;
}
avCodecContext->sample_fmt = preferedSampleFMT; // 基本属性 - 量化格式
printf("audioChannels is %d\n", audioChannels);
avCodecContext->channel_layout = preferedChannels == 1 ? AV_CH_LAYOUT_MONO : AV_CH_LAYOUT_STEREO; // 基本属性 - 声道
avCodecContext->channels = av_get_channel_layout_nb_channels(avCodecContext->channel_layout); // 基本属性 - 声道
avCodecContext->profile = FF_PROFILE_AAC_LOW;
printf("avCodecContext->channels is %d\n", avCodecContext->channels);
avCodecContext->flags |= CODEC_FLAG_GLOBAL_HEADER;

第六步,查找编码器

根据编码器名称查找编码器:

codec = avcodec_find_encoder_by_name(codec_name); // 寻找 encoder
if (!codec) {
    printf("Couldn't find a valid audio codec\n");
    return -1;
}
avCodecContext->codec_id = codec->id;

第七步,根据情况初始化音频转换上下文

如果期望输入的 PCM 格式和实际输入的 PCM 格式有差异,就需要初始化音频转换上下文:

// 有些编码器只允许特定格式的 PCM 作为输入源,所以有时需要构造一个重采样器来将 PCM 数据转换为可适配编码器输入的 PCM 数据
if (preferedChannels != avCodecContext->channels
    || preferedSampleRate != avCodecContext->sample_rate
    || preferedSampleFMT != avCodecContext->sample_fmt) {
    swrContext = swr_alloc_set_opts(NULL,
                                    av_get_default_channel_layout(avCodecContext->channels),
                                    avCodecContext->sample_fmt,
                                    avCodecContext->sample_rate,
                                    av_get_default_channel_layout(preferedChannels),
                                    preferedSampleFMT,
                                    preferedSampleRate,
                                    0,
                                    NULL);
    if (!swrContext || swr_init(swrContext)) {
        if (swrContext) {
            swr_free(&swrContext);
        }
        return -1;
    }
}

第八步,打开编码器

if (avcodec_open2(avCodecContext, codec, NULL) < 0) {
    printf("Couldn't open codec\n");
    return -2;
}

第九步,申请帧结构 AVFrame

申请音频帧存储空间,配置音频参数,根据音频参数计算出缓冲大小,再创建缓冲,把缓冲和帧结构关联起来:

int ret = 0;
AVSampleFormat preferedSampleFMT = AV_SAMPLE_FMT_S16;
int preferedChannels = audioChannels;
int preferedSampleRate = audioSampleRate;
input_frame = av_frame_alloc();
if (!input_frame) {
    printf("Could not allocate audio frame\n");
    return -1;
}
input_frame->nb_samples = avCodecContext->frame_size;
input_frame->format = preferedSampleFMT;
input_frame->channel_layout = preferedChannels == 1 ? AV_CH_LAYOUT_MONO : AV_CH_LAYOUT_STEREO;
input_frame->sample_rate = preferedSampleRate;
buffer_size = av_samples_get_buffer_size(NULL, av_get_channel_layout_nb_channels(input_frame->channel_layout), input_frame->nb_samples, preferedSampleFMT, 0); // 计算公式 frame_size * sizeof(SInt16) * channels
samples = (uint8_t*)av_malloc(buffer_size);
samplesCursor = 0;
if (!samples) {
    printf("Could not allocate %d bytes for samples buffer\n", buffer_size);
    return -2;
}
printf("allocate %d bytes for samples buffer\n", buffer_size);
ret = avcodec_fill_audio_frame(input_frame, av_get_channel_layout_nb_channels(input_frame->channel_layout), preferedSampleFMT, samples, buffer_size, 0);
if (ret < 0) {
    printf("Could not setup audio frame\n");
}

第十步,申请用于转换的帧结构 AVFrame

如果需要音频转换,还需要额外申请用于转换的帧结构 AVFrame:

if (swrContext) {
    if (av_sample_fmt_is_planar(avCodecContext->sample_fmt)) {
        printf("Codec Context SampleFormat is Planar...\n");
    }
    convert_data = (uint8_t**)calloc(avCodecContext->channels, sizeof(*convert_data));
    av_samples_alloc(convert_data, NULL, avCodecContext->channels, avCodecContext->frame_size, avCodecContext->sample_fmt, 0);
    swrBufferSize = av_samples_get_buffer_size(NULL, avCodecContext->channels, avCodecContext->frame_size, avCodecContext->sample_fmt, 0);
    swrBuffer = (uint8_t*)av_malloc(swrBufferSize);
    printf("After av_malloc swrBuffer\n");
    swrFrame = av_frame_alloc();
    if (!swrFrame) {
        printf("Could not allocate swrFrame frame\n");
        return -1;
    }
    swrFrame->nb_samples = avCodecContext->frame_size;
    swrFrame->format = avCodecContext->sample_fmt;
    swrFrame->channel_layout = avCodecContext->channels == 1 ? AV_CH_LAYOUT_MONO : AV_CH_LAYOUT_STEREO;
    swrFrame->sample_rate = avCodecContext->sample_rate;
    ret = avcodec_fill_audio_frame(swrFrame, avCodecContext->channels, avCodecContext->sample_fmt, (uint8_t*)swrBuffer, swrBufferSize, 0);
    printf("After avcodec_fill_audio_frame\n");
    if (ret < 0) {
        printf("avcodec_fill_audio_frame error\n");
        return -1;
    }
}

第十一步,编码音频数据

把读入的缓冲数据根据帧结构的缓冲大小,分批拷贝到帧结构的缓冲:

void AudioEncoder::encode(uint8_t *buffer, int size) {
    int bufferCursor = 0;
    int bufferSize = size;
    while (bufferSize >= (buffer_size - samplesCursor)) {
        int cpySize = buffer_size - samplesCursor;
        memcpy(samples + samplesCursor, buffer + bufferCursor, cpySize);
        bufferCursor += cpySize;
        bufferSize -= cpySize;
        this->encodePacket();
        samplesCursor = 0;
    }
    if (bufferSize > 0) {
        memcpy(samples + samplesCursor, buffer + bufferCursor, bufferSize);
        samplesCursor += bufferSize;
    }
}

如果需要音频转换,先进行音频转换:

int ret, got_output;
AVPacket pkt;
av_init_packet(&pkt);
AVFrame* encode_frame;
if (swrContext) {
    swr_convert(swrContext, convert_data, avCodecContext->frame_size, (const uint8_t**)input_frame->data, avCodecContext->frame_size);
    int length = avCodecContext->frame_size * av_get_bytes_per_sample(avCodecContext->sample_fmt);
    for (int k = 0; k < 2; ++k) {
        for (int j = 0; j < length; ++j) {
            swrFrame->data[k][j] = convert_data[k][j];
        }
    }
    encode_frame = swrFrame;
} else {
    encode_frame = input_frame;
}
pkt.stream_index = 0;
pkt.duration = (int)AV_NOPTS_VALUE;
pkt.pts = pkt.dts = 0;
pkt.data = samples;
pkt.size = buffer_size;

编码 AVFrame 为 AVPacket,再将 AVPacket 写入文件:

ret = avcodec_encode_audio2(avCodecContext, &pkt, encode_frame, &got_output); // 编码音频
if (ret < 0) {
    printf("Error encoding audio frame\n");
    return;
}
if (got_output) {
    if (avCodecContext->coded_frame && avCodecContext->coded_frame->pts != AV_NOPTS_VALUE) {
        pkt.pts = av_rescale_q(avCodecContext->coded_frame->pts, avCodecContext->time_base, audioStream->time_base);
    }
    pkt.flags |= AV_PKT_FLAG_KEY;
    this->duration = pkt.pts * av_q2d(audioStream->time_base);
    /*
     * 会将编码后的 AVPacket 结构体作为 Muxer 中的 write_packet 生命周期方法的输入,
     * write_packet函数会加上自己封装格式的头信息,然后调用协议层写到本地文件或者网络服务器上。
     */
    int writeCode = av_interleaved_write_frame(avFormatContext, &pkt); // 写入文件
}
av_free_packet(&pkt);

可以利用 iOS Audio Unit 录制音频文件和播放音频文件iOS AVAudioEngine 录制音频文件和播放音频文件 录制 PCM 音频文件,然后参考 iOS 利用 AudioToolbox 将 PCM 编码为 AAC 得到 AAC 音频文件,接着就可以用上面代码将 AAC 解码成 PCM,再将 PCM 编码成 AAC。

ffplay

ffplay 播放 pcm 音频数据,要指定量化格式的表示格式、声道数和采样率,s16le 的含义是 signed 16 bits little endian:

$ ffplay audio.pcm -f s16le -channels 2 -ar 44100

ffprobe

$ ffprobe -show_format -pretty audio.aac
Input #0, aac, from 'audio.aac':
  Duration: 00:00:03.50, bitrate: 134 kb/s
    Stream #0:0: Audio: aac (LC), 44100 Hz, stereo, fltp, 134 kb/s
[FORMAT]
filename=audio.aac
nb_streams=1
nb_programs=0
format_name=aac
format_long_name=raw ADTS AAC (Advanced Audio Coding)
start_time=N/A
duration=0:00:03.496978
size=57.210938 Kibyte
bit_rate=134.022000 Kbit/s
probe_score=51
[/FORMAT]

AACParser

AACParser 对每一个 ADTS Frame 的输出(截取了部分):

  Num|     Profile| Frequency|  Channel| Size|
    0|      AAC LC|   44100Hz|        2|  387|
    1|      AAC LC|   44100Hz|        2|  387|
    2|      AAC LC|   44100Hz|        2|  388|
    3|      AAC LC|   44100Hz|        2|  489|
    4|      AAC LC|   44100Hz|        2|  488|
    5|      AAC LC|   44100Hz|        2|  459|
    6|      AAC LC|   44100Hz|        2|  438|
    7|      AAC LC|   44100Hz|        2|  413|
    8|      AAC LC|   44100Hz|        2|  402|
    9|      AAC LC|   44100Hz|        2|  403|
   10|      AAC LC|   44100Hz|        2|  405|

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK