FFmpeg是一个强大的音视频处理库,但是通常接触时以命令形式较多。本篇文章讲了FFmpeg相关api的使用,尤其是它强大的过滤器filter库的使用。
1.1 能学到什么
本项目主要素材为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
进入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)'
新建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文件和头文件
★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
打开Android Studio,新建项目FFmpegAudioPlayer,添加C++支持
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读取音频信息,然后得到对应的解码器,然后循环读取每一帧音频数据,并且通过解码器解码。大致解码流程如下:
在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
#include
#include
extern "C" {
#include
#include
#include
}
#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(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文件了。
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(具体过滤器)
通过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(frame->linesize[0]),
out_file); //想将单个声道pcm数据写入文件
}
}
}
}
最终解码出来pcm和原始mp3波形对比
可以明显看出音量已经发生变化。
在播放音频时,可以听见有一些噪声,需要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(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个声道的数据,而且也没有噪声了。
将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的播放的,而且音调保持不变,没有因为速度的改变而变高或者变低。
★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
6. 多输入AVFilter过滤器
FFmpeg使用过滤器filter的另外一个场景就是处理多个输入数据,比如视频添加水印,添加字幕,音视频合并等。这类场景需要两个及以上输入端。本节讲amix,它可以将多个音频混音。
6.1 多输入filter处理流程
输入AVFrame1 -> abuffer
-> amix -> aformat -> abuffersink -> 输出AVFrame
输入AVFrame2 -> abuffer
处理流程和单输入过滤器大致相同,只不过接收了多个输入端。因此需要多个filter上下文作为输入端。
//初始化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过滤器,这样就可以接收多个输入端了。
为了能够传入多个音频数据,我们需要同时解码多个音频文件,因此在Java层,传入字符串数组。
external fun mixAudio(arr: Array,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(env->GetObjectArrayElement(_srcs, i));
pathArr[i] = const_cast(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,使用时加入头文件
7.1 OpenSLES播放器流程
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(¬full, NULL);
pthread_cond_init(¬empty, NULL);
//初始化解码线程
pthread_t pid;
char *path = (char *) env->GetStringUTFChars(_path, 0);
pthread_create(&pid, NULL, decodeAudio, path);
申明全局变量
static std::vector 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(¬full, &mutex);//等待队列不为满信号
}
queue.push_back(frame);
pthread_cond_signal(¬empty);//发送不为空信号
pthread_mutex_unlock(&mutex);
}
}
通过之前注册的缓冲回调,我们可以把加入到队列的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(¬full);//发送notfull信号
pthread_mutex_unlock(&mutex);
return out;
} else {//为空等待添加
LOGI("wait for get frame");
pthread_cond_wait(¬empty, &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 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(¬_full, NULL);
pthread_cond_init(¬_empty, NULL);
initCodecs(pathArr);
avfilter_register_all();
initSwrContext();
initFilters();
createPlayer();
}
这里我们还初始化了控制各个音频音量和速度的变量,同步锁和条件变量(生产消费控制)。
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初始化。
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设置的参数要一致
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(¬_full, &mutex);
}
queue.push_back(out);
pthread_cond_signal(¬_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(¬_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(¬_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)
/**
* 播放
*/
external fun play()
/**
* 暂停
*/
external fun pause()
/**
* 释放资源
*/
external fun release()
/**
* 修改每个音量
*/
external fun changeVolumes(volumes: Array)
/**
* 变速
*/
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(env->GetObjectArrayElement(_srcs, i));
pathArr[i] = const_cast(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(env->GetObjectArrayElement(_volumes, i));
char *volume = const_cast(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(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中
为了能够实现变速,调音,我们要在解码之前重新修改过滤器的参数。这里使用一个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:
...
}
这样就可以实现音量和速度的控制了。
暂停可以通过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);
}
进度控制是使用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(¬_full);
pthread_mutex_unlock(&mutex);
}
设置播放器状态为停止,释放Open SLES相关资源,释放过滤器资源,释放解码器资源,关闭输入流。
void AudioPlayer::release() {
pthread_mutex_lock(&mutex);
isPlay = 0;
pthread_cond_signal(¬_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...");
}
具体效果可以运行项目查看
原文 一篇文章助你入门FFmpeg编程 - 掘金
★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓