今天的主题是音频解码,主要实现将视频中的音频解码为音频采用格式pcm。
首先在文章开头给出两个问题
第一个问题:音频解码和视频解码目的是什么?为什么要进行音频压缩或者是视频压缩?
目的:压缩音频流、视频流、字幕流等等…(减小数据量)
第二个问题:音频采样数据作用?
保存音频中每一个采样点的值
我们来计算2分钟pcm音频的大小:
规定:采样率:44100HZ
在图像学中:每8位 = 1字节
编码(采样精度):16位 = 2字节
声道数量:2个
pcm格式体积 = 2 * 60 * 44100 * 2 * 2 = 21MB
mp3 = 2MB
从计算过程就可以看出,实际情况必须采用压缩来存储音频。
那么pcm有哪些格式,我们平常说的单声道,双声道又是什么回事?这里又分两种情况
第一种:单声道(左右声道)
第二种:双声道(排版顺序"左右","左右")
二者都是采样点顺序排版存储
那么音频解码的环节又是怎样的过程呢,参照官方给出的流程图
从上图我们可以看出音频解码流程
第一步:注册所有的组件(编解码、滤镜特效处理库、封装格式处理库、工具库、音频采样数据格式转换库、视频像素数据格式转换等等...)
第二步:获取音频封装格式信息
第三步:查找音频流
第四步:查找音频解码器
第五步:打开音频解码器
第六步:读取音频压缩数据进行解码(循环解码)
第七步:关闭音频解码器释放内存
源码
#include <jni.h>
#include <string>
//导入android-log日志
#include <android/log.h>
//当前C++兼容C语言
extern "C"{
//avcodec:编解码(最重要的库)
#include "libavcodec/avcodec.h"
//avformat:封装格式处理
#include "libavformat/avformat.h"
//avutil:工具库(大部分库都需要这个库的支持)
#include "libavutil/imgutils.h"
//swscale:视频像素数据格式转换
#include "libswscale/swscale.h"
//导入音频采样数据格式转换库
#include "libswresample/swresample.h"
JNIEXPORT void JNICALL Java_com_samychen_ffmpeg_FFmpegTest_ffmpegTest
(JNIEnv *, jobject);
JNIEXPORT void JNICALL Java_com_samychen_ffmpeg_FFmpegTest_ffmpegDecoder
(JNIEnv *env, jobject jobj,jstring jInFilePath,jstring jOutFilePath);
JNIEXPORT void JNICALL Java_com_samychen_ffmpeg_FFmpegTest_ffmpegDecoderAudio
(JNIEnv *env, jobject jobj,jstring jInFilePath,jstring jOutFilePath);
}
//1、NDK音视频编解码:FFmpeg-测试配置
JNIEXPORT void JNICALL Java_com_samychen_ffmpeg_FFmpegTest_ffmpegTest(
JNIEnv *env, jobject jobj) {
//(char *)表示C语言字符串
const char *configuration = avcodec_configuration();
__android_log_print(ANDROID_LOG_INFO,"main","%s",configuration);
}
//NDK音视频编解码:FFmpeg-音频解码-音频采样数据pcm格式
JNIEXPORT void JNICALL Java_com_samychen_ffmpeg_FFmpegTest_ffmpegDecoderAudio
(JNIEnv *env, jobject jobj,jstring jInFilePath,jstring jOutFilePath) {
//第一步:注册所有的组件
// (编解码、滤镜特效处理库、封装格式处理库、工具库、音频采样数据格式转换库、视频像素数据格式转换等等...)
av_register_all();
avcodec_register_all();
//第二步:获取音频封装格式信息
AVFormatContext *avformat_context = avformat_alloc_context();
//参数一:封装格式上下文->保存了音频信息
//参数二:输入文件(你要对那一个文件进行解封装)
//普及知识:env是JNI环境指针(作用:专门用于管理对象创建和销毁)
const char *cInFilePath = env->GetStringUTFChars(jInFilePath, NULL);
//参数三:封装格式类型(NULL:表示系统自动获取格式类型)
//返回值:avformat_open_input_result = 0表示成功,否则失败
int avformat_open_input_result = avformat_open_input(&avformat_context, cInFilePath, NULL,
NULL);
if (avformat_open_input_result != 0) {
char *error_info;
av_strerror(avformat_open_input_result, error_info, 1024);
__android_log_print(ANDROID_LOG_INFO, "main", "获取失败,错误信息:%s", error_info);
return;
}
//第三步:查找音频流
//返回值:>=0 if OK, AVERROR_xxx on error(>=0表示成功,否则失败)
int avformat_find_stream_info_result = avformat_find_stream_info(avformat_context, NULL);
if (avformat_find_stream_info_result < 0) {
char *error_info;
av_strerror(avformat_find_stream_info_result, error_info, 1024);
__android_log_print(ANDROID_LOG_INFO, "main", "查找失败,错误信息:%s", error_info);
return;
}
//第四步:查找音频解码器
//第一点:查找音频流索引位置
int av_stream_index_audio = -1;
for (int i = 0; i < avformat_context->nb_streams; ++i) {
if (avformat_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
av_stream_index_audio = i;
break;
}
}
if (av_stream_index_audio == -1) {
__android_log_print(ANDROID_LOG_INFO, "main", "没有找到音频流");
return;
}
//第二点:查找音频解码器上下文(根据流索引位置->获取音频解码买上下文)
//新的API
// AVCodecParameters *avcodec_parameters = avformat_context->streams[av_stream_index_audio]->codecpar;
// avcodec_parameters->codec_id;
//老的API
AVCodecContext *avcodec_context = avformat_context->streams[av_stream_index_audio]->codec;
//第三点:根据音频解码器上下文->获取到->音频解码器
AVCodec *avcodec = avcodec_find_decoder(avcodec_context->codec_id);
if (avcodec == NULL) {
__android_log_print(ANDROID_LOG_INFO, "main", "找不到这个音频解码器");
return;
}
//第五步:打开音频解码器(调试运行->观察锁定C/C++基于NDK开发异常->便于调试运行)
int avcodec_open2_result = avcodec_open2(avcodec_context, avcodec, NULL);
if (avcodec_open2_result != 0) {
char *error_info;
av_strerror(avcodec_open2_result, error_info, 1024);
__android_log_print(ANDROID_LOG_INFO, "main", "打开音频解码器失败,错误信息:%s", error_info);
return;
}
//打印音频信息
//输出视频信息
//输出:文件格式
__android_log_print(ANDROID_LOG_INFO, "main", "文件格式:%s", avformat_context->iformat->name);
//输出:解码器名称
__android_log_print(ANDROID_LOG_INFO, "main", "解码器名称:%s", avcodec->name);
//第六步:读取音频压缩数据进行解码(循环解码)
//读取一帧音频压缩数据(缓冲区)
AVPacket *av_packet = (AVPacket *) av_malloc(sizeof(AVPacket));
//接收一帧音频采样数据(缓冲区)
AVFrame *av_frame_in = av_frame_alloc();
//音频解码返回结果
int avcodec_receive_frame_result;
//音频采样数据上下文->SwrContext
//第一点:创建上下文->开辟内存空间(声明)
SwrContext *swr_context = swr_alloc();
//第二点:给我们的音频采样数据上下文->绑定数据
//参数一:音频采样数据上下文
//参数二:输出声道布局类型(立体声、环绕、室内等等...)
//立体声
int out_ch_layout = AV_CH_LAYOUT_STEREO;
//参数三:输出音频采样数据格式(说白了:采样精度)
AVSampleFormat av_sample_format = AV_SAMPLE_FMT_S16;
//参数四:输出音频采样数据->采样率
int out_sample_rate = avcodec_context->sample_rate;
//参数五:输入声道布局类型(立体声、环绕、室内等等...)->默认格式
int in_ch_layout = av_get_default_channel_layout(avcodec_context->channels);
//参数六:输入音频采样数据格式(说白了:采样精度)
AVSampleFormat in_sample_fmt = avcodec_context->sample_fmt;
//参数七:输入音频采样数据->采样率
int in_sample_rate = avcodec_context->sample_rate;
//参数八:Log日志偏移量
//参数九:Log日志统计上下文
swr_alloc_set_opts(swr_context,
out_ch_layout,
av_sample_format,
out_sample_rate,
in_ch_layout,
in_sample_fmt,
in_sample_rate,
0, NULL);
//输出音频采样数据缓冲区(目标)->人的耳朵最大采样率->44100HZ
int out_count = 16000;
uint8_t *out_buffer = (uint8_t *) av_malloc(out_count);
int pktsize, flush_complete = 0;
//获取声道数量
int out_nb_layout = av_get_channel_layout_nb_channels(out_ch_layout);
//打开文件
const char *coutputFilePath = env->GetStringUTFChars(jOutFilePath, NULL);
FILE *out_file_pcm = fopen(coutputFilePath, "wb+");
if (out_file_pcm == NULL) {
__android_log_print(ANDROID_LOG_INFO, "main", "文件不存在");
return;
}
int frame_index = 0;
// while (av_read_frame(avformat_context, av_packet) == 0) {
// ++frame_index;
// if (av_packet->stream_index == av_stream_index_audio) {
// out_buffer = av_packet->data;
// pktsize = av_packet->size;
// int frameFinished = 0;
// int len = avcodec_decode_audio4(avcodec_context, av_frame_in, &frameFinished,
// av_packet);
// if (frameFinished) {
// pktsize -= len;
// out_buffer += len;
// int data_size = av_samples_get_buffer_size(NULL,
// out_nb_layout,
// av_frame_in->nb_samples,
// av_sample_format,
// 1);
// /*****************************************************
// 以下代码使用swr_convert函数进行转换,但是转换后的文件连mp3到pcm文件都不能播放了,所以注释了
// const uint8_t *in[] = {frame->data[0]};
//
// int len=swr_convert(swrContext,out,sizeof(audio_buf)/codecContext->channels/av_get_bytes_per_sample(AV_SAMPLE_FMT_S16P),
// in,frame->linesize[0]/codecContext->channels/av_get_bytes_per_sample(codecContext->sample_fmt));
//
// len=len*codecContext->channels*av_get_bytes_per_sample(AV_SAMPLE_FMT_S16P);
//
// if (len < 0) {
// fprintf(stderr, "audio_resample() failed\n");
// break;
// }
// if (len == sizeof(audio_buf) / codecContext->channels / av_get_bytes_per_sample(AV_SAMPLE_FMT_S16P)) {
// fprintf(stderr, "warning: audio buffer is probably too small\n");
// swr_init(swrContext);
// }
// *****************************************************/
// char *data = (char *) malloc(data_size);
// short *sample_buffer = (short *) av_frame_in->data[0];
// for (int i = 0; i < data_size / 2; i++) {
// data[i * 2] = (char) (sample_buffer[i / 2] & 0xFF);
// data[i * 2 + 1] = (char) ((sample_buffer[i / 2] >> 8) & 0xFF);
//
// }
// fwrite(data, data_size, 1, out_file_pcm);
// fflush(out_file_pcm);
// }
// }
//返回值:<0表示读取完毕,否则正在读取
while (av_read_frame(avformat_context, av_packet) >= 0) {
//判定当前帧是否是音频流->音频采样数据
if (av_packet->stream_index == av_stream_index_audio) {
//确定是我们的音频流->解码
//解码一帧音频流数据
//老的API
//avcodec_decode_audio4();
//新的API(发送->接收)
//发送
avcodec_send_packet(avcodec_context, av_packet);
//接收
avcodec_receive_frame_result = avcodec_receive_frame(avcodec_context, av_frame_in);
if (avcodec_receive_frame_result == 0) {
//解码一帧音频压缩数据成功->得到了->一帧音频采样数据
//音频采样数据->转成pcm格式
//将输入->输出(pcm格式)
//参数一:音频采样数据上下文->SwrContext
//参数二:输出音频采样数据缓冲区(目标)
//参数三:输出缓冲区大小
//参数四:输入音频采样数据缓冲区
//参数五:输入缓冲区大小
swr_convert(swr_context,
&out_buffer,
out_count,
(const uint8_t **) av_frame_in->data,
av_frame_in->nb_samples);
//获取缓冲区实际数据大小
//参数一:行大小
//参数二:声道数量
//参数三:输出大小
//参数四:输出音频采样数据格式
//参数五:字节对齐类型
int out_buffer_size = av_samples_get_buffer_size(NULL,
out_nb_layout,
av_frame_in->nb_samples,
av_sample_format,
1);
//写入文件
fwrite(av_frame_in->data[0], 1, out_buffer_size, out_file_pcm);
fflush(out_file_pcm);
frame_index++;
__android_log_print(ANDROID_LOG_INFO, "main", "当前是第%d帧", frame_index);
__android_log_print(ANDROID_LOG_INFO, "main", "当前是第%d帧", out_buffer);
}
}
}
//第七步:关闭音频解码器释放内存
av_packet_free(&av_packet);
//关闭流
fclose(out_file_pcm);
swr_free(&swr_context);
av_free(out_buffer);
avcodec_close(avcodec_context);
avformat_free_context(avformat_context);
}
是不是和视频解码大体流程完全一样,唯一的区别就是视频像素数据格式的转换和音频采样数据的重新采样了。
上述代码实现完成之后可以在pc端下载Adobe audition cs6来直接播放