在之前的文章中,我们有讲解了FFmpeg的音频的解码流程:FFmpeg音频解码流程详解,本文于此基础上,讲解在Android平台上对mp3文件进行解码并播放。本文例子使用AudioTrack来对音频解码后的数据进行播放。
一、音频解码播放流程图
与音频的解码流程基本一致,只是最终不是保存为文件,而是回调播放:
二、音频解码播放整体流程
1、注册各大组件
先注册ffmpeg相关的各大组件的
//注册各大组件
av_register_all();
LOGE("注册成功")
2、打开音频文件并获取相关上下文
在解码播放之前我们得获取里面的内容,这一步就是打开地址并且获取里面的内容。其中avFormatContext是内容的一个上下文。
并使用avformat_open_input打开播放源,inputPath为输入的地址,也就是音频文件,然后使用avformat_find_stream_info从获取的内容中寻找相关流。
AVFormatContext *avFormatContext = avformat_alloc_context();//获取上下文
int error;
//打开音频地址并获取里面的内容(解封装)
error = avformat_open_input(&avFormatContext, inputPath, NULL, NULL);
if (error < 0){
LOGE("打开音频文件失败\n");
return;
}
if (avformat_find_stream_info(avFormatContext, NULL) < 0){
LOGE("获取内容失败")
return;
}
3、寻找音频流
我们在上面已经获取了内容,我们再从中找出相对应的音频流。
//获取音频的编码信息
int mAudioStreamIdx = -1;
for (int i = 0; i < avFormatContext->nb_streams; ++i) {
if (avFormatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
LOGE(" 找到音频id %d", avFormatContext->streams[i]->codec->codec_type);
mAudioStreamIdx=i;
break;
}
}
4、获取并打开解码器
如果要进行解码,那么得有解码器并打开解码器。在这一步中我加了个过滤器的相关配置,这个东西不是非必要的,所以我加了个宏ABSFILTER_ENABLE可以选择是否打开。
// 寻找解码器 {start
//获取解码器上下文
AVCodecContext *mAvContext=avFormatContext->streams[mAudioStreamIdx]->codec;
//获取解码器
AVCodec *mAcodec = NULL;
mAcodec = avcodec_find_decoder(mAvContext->codec_id);
#if ABSFILTER_ENABLE
//过滤器相关配置,这个与音频码流格式相关,也可以不用
const AVBitStreamFilter * absFilter = NULL;
AVBSFContext *absCtx = NULL;
AVCodecParameters *codecpar = NULL;
absFilter = av_bsf_get_by_name("mp3decomp");
//过滤器分配内存
av_bsf_alloc(absFilter, &absCtx);
//添加解码器属性
codecpar = avFormatContext->streams[mAudioStreamIdx]->codecpar;
avcodec_parameters_copy(absCtx->par_in, codecpar);
absCtx->time_base_in = avFormatContext->streams[mAudioStreamIdx]->time_base;
//初始化过滤器上下文
av_bsf_init(absCtx);
#endif
// 打开解码器
if (avcodec_open2(mAvContext, mAcodec, NULL) != 0){
LOGE("打开失败")
return;
}
LOGE("解码器打开成功")
// 寻找解码器 end}
5、申请AVPacket和AVFrame以及相关设置
申请AVPacket和AVFrame,其中AVPacket的作用是:保存解码之前的数据和一些附加信息等;AVFrame的作用是:存放解码过后的数据。
//申请AVPacket
AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
av_init_packet(packet);
//申请AVFrame
AVFrame *frame = av_frame_alloc();//分配一个AVFrame结构体,AVFrame结构体一般用于存储原始数据,指向解码后的原始帧
6、初始化SwrContext,进行重采样
配置解码输出的数据的重采样,这一步可以根据自己的需求对输出进行一个重采样的设置,也可以不要采用默认。一般会配置。
//得到SwrContext ,进行重采样 {start
SwrContext *swrContext = swr_alloc();
//缓存区
uint8_t *out_buffer = (uint8_t *) av_malloc(44100 * 2);
//输出的声道布局(立体声)
uint64_t out_ch_layout=AV_CH_LAYOUT_STEREO;
//输出采样位数 16位
enum AVSampleFormat out_formart=AV_SAMPLE_FMT_S16;
//输出的采样率必须与输入相同
int out_sample_rate = mAvContext->sample_rate;
//swr_alloc_set_opts将PCM源文件的采样格式转换为自己希望的采样格式
swr_alloc_set_opts(swrContext, out_ch_layout, out_formart, out_sample_rate,
mAvContext->channel_layout, mAvContext->sample_fmt, mAvContext->sample_rate, 0,
NULL);
swr_init(swrContext);
LOGE("设置重采样成功")
//end}
7、设置好与java层的反射回调
因为我们是使用java层的audioTrack进行音频数据播放的,因此在JNI层需要先设置好与java层接口的反射相关:
//获取通道数 2
int out_channer_nb = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO);
//反射得到Class类型
jclass david_player = env->GetObjectClass(thiz);
//反射得到createAudio方法
jmethodID createAudio = env->GetMethodID(david_player, "createTrack", "(II)V");
//反射调用createAudio
env->CallVoidMethod(thiz, createAudio, 44100, out_channer_nb);
//反射得到playTrack方法
jmethodID audio_write = env->GetMethodID(david_player, "playTrack", "([BI)V");
jni通过调用java层的audiotrack方法来实现播放, java层的播放代码是:
private AudioTrack audioTrack;
// 这个方法 是C进行调用 通道数
public void createTrack(int sampleRateInHz,int nb_channals) {
int channaleConfig;//通道数
if (nb_channals == 1) {
channaleConfig = AudioFormat.CHANNEL_OUT_MONO;
} else if (nb_channals == 2) {
channaleConfig = AudioFormat.CHANNEL_OUT_STEREO;
}else {
channaleConfig = AudioFormat.CHANNEL_OUT_MONO;
}
int buffersize=AudioTrack.getMinBufferSize(sampleRateInHz,
channaleConfig, AudioFormat.ENCODING_PCM_16BIT);
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,sampleRateInHz,channaleConfig,
AudioFormat.ENCODING_PCM_16BIT,buffersize,AudioTrack.MODE_STREAM);
audioTrack.play();
}
//C传入音频数据
public void playTrack(byte[] buffer, int lenth) {
if (audioTrack != null && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
audioTrack.write(buffer, 0, lenth);
}
}
8、开始解码播放
与音频解码核心段代码一致,只不过最终拿到的不是保存进文件,而是通过回调java层audioTrack接口进行播放。
另外这里有用了上面所说的过滤器的东西,我们暂时先不关心。
完整过程如下:
while(1)
{
int ret = av_read_frame(avFormatContext, packet);
if (ret != 0){
av_strerror(ret,buf,sizeof(buf));
LOGE("--%s--\n",buf);
av_packet_unref(packet);
break;
}
if (ret >= 0 && packet->stream_index != mAudioStreamIdx){
av_packet_unref(packet);
continue;
}
#if ABSFILTER_ENABLE
if (av_bsf_send_packet(absCtx, packet) < 0){
LOGE("av_bsf_send_packet faile \n");
av_packet_unref(packet);
continue;
}
if (av_bsf_receive_packet(absCtx, packet) < 0) {
LOGE("av_bsf_receive_packet faile \n");
av_packet_unref(packet);
continue;
}
#endif
{
// 发送待解码包
int result = avcodec_send_packet(mAvContext, packet);
av_packet_unref(packet);
if (result < 0){
av_log(NULL, AV_LOG_ERROR, "Error submitting a packet for decoding\n");
continue;
}
// 接收解码数据
while (result >= 0){
result = avcodec_receive_frame(mAvContext, frame);
if (result == AVERROR_EOF)
break;
else if (result == AVERROR(EAGAIN)){
result = 0;
break;
}
else if (result < 0){
av_log(NULL, AV_LOG_ERROR, "Error decoding frame\n");
av_frame_unref(frame);
break;
}
LOGE("解码播放")
swr_convert(swrContext, &out_buffer, 44100 * 2, (const uint8_t **) frame->data, frame->nb_samples);
//缓冲区的大小
int size = av_samples_get_buffer_size(NULL, out_channer_nb, frame->nb_samples,
AV_SAMPLE_FMT_S16, 1);
jbyteArray audio_sample_array = env->NewByteArray(size);
env->SetByteArrayRegion(audio_sample_array, 0, size, (const jbyte *) out_buffer);
env->CallVoidMethod(thiz, audio_write, audio_sample_array, size);
env->DeleteLocalRef(audio_sample_array);
av_frame_unref(frame);
}
}
}
9、收尾释放
最后释放相关资源
swr_free(&swrContext);
av_frame_free(&frame);
avcodec_close(mAvContext);
avformat_free_context(avFormatContext);
#if ABSFILTER_ENABLE
av_bsf_free(&absCtx);
absCtx = NULL;
#endif
env->ReleaseStringUTFChars(input, inputPath);
至此,mp3文件就可以解码,并通过audioTrack对解码后的数据进行播放。
java层的audiotrack方法最终是通过调用底层的openesl es来进行播放的,相当于绕了一个圈,在下篇文章中我们将实现直接使用opensl es来播放音频。
FFmpeg_Android音频播放demo--openSLES方式
三、demo运行
demo中指定了播放的文件是/sdcard/input.mp3,如下代码,若要改文件,可以在此处修改:
findViewById(R.id.btnStartVideo).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String PATH = Environment.getExternalStorageDirectory().getPath();
String input = PATH + File.separator + "input.mp3";
musicPlay.playSound(input);
}
});
运行后截图如下:
点击“PLAY MUSIC”按钮进行播放,可以听到音乐,说明demo运行正常。
过程中log如下:
完整例子已经放到github上,如下:
https://github.com/weekend-y/FFmpeg_Android_Demo/tree/master/demo8_byAudioTrack