一篇文章助你入门FFmpeg编程

12,200 阅读16分钟

1. 前言

FFmpeg是一个强大的音视频处理库,但是通常接触时以命令形式较多。本篇文章讲了FFmpeg相关api的使用,尤其是它强大的过滤器filter库的使用。

1.1 能学到什么

  • Android下集成FFmpeg
  • 使用avcodec解码库解码音频
  • 使用avfilter过滤器对音频做变速,调音,混音等处理
  • C/C++下多线程编程,生产者/消费者实现
  • NDK下通过OpenSL ES进行音频播放
  • NDK下对音频进行播放控制

1.2 实现了啥

本项目主要素材为five hundred mile吉他,尤克里里,鼓等4个音轨素材。实现多音轨实时播放,多音轨音量调节,变速播放,进度调节等功能

1.3 项目地址

github.com/iamyours/FF…

2. FFmpeg动态库编译

2.1 下载NDK和FFmpeg

Android Studio默认下载对NDK版本会出现一些兼容问题,因此我们这里使用ndk-r15c(win64|linux64|mac64)版本。 FFmpeg官网下载源码,我用的是3.2.12

2.2 解压文件

首先解压NDK和ffmpeg

tar -zxf ffmpeg-3.2.12.tar.gz
unzip android-ndk-r15c-darwin-x86_64.zip -d android-ndk-r15c

2.3 修改FFmpeg配置,适配Android

进入ffmpeg目录,修改configure文件

SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'
LIB_INSTALL_EXTRA_CMD='?(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'
SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR)$(SLIBNAME)'

替换为

SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='?(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'

2.4 编写FFmpeg脚本,生成动态so库

新建build_android.sh脚本

#!/bin/sh
NDK=/Users/xxx/Desktop/soft/android-ndk-r15c
SYSROOT=$NDK/platforms/android-21/arch-arm
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64
function build_one
{
./configure \
--prefix=$PREFIX \
--enable-shared \
--disable-static \
--disable-doc \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--disable-avdevice \
--disable-doc \
--disable-symver \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--target-os=linux \
--arch=arm \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $ADDI_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
$ADDITIONAL_CONFIGURE_FLAG
make clean
make
make install
}
CPU=arm
PREFIX=$(pwd)/android/$CPU
ADDI_CFLAGS="-marm"
build_one

添加执行权限,执行sh脚本

chmod +x build_android.sh
./build_android.sh

整个编译花了10分钟左右(mbp i5配置),编译完成后,可以在android目录看到相关so文件和头文件

so文件目录

3. 将FFmpeg加入到Android项目中

3.1 新建Android项目,添加C++支持

打开Android Studio,新建项目FFmpegAudioPlayer,添加C++支持

NDK支持

3.2 配置FFmpeg动态库

在src下的main文件中创建jniLibs文件夹,在jniLibs创建armeabi文件夹,将ffmpeg下android/arm/lib/目录下的so文件(libavcodec-57.so/libavfilter-6.so/libavformat-57.so/libavutil-55.so/libswresample-2.so/libswscale-4.so)拷贝至此目录。将android/arm/include 整个目录拷贝至jniLibs下,最终目录如下

工程目录
修改app/build.gradle文件,添加abiFilters

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            ndk{
                abiFilters "armeabi"
            }
        }
    }
...
}

打开app目录下的CMakeLists.txt文件,修改成如下配置

cmake_minimum_required(VERSION 3.4.1)
add_library( native-lib
             SHARED
             src/main/cpp/native-lib.cpp)
find_library( log-lib
              log )
find_library( android-lib
              android )
set(distribution_DIR ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI})


add_library( avutil-55
             SHARED
             IMPORTED )
set_target_properties( avutil-55
                       PROPERTIES IMPORTED_LOCATION
                       ${distribution_DIR}/libavutil-55.so)

...
# 同上还要通过add_library,set_target_properties 
# 设置swresample-2,avcodec-57,avfilter-6,swscale-4avformat-57 
...

set(CMAKE_VERBOSE_MAKEFILE on)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
include_directories(src/main/cpp)
include_directories(src/main/jniLibs/include)

target_link_libraries(native-lib
                      avutil-55       #工具库
                      swresample-2    #音频采样数据格式转换
                      avcodec-57      #编解码
                      avfilter-6      #滤镜特效处理
                      swscale-4       #视频像素数据格式转换
                      avformat-57     #封装格式处理
                      OpenSLES
                      ${log-lib}
                      ${android-lib})

配置完成后,我们先编译运行一次,如果能顺利成功安装到手机上,正常运行,则说明配置正确。

4. 解码mp3为pcm

FFmpeg的第一个强大之处是它的编解码能力。它可以将市面上的任意一种音频格式(mp3,wav,aac,ogg等)和视频格式(mp4,avi,rm,rmvb,mov等)解码。通过解码器将音频视频解码成一个个AVFrame,每个frame包含了音频的pcm信息或视频的yuv信息。通过编码器,FFmpeg可将frame编码成不同格式的音视频文件。因此我们可以用FFmpeg很简单的实现格式转换,而不需要了解各种格式的相关协议。

4.1 解码流程

为了能够解码mp3文件,需要通过ffmpeg读取音频信息,然后得到对应的解码器,然后循环读取每一帧音频数据,并且通过解码器解码。大致解码流程如下:

解码流程

4.2 完整代码

在MainActivity.kt中引入so库

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    fun decodeAudio(v: View) {
        val src = "${Environment.getExternalStorageDirectory()}/test1.mp3"
        val out = "${Environment.getExternalStorageDirectory()}/out.pcm"
        decodeAudio(src, out)
    }

    external fun decodeAudio(src: String, out: String)
    companion object {
        init {
            System.loadLibrary("avutil-55")
            System.loadLibrary("swresample-2")
            System.loadLibrary("avcodec-57")
            System.loadLibrary("avfilter-6")
            System.loadLibrary("swscale-4")
            System.loadLibrary("avformat-57")
            System.loadLibrary("native-lib")
        }
    }
}

在native-lib.cpp中编写音频解码代码

#include <jni.h>
#include <android/log.h>
#include <string>

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
}
#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"FFmpegAudioPlayer",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"FFmpegAudioPlayer",FORMAT,##__VA_ARGS__);
extern "C" JNIEXPORT void
JNICALL
Java_io_github_iamyours_ffmpegaudioplayer_MainActivity_decodeAudio(
        JNIEnv *env,
        jobject /* this */, jstring _src, jstring _out) {
    const char *src = env->GetStringUTFChars(_src, 0);
    const char *out = env->GetStringUTFChars(_out, 0);

    av_register_all();//注册所有容器解码器
    AVFormatContext *fmt_ctx = avformat_alloc_context();

    if (avformat_open_input(&fmt_ctx, src, NULL, NULL) < 0) {//打开文件
        LOGE("open file error");
        return;
    }
    if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {//读取音频格式文件信息
        LOGE("find stream info error");
        return;
    }
    //获取音频索引
    int audio_stream_index = -1;
    for (int i = 0; i < fmt_ctx->nb_streams; i++) {
        if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            audio_stream_index = i;
            LOGI("find audio stream index");
            break;
        }
    }
    //获取解码器
    AVCodecContext *codec_ctx = avcodec_alloc_context3(NULL);
    avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[audio_stream_index]->codecpar);
    AVCodec *codec = avcodec_find_decoder(codec_ctx->codec_id);
    //打开解码器
    if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
        LOGE("could not open codec");
        return;
    }
    //分配AVPacket和AVFrame内存,用于接收音频数据,解码数据
    AVPacket *packet = av_packet_alloc();
    AVFrame *frame = av_frame_alloc();
    int got_frame;//接收解码结果
    int index = 0;
    //pcm输出文件
    FILE *out_file = fopen(out, "wb");
    while (av_read_frame(fmt_ctx, packet) == 0) {//将音频数据读入packet
        if (packet->stream_index == audio_stream_index) {//取音频索引packet
            if (avcodec_decode_audio4(codec_ctx, frame, &got_frame, packet) <
                0) {//将packet解码成AVFrame
                LOGE("decode error:%d", index);
                break;
            }
            if (got_frame > 0) {
                LOGI("decode frame:%d", index++);
                fwrite(frame->data[0], 1, static_cast<size_t>(frame->linesize[0]),
                       out_file); //想将单个声道pcm数据写入文件

            }
        }
    }
    LOGI("decode finish...");
    //释放资源
    av_packet_unref(packet);
    av_frame_free(&frame);
    avcodec_close(codec_ctx);
    avformat_close_input(&fmt_ctx);
    fclose(out_file);
}

注意添加文件权限,将测试音频test1.mp3放入手机sd卡中,点击解码按钮,完成后,我们就可以看到pcm文件了,可以通过Audition打开(mac下可以通过Parallels Desktop装xp使用软件,融合模式不要太好用),选择48000hz,1声道(只写入了一个声道),打开后,就可以通过Audition查看和播放pcm文件了。

Adobe Audition CS6打开pcm文件

5. 单输入AVFilter过滤器

FFmpeg另一个强大之处在于它实现了各式各样的filter,可以将音视频出来成不同的效果,视频可以裁剪、缩放、旋转、合并、添加水印等效果,音频可以去噪、回声、延迟、混音、变速等效果。一个filter的输出可以作为另一个filter的输入。通过filter组合使用,我们可以定制自己想要的音视频特效。此次分两节讲两种音频filter的api用法,一种是单个输入volume(音量调节),atempo(变速)

5.1 单输入音频过滤处理流程

音频解码后,可以avfilter api对解码出来的AVFrame进行效果处理,如音量调节,变速处理。多个音频输入还可以进行混音处理(见6.1) 单输入过滤器解码流程

解码出AVFrame -> abuffer-> 其他过滤器(volume)...->aformat->abuffersink->过滤后的AVFrame

这里看到有三个通用的过滤器,abuffer,aformat,abuffersink。 abuffer用于接收输入frame,形成待处理的数据缓存,abuffersink用于传出输出Frame,aformat过滤器约束最终的输出格式(采样率,声道数,存储位数等),这三个不可缺少。 而中间的其他过滤器可以串联多个filter,如volume,atempo

5.2 过滤器初始化

这里我们先要知道三个重要的结构体 AVFilterGraph (管理所有的filter) AVFilterContext (filter上下文) AVFilter(具体过滤器)

过滤器初始化

5.3 过滤器初始化代码

通过value作为音量调节参数,具体代码如下

int init_volume_filter(AVFilterGraph **pGraph, AVFilterContext **src, AVFilterContext **out,
                       char *value) {

    //初始化AVFilterGraph
    AVFilterGraph *graph = avfilter_graph_alloc();
    //获取abuffer用于接收输入端
    AVFilter *abuffer = avfilter_get_by_name("abuffer");
    AVFilterContext *abuffer_ctx = avfilter_graph_alloc_filter(graph, abuffer, "src");
    //设置参数,这里需要匹配原始音频采样率、数据格式(位数)
    if (avfilter_init_str(abuffer_ctx, "sample_rate=48000:sample_fmt=s16p:channel_layout=stereo") <
        0) {
        LOGE("error init abuffer filter");
        return -1;
    }
    //初始化volume filter
    AVFilter *volume = avfilter_get_by_name("volume");
    AVFilterContext *volume_ctx = avfilter_graph_alloc_filter(graph, volume, "volume");
    //这里采用av_dict_set设置参数
    AVDictionary *args = NULL;
    av_dict_set(&args, "volume", value, 0);//这里传入外部参数,可以动态修改
    if (avfilter_init_dict(volume_ctx, &args) < 0) {
        LOGE("error init volume filter");
        return -1;
    }

    AVFilter *aformat = avfilter_get_by_name("aformat");
    AVFilterContext *aformat_ctx = avfilter_graph_alloc_filter(graph, aformat, "aformat");
    if (avfilter_init_str(aformat_ctx,
                          "sample_rates=48000:sample_fmts=s16p:channel_layouts=stereo") < 0) {
        LOGE("error init aformat filter");
        return -1;
    }
    //初始化sink用于输出
    AVFilter *sink = avfilter_get_by_name("abuffersink");
    AVFilterContext *sink_ctx = avfilter_graph_alloc_filter(graph, sink, "sink");
    if (avfilter_init_str(sink_ctx, NULL) < 0) {//无需参数
        LOGE("error init sink filter");
        return -1;
    }
    //链接各个filter上下文
    if (avfilter_link(abuffer_ctx, 0, volume_ctx, 0) != 0) {
        LOGE("error link to volume filter");
        return -1;
    }
    if (avfilter_link(volume_ctx, 0, aformat_ctx, 0) != 0) {
        LOGE("error link to aformat filter");
        return -1;
    }
    if (avfilter_link(aformat_ctx, 0, sink_ctx, 0) != 0) {
        LOGE("error link to sink filter");
        return -1;
    }
    if (avfilter_graph_config(graph, NULL) < 0) {
        LOGI("error config filter graph");
        return -1;
    }
    *pGraph = graph;
    *src = abuffer_ctx;
    *out = sink_ctx;
    LOGI("init filter success...");
    return 0;
}

5.4 使用过滤器,模拟实时音量调节

完成过滤器初始化后,就可以在解码后使用过滤器处理音频了。使用方法很简单,将解码后的AVFrame通过av_buffersrc_add_frame(abuffer_ctx,frame)加入到输入过滤器上下文abuffer_ctx中,通过av_buffersink_get_frame(sink_ctx,frame)获取处理完成的frame。这里每个1000个音频帧修改一次过滤器,模拟实时音量调节。 代码如下

    AVFilterGraph *graph;
    AVFilterContext *in_ctx;
    AVFilterContext *out_ctx;
    //注册所有过滤器
    avfilter_register_all();
    init_volume_filter(&graph, &in_ctx, &out_ctx, "0.5");
    //初始化
    while (av_read_frame(fmt_ctx, packet) == 0) {//将音频数据读入packet
        if (packet->stream_index == audio_stream_index) {//取音频索引packet
           ... 解码音频
            if (got_frame > 0) {
                LOGI("decode frame:%d", index++);
               if (index == 1000) {//模拟动态修改音量
                    init_volume_filter(&graph, &in_ctx, &out_ctx, "0.01");
                }
                if (index == 2000) {
                    init_volume_filter(&graph, &in_ctx, &out_ctx, "1.0");
                }
                if (index == 3000) {
                    init_volume_filter(&graph, &in_ctx, &out_ctx, "0.01");
                }
                if (index == 4000) {
                    init_volume_filter(&graph, &in_ctx, &out_ctx, "1.0");
                }
                if (av_buffersrc_add_frame(in_ctx, frame) < 0) {//将frame放入输入filter上下文
                    LOGE("error add frame");
                    break;
                }
                while (av_buffersink_get_frame(out_ctx, frame) >= 0) {//从输出filter上下文中获取frame
                    fwrite(frame->data[0], 1, static_cast<size_t>(frame->linesize[0]),
                           out_file); //想将单个声道pcm数据写入文件
                }
            }
        }
    }

最终解码出来pcm和原始mp3波形对比

修改音量前

修改音量后
可以明显看出音量已经发生变化。

5.5 使用swr_convert重新采样

在播放音频时,可以听见有一些噪声,需要swr_convert来重新采样,取出完整的pcm数据。

    //初始化SwrContext
    SwrContext *swr_ctx = swr_alloc();
    enum AVSampleFormat in_sample = codec_ctx->sample_fmt;
    enum AVSampleFormat out_sample = AV_SAMPLE_FMT_S16;
    int inSampleRate = codec_ctx->sample_rate;
    int outSampleRate = inSampleRate;
    uint64_t in_ch_layout = codec_ctx->channel_layout;
    uint64_t outChannelLayout = AV_CH_LAYOUT_STEREO;
    swr_alloc_set_opts(swr_ctx, outChannelLayout, out_sample, outSampleRate, in_ch_layout, in_sample,
                       inSampleRate, 0, NULL);
    swr_init(swr_ctx);
    int out_ch_layout_nb = av_get_channel_layout_nb_channels(out_ch_layout);//声道个数
    uint8_t *out_buffer = (uint8_t *) av_malloc(MAX_AUDIO_SIZE);//重采样数据

写入pcm数据之前,用swr_convert重新采样一下

 while (av_buffersink_get_frame(out_ctx, frame) >= 0) {//从输出filter上下文中获取frame
//                    fwrite(frame->data[0], 1, static_cast<size_t>(frame->linesize[0]),
//                           out_file); //想将单个声道pcm数据写入文件
  swr_convert(swr_ctx, &out_buffer, MAX_AUDIO_SIZE,
                                (const uint8_t **) frame->data, frame->nb_samples);
  int out_size = av_samples_get_buffer_size(NULL,out_ch_layout_nb,frame->nb_samples,out_sample_fmt,0);
                    fwrite(out_buffer,1,out_size,out_file);
}

这次我们写入的是完整2个声道的数据,而且也没有噪声了。

5.6 使用atempo过滤器实现变速不变调

将volumefilter改成atempo,注意参数设置名为tempo

//初始化volume filter
    AVFilter *volume = avfilter_get_by_name("atempo");
    AVFilterContext *volume_ctx = avfilter_graph_alloc_filter(graph, volume, "atempo");
    //这里采用av_dict_set设置参数
    AVDictionary *args = NULL;
    av_dict_set(&args, "tempo", value, 0);//调节音量为原先的一半
    if (avfilter_init_dict(volume_ctx, &args) < 0) {
        LOGE("error init volume filter");
        return -1;
    }

解码时模拟动态修改速度改成如下

if (index == 1000) {//模拟动态修改音量
    init_volume_filter(&graph, &in_ctx, &out_ctx, "1.0");
}
if (index == 2000) {
    init_volume_filter(&graph, &in_ctx, &out_ctx, "0.8");
}
if (index == 3000) {
    init_volume_filter(&graph, &in_ctx, &out_ctx, "1.5");
}
if (index == 4000) {
    init_volume_filter(&graph, &in_ctx, &out_ctx, "2.0");
}

成功后,就可以一个不同速度的音频,使用Audition打开,选择48000,2通道播放,可以听出它先是按照0.5,1.0,0.8,1.5,2.0的播放的,而且音调保持不变,没有因为速度的改变而变高或者变低。

6. 多输入AVFilter过滤器

FFmpeg使用过滤器filter的另外一个场景就是处理多个输入数据,比如视频添加水印,添加字幕,音视频合并等。这类场景需要两个及以上输入端。本节讲amix,它可以将多个音频混音。

6.1 多输入filter处理流程

输入AVFrame1 -> abuffer  
                         -> amix -> aformat -> abuffersink -> 输出AVFrame
输入AVFrame2 -> abuffer  

处理流程和单输入过滤器大致相同,只不过接收了多个输入端。因此需要多个filter上下文作为输入端。

6.2 amix过滤器初始化

//初始化amix filter
int init_amix_filter(AVFilterGraph **pGraph, AVFilterContext **srcs, AVFilterContext **pOut,
                     jsize len) {
    AVFilterGraph *graph = avfilter_graph_alloc();
    for (int i = 0; i < len; i++) {
        AVFilter *filter = avfilter_get_by_name("abuffer");
        char name[50];
        snprintf(name, sizeof(name), "src%d", i);
        AVFilterContext *abuffer_ctx = avfilter_graph_alloc_filter(graph, filter, name);
        if (avfilter_init_str(abuffer_ctx,
                              "sample_rate=48000:sample_fmt=s16p:channel_layout=stereo") < 0) {
            LOGE("error init abuffer filter");
            return -1;
        }
        srcs[i] = abuffer_ctx;
    }
    AVFilter *amix = avfilter_get_by_name("amix");
    AVFilterContext *amix_ctx = avfilter_graph_alloc_filter(graph, amix, "amix");
    char args[128];
    snprintf(args, sizeof(args), "inputs=%d:duration=first:dropout_transition=3", len);
    if (avfilter_init_str(amix_ctx, args) < 0) {
        LOGE("error init amix filter");
        return -1;
    }
    AVFilter *aformat = avfilter_get_by_name("aformat");
    AVFilterContext *aformat_ctx = avfilter_graph_alloc_filter(graph, aformat, "aformat");
    if (avfilter_init_str(aformat_ctx,
                          "sample_rates=48000:sample_fmts=s16p:channel_layouts=stereo") < 0) {
        LOGE("error init aformat filter");
        return -1;
    }
    AVFilter *sink = avfilter_get_by_name("abuffersink");
    AVFilterContext *sink_ctx = avfilter_graph_alloc_filter(graph, sink, "sink");
    avfilter_init_str(sink_ctx, NULL);
    for (int i = 0; i < len; i++) {
        if (avfilter_link(srcs[i], 0, amix_ctx, i) < 0) {
            LOGE("error link to amix");
            return -1;
        }
    }
    if (avfilter_link(amix_ctx, 0, aformat_ctx, 0) < 0) {
        LOGE("error link to aformat");
        return -1;
    }
    if (avfilter_link(aformat_ctx, 0, sink_ctx, 0) < 0) {
        LOGE("error link to sink");
        return -1;
    }
    if (avfilter_graph_config(graph, NULL) < 0) {
        LOGE("error config graph");
        return -1;
    }
    *pGraph = graph;
    *pOut = sink_ctx;
    return 0;
}

这里用一个数组保存输入AVFilterContex,通过遍历循环将每个输入端链接到amix过滤器,这样就可以接收多个输入端了。

6.3 使用amix实现多音轨合成

为了能够传入多个音频数据,我们需要同时解码多个音频文件,因此在Java层,传入字符串数组。

external fun mixAudio(arr: Array<String>,out:String)
val path = "${Environment.getExternalStorageDirectory()}/test"
val paths = arrayOf(
                    "$path/a.mp3",
                    "$path/b.mp3",
                    "$path/c.mp3",
                    "$path/d.mp3"
)
mixAudio(paths,"$path/mix.pcm")

在jni层使用多个解码器解码每个文件

extern "C" JNIEXPORT void
JNICALL
Java_io_github_iamyours_ffmpegaudioplayer_MainActivity_mixAudio(
        JNIEnv *env,
        jobject /* this */, jobjectArray _srcs, jstring _out) {
    //将java传入的字符串数组转为c字符串数组
    jsize len = env->GetArrayLength(_srcs);
    const char *out_path = env->GetStringUTFChars(_out, 0);
    char **pathArr = (char **) malloc(len * sizeof(char *));
    int i = 0;
    for (i = 0; i < len; i++) {
        jstring str = static_cast<jstring>(env->GetObjectArrayElement(_srcs, i));
        pathArr[i] = const_cast<char *>(env->GetStringUTFChars(str, 0));
    }
    //初始化解码器数组
    av_register_all();
    AVFormatContext **fmt_ctx_arr = (AVFormatContext **) malloc(len * sizeof(AVFormatContext *));
    AVCodecContext **codec_ctx_arr = (AVCodecContext **) malloc(len * sizeof(AVCodecContext *));
    int stream_index_arr[len];
    for (int n = 0; n < len; n++) {
        AVFormatContext *fmt_ctx = avformat_alloc_context();
        fmt_ctx_arr[n] = fmt_ctx;
        ...
        //依次打开每个文件,获取音频索引,获取每个解码器
        ...
        AVCodecContext *codec_ctx = avcodec_alloc_context3(NULL);
        codec_ctx_arr[n] = codec_ctx;
        ...
    }
    //初始化SwrContext
    SwrContext *swr_ctx = swr_alloc();
    ...
    //设置swr_ctx参数
    ...
    swr_init(swr_ctx);
    //初始化amix过滤器
    ...
    init_amix_filter(&graph, srcs, &sink, len);
    //开始解码
    FILE *out_file = fopen(out_path, "wb");
    AVFrame *frame = av_frame_alloc();
    AVPacket *packet = av_packet_alloc();
    int ret = 0, got_frame;
    int index = 0;
    while (1) {
        for (int i = 0; i < len; i++) {
            ret = av_read_frame(fmt_ctx_arr[i], packet);
            if (ret < 0)break;
            if (packet->stream_index == stream_index_arr[i]) {
                ret = avcodec_decode_audio4(codec_ctx_arr[i], frame, &got_frame, packet);//解码音频
                if (ret < 0)break;
                if (got_frame > 0) {
                    ret = av_buffersrc_add_frame(srcs[i], frame);//将解码后的AVFrame加入到amix输入端
                    if (ret < 0) {
                        break;
                    }
                }
            }
        }
        while (av_buffersink_get_frame(sink, frame) >= 0) {//从sink输出端获取处理完成的AVFrame
            swr_convert(swr_ctx, &out_buffer, MAX_AUDIO_SIZE, (const uint8_t **) frame->data,
                        frame->nb_samples);
            int out_size = av_samples_get_buffer_size(NULL, out_ch_layout_nb, frame->nb_samples,
                                                      out_sample_fmt, 0);
            fwrite(out_buffer, 1, out_size, out_file);
        }
        if (ret < 0) {
            break;
        }
        LOGI("decode frame :%d", index);
        index++;
    }
    LOGI("finish");
}

使用audition打开输出文件mix.pcm,可以听到四个文件混音后的音频。 具体的音频在assets目录下,可以自行对比下效果

7. 使用OpenSLES播放音频

为了能够在Android播放pcm格式的音频,我们使用OpenSLES库。在cmke的target_link_libraries中加入OpenSLES,使用时加入头文件<SLES/OpenSLES_Android.h>

7.1 OpenSLES播放器流程

7.1.1.创建并且实现引擎对象
SLObjectItf engineObject;
slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
 (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
7.1.2.获取引擎接口
SLEngineItf engineItf;
 (*enginObject)->GetInterface(engineObject,SL_IID_ENGINE,&engineItf);
7.1.3.创建并且实现输出混音器对象
SLObjectItf mixObject;
(*engineItf)->CreateOutputMix(engineItf, &mixObject, 0, 0, 0);
7.1.4.设置播放器参数,创建初始化播放器对象
SLDataLocator_AndroidSimpleBufferQueue
        android_queue = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
//pcm格式
SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM,
                        2,//两声道
                        SL_SAMPLINGRATE_48,//48000采样率
                        SL_PCMSAMPLEFORMAT_FIXED_16,
                        SL_PCMSAMPLEFORMAT_FIXED_16,
                        SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,//
                        SL_BYTEORDER_LITTLEENDIAN};

SLDataSource slDataSource = {&android_queue, &pcm};

//输出管道
SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, mixObject};
SLDataSink audioSnk = {&outputMix, NULL};

const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME};
const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};

SLObjectItf playerObject;//播放器对象
(*engineItf)->CreateAudioPlayer(engineItf, &playerObject,&slDataSource,&audioSnk,1,ids,req);
(*playerObject)->Realize(playerObject,SL_BOOLEAN_FALSE);
7.1.5.通过播放器对象获取相关接口
//获取播放接口
SLPlayItf playItf;
(*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playItf);
//获取缓冲接口
SLBufferQueueItf bufferQueueItf;
(*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &bufferQueueItf);
7.1.6. 注册回调缓冲,设置播放状态,调用回调函数
//注册缓冲回调
(*bufferQueueItf)->RegisterCallback(bufferQueueItf, playCallback, NULL);
//设置播放状态
(*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PLAYING);
playCallback(bufferQueueItf, NULL);

具体回调如下,getPCM在后面会有实现

void playCallback(SLAndroidSimpleBufferQueueItf bq, void *args) {
    //获取pcm数据
    uint8_t *data;
    int size = getPCM(&data);
    if (size > 0) {
        (*bq)->Enqueue(bq, data, size);
    }
}

7.2 多线程解码播放音频

为了能够获取pcm数据,我们使用多线程进行音频解码,通过条件变量,实现一个生产者消费者的模型,解码过程为生产过程,回调播放为消费过程。将解码得到的AVFrame加入到vector队列中,然后在播放回调时取出AVFrame,使用swr_convert转成pcm数据。

7.2.1.初始化同步锁,条件变量,启动解码线程

申明全局的变量

static pthread_mutex_t mutex;
//条件变量
static pthread_cond_t notfull; //队列未达到最大缓冲容量,存
static pthread_cond_t notempty;//队列不为空,取

初始化同步锁和条件变量,启动解码线程(放在创建播放器前面)

//初始化同步锁和条件变量
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&notfull, NULL);
pthread_cond_init(&notempty, NULL);

//初始化解码线程
pthread_t pid;
char *path = (char *) env->GetStringUTFChars(_path, 0);
pthread_create(&pid, NULL, decodeAudio, path);
7.2.2.解码音频,将AVFrame加入vector队列

申明全局变量

static std::vector<AVFrame *> queue;
static SwrContext *swr_ctx;
static int out_ch_layout_nb;
static enum AVSampleFormat out_sample_fmt;
#define QUEUE_SIZE 5
#define MAX_AUDIO_SIZE 48000*4

解码音频和[第4节]类似,只不过把解码出来的AVFrame加入到队列了。

void *decodeAudio(void *args) {
    //打开文件,获取初始化上下文,解码器,分配packet/frame内存
    ...
    while (av_read_frame(fmt_ctx, packet) == 0) {//将音频数据读入packet
        if (packet->stream_index == audio_stream_index) {//取音频索引packet
            if (avcodec_decode_audio4(codec_ctx, frame, &got_frame, packet) <
                0) {//将packet解码成AVFrame
                LOGE("decode error:%d", index);
                break;
            }
            if (got_frame > 0) {
                LOGI("decode frame:%d", index++);
                addFrame(frame);
            }
        }
    }
    //释放资源
    ...
}

为了保证音频播放的实时性,队列里的AVFrame数量不能太多。在后面章节中,我们会将AVFrame通过Filter过滤,然后才加入到队列中。因此在addFrame方法中,如果超出最大缓冲大小,需要通过pthread_cond_wait阻塞住,等待消费,代码如下:

void addFrame(AVFrame *src) {
    AVFrame *frame = av_frame_alloc();
    if (av_frame_ref(frame, src) >= 0) {//复制frame
        pthread_mutex_lock(&mutex);
        if (queue.size() == QUEUE_SIZE) {
            LOGI("wait for add frame...%d", queue.size());
            pthread_cond_wait(&notfull, &mutex);//等待队列不为满信号
        }
        queue.push_back(frame);
        pthread_cond_signal(&notempty);//发送不为空信号
        pthread_mutex_unlock(&mutex);
    }
}
7.2.3.获取pcm数据,通过opensles回调函数播放pcm

通过之前注册的缓冲回调,我们可以把加入到队列的AVFrame消费掉。 首先要有一个getFrame方法

AVFrame *getFrame() {
    pthread_mutex_lock(&mutex);
    while (true) {
        if (!queue.empty()) {
            AVFrame *out = av_frame_alloc();
            AVFrame *src = queue.front();
            if (av_frame_ref(out, src) < 0)break;
            queue.erase(queue.begin());//删除元素
            av_free(src);
            if (queue.size() < QUEUE_SIZE)pthread_cond_signal(&notfull);//发送notfull信号
            pthread_mutex_unlock(&mutex);
            return out;
        } else {//为空等待添加
            LOGI("wait for get frame");
            pthread_cond_wait(&notempty, &mutex);
        }
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}

然后是实现最初的getPCM方法,如下:

int getPCM(uint8_t **out) {
    AVFrame *frame = getFrame();
    if (frame) {
        uint8_t *data = (uint8_t *) av_malloc(MAX_AUDIO_SIZE);
        swr_convert(swr_ctx, &data, MAX_AUDIO_SIZE, (const uint8_t **) frame->data,
                    frame->nb_samples);
        int out_size = av_samples_get_buffer_size(NULL, out_ch_layout_nb, frame->nb_samples,
                                                  out_sample_fmt, 0);
        *out = data;
        return out_size;
    }
    return 0;
}

这里,通过swr_convert将AVFrame数据转化成uint8_t数组,然后就可以缓冲队列接口里的Enqueue播放了。

8. FFmpeg播放器实现

有了前面一系列的准备知识,可以开始打造FFmpeg音频播放器了。主要需求,多个音频混音播放,每个音轨音量控制,合成音频变速播放。

8.1 AudioPlayer类

首先我们创建一个C++ Class名为AudioPlayer,为了能够实现音频解码,过滤,播放功能,我们需要解码、过滤、队列、输出pcm相关、多线程、Open SL ES相关的成员变量,代码如下:

//解码
int fileCount;                  //输入音频文件数量
AVFormatContext **fmt_ctx_arr;  //FFmpeg上下文数组
AVCodecContext **codec_ctx_arr; //解码器上下文数组
int *stream_index_arr;          //音频流索引数组
//过滤
AVFilterGraph *graph;
AVFilterContext **srcs;         //输入filter
AVFilterContext *sink;          //输出filter
char **volumes;                 //各个音频的音量
char *tempo;                    //播放速度0.5~2.0

//AVFrame队列
std::vector<AVFrame *> queue;   //队列,用于保存解码过滤后的AVFrame

//输入输出格式
SwrContext *swr_ctx;            //重采样,用于将AVFrame转成pcm数据
uint64_t in_ch_layout;
int in_sample_rate;            //采样率
int in_ch_layout_nb;           //输入声道数,配合swr_ctx使用
enum AVSampleFormat in_sample_fmt; //输入音频采样格式

uint64_t out_ch_layout;
int out_sample_rate;            //采样率
int out_ch_layout_nb;           //输出声道数,配合swr_ctx使用
int max_audio_frame_size;       //最大缓冲数据大小
enum AVSampleFormat out_sample_fmt; //输出音频采样格式

// 进度相关
AVRational time_base;           //刻度,用于计算进度
double total_time;              //总时长(秒)
double current_time;            //当前进度
int isPlay = 0;                 //播放状态1:播放中

//多线程
pthread_t decodeId;             //解码线程id
pthread_t playId;               //播放线程id
pthread_mutex_t mutex;          //同步锁
pthread_cond_t not_full;        //不为满条件,生产AVFrame时使用
pthread_cond_t not_empty;       //不为空条件,消费AVFrame时使用

//Open SL ES
SLObjectItf engineObject;       //引擎对象
SLEngineItf engineItf;          //引擎接口
SLObjectItf mixObject;          //输出混音对象
SLObjectItf playerObject;       //播放器对象
SLPlayItf playItf;              //播放器接口
SLAndroidSimpleBufferQueueItf bufferQueueItf;   //缓冲接口

8.2 播放器解码播放流程

解码播放流程
整个音频处理播放流程如上图,首先,我们需要两个线程一个是解码线程,一个是播放线程。解码线程负责多个音频文件的解码,过滤,加入队列操作,播放线程则需要从队列中取出处理后的AVFrame,然后转成pcm输入,通过缓冲回调播放音频。 为了初始化这些成员变量,我们按照每块成员列表定义了对于的初始化方法。

int createPlayer();                     //创建播放器
int initCodecs(char **pathArr);         //初始化解码器
int initSwrContext();                   //初始化SwrContext
int initFilters();                      //初始化过滤器

而在构造函数中传入音频文件数组,和文件数量,初始化相关方法

AudioPlayer::AudioPlayer(char **pathArr, int len) {
    //初始化
    fileCount = len;
    //默认音量1.0 速度1.0
    volumes = (char **) malloc(fileCount * sizeof(char *));
    for (int i = 0; i < fileCount; i++) {
        volumes[i] = "1.0";
    }
    tempo = "1.0";

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&not_full, NULL);
    pthread_cond_init(&not_empty, NULL);

    initCodecs(pathArr);
    avfilter_register_all();
    initSwrContext();
    initFilters();
    createPlayer();
}

这里我们还初始化了控制各个音频音量和速度的变量,同步锁和条件变量(生产消费控制)。

8.3 具体实现

8.3.1 初始化解码器数组
int AudioPlayer::initCodecs(char **pathArr) {
    LOGI("init codecs");
    av_register_all();
    fmt_ctx_arr = (AVFormatContext **) malloc(fileCount * sizeof(AVFormatContext *));
    codec_ctx_arr = (AVCodecContext **) malloc(fileCount * sizeof(AVCodecContext *));
    stream_index_arr = (int *) malloc(fileCount * sizeof(int));
    for (int n = 0; n < fileCount; n++) {
    	//初始化上下文,打开文件,获取音频索引
        ...

        stream_index_arr[n] = audio_stream_index;
        //获取解码器
        AVCodecContext *codec_ctx = avcodec_alloc_context3(NULL);
        codec_ctx_arr[n] = codec_ctx;
        AVStream *stream = fmt_ctx->streams[audio_stream_index];
        avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[audio_stream_index]->codecpar);
        AVCodec *codec = avcodec_find_decoder(codec_ctx->codec_id);
        if (n == 0) {//获取输入格式
            in_sample_fmt = codec_ctx->sample_fmt;
            in_ch_layout = codec_ctx->channel_layout;
            in_sample_rate = codec_ctx->sample_rate;
            in_ch_layout_nb = av_get_channel_layout_nb_channels(in_ch_layout);
            max_audio_frame_size = in_sample_rate * in_ch_layout_nb;
            time_base = fmt_ctx->streams[audio_stream_index]->time_base;
            int64_t duration = stream->duration;
            total_time = av_q2d(stream->time_base) * duration;
            LOGI("total time:%lf", total_time);
        } else {//如果是多个文件,判断格式是否一致(采用率,格式、声道数)
            if (in_ch_layout != codec_ctx->channel_layout
                || in_sample_fmt != codec_ctx->sample_fmt
                || in_sample_rate != codec_ctx->sample_rate) {
                LOGE("输入文件格式不同");
                return -1;
            }
        }
        //打开解码器
        if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
            LOGE("could not open codec");
            return -1;
        }
    }
    return 1;
}

这里将输入音频的格式信息保存起来,用于SwrContext初始化、Filter初始化。

8.3.2 初始化filter数组
int AudioPlayer::initFilters() {
    LOGI("init filters");
    graph = avfilter_graph_alloc();
    srcs = (AVFilterContext **) malloc(fileCount * sizeof(AVFilterContext **));
    char args[128];
    AVDictionary *dic = NULL;
    //混音过滤器
    AVFilter *amix = avfilter_get_by_name("amix");
    AVFilterContext *amix_ctx = avfilter_graph_alloc_filter(graph, amix, "amix");
    snprintf(args, sizeof(args), "inputs=%d:duration=first:dropout_transition=3", fileCount);
    if (avfilter_init_str(amix_ctx, args) < 0) {
        LOGE("error init amix filter");
        return -1;
    }

    const char *sample_fmt = av_get_sample_fmt_name(in_sample_fmt);
    snprintf(args, sizeof(args), "sample_rate=%d:sample_fmt=%s:channel_layout=0x%" PRIx64,
             in_sample_rate, sample_fmt, in_ch_layout);

    for (int i = 0; i < fileCount; i++) {
    	//这里初始化每个输入端对应的abuffer,volume过滤器
    	...
    	//接着连接到amix
        if (avfilter_link(volume_ctx, 0, amix_ctx, i) < 0) {
            LOGE("error link to amix filter");
            return -1;
        }
    }

    //变速过滤器atempo
    AVFilter *atempo = avfilter_get_by_name("atempo");
    //设置变速参数
    ...
    //初始化aformat过滤器用于输出端格式转换

    AVFilter *aformat = avfilter_get_by_name("aformat");
    AVFilterContext *aformat_ctx = avfilter_graph_alloc_filter(graph, aformat, "aformat");
    snprintf(args, sizeof(args), "sample_rates=%d:sample_fmts=%s:channel_layouts=0x%" PRIx64,
             in_sample_rate, sample_fmt, in_ch_layout);
    if (avfilter_init_str(aformat_ctx, args) < 0) {
        LOGE("error init aformat filter");
        return -1;
    }
    //输出缓冲
    AVFilter *abuffersink = avfilter_get_by_name("abuffersink");
    //设置abuffersink参数
    ...
    //将amix链接到atempo
    if (avfilter_link(amix_ctx, 0, atempo_ctx, 0) < 0) {
        LOGE("error link to atempo filter");
        return -1;
    }
    if (avfilter_link(atempo_ctx, 0, aformat_ctx, 0) < 0) {
        LOGE("error link to aformat filter");
        return -1;
    }
    if (avfilter_link(aformat_ctx, 0, sink, 0) < 0) {
        LOGE("error link to abuffersink filter");
        return -1;
    }
    if (avfilter_graph_config(graph, NULL) < 0) {
        LOGE("error config graph");
        return -1;
    }

    return 1;
}

通过初始化解码器获取的输入音频格式信息,可以初始化abuffer输入filter(采样率、格式、声道必须匹配),然后可以链接volume ,amix,atempo filter。这样音频就可以实现调音,混音,变速的效果。

8.3.3 初始化SwrContext
int AudioPlayer::initSwrContext() {
    LOGI("init swr context");
    swr_ctx = swr_alloc();
    out_sample_fmt = AV_SAMPLE_FMT_S16;
    out_ch_layout = AV_CH_LAYOUT_STEREO;
    out_ch_layout_nb = 2;
    out_sample_rate = in_sample_rate;
    max_audio_frame_size = out_sample_rate * 2;

    swr_alloc_set_opts(swr_ctx, out_ch_layout, out_sample_fmt, out_sample_rate, in_ch_layout,
                       in_sample_fmt, in_sample_rate, 0, NULL);
    if (swr_init(swr_ctx) < 0) {
        LOGE("error init SwrContext");
        return -1;
    }
    return 1;
}

为了能使得解码出来的AVFrame能在OpenSL ES下播放,我们将采用格式固定为16位的AV_SAMPLE_FMT_S16,声道为立体声AV_CH_LAYOUT_STEREO,声道数为2,采样率和输入一样。缓冲回调pcm数据最大值为采样率*2。

8.3.4 初始化OpenSL ES播放器
int AudioPlayer::createPlayer() {
    //创建播放器
    //创建并且初始化引擎对象
//    SLObjectItf engineObject;
    slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
    (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    //获取引擎接口
//    SLEngineItf engineItf;
    (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineItf);
    //通过引擎接口获取输出混音
//    SLObjectItf mixObject;
    (*engineItf)->CreateOutputMix(engineItf, &mixObject, 0, 0, 0);
    (*mixObject)->Realize(mixObject, SL_BOOLEAN_FALSE);

    //设置播放器参数
    SLDataLocator_AndroidSimpleBufferQueue
            android_queue = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
    SLuint32 samplesPerSec = (SLuint32) out_sample_rate * 1000;
    //pcm格式
    SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM,
                            2,//两声道
                            samplesPerSec,
                            SL_PCMSAMPLEFORMAT_FIXED_16,
                            SL_PCMSAMPLEFORMAT_FIXED_16,
                            SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,//
                            SL_BYTEORDER_LITTLEENDIAN};

    SLDataSource slDataSource = {&android_queue, &pcm};

    //输出管道
    SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, mixObject};
    SLDataSink audioSnk = {&outputMix, NULL};

    const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME};
    const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
    //通过引擎接口,创建并且初始化播放器对象
//    SLObjectItf playerObject;
    (*engineItf)->CreateAudioPlayer(engineItf, &playerObject, &slDataSource, &audioSnk, 1, ids,
                                    req);
    (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);

    //获取播放接口
//    SLPlayItf playItf;
    (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playItf);
    //获取缓冲接口
//    SLAndroidSimpleBufferQueueItf bufferQueueItf;
    (*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &bufferQueueItf);

    //注册缓冲回调
    (*bufferQueueItf)->RegisterCallback(bufferQueueItf, _playCallback, this);
    return 1;
}

这里的pcm格式和SwrContext设置的参数要一致

8.3.5 启动播放线程和解码线程
void *_decodeAudio(void *args) {
    AudioPlayer *p = (AudioPlayer *) args;
    p->decodeAudio();
    pthread_exit(0);
}

void *_play(void *args) {
    AudioPlayer *p = (AudioPlayer *) args;
    p->setPlaying();
    pthread_exit(0);
}

void AudioPlayer::setPlaying() {
    //设置播放状态
    (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PLAYING);
    _playCallback(bufferQueueItf, this);
}

void AudioPlayer::play() {
    isPlay = 1;
    pthread_create(&decodeId, NULL, _decodeAudio, this);
    pthread_create(&playId, NULL, _play, this);
}

play方法中我们pthread_create启动播放和解码线程,播放线程通过播放接口设置播放中状态,然后回调缓冲接口,在回调中,取出队列中的AVFrame转成pcm,然后通过Enqueue播放。解码线程负责解码过滤出AVFrame,加入到队列中。

8.3.6 缓冲回调
void _playCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {
    AudioPlayer *player = (AudioPlayer *) context;
    AVFrame *frame = player->get();
    if (frame) {
        int size = av_samples_get_buffer_size(NULL, player->out_ch_layout_nb, frame->nb_samples,
                                              player->out_sample_fmt, 1);
        if (size > 0) {
            uint8_t *outBuffer = (uint8_t *) av_malloc(player->max_audio_frame_size);
            swr_convert(player->swr_ctx, &outBuffer, player->max_audio_frame_size,
                        (const uint8_t **) frame->data, frame->nb_samples);
            (*bq)->Enqueue(bq, outBuffer, size);
        }
    }
}
8.3.7 解码过滤
void AudioPlayer::decodeAudio() {
    LOGI("start decode...");
    AVFrame *frame = av_frame_alloc();
    AVPacket *packet = av_packet_alloc();
    int ret, got_frame;
    int index = 0;
    while (isPlay) {
        LOGI("decode frame:%d", index);
        for (int i = 0; i < fileCount; i++) {
            AVFormatContext *fmt_ctx = fmt_ctx_arr[i];
            ret = av_read_frame(fmt_ctx, packet);
            if (packet->stream_index != stream_index_arr[i])continue;//不是音频packet跳过
            if (ret < 0) {
                LOGE("decode finish");
                goto end;
            }
            ret = avcodec_decode_audio4(codec_ctx_arr[i], frame, &got_frame, packet);
            if (ret < 0) {
                LOGE("error decode packet");
                goto end;
            }
            if (got_frame <= 0) {
                LOGE("decode error or finish");
                goto end;
            }
            ret = av_buffersrc_add_frame(srcs[i], frame);
            if (ret < 0) {
                LOGE("error add frame to filter");
                goto end;
            }
        }
        LOGI("time:%lld,%lld,%lld", frame->pkt_dts, frame->pts, packet->pts);
        while (av_buffersink_get_frame(sink, frame) >= 0) {
            frame->pts = packet->pts;
            LOGI("put frame:%d,%lld", index, frame->pts);
            put(frame);
        }
        index++;
    }
    end:
    av_packet_unref(packet);
    av_frame_unref(frame);
}

这里有一个注意的点是,通过av_read_frame读取的packet不一定是音频流,所以需要通过音频流索引过滤packet。在av_buffersink_get_frame获取的AVFrame中,将pts修改为packet里的pts,用于保存进度(过滤后的pts时间进度不是当前解码的进度)。

8.3.8 AVFrame存和取
/**
 * 将AVFrame加入到队列,队列长度为5时,阻塞等待
 * @param frame
 * @return
 */
int AudioPlayer::put(AVFrame *frame) {
    AVFrame *out = av_frame_alloc();
    if (av_frame_ref(out, frame) < 0)return -1;//复制AVFrame
    pthread_mutex_lock(&mutex);
    if (queue.size() == 5) {
        LOGI("queue is full,wait for put frame:%d", queue.size());
        pthread_cond_wait(&not_full, &mutex);
    }
    queue.push_back(out);
    pthread_cond_signal(&not_empty);
    pthread_mutex_unlock(&mutex);
    return 1;
}

/**
 * 取出AVFrame,队列为空时,阻塞等待
 * @return
 */
AVFrame *AudioPlayer::get() {
    AVFrame *out = av_frame_alloc();
    pthread_mutex_lock(&mutex);
    while (isPlay) {
        if (queue.empty()) {
            pthread_cond_wait(&not_empty, &mutex);
        } else {
            AVFrame *src = queue.front();
            if (av_frame_ref(out, src) < 0)return NULL;
            queue.erase(queue.begin());//删除取出的元素
            av_free(src);
            if (queue.size() < 5)pthread_cond_signal(&not_full);
            pthread_mutex_unlock(&mutex);
            current_time = av_q2d(time_base) * out->pts;
            LOGI("get frame:%d,time:%lf", queue.size(), current_time);
            return out;
        }
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}

通过两个条件变量,实现一个缓冲太小为5的生产消费模型,用于AVFrame队列的存和取。 通过以上代码就可以实现音量为1,速度为1的多音频播放了

9. NDK播放控制

前面一节我们已经创建了一个基于FFmpeg的播放器,这一节开始对播放器进行各种控制操作。主要有调音,变速,暂停,播放,进度切换,停止(释放资源)。

9.1 创建FFmpegAudioPlayer

首先在java层创建FFmpegAudioPlayer.kt(kotlin),加入以下方法用于jni

class FFmpegAudioPlayer {
    /**
     * 初始化
     */
    external fun init(paths: Array<String>)
    /**
     * 播放
     */
    external fun play()

    /**
     * 暂停
     */
    external fun pause()

    /**
     * 释放资源
     */
    external fun release()

    /**
     * 修改每个音量
     */
    external fun changeVolumes(volumes: Array<String>)

    /**
     * 变速
     */
    external fun changeTempo(tempo: String)

    /**
     * 总时长 秒
     */
    external fun duration(): Double

    /**
     * 当前进度 秒
     */
    external fun position(): Double

    /**
     * 进度跳转
     */
    external fun seek(sec: Double)

    companion object {
        init {
            System.loadLibrary("avutil-55")
            System.loadLibrary("swresample-2")
            System.loadLibrary("avcodec-57")
            System.loadLibrary("avfilter-6")
            System.loadLibrary("swscale-4")
            System.loadLibrary("avformat-57")
            System.loadLibrary("native-lib")
        }
    }
}

然后在jni层,实现对应的方法。

#include "AudioPlayer.h"
static AudioPlayer *player;
extern "C" JNIEXPORT void
JNICALL
Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_init(
        JNIEnv *env,
        jobject /* this */, jobjectArray _srcs) {
    jsize len = env->GetArrayLength(_srcs);
    char **pathArr = (char **) malloc(len * sizeof(char *));
    int i = 0;
    for (i = 0; i < len; i++) {
        jstring str = static_cast<jstring>(env->GetObjectArrayElement(_srcs, i));
        pathArr[i] = const_cast<char *>(env->GetStringUTFChars(str, 0));
    }
    player = new AudioPlayer(pathArr, len);
}

extern "C" JNIEXPORT void
JNICALL
Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_changeVolumes(
        JNIEnv *env,
        jobject /* this */, jobjectArray _volumes) {
    jsize len = env->GetArrayLength(_volumes);
    int i = 0;
    for (i = 0; i < len; i++) {
        jstring str = static_cast<jstring>(env->GetObjectArrayElement(_volumes, i));
        char *volume = const_cast<char *>(env->GetStringUTFChars(str, 0));
        player->volumes[i] = volume;
    }
    player->change = 1;//通过change标记表面参数发生变化,从而修改过滤器参数
}

extern "C" JNIEXPORT void
JNICALL
Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_changeTempo(
        JNIEnv *env,
        jobject /* this */, jstring _tempo) {
    char *tempo = const_cast<char *>(env->GetStringUTFChars(_tempo, 0));
    player->tempo = tempo;
    player->change = 1;
}
extern "C" JNIEXPORT void
JNICALL
Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_play(
        JNIEnv *env,
        jobject /* this */) {
    player->play();
}

extern "C" JNIEXPORT void
JNICALL
Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_pause(
        JNIEnv *env,
        jobject /* this */) {
    player->pause();
}

extern "C" JNIEXPORT void
JNICALL
Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_release(
        JNIEnv *env,
        jobject /* this */) {
    player->release();
}
extern "C" JNIEXPORT void
JNICALL
Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_seek(
        JNIEnv *env,
        jobject /* this */, jdouble secs) {
    player->seek(secs);
}

extern "C" JNIEXPORT jdouble
JNICALL
Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_duration(
        JNIEnv *env,
        jobject /* this */) {
    return player->total_time;
}

extern "C" JNIEXPORT jdouble
JNICALL
Java_io_github_iamyours_ffmpegaudioplayer_FFmpegAudioPlayer_position(
        JNIEnv *env,
        jobject /* this */) {
    return player->current_time;
}

最终的实现在AudioPlayer.cpp中

9.2 调音,变速

为了能够实现变速,调音,我们要在解码之前重新修改过滤器的参数。这里使用一个change参数作为标记,表明需要重新初始化filter,初始化完成后,把change重新修改成0。

int AudioPlayer::initFilters() {
    LOGI("init filters");
    if (change)avfilter_graph_free(&graph);
    graph = avfilter_graph_alloc();
    ...
    change = 0;
    return 1;
}

这里需要将之前的过滤器资源释放掉,以免内存溢出。 在解码之前,通过change标志,重新初始化。

void AudioPlayer::decodeAudio() {
    ...
    while (isPlay) {
        LOGI("decode frame:%d", index);
        if (change) {
            initFilters();
        }
        for (int i = 0; i < fileCount; i++) {
            AVFormatContext *fmt_ctx = fmt_ctx_arr[i];
            ret = av_read_frame(fmt_ctx, packet);
            if (packet->stream_index != stream_index_arr[i])continue;
           ...
            ret = av_buffersrc_add_frame(srcs[i], frame);
            if (ret < 0) {
                LOGE("error add frame to filter");
                goto end;
            }
        }
        while (av_buffersink_get_frame(sink, frame) >= 0) {
            frame->pts = packet->pts;
            put(frame);
        }
        index++;
    }
    end:
   ...
}

这样就可以实现音量和速度的控制了。

9.3 暂停,播放

暂停可以通过OpenSLES播放器接口通过设置暂停状态来暂停播放。设置此状态后,缓冲回调就会暂停回调。

void AudioPlayer::pause() {
    (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PAUSED);
}

而重新播放我们也只需要设置播放中SL_PLAYSTATE_PLAYING状态

void AudioPlayer::play() {
    LOGI("play...");
    if (isPlay) {
        (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PLAYING);
        return;
    }
    isPlay = 1;
    seek(0);
    pthread_create(&decodeId, NULL, _decodeAudio, this);
    pthread_create(&playId, NULL, _play, this);
}

9.4 进度控制

进度控制是使用av_seek_frame来实现,使用av_q2d将秒数转为ffmpeg内部的时间戳

void AudioPlayer::seek(double secs) {
    pthread_mutex_lock(&mutex);
    for (int i = 0; i < fileCount; i++) {
        av_seek_frame(fmt_ctx_arr[i], stream_index_arr[i], (int64_t) (secs / av_q2d(time_base)),
                      AVSEEK_FLAG_ANY);
    }
    current_time = secs;
    queue.clear();
    pthread_cond_signal(&not_full);
    pthread_mutex_unlock(&mutex);
}

9.5 释放资源

设置播放器状态为停止,释放Open SLES相关资源,释放过滤器资源,释放解码器资源,关闭输入流。

void AudioPlayer::release() {
    pthread_mutex_lock(&mutex);
    isPlay = 0;
    pthread_cond_signal(&not_full);
    pthread_mutex_unlock(&mutex);
    if (playItf)(*playItf)->SetPlayState(playItf, SL_PLAYSTATE_STOPPED);
    if (playerObject) {
        (*playerObject)->Destroy(playerObject);
        playerObject = 0;
        bufferQueueItf = 0;
    }
    if (mixObject) {
        (*mixObject)->Destroy(mixObject);
        mixObject = 0;
    }
    if (engineObject) {
        (*engineObject)->Destroy(engineObject);
        engineItf = 0;
    }
    if (swr_ctx) {
        swr_free(&swr_ctx);
    }
    if (graph) {
        avfilter_graph_free(&graph);
    }
    for (int i = 0; i < fileCount; i++) {
        avcodec_close(codec_ctx_arr[i]);
        avformat_close_input(&fmt_ctx_arr[i]);
    }
    free(codec_ctx_arr);
    free(fmt_ctx_arr);
    LOGI("release...");
}

9.6 最终效果

具体效果可以运行项目查看

播放测试