零、写在前面
“我正在参加「掘金·启航计划」” 利用FFmpeg的基础库方法我们能完成大多数的音视频操作,但同时FFmpeg也封装了常用的操作,只需要我们执行命令即可,极大的降低了开发成本。例如实现一个转码的业务,哪怕简单点来说我们也要要解码-重采样-编码。这其中我们要考虑很多条件,音频信息导致的不同处理,异常的捕捉等等,麻烦不说,兼容性都有一段路走。所以如果不是因为要对音频有特殊的逻辑处理,我们完全可以用命令来解决。但是看看FFmpeg的源码,它的命令是针对命令行环境的,和Android上运行还是有一定的差距,所以我们需要在原基础上做一些Android端的适配,使我们Android端也能轻松使用命令,低成本完成各类音频操作。后续操作都在已编译好Android端so库的情况下进行,如果还没有走过这一步的朋友可以先看看我之前的文章 《Unbuntu环境编译 Android平台可用ffmpeg(带三方库fdk-aac和lame)》
一、导入资源和编译
1.1、导入相关源文件
FFmpge源码下有一个fftools目录,里面就是一些ffmpeg的命令行程序,我们将他们导入进来,进行一定的修改后再自行编译成我们的lib。
首先是对库文件进行相应的修改,因为ffmpeg.c在windows上的目的是编译成一个可执行的应用,所以它的入口是main 通过命令行传参,所以我们这里需要把main函数改一下,改成一个我们具体功能的名字,例如我喜欢叫它exc,然后再再头文件中声明我们刚改的函数名,这样它就变成了一个库文件。我们在jni的地方调用exc进行传参,就达到了电脑端命令行的效果。
还有一个地方,ffmpeg如果指令执行出错是执行的退出程序指令,我们在Android端肯定不行,那不然指令执行不成功就一个闪退这谁都顶不住,所以我们需要把exit_program函数的exit(ret);去掉,这里我们可以做我们自己的逻辑,进行自己的错误码回调。
原代码
void exit_program(int ret)
{
if (program_exit)
program_exit(ret);
exit(ret);
}
修改后
void exit_program(int ret)
{
if (program_exit)
program_exit(ret);
}
编译过程中发现会有些库丢失,可以删掉也可以把库引进来.主要看这些库是否是你所需要的功能,例如有些是一些打印函数缺失的我不需要我就直接删掉也懒得导包了。
1.2、cmdkelist的编写
首先是引入头文件目录 以我的文件目录为例我的为 include_directories(ffmpeg/fftools),后续的依赖完整如下:
add_library( # Sets the name of the library.
ffmpeg-cmd
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
ffmpeg/fftools/cmdutils.c
ffmpeg/fftools/ffmpeg.c
ffmpeg/fftools/ffmpeg_opt.c
ffmpeg/fftools/ffmpeg_filter.c
ffmpeg/fftools/ffmpeg_hw.c
ffmpeg-cmd.c
)
target_link_libraries( # Specidefies the target library.
ffmpeg-cmd
# Links the target library to the log library
# included in the NDK.
fdk-aac
mp3lame
avdevice
postproc
avfilter
avformat
avcodec
avutil
swresample
swscal
)
cmdutils.c、ffmpeg.c、ffmpeg_opt.c、ffmpeg_filter.c、ffmpeg_hw.c、ffmpeg-cmd.c都有一定的依赖关系,这几个C文件共同编译成一个库ffmpeg-cmd,ffmpeg-cmd是我的JNI层方法,负责双边沟通,fftools中依赖了其它例如解码,滤镜,重采样等外部库,也是我们之前编译了的其它库,我们需要把它们作为ffmpeg-cmd的库链接进去。
二、加入执行回调
对于命令的执行很多时候是一个耗时操作,我们UI不可能在那里干等,所以我们需要给命令执行加入回调,告诉Java层指令的执行状态,好做一些进度的变更,成功或失败的处理。最基本的我们给指令执行加入进度,失败,成功的回调。
2.1、执行进度回调
ffmpeg.c里有一个print_report方法,里面主要是一个它自身进度的打印,我们看其中一段主要代码
static void print_report(int is_last_report, int64_t timer_start, int64_t cur_time)
{
......
secs = FFABS(pts) / AV_TIME_BASE;
us = FFABS(pts) % AV_TIME_BASE;
mins = secs / 60;
secs %= 60;
hours = mins / 60;
mins %= 60;
hours_sign = (pts < 0) ? "-" : "";
//program_progress(secs); //本身没有,这是我们自己加的,这是简单逻辑,可以自己再写逻辑
bitrate = pts && total_size >= 0 ? total_size * 8 / (pts / 1000.0) : -1;
speed = t != 0.0 ? (double)pts / AV_TIME_BASE / t : -1;
......
}
在这里它计算出了已经执行完成的文件时长,同样的我们传入函数指针,把这个时长传到Java层,稍加计算就能得到我们需要的进度。
//ffmpeg.h
/**
* Register a register_progress.
*/
void register_progress(void (*cb)(long progress));
//ffmpeg.c
static void (*program_progress)(long progress);
void register_progress(void (*cb)(long progress))
{
program_progress = cb;
}
2.1、执行成功/失败回调
不管成功还是失败,ffmpeg都会走void exit_program(int ret) 方法,它自身会先走一个ffmpeg_cleanup方法,做一些他自己的收尾工作。我们可以在它自身方法的后面插入我们自己的回调方法,就是我们自己传入一个函数指针,其实就是我们重写一个相同签名的方法然后把这个方法的地址传过去,程序自己进行地址切换,跳转执行。回调0表示成功,回调其它表示失败。Java层怎么把接口传给C层在这里就不在赘述,也可以看看我之前的文章《JNI常用开发技巧》
//cmdutils.h
/**
* Register a register_status.
*/
void register_status(void (*cb)(int ret));
//cmdutils.c
static void (*program_status)(int ret);
void register_status(void (*cb)(int ret))
{
program_status = cb;
}
void exit_program(int ret)
{
if (program_exit)
program_exit(ret);
if (program_status)//加入我们的状态回调
program_status(ret);
}
使用时我们把这两个函数指针传进去即可。执行一段命令就会有进度和状态的回调,我们可以把它展示到UI上。
static void exit_call(int ret){
LOGE("exit_call %d", ret);
}
static void cmd_progress(long progress){
LOGE("onProgress %d", progress);
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* jniEnv= NULL;
int result=(*vm)->GetEnv((vm),(void **)&jniEnv,JNI_VERSION_1_4);
if (result!=JNI_OK){
return -1;
}
av_log_set_callback(log_callback_test2);
register_status(exit_call);//传入函数指针
register_progress(cmd_progress);//传入函数指针
return JNI_VERSION_1_4;
}
三、常见音频操作命令
当然命令还是得看FFmpeg官方文档,但是官方有些好像并没有中文文档,大家可以找找中文翻译的。我这里列出几个我在用的一些命令,包括音频操作和音频滤镜,其余的大家可以自由发挥。
//在任意位置混音
//"ffmpeg -i %s -i %s -filter_complex " +"[1]adelay=delays=%s|%s[aud1];[0][aud1]amix=inputs=2 -y %s";
/**
* 音频操作
*/
//裁剪
private final String cropCmd="ffmpeg -i %s -ss %s -t %s -acodec copy %s";
//pcm转MP3
private final String pcm2Mp3="ffmpeg -f s16le -ar 44100 -ac 2 -i %s -ar 44100 -ac 2 -y %s";
//静音移除 大于间隔0.3s的静音全部移除
private final String muteRemove="ffmpeg -i %s -af silenceremove=stop_periods=-1:stop_duration=0.3:stop_threshold=-30dB %s";
private final String highPassCmd="ffmpeg -i %s -af highpass=300 %s";//低切300 150 75
private final String gateCmd="ffmpeg -i %s -filter agate=knee=1:ratio=1.5:range=0.08 %s";//降噪
/**
* chorus例子
* 一个延迟(二人合唱效果):
* chorus=0.7:0.9:55:0.4:0.25:2
* 2个延迟(三人合唱效果):
* chorus=0.6:0.9:50|60:0.4|0.32:0.25|0.4:2|1.3
* 3个延迟(四人及更多合唱效果):
* chorus=0.5:0.9:50|60|40:0.4|0.32|0.3:0.25|0.4|0.3:2|2.3|1.3
*/
private final String chorusCmd="ffmpeg -i %s -filter chorus=0.6:0.9:50|60:0.4|0.32:0.25|0.4:2|1.3 %s";//三人合唱效果 没什么效果
/**
* 滤镜
*/
private final String echoMountainCmd="ffmpeg -i %s -filter aecho=0.8:0.9:1000:0.3 %s";//山间回音特效
private final String echoRobotCmd="ffmpeg -i %s -filter aecho=0.8:0.88:6:0.4 %s";//机器人特效
private final String afadeCmd="ffmpeg -i %s -filter afade=t=%s:ss=%:ns=%ld:st=%d:d=%d:curve=tri %s";
private final String volumeCmd="ffmpeg -i %s -filter volume=%s %s";//音量
private final String vibratoCmd="ffmpeg -i %s -filter vibrato=f=%f:d=%f %s";//颤音
private final String asetrateCmd="ffmpeg -i %s -filter asetrate=sample_rate=%s,atempo=%s %s";//男低音 30000 1.25变调 44100
private final String asetrate2Cmd="ffmpeg -i %s -filter asetrate=sample_rate=%s,atempo=%s %s";//娃娃音 73500 0.6 44100
private final String atempo2Cmd="ffmpeg -i %s -filter atempo=%s %s";//变速
private final String karaokeCmd="ffmpeg -i %s -filter stereotools=mlev=0.015625 %s";//卡拉OK
private final String compandCmd=""ffmpeg -i %s -filter compand=.3|.3:1|1:-90/-60|-60/-40|-40/-30|-20/-20:6:0:-90:0.2 %s";//感觉像声音加强了
private final String flangeCmd="ffmpeg -i %s -filter flanger=delay=0 %s";//环绕效果
四、写在最后
欢迎大家交流讨论,批评指正,觉得有帮助的点个赞吧。
作者:Mystatic
链接:https://juejin.cn/post/7154555639467868190
网友评论