Android Studio引入JNI第三方库
前言
JNI作为Java与native沟通的桥梁,项目开发中难免要使用到;而我们除了自己开发JNI之外,有时候还要在Android Studio引入别人开源的C++第三方库,并在jni层实现第三方库的调用。
记录一下第一次引入第三方库的步骤和一些需要注意的地方。
这里使用的第三方库是FMOD,一个操作音频的开放平台,
点击跳转官网地址~
官方API使用方法戳:
API官网地址
具体操作可以通过下载exe文件来操作各种音频配置,设置不同的声音属性,比如要设置一个声音的变化可以设置它的Pitch、Overlap等属性,会让声音发生各种变化,比如正常声音变化小黄人的声音等操作,如图:
安装了客户端之后可以直接通过上拉和下拉鼠标来实现设置这些值的操作,通过第三方库可以使用代码来设置这些参数,直接上代码。
创建应用
既然要使用到JNI模块,就不能直接创建新引用,而且创建应用的时候需要注意以下几点:
- 项目目录不能存在空格或者特殊符号,不然会报错,导致CMakeLists.txt文件无法编译成功。
-
不能直接创建空的Activity项目,要创建Native C++项目,如图:
创建完成后会得到一个目录如下的工程:
- cpp 存放native层代码的模块
- java java层代码模块
主要区别就是之前创建的空的Activity项目多了一个cpp模块,所有与native交互的操作,以及操作完成了反馈给java层的操作都在这里完成。
导入第三方配置
将第三方库的相关资源复制到项目里面去。
导入头文件和实现文件
将第三方库下载下来之后可以得到一个这种样子的文件夹
fmodstudioapi11009android
找到
api
下面的
lowlevel
就是android平台的相关资源:
- examples 例子
- inc 头文件
- lib 执行文件(so文件)
将inc复制到cpp文件夹下:
这就完成了头文件的导入,光导入头文件是无法实现功能的,因为头文件只是做函数的声明,所以还要导入实现文件,so文件。
将so文件导入到jniLibs文件夹下:
头文件和实现文件的导入就完成了。
最后还要导入以下jar包资源,先将jar包复制到libs文件夹下,再导入。
implementation files('libs\\fmod.jar')
配置CPU运行环境
如果不配置CPU运行环境是会默认支持五个平台的,自然而然所需要的内容就会变大,所以要对CPU的运行环境进行配置。
首先配置Cmake本地库的CPU运行环境,在build.gradle文件的defaultConfig进行配置:
externalNativeBuild {
cmake {
// cppFlags "" // 默认五大平台
abiFilters "armeabi-v7a"
}
}
同时还要配置ndk的cpu环境:
// 指定CPU架构,打入APK lib/CPU平台
ndk {
abiFilters "armeabi-v7a"
}
CMakeLists.txt配置
导入了资源要将资源配置到
CMakeLists.txt
文件才能进行使用。
导入头文件:
include_directories("inc")
批量导入.c .h .cpp文件:
#批量查询到.c .h .cpp文件
file(GLOB allCpp *.c *.h *.cpp *.hpp)
add_library( # Sets the name of the library.
qq_sounddemo
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${allCpp})//将这些文件导入到qq_sounddemo里面去
设置库文件路径,就是要链接到的so文件路径:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")
链接具体的库到qq_sounddemo.so里面去,不然使用会报错:找不到对应的库:
target_link_libraries( # Specifies the target library.
qq_sounddemo
# Links the target library to the log library
# included in the NDK.
${log-lib}
fmod
fmodL
)
以上工作就是对第三方库的导入和配置。
FMOD的使用
具体使用方法可以查看上面的官方API文档,这里只做简单的记录。
初始化和释放
FMOD.init()初始化:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
//初始化FMOD
FMOD.init(this)
}
close关闭:
override fun onDestroy() {
super.onDestroy()
//释放资源
FMOD.close()
}
C++宏的创建
居然是要处理不同模式下的声音,所以要定义几个声音常量,而这些常量在C++的表现就是宏,定义如下:
private val MODE_NORMAL = 0 //正常
private val MODE_LOLITA = 1 //萝莉
private val MODE_UNCLE = 2 //大叔
private val MODE_THRILLER = 3 //惊悚
private val MODE_FUNNY = 4 //搞怪
private val MODE_ETHEREAL = 5 //空灵
宏的创建可以直接使用javah的命令进行创建,但是此命令只适用于.class文件,不使用于.kt文件;先进入到项目的java文件:
然后输入以下命令:
javah 包名.文件名
TestActivity定义的常量是这样的:
public class TestActivity extends AppCompatActivity {
public static final int MODE_NORMAL = 0;//正常
public static final int MODE_LOLITA = 1;//萝莉
public static final int MODE_UNCLE = 2;//大叔
public static final int MODE_THRILLER = 3;//惊悚
public static final int MODE_FUNNY = 4;//搞怪
public static final int MODE_ETHEREAL = 5;//空灵
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
使用该命令后就会自动生成一个.h文件,如下:
打开就可以看到自动帮我们定义好的宏了:
但是这个方法只适用于.class文件,如果是kt文件执行命令会出现找不到文件的错误;所以如果是kotlin开发,那么宏只能自己慢慢写。
头文件的编写
头文件存在的目的是为了方便执行文件的使用,部分函数或者变量的声明放到头文件里面,执行文件就可以直接调用。
这里的头文件需要将第三方库的头文件,以及部分执行文件开发过程中要使用到的系统头文件都导入进来:
#include <jni.h>
#include <unistd.h>
#include <string>//系统头文件
#include "fmod.hpp"//第三方库头文件
下面是手写的宏的定义,因为是kotlin开发,所以只能手写了:
#ifndef QQ_SOUNDDEMO_COM_YANGCHOI_QQ_SOUNDDEMO_MAINACTIVITY_H
#define QQ_SOUNDDEMO_COM_YANGCHOI_QQ_SOUNDDEMO_MAINACTIVITY_H
#ifdef __cplusplus
extern "C"{
//定义宏
#endif
#undef demo_MainActivity_MODE_NORMAL
#define demo_MainActivity_MODE_NORMAL 0L
#undef demo_MainActivity_MODE_LOLITA
#define demo_MainActivity_MODE_LOLITA 1L
#undef demo_MainActivity_MODE_UNCLE
#define demo_MainActivity_MODE_UNCLE 2L
#undef demo_MainActivity_MODE_THRILLER
#define demo_MainActivity_MODE_THRILLER 3L
#undef demo_MainActivity_MODE_FUNNY
#define demo_MainActivity_MODE_FUNNY 4L
#undef demo_MainActivity_MODE_ETHEREAL
#define demo_MainActivity_MODE_ETHEREAL 5L
};
#endif //QQ_SOUNDDEMO_COM_YANGCHOI_QQ_SOUNDDEMO_MAINACTIVITY_H
native-lib.cpp执行文件的编写
在编写执行文件之前,需要先java层定义一个可供交互的方法,在java层调用此方法跳转到native-lib.cpp执行相关操作,定义如下:
private external fun voiceChangeNative(mode: Int, path: String)
JNI方法的定义,kotlin的关键字是
external
,java的关键字是
native
;接下来是编写头文件。
执行文件实现逻辑操作,首先要导入编写好的头文件:
#include "demo_MainActivity.h"
然后导入第三方库的命名空间:
using namespace FMOD;//导入FMOD命名空间
具体实现代码是照着api来写的,就不做解释直接贴了,如下:
extern "C"
JNIEXPORT void JNICALL
Java_com_yangchoi_qq_1sounddemo_MainActivity_voiceChangeNative(JNIEnv *env, jobject thiz, jint mode,
jstring path) {
//播放完毕的文字
char *playOverStr = "播放完毕";
//获取音频路径
const char * path_ = env->GetStringUTFChars(path,NULL);
System * system = 0; // fmod 音效引擎系统
Sound * sound = 0; // fmod 声音
Channel * channel = 0; // 通道 音轨
DSP * dsp = 0; // digital signal process == 数字信号处理
System_Create(&system); // 创建系统
system->init(32, FMOD_INIT_NORMAL, 0);//系统初始化
system->createSound(path_, FMOD_DEFAULT, 0, &sound);//创建声音
system->playSound(sound, 0, false, &channel);//播放声音
switch (mode) {
case com_yanghcoi_qq_sounddemo_MainActivity_MODE_NORMAL:
playOverStr = "原生:播放完毕";
break;
case com_yanghcoi_qq_sounddemo_MainActivity_MODE_LOLITA:
playOverStr = "萝莉:播放完毕";
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 2.0f);
channel->addDSP(0, dsp);
break;
case com_yanghcoi_qq_sounddemo_MainActivity_MODE_UNCLE:
playOverStr = "大叔:播放完毕";
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);
channel->addDSP(0, dsp);
break;
case com_yanghcoi_qq_sounddemo_MainActivity_MODE_THRILLER:
playOverStr = "搞怪 小黄人:播放完毕";
float frequency;
channel->getFrequency(&frequency);
channel->setFrequency(frequency * 1.5f);
break;
case com_yanghcoi_qq_sounddemo_MainActivity_MODE_FUNNY:
playOverStr = "惊悚音:播放完毕";
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);
channel->addDSP(0, dsp);
system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 400); // 延时的回音
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 40); // 默认:50 0完全衰减了
channel->addDSP(1, dsp);
system->createDSPByType(FMOD_DSP_TYPE_TREMOLO, &dsp);
dsp->setParameterFloat(FMOD_DSP_TREMOLO_FREQUENCY, 0.8f);
dsp->setParameterFloat(FMOD_DSP_TREMOLO_SKEW, 0.8f);
channel->addDSP(2, dsp);
break;
case com_yanghcoi_qq_sounddemo_MainActivity_MODE_ETHEREAL:
playOverStr = "空灵:播放完毕";
system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 200); // 延时的回音
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 10); // 默认:50 0完全衰减了
channel->addDSP(0, dsp);
break;
}
// 死循环
bool isPalyer = true;
while (isPalyer) {
channel->isPlaying(&isPalyer);
usleep(1000 * 1000); // 每隔一秒
}
sound->release();
system->close();
system->release();
env->ReleaseStringUTFChars(path, path_);
}
头文件编写完成之后就可以在java层进行调用了,直接传入对应的参数和音频播放路径就可以,如下:
voiceChangeNative(MODE_NORMAL, path)
voiceChangeNative(MODE_LOLITA, path)
voiceChangeNative(MODE_UNCLE, path)
voiceChangeNative(MODE_THRILLER, path)
voiceChangeNative(MODE_FUNNY, path)
voiceChangeNative(MODE_ETHEREAL, path)
以上就是Android Studio引入第三方类库的全部内容~
如果有知道kotin怎么快捷生成宏的,麻烦评论区告知一下~